The sic code generator

sic is a tool primarily for generating the boilerplate code that makes it possible to seamlessly interact with a CSL contract from another language. Currently it supports two target languages: Kotlin and TypeScript. Because Kotlin is a JVM-based language sic indirectly supports other such languages like Java. Likewise, TypeScript is a language that is compiled to JavaScript and is designed to be interoperable with JavaScript code, so sic indirectly supports JavaScript as well.

In addition to generating ergonomic and typesafe interfaces to CSL contracts, sic also supports compiling CSL declarations into different structured formats to be used by other applications. This aspect of sic is not the main focus of this documentation, but the functionality is described briefly at the end of this page (Core AST, Ontology, Signature).

Overview

When you have written a CSL contract and want to integrate it in a larger application you need some way of communicating with the system responsible for running the CSL contract. This can be done using the public API and one of the API clients, however, this means that you would have to take care of sending the right JSON-encoded data yourself, with no possibility of help from your language’s type checker.

sic generates mappings from the CSL data types to native data types in the target language, meaning that you can get help from the target language’s type checker and IDE support when building values of these types. Moreover, reports, events, and entrypoints in the contract are mapped to suitable constructs in the target language. Hence, instead of communicating directly using the Deon API, you can instead use specialized functions that use the generated native data types as input and output types, and which takes care of (de)serializing to the JSON format expected by Deon’s API. This makes it easier to work with the contract as you get the support that you would otherwise get when working in the target language. On top of that, it makes it dramatically less time-consuming to make changes to the CSL contract, as you will get the updated mappings for free by re-running sic, and any inconsistencies in the way you use the generated functions will be immediately caught by the target language’s typechecker.

Given a CSL contract, sic will generate the following components in the target language:

Data type definitions

Each data type in the contract will be mapped to a data type in the target language.

Report functions

For each report in the contract a corresponding function for invoking that report is created.

Event application functions

For each Event type in the contract a function for applying that event to a contract is created.

Contract instantiation functions

For each entrypoint in the CSL source a function for instantiating a contract from it is created.

Usage

sic is a command-line tool that works on Windows, MacOS, and Linux:

$ sic --help
sic <VERSION>

Usage: sic [-V|--version] [-n|--namespace NAMESPACE] [-t|--target ARG]
           [--stdlib PATH] [-w|--write] [-d|--destination PATH] [-m|--msgpack]
           FILES

Available options:
  -V,--version             Print version information
  -h,--help                Show this help text
  -n,--namespace NAMESPACE Namespace to put generated code in
  -t,--target ARG          Output target (default: Kotlin)
  --stdlib PATH            Use alternative CSL standard library
  -w,--write               Write generated source files to disk instead of just
                           printing them to stdout
  -d,--destination PATH    Root directory for generated source
                           files (default: "generated")
  -m,--msgpack             Use MessagePack serialization for serialized output
                           data.
  FILES                    Either: a list of .csl files to read, or: the string
                           "-" (a single dash), making sic read from stdin.

We shall use the contract “sic1.csl” for demonstration of how to use sic and for illustrating key points about the structure of the generated code:

type CustomerType
    | Regular Int
    | OneTime

type Address {
    street: String,
    number: Int,
    floor: Int
}

type Customer {
    name: String,
    age: Int,
    address: Address,
    customerType: CustomerType
}

type AddCustomer : Event {
    id : Int,
    customer : Customer
}

//  sum : List Int -> Int
val report sum = \ints -> foldl (\(x : Int) -> \y -> x + y) 0 ints

//  sumCustomerAge : List Customer -> Int
val report sumCustomerAge =
  \(customers : List Customer) ->
    sum (List::map (\(c : Customer) -> c.age) customers)

contract rec entrypoint shop = \total -> <*> a: AddCustomer
  where a.id = total then shop (total + 1)

Using Kotlin as the target language

The default target language of sic is Kotlin, and the default behaviour is to write the generated code to standard output. Thus, when we run the command

$ sic sic1.csl

it will print a bunch of Kotlin code to the terminal. If we pass the flag --write to sic it will write the code to disk:

$ sic --write contract.csl
Generating interface for sic1.csl
Wrote file generated/com/deondigital/api/contract/sic1/ContractDetails.kt
Wrote file generated/com/deondigital/api/contract/sic1/builtins.kt
Wrote file generated/com/deondigital/api/contract/sic1/sic1.kt
Wrote file generated/com/deondigital/api/contract/sic1/sic1.csl.kt
Wrote file generated/com/deondigital/api/contract/sic1/fromValue.kt
Wrote file generated/com/deondigital/api/contract/sic1/InstanceDispatcher.kt
Wrote file generated/com/deondigital/api/contract/sic1/ReportService.kt
Wrote file generated/com/deondigital/api/contract/sic1/ContractService.kt

