Table of Contents

Xact Language Reference

Introduction

Xact is a domain-specific programming language for creating fast, transparent, and maintainable actuarial/financial models.

Fast -- Calculations are optimized so that they can finish before your coffee is ready.

Transparent -- Model logic is completely clear and available to the user.

Maintainable -- User can conveniently make any changes they need to their models without unnecessary repetition.

Xact support exporting all model calculations into the xlsx file format with all Excel formulas populated. This allows user to easily examine model logic and communicate to all stakeholders.

Examples are usually the best way to learn, so we lead with examples for all of xact's major features. But first, let's install it if you haven't already.

Installation

You can install Xval (which includes Xact) from the install page at xval.io. On Visual Studio Code, the xval extension is recommended because it provides xact syntax highlighting.

After install, you can verify the installation by opening a terminal (e.g. command prompt) and typing "xval". You should see something like the following.

Terminal
> xval
info: Usage: xval [command] [options]

Commands:
  run             Run an xact file

General Options:
  -h, --help      Print command-specific usage

error: command argument expected; not found
This means Xval is installed and you are ready to roll.

Hello, World!

Xact cannot print text to a terminal, but we can write "Hello, World!" to our output.

hello.xact
table hello {
    nrows: 1,
    columns: {
        hello <string>: "Hello, World!";
    }
}
Terminal
> xval run hello.xact

Now you should see hello.csv in the current directory.

Comments

Xact uses // for single-line comments and /**/ for multi-line comments.

comments.xact
table comments {
    // nrows: 7,
    nrows: 10,
    columns: {
        some_incredible_calc: t * 9001; // It's over 9000!
        /*
            Multi-line comments
            let you easily include
            an inline haiku.
        */
    }
}
            
Terminal
> xval run comments.xact
comments.csv
t,some_incredible_calc
0,0
1,9001
2,18002
...
9,81009

Comments will not be executed, but can be very useful -- especially for inlining haikus.

Blocks

Xact code is organized into blocks declared at the top-level of a module.

Inputs

An input block declares an expected tabular input, e.g. a csv file. Any columns you use from the input file must be declared in the input block. The input file is allowed to contain unused columns. Types should generally always be declared, but if the type is omitted, the default is float.

inputs.xact
input mortality {
    type: "csv";
    columns: {
        AGE <int>;
        PROBABILITY <float>;
    }
}

input extract {
    type: "csv";
    columns: {
        POLNUM <string>;
        ISSUE_DATE <date "%Y-%m-%d">;
        'ISSUE AGE'n <int>;
        "Is Qualified" <bool>;
        OPEN_SEGMENT_YEARS <float>;
    }
}
extract.csv
POLNUM,ISSUE_DATE,ISSUE AGE,Is Qualified,OPEN_SEGMENT_YEARS
A123,2025-01-15,22,TRUE,0.133699998
A456,2024-02-29,1,TRUE,3.140000105
A789,2025-05-27,47,FALSE,0.965699971
File System
model/
├── inputs.xact 
├── mortality.csv
└── extract.csv

If we run inputs.xact, it will import mortality.csv and extract.csv.

Terminal
> xval run models/inputs.xact

Now we can use the inputs in a table.

Tables

The table is the fundamental unit of Xact. Each table will produce one output sheet. Using the extract from inputs, we can do tranformations in a table.

table.xact
// ... code from inputs.xact ...

table policy_metadata {
    nrows: extract;
    columns: {
        polnum <string> : extract.POLNUM[t];
        issue_year <int> : $year(extract.ISSUE_DATE[t]);
        issue_month <int> : $month(extract.ISSUE_DATE[t]);
        issue_day <int> : $day(extract.ISSUE_DATE[t]);
        issue_date_reconstructed <date> : $date(
            self.issue_year[t], self.issue_month[t], self.issue_day[t]
        );
        open_segment_full_years <float> : $floor(extract.OPEN_SEGMENT_YEARS[t]);
    }
}
#ecfdf5

Here, we set nrows to extract, telling xact that the table will have one row for each row of extract. The nrows keyword accepts an integer or the name of an input.

