Project building and deployment

In the previous section, we defined a directory containing a Catala project with a clerk.toml configuration file that contained two main targets (us-tax-code and housing-benefits) that we aim to build and export them as source libraries in different languages.

Recap from previous section: clerk.toml configuration file and project hierarchy

clerk.toml configuration file of our mock project

[project]
include_dirs = [ "src/common",              # Which directories to include
                 "src/tax_code",            # when looking for Catala modules
                 "src/housing_benefits" ]   # and dependencies.
build_dir    = "_build"    # Defines where to output the generated compiled files.
target_dir   = "_target"   # Defines where to output the targets final files.

# Each [[target]] section describes a build target for the project

[[target]]
name     = "us-tax-code"                          # The name of the target
modules  = [ "Section_121", "Section_132", ... ]  # Modules components
tests    = [ "tests/test_income_tax.catala_en" ]  # Related test(s)
backends = [ "c", "java" ]                        # Output language backends

[[target]]
name     = "housing-benefits"
modules  = [ "Section_8, ... ]
tests    = [ "tests/test_housing_benefits.catala_en" ]
backends = [ "ocaml", "c", "java" ]

Project file hierarchy:

my-project/
│   clerk.toml
├───src/
│   ├───tax_code/
│   │   │   section_121.catala_en
│   │   │   section_132.catala_en
│   │   │   ...
│   │
│   ├───housing_benefits/
│   │   │   section_8.catala_en
│   │   │   ...
│   │
│   └───common/
│       │   prorata.catala_en
│       │   household.catala_en
│       │   ...
│
└───tests/
    │   test_income_tax.catala_en
    │   test_housing_benefits.catala_en

Building the project

Now that you have everything setup, you can build the project, which means compiling the Catala source code files into the different target programming languages. That is the job of the clerk build command:

Where to run clerk?

The Catala team advises you to always run clerk from the root directory of your project.

You can refer to subdirectories in your clerk commands but beware of referring to sibling directories (../bar) which can cause path resolution failures in the Catala tooling.

$ clerk build
┌─[RESULT]─
│ Build successful. The targets can be found in the following files:
│     [us-tax-code] → _targets/us-tax-code
│     [housing-benefits] → _targets/housing-benefits
└─

The output of the command shows you where to find the results. Each [[target]] section yields a subdirectory in the _targets/ directory, which the compilation artifacts inside. In our example, it could look like this:

_targets/
├───us-tax-code/
│   ├───c/
│   │   │   Section_121.c
│   │   │   Section_121.h
│   │   │   Section_121.o
│   │   │   ...
│   │
│   ├───java/
│   │   │   Section_121.java
│   │   │   Section_121.class
│   │   │   ...
│   housing-benefits/
│   │   ...

Deploying the generated code

Now that everything is properly built in the different backends, it is time to integrate them! The purpose of Catala is to provide ready-to-use source libraries in a target programming language; Catala does not make a whole user-facing app for you. Hence, you usually take what Catala builds an integrate it in another existing project.

From this point on, the deployment requires some manual labor as it depends on the specifics of your use cases. Basically, it is up to you to copy the artifacts in _targets to your other project, compile them and link them to your existing codebase.

For instance, if you want to integrate the Catala program as part of a Java application, you will have to copy over the generated Java source files from the _target/<target_name>/java/ directory to a sub-directory of your Java project, and update your pom.xml Maven configuration accordingly so that Maven can build the source files generated by Catala.

Can I tweak the generated files to fit my workflow?

The Catala team does not recommend tweaking the files generated by the Catala compiler for two reasons:

  1. every time you will update the source Catala files, the compiler will re-generate a new file ijn your target programming language that you will have to re-tweak by hand;
  2. even if you automate the tweaking, every tweaking of the generated file might introduce a difference in behavior with how the original source Catala file behaves with the Catala interpreter.

Indeed, the Catala compiler guarantees that the generated file in your target programming language will behaves exactly as the source Catala file run with the Catala interpreter. Any tweak to the generated file might break that guarantee, which is why you should not tweak the generated files.

To fit the generated files to your workflow, we recommend instead that you build "glue" code in your target programming language on top of the generated files. This "glue" code is likely to contain some utilities to convert your existing data structures into the data structures expected by the generated files, and back.

Calling the generated functions in the target programming languages

Let's illustrate with an example. Consider this very simple Catala program:

> Module SimpleTax

```catala
declaration scope IncomeTaxComputation:
  input income content money
  output income_tax content money

scope IncomeTaxComputation:
  definition income_tax equals income * 10%
```

With its Java-compiled version:

Generated 'SimpleTax.java' file

/* This file has been generated by the Catala compiler, do not edit! */

import catala.runtime.*;
import catala.runtime.exception.*;

public class Test {

    public static class IncomeTaxComputation implements CatalaValue {

        final CatalaMoney income_tax;

        IncomeTaxComputation (final CatalaMoney income_in) {
            final CatalaMoney income = income_in;
            final CatalaMoney
                incomeTax = income.multiply
                             (new CatalaDecimal(new CatalaInteger("1"),
                                                new CatalaInteger("5")));
            this.income_tax = incomeTax;
        }

        static class IncomeTaxComputationOut {
            final CatalaMoney income_tax;
            IncomeTaxComputationOut (final CatalaMoney income_tax) {
                this.income_tax = income_tax;
            }
        }

        IncomeTaxComputation (IncomeTaxComputationOut result) {
            this.income_tax = result.income_tax;
        }

        @Override
        public CatalaBool equalsTo(CatalaValue other) {
          if (other instanceof IncomeTaxComputation v) {
              return this.income_tax.equalsTo(v.income_tax);
          } else { return CatalaBool.FALSE; }
        }

        @Override
        public String toString() {
            return "income_tax = " + this.income_tax.toString();
        }
    }

}

If you inspect the generated file, you will notice that the Catala scopes will be translated as a Java class (and as functions in C or Python). Scope computations are done in the class constructor. Hence, to execute the scope, we need to instantiate this class and retrieve the result.

Moreover, for every backend, there exists a dedicated version of the Catala runtime. This component is necessary for the compilation and execution of the generated Catala programs. Runtimes will describe Catala types and data-structures, specific errors as well as an API to manipulate them from the targeted languages. The files for the runtime should be included in the _targets/<target-name>; you can also copy them over to your project and reference their types and functions from your app.

Putting this all together, here is for instance a simple Java program that executes our scope:

import catala.runtime.CatalaMoney;

class Main {
    public static void main(String[] args){
        CatalaMoney income_input = CatalaMoney.ofCents(50000*100);
        IncomeTaxComputation result = new IncomeTaxComputation(income_input);
        CatalaMoney tax_result = result.income_tax;
        System.out.println("Income tax: " + tax_result);
    }
}

As mentioned, Catala runtimes offer an API to build the catala-specific values, e.g., the CatalaMoney.ofCents java static method that build a CatalaMoney value equivalent to a money-type value. Only sky is the limit afterwards as to what you can build!

In this section, we have seen how to build a project, export it and integrate it in an existing application. In the following section, we will dive into Catala's tests and setting up continuous integration.