We see here that one CSL contract is represented as Kotlin source files in the package com.deondigital.api.contract.sic1. The generated code will be put into the root directory generated/. Both the package name and the root directory can be changed with the flags --namespace (short: -n) and --destination (short: -d), respectively.

The file sic1.kt contains the interface and data definitions that enable us to interact with the CSL contract from Kotlin in a convenient manner. The file sic1.csl.kt is an embedding of the contract source in Kotlin. The remaining files contain code relevant for reporting (ReportService.kt), event application (ContractService.kt), and various internal marshalling/unmarshalling infrastructure. Moreover, the file ContractDetails.kt contains the class Sic1Module which implements the interface ContractDetails from the package com.deondigital:sic-preamble-core. This class is used to tie together all the generated contract-specific Kotlin code with the sic-compatible packages that implements the operations on specific backends (com.deondigital:sic-rest-operations, com.deondigital:sic-dbledger-operations). Thus, the generated code does not “know” about any ledger backend in and of itself – that is something that the application developer decides later by picking an implementation of the ContractOperations interface from com.deondigital:sic-preamble-core.

Data types

The file sic1.kt will contain, amongst many other things, the definitions of the following data types:

sealed class CustomerType : ToApiValue, ToPrettyString {
  companion object: FromApiValue<CustomerType> { /* ... */ }
  /* ... */
  data class Regular(val field0: Long) : CustomerType() {
    /* .. */
  }
  object OneTime : CustomerType() {
    /* .. */
  }
}
open class Address(
             val street: String,
             val number: Long,
             val floor: Long) : Record() {
  /* ... */
}
open class Customer(
             val name: String,
             val age: Long,
             val address: Address,
             val customerType: CustomerType) : Record() {
  /* ... */
}

We have left out a lot of details here, but the snippet demonstrates how a sum type in CSL is converted to a sealed class in CSL with a subclass for each constructor while a CSL record type is converted to an open class. The names of parameters of an open class match the names in the CSL record. Base types such as Int and String are represented by their native counterparts in Kotlin: kotlin.Long and kotlin.String.

Reports

The CSL reports are converted to functions in the target language with appropriate types. That is, the input and output types are mappings from the CSL type to the target language type as described in the above section.

In our generated Kotlin code, we find the following class:

open class ReportService(/* ... */) {

  fun sum(ints: List<Long>) : CompletableFuture<Long> = /* implementation */
  fun sumCustomerAge(customers: List<Customer>) : CompletableFuture<Long> = /* implementation */
}

The class com.deondigital.api.contract.sic1.ReportService declares two functions, one for each of the CSL reports in sic1.csl. Input and output types of the functions are mapped from the corresponding CSL types; note that the CSL List a type is mapped to Kotlin/Java’s List<T> type.

To get an instantiated report service, one must construct an instance of the ContractOperations class and project out the reports field of it. The following snippet illustrates how one could run a report up against the Deon REST API using this interface:

val apiClient = DeonAPIClient(API_URL) // Connect to the ledger
// Construct a 'RESTContractOperations' (from 'com.deondigital:sic-rest-operations')
// specialised to the code we've just generated. This uses the 'Sic1Module' class that
// implements the 'ContractDetails' interface.
val ops = RESTContractOperations(apiClient, Sic1Module())
val reportService = ops.reports
val s = reportService.sumCustomerAge(listOf(
    Customer("bob", 42, Address("Main st.", 1, 5), CustomerType.OneTime),
    Customer("alice", 30, Address("Main st.", 10, 2), CustomerType.Regular(1)))
).get() // == 72

Contract instantiation

Every entrypoint declaration in a CSL contract represents a possible instantiation point of a contract in the system. The generated Kotlin code for sic1.csl contains the following class:

class shop<EventApplyResult>(/* ..., */
             val total: kotlin.Long
) : ContractInstance<ContractService<EventApplyResult>, Event>(/* ... */) {
  companion object {
    fun <EventApplyResult> instantiate(/*...,*/ total: Int) =
      /* ... */
    fun <EventApplyResult> getInstance(ops: ContractOperations, contractId: ContractId) =
      /* ... */
  }
}