The index of each row is denoted by t. So, we can refer to the row in the extract that corresponds to each table row.

Terminal
> xval run table.xact
policy_metadata.csv
t,polnum,issue_year,issue_month,issue_day,issue_date_reconstructed,open_segment_full_years
0,A123,2025,1,15,2025-01-15,0
1,A456,2024,2,29,2024-02-29,3
2,A789,2025,5,27,2025-05-27,0

But what if we need a whole table for each policy, instead of just a single row. For example, we might want a projection for each policy. That's where we would use a vtable.

Vtables

When you use a vtable, xact creates one table per row of the caller. Vtables will not be calculated by themselves. They must be called by a table.

vtable.xact
// ... code from table.xact ...

vtable projection {
    nrows: 24; // years
    columns: {
        projection_date <date>: $date(
            policy_metadata.issue_year[t1] + t,
            policy_metadata.issue_month[t1],
            policy_metadata.issue_day[t1]
        );
        open_segment_years <float>: $if(
            extract.OPEN_SEGMENT_YEARS[t1] - t > 1,
            extract.OPEN_SEGMENT_YEARS[t1] - t,
            extract.OPEN_SEGMENT_YEARS[t1] - policy_metadata.open_segment_full_years[t1]
        );
        age <int>: extract."ISSUE AGE"[t1] + t;
        annual_mort <float>: $lookup(
            mortality.PROBABILITY[:],
            mortality.AGE[:] == self.age[t]
        );
    }
}

table summary {
    nrows: extract;
    vtable: { source: projection; }
    columns: {
        polnum <string>: extract.POLNUM[t];
        is_qualified <bool>: extract.'Is Qualified'n[t];
        all_the_ages <int>: $sum(summary.projection.age[:, t]);
        all_the_mort <float>: $sum(summary.projection.annual_mort[:, t]);
        a_single_mort <float>: summary.projection.annual_mort[13, t];
    }
}

The value t1 here is similar to t. Except instead of refering to the row index in projection, it refers to the row index of the table or vtable that called projection. This lets projection refer to the corresponding row in policy_metadata and extract.

Terminal
> xval run vtable.xact
summary.projection.csv
t,t1,projection_date,open_segment_years,age,annual_mort
0,0,2025-01-15,0.1337,22,0.000473
1,0,2026-01-15,0.1337,23,0.000513
... more rows ...
22,0,2047-01-15,0.1337,44,0.001142
23,0,2048-01-15,0.1337,45,0.001219
0,1,2024-02-29,3.14,1,0.000401
1,1,2025-03-01,2.14,2,0.000275
... more rows ...
22,1,2046-03-01,0.1400001,23,0.000513
23,1,2047-03-01,0.1400001,24,0.000554
0,2,2025-05-27,0.9657,47,0.001454
1,2,2026-05-27,0.9657,48,0.001627
... more rows ...
22,2,2047-05-27,0.9657,69,0.010463
23,2,2048-05-27,0.9657,70,0.011357

Note that t1 goes from 0 to 2 because extract has 3 rows.

A vtable can be called by a table or a vtable, meaning that vtables can be nested. This would be useful, for example, if you wanted to do N policies * 1,000 scenarios * 360 projection months.

Expressions

Now that we have background on the blocks available in xact, let's dive into parts that make up an expression.

Types

Xact has 5 basic types: float, int, bool, date, and string. You declare types after a column name. If you omit the type The column will default to float. This is usually not a problem because xact supports standard type coercion. However if you need a column to be a string or a date, you must include the type.

types example
my_marvelously_typed_column <date>: $date( 1984, 2, 1); // February 1st, 1984

Special functions and operators usually restrict allowed types, making it important to be explicit with what types you expect in each column.

String types are currently limited to length 255.

Values

Literal values can be specified for each type as follows.

literal values example
int_value <int>: 7;
float_value <float>: 7.0;
bool_value <bool>: true; // or false
date_value <date>: $date(2018, 7, 7);
string_value <string>: "My auspicious string"; // limited to length 255

