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
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:
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.
The Catala team does not recommend tweaking the files generated by the Catala compiler for two reasons:
- 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;
- 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
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.