The shop contract entrypoint is the basis of a class with the same name that holds a static method instantiate. When used, the instantiate method gives an instance of shop that can be used to query the state of the running contract and to apply events. It also provides access to instantiation arguments for the contract. The class shop implements the interface ContractInstance from com.deondigital:sic-preamble-core, using some of the generated classes as concrete type paramters. This is used to tie the code generated here together with the machinery provided by the ledger-specific preamble packages in a (Kotlin) type-preserving way.

In order to make it possible to use any backend, the instantiate function takes a ContractOperations object that describes how contract state is being managed. Moreover, all instantiation functions take the same parameters as the CSL entrypoints (mapped to the Kotlin type), plus two additional optional parameters:

  1. MetaArgs that can be used to supply additional information about the contract that will be instantiated, such as which peers should be used in the Corda backend and which name will be given to the contract instance

  2. timeProvider to manage how event timestamps are set.

To instantiate the shop entrypoint using the REST backend, we use the RESTContractOperations implementation of ContractOperations:

val apiClient = DeonAPIClient(API_URL)
// Construct a ContractOperations for a REST backend and use the types in 'Sic1'
val ops = RESTContractOperations(apiClient, Sic1())
// Instantiate the contract 'shop'
val contract1 = shop.instantiate(ops, 42)
// Instantiate a new contract from 'shop'
val contract2 = shop.instantiate(ops, 11)
// Instantiate yet another contract from 'shop',
// but give the instance a custom name
val namedContract = shop.instantiate(ops, 47, MetaArgs(name = "shopContract47"))

Event application

Every subtype of Event in the contract gets mapped to a function that applies an event of that type to a running contract. Any fields that the event record might have is represented as a parameter to the event application function.

The classes created with .instantiate(...) exposes a field applyEvent that allows application of events as functions, i.e. AddCustomer(). Because the AddCustomer event record contains two fields in addition to the fields in Event, id : Int and customer : Customer, the Kotlin function accepts two parameters corresponding to the fields. The return type is parameterized like it was the case for contract instantiation from Kotlin.

The example snippet below uses the contract contract1 instantiated above:

// Now we can apply two 'AddCustomer' events on the contract:
contract1.applyEvent.AddCustomer(0, Customer("bob",
                                 42,
                                 Address("Main st.", 1, 5),
                                 CustomerType.OneTime));
contract1.applyEvent.AddCustomer(1, Customer("alice",
                                 30,
                                 Address("Main st.", 10, 2),
                                 CustomerType.Regular(1)));

Gradle plugin

The sic boilerplate generator comes with a Gradle plugin that makes it simple to integrate it into projects. To use it, the root gradle project will need the following additions:

  • build.gradle must include:

plugins {
  id 'com.deondigital.gradle-sic-plugin' version "<CSL_VERSION>" // replace CSL_VERSION with the current version
}

csl {
  destinationDir       = 'generated/sic/'
  cslDir               = 'src/main/csl/'
  sicNamespace         = project.group
}

The plugin provides the following tasks (use gradle tasks for an overview):

Task

Description

generateKotlinFromCSL

Generates Kotlin code with sic

compileCSL

Compiles your CSL code into a .cslpkg bundle which is used by the runtime

There are a number of additional internal tasks exposed by the plugin, but they should generally not be used in most applications.

If you need IDE support for working with generated classes, add the following to the relevant project. This will make it possible for, e.g., IntelliJ IDEA to show the generated code.

sourceSets {
  main.kotlin {
    srcDir 'generated/sic/kotlin' // matches csl.destinationDir + '/kotlin'
  }
}

Note that you will need to add dependencies on com.deondigital:sic-rest-operations.

Using TypeScript as the target language

To instruct sic to generate TypeScript mappings for the CSL contract, run it with the flag --target TypeScript:

$ sic --write --target TypeScript sic1.csl
Generating interface for sic1.csl
Wrote file generated/./sic1.ts
Wrote file generated/./sic1.csl.ts
Wrote file generated/./preamble.ts
Wrote file generated/./builtins.ts

We get three files: sic1.ts contains the data definitions and interfaces for our CSL contract, sic1.csl.ts contains the CSL source, and builtins.ts and preamble.ts contains definitions that are not tied to the particular contract.

The generated code supports commonjs and the following compiler targets and libraries:

Library

Target

es2017

es2018

es2020

es2017

es2018

es2020

We recommend the following compiler options:

{
  "compilerOptions": {
      "lib": [
          "es2020"
      ],
      "module": "commonjs",
      "declaration": true,
      "strict": true,
      "noUnusedLocals": false,
      "noUnusedParameters": true,
      "noImplicitReturns": true,
      "noFallthroughCasesInSwitch": true,
      "esModuleInterop": true,
      "outDir": "./dist"
  },
  "include": [
      "generated/**/*.ts",
      "test/**/*.ts"
  ],
  "module": "commonjs",
  "esModuleInterop": true,
  "target": "es2020",
  "noImplicitAny": true,
  "moduleResolution": "node",
  "sourceMap": true,
  "outDir": "./dist/",
  "baseUrl": "."
}

Data types

Amongst the definitions in the file sic1.ts are the mappings of the CSL types:

export type CustomerType
 = Regular
 | OneTime
export class Regular {
 constructor(field0 : number) { /* ... */ }
 /* ... */
}
export class OneTime {
 constructor() { /* ... */ }
}
export class Address extends Record {
 street : string
 _number : number
 floor : number
 constructor (street : string, _number : number, floor : number) { /* ... */ }
 /* ... */
}

export class Customer extends Record {
 name : string
 age : number
 address : Address
 customerType : CustomerType
 constructor (name : string, age : number, address : Address, customerType : CustomerType) {
   /* ... */
 }
 /* ... */
}

These definitions allow us to work in TypeScript directly with, e.g., a Customer class with the field name as a native string. Note that, because “number” is a reserved word in TypeScript, the field number in the CSL contract has been renamed to _number in the TypeScript embedding.

Reports

The generated TypeScript code contains the class:

// In sic1.ts
export class Reports extends builtins.Reports {
    constructor(client : p.DeclarationsApi) {
        super(client);
        this.declarationId = addDeclaration(client);
    }
    sum(ints : number[]) : Promise<number> {
        /* implementation */
    }
    sumCustomerAge(customers : Customer[]) : Promise<number> {
        /* implementation */
    }
}

Reports requires one parameter in its constructor, an implementation of the DeclarationsApi (from @deondigital/api-client), used for issuing the actual call to the API.

Prior to calling reports, a check is made to ensure the declaration is stored on the server. Otherwise it is added.

We can call a report with native TypeScript types analogously to the way we did it for Kotlin:

// 'deonApiClient' and 'contractId' defined elsewhere
const r = new Reports(deonApiClient.declarations)
const s = await r.sumCustomerAge([new Customer (
    "bob",
    42,
    new Address (
        "Main st.",
        1,
        5
      ),
    new OneTime()
  ), new Customer (
    "alice",
    30,
    new Address (
      "Main st.",
      10,
      5
    ),
    new Regular(1)
  )])

Contract instantiation

The generated TypeScript code for sic1.csl contains the following class for contract instantiation:

export class shop {
    private constructor(
        readonly contractId : string,
        private readonly client : p.DeonApi,
        readonly total : number
    ) {}

    static async instantiate(
        client: p.DeonApi,
        total: number,
        $args? : { peers? : p.ExternalObject[], name? : string }
    ) : Promise<Shop> {
        const declarationId = await addDeclaration(client.declarations);
        return await new Instantiate(client.contracts, declarationId).Shop(total, $args).then(cid => {
            return {
               contractId: cid,
               total: total,
               commands: (agent: string, timeProvider : () => Date = () => new Date()) => new Commands(client.contracts, cid, agent, timeProvider)
            }
        })
    }

    commands(agent: string, timeProvider? : () => Date){
        return new Commands(this.client.contracts, this.contractId, agent, timeProvider)
    }
}

This is similar to the Kotlin code: sic generates one class per entrypoint. The generated class is a representation of the instantiated contract, with instantiation parameters, contract id, and a commands function to apply events. Furthermore, it contains a static method instantiate that takes as parameters:

  1. A DeonApi client (from @deondigital/api-client) to do the declaration and instantiation call.

  2. Any parameters the contract expects (in this example, total)

  3. An optional $args for specifying peers and name of the contract instance.

// 'deonApiClient' defined elsewhere
// Instantiate 'shop'
const contract1 = await shop.instantiate(deonApiClient, 42);
// Instantiate a new contract from 'shop'
const contract2 = await shop.instantiate(deonApiClient, 11);
// Instantiate yet another contract from the 'shop',
// but give the instance a custom name
const namedContract = await shop.instantiate(deonApiClient, 47, { name : "shopContract47" });

Event application

sic has generated the following TypeScript class:

// In sic1.ts
export class Commands extends builtins.Commands {
    async AddCustomer(
        id : number,
        customer : Customer,
        $tag? : p.Tag) : Promise<p.Tag | void> {
        /* implementation */
    }
}