Dates require using a function to initialize a date literal. Expressions will be cast into their column's type if possible, otherwise you will get an error. Therefore, as mentioned before, you must specify the type for date and string columns.

Tn

The identifiers t and tn where n is any integer (e.g. t1, t2, t7) are special symbols that work as 0-indexed row indexers. 0-indexed means they start from 0.

For example, t is the indexer for the current table. It's value is 0 in the first row, 1 in the second, and so on...

t.xact
table t_example {
    nrows: 5;
    columns: {
        double_my_index <int>: t * 2; // 0, 2, 4, 6, 8
    }
}

tn is a bit more difficult to understand. It is only usable in a vtable that has a parent table at least n levels above it. It represents the index of the row in the parent n levels above it.

For example, t1 in the summary.projection vtable corresponds to the index of the policy being projected. This is because the table summary, 1 level above summary.projection, has one row for each row of extract. If it isn't clear, take a look at summary.projection and see if you can figure out why t1 is what it is in each row.

References

References allow an expression to refer to columns from itself, an input, or another table or vtable.

references example
self_column: t + 3;
self_ref: self.self_column[t]; // 3, 6, 9, ...
input_or_table_ref: input_or_table.column_name[t]; // get the value in row t
vtable_ref: table_name.vtable_name.column_name[t, 7]; // get the value in row t for vtable index 7
array_ref: $sum(table_name.vtable_name.column_name[t, :]); // sum values from row t across all vtables

A reference to a column in the same table has the form self.column_name[ row_index ]. Here, row_index is usually just t, but it can be any expression. Self-references do not support ranges.

A reference to a input column has the form input_name.column_name[ row_index ] or input_name.column_name[:]. As before, row_index can be any expression. The ":" indicates a range.

A reference to a table or vtable column has the form:
table_name.vtable_name.column_name[ row_index or :, vtable_index or :].
Since vtables can be nested, we can expand this ad-infinitum to:
t_name.vtn....vt1.column_name[ t_ndx or :, t1_ndx or :, ..., tn_ndx].
Note the inversion of the order of the tables and the indices.

A reference to a table column must have the same number of indices and/or ranges as it has tables. So for example:
t_name.vt2.vt1.column_name[ t_ndx, t1_ndx, t2_ndx].requires 3 indices.

Ranges

Ranges allow selecting more than one element from a column. Currently, xact only supports full ranges. The range symbol ":" means "take all of the items along this dimension". You can only use ranges with built-in functions that support it, such as sum and lookup.

Operators

Xact supports the following operators:

operators.xact
table operators {
    nrows: 4;
    columns: {
        plus <int>: t + 2; // 2, 3, 4, 5
        minus <int>: t - 4; // -4, -3, -2, -1
        times <int>: t * 3; // 0, 3, 6, 9
        div <float>: t / 2; // 0, .5, 1, 1.5
        unary_minus <int>: -t; // 0, -1, -2, -3
        power <int>: t ^ 2; // 0, 1, 4, 9
        parentheses <int>: t * ( t + 1); // 0, 2, 6, 12

        // comparisons
        greater_than <bool>: t > 1; // false, false, true, true
        greater_than_eq <bool>: t >= 1; // false, true, true, true
        equal <bool>: t == 1; // false, true, false, false
        not_equal <bool>: t != 1; // true, false, true, true
        less_than <bool>: t < 1; // true, false, false, false
        less_than_eq <bool>: t <= 1; // true, true, false, false
    }
}

Functions

Xact has the following built-in functions:

functions.xact
// ... code from operators.xact

table functions {
    nrows: 1;
    columns: {
        date <date>: $date(2024, 2, 28); // Year, Month, Day
        year <int>: $year( self.date[t] ); // 2024
        month <int>: $month( self.date[t] ); // 2
        day <int>: $day( self.date[t] ); // 28
        floor <int>: $floor( 3.14 ); // 3
        if <int>: $if( t == 1, 1, 0); // 0
        lookup <int>: $lookup(
            operators.plus[:],
            operators.minus[:] == -2
        ); // 4
        sum <int>: $sum(operators.plus[:]); // 14
    }
}

Audits

Xact can export all model calculations into the xlsx file format with all formulas populated. Rehashing the complete example from Blocks:

example.xact
input mortality {
    type: "csv";
    columns: {
        AGE <int>;
        PROBABILITY <float>;
    }
}

input extract {
    type: "csv";
    columns: {
        POLNUM <string>;
        ISSUE_DATE <date "%Y-%m-%d">;
        'ISSUE AGE'n <int>;
        "Is Qualified" <bool>;
        OPEN_SEGMENT_YEARS <float>;
    }
}

table policy_metadata {
    nrows: extract;
    columns: {
        polnum <string> : extract.POLNUM[t];
        issue_year <int> : $year(extract.ISSUE_DATE[t]);
        issue_month <int> : $month(extract.ISSUE_DATE[t]);
        issue_day <int> : $day(extract.ISSUE_DATE[t]);
        issue_date_reconstructed <date> : $date(
            self.issue_year[t], self.issue_month[t], self.issue_day[t]
        );
        open_segment_full_years <float> : $floor(extract.OPEN_SEGMENT_YEARS[t]);
    }
}

vtable projection {
    nrows: 24; // years
    columns: {
        projection_date <date>: $date(
            policy_metadata.issue_year[t1] + t,
            policy_metadata.issue_month[t1],
            policy_metadata.issue_day[t1]
        );
        open_segment_years <float>: $if(
            extract.OPEN_SEGMENT_YEARS[t1] - t > 1,
            extract.OPEN_SEGMENT_YEARS[t1] - t,
            extract.OPEN_SEGMENT_YEARS[t1] - policy_metadata.open_segment_full_years[t1]
        );
        age <int>: extract."ISSUE AGE"[t1] + t;
        annual_mort <float>: $lookup(
            mortality.PROBABILITY[:],
            mortality.AGE[:] == self.age[t]
        );
    }
}

table summary {
    nrows: extract;
    vtable: { source: projection; }
    columns: {
        polnum <string>: extract.POLNUM[t];
        is_qualified <bool>: extract.'Is Qualified'n[t];
        all_the_ages <int>: $sum(summary.projection.age[:, t]);
        all_the_mort <float>: $sum(summary.projection.annual_mort[:, t]);
        a_single_mort <float>: summary.projection.annual_mort[13, t];
    }
}

If we have the file structure:

File System
example/
├── example.xact 
├── mortality.csv
└── extract.csv

And we run this command:

Terminal
> xval run example/example.xact --audit

We will get the following:

File System
example/
├── example.xact 
├── mortality.csv
├── extract.csv
├── example.xlsx 
└── example-audit.xlsx

The files example.xlsx and example-audit.xlsx will have near identical values (up to rounding differences) but the audit file will have all formulas populated.

Command Line Interface

Xval serves as the command line interface for running xact models. You can view all available commands in xval using the help option.

Terminal
> xval --help
info: Usage: xval [command] [options]

Commands:
  run             Run an xact file

General Options:
  -h, --help      Print command-specific usage

Run

You can run xact models from the command line using xval run. Use the help option to view all options.

Terminal
> xval run -h
info: Usage: xval run [path] [options]

Arguments:
  path                    Path to .xact file to run 

Options:
  -h, --help              Print this help and exit 
  --audit                 Include .xlsx audit file with output
  --input-dir [path]      Add path where input files may be found;
                              may be specified multiple times
Output Options:
  --write-all-vtables,    Write all vtables to output directory
      --wav     
  --write-all-inputs,     Write all inputs to output directory
      --wai
  --out-name [name]       Set name of the output file(s)
                              default is .xact file name
  --out-dir [path]        Set output directory to path; 
                              default is current working directory
  --out-ft [type]         Set the output file type
      xlsx                    (Default) Excel .xlsx format
      csv                     Comma separated values
        

For example, the following would put all output into csv files and include the inputs with the output.

Terminal
> xval run myfile.xact --wai --out-ft csv