Again, the pattern is quite like it was for Kotlin: one function per event type and the functions take as parameters the fields of the event. Note that the function for applying the basic event type Event is contained in the super class builtins.Commands.

Access to the Commands object is through the .commands(...) field on the entrypoint interface (here: shop).

Supply the following parameters to the commands function:

  1. The originating agent for the event.

  2. An optional “time provider” that returns the timestamp to be used in the event. It can be left out, in which case the current time is used.

In this snippet we apply two AddCustomer events on a contract:

// 'deonApiClient', 'contract', and 'agent' defined elsewhere
const c = contract.commands(agent);
// Now we can apply two 'AddCustomer' events on the contract:
await c.AddCustomer(0, new Customer (
   "bob",
   42,
   new Address ("Main st.", 1, 5),
   new OneTime()
));
await c.AddCustomer(1, new Customer (
   "alice",
   30,
   new Address ("Main st.", 10, 5),
   new Regular(1)
));

The @deondigital/sic NPM package

The sic tool is distributed in the NPM package @deondigital/sic. It provides a handy way to install sic:

$ npx @deondigital/sic

This will download the latest version of sic and run it. If you want sic code generation as part of your build process, add @deondigital/sic as a project dependency and add a "generate" entry to the "scripts" section of package.json:

/* ... */
"scripts": {
  /* ... */
  "generate": "sic generate --target TypeScript --write mycslfile.csl"
},
"dependencies": {
  /* ... */
  "@deondigital/sic": <CSL_VERSION> // <-- replace this with the current version of the CSL platform
}

Now you can use the generate script in your project:

$ npm run generate

Note

The version number of the @deondigital/sic package follows that of the rest of Deon Digital CSL. Version xx.yy.zz of @deondigital/sic will download a version of sic that is compatible with version xx.yy.zz of the Deon Digital CSL platform.

Compile CSL code to a .cslpkg file

sic can compile a CSL file (or a deon-project) to a .cslpkg file which is used by the CSL runtime.

$ sic compile sic1.csl --output sic1.declaration.cslpkg
Generating interface for sic1.csl
No project file found
Wrote file generated/./sic1.declaration.cslpkg

Manually referencing the .cslpkg is only necessary in advanced use cases.

The .cslpkg file

The .cslpkg contains four distinct components that you can extract using the provided methods in the runtime library.

A representation of core AST

The .cslpkg file contains a structured core representation of a CSL project. This core representation is used by the evaluator.

Note that this core representation is the result of several steps of internal processing that among other things strips away type information.

A representation of type definitions

The Ontology of a contract is a representation of all types used in a contract. The Ontology component in the .cslpkg file includes the preamble and built-ins. The output is an array of ontology elements. Each element has the following form:

A representation of declaration signature

It is possible to get a representation of the types of all top-level definitions in a CSL file. This is called the Signature of the CSL declaration. The .cslpkg file contains the Signature of the input CSL declaration, including the signature for the the preamble and built-ins.

Projects with multiple CSL files

CSL contracts can be grouped into projects by defining a file called deon-project in a folder. This file contains a newline-separated list of relative or absolute paths to CSL files. Its presence in the folder foo means that the folder is a “project”, and that the CSL files should be loaded in the order specified in the deon-project file. For example, the following deon-project file specifies a project that contains the files sic1.csl, sic2.csl, and sic3.csl:

sic1.csl # comments are also supported
subfolder/sic2.csl
/absolute/folder/sic3.csl

Contracts that are part of a deon-project are typechecked in the context of all contracts that come before them in the project specification. Thus, sic1.csl may only refer to names declared in the same file or in the standard library, whereas anything declared in sic1.csl is in scope in sic2.csl, and anything in sic1.csl and sic2.csl is in scope in sic3.csl.

Using projects in sic

Given the project file myproject/deon-project:

sic1.csl
sic2.csl

Running the command:

$ sic generate --write --target Kotlin --namespace org.foo myproject

Will output the following:

Wrote file generated/org/foo/myproject/builtins.kt
Wrote file generated/org/foo/myproject/sic1.kt
Wrote file generated/org/foo/myproject/sic1.csl.kt
Wrote file generated/org/foo/myproject/sic2.kt
Wrote file generated/org/foo/myproject/sic2.csl.kt
Wrote file generated/org/foo/myproject/fromValue.kt
Wrote file generated/org/foo/myproject/InstanceDispatcher.kt
Wrote file generated/org/foo/myproject/ReportService.kt
Wrote file generated/org/foo/myproject/ContractService.kt
Wrote file generated/org/foo/myproject/ContractDetails.kt