Compilation (experimental)


Compilation is an experimental feature used for instance in differential curves. The purpose of compilation is to produce an equivalent but more efficient version of a given function. For that, a C++ code is produced, compiled and linked on-the-fly with the running antescofo instance. Only two kinds of functions are compiled:

  • user-defined functions

  • and object's methods corresponding to functions.

Processes, including object's routines cannot be compiled.

Compilation can be quite effective. It can be also useless because the traduction of the Antescofo data representation into an equivalent C++ representation, which is necessary to call the compiled function, may have a certain cost that may cancel out the benefice of the compilation.

Compilation can be done implicitly by a differential curve. In this case the compilation is done when the score is loaded. The compilation of a function can also be done explicitly through a call to the function @compilation. This function takes only one argument which is the signature of the function to compile.

Signature

A signature is an antescofo value which represents the types associated to some entities in the language. These entities are functions and objects. This information is used during the compilation to produce the correct C++ code.

More precisely, a signature is a map that describe the type of the functions and methods that must be compiled. The keys in the map denote a function or an object or a field in an object, and the values specify the type assigned to the keys:

  • the type of a function is given by the type of its arguments and the type of the returned value, gathered in a tab value;

  • the type of an object is given by a signature that defines the type of (some) of its fields and the type of (some) of its methods. These informations are given in a map.

The methods that are compiled are the methods whose type is described in the signature of an object.

In the current version1, types are restricted to monormophic type. This severely restrict the compilation: we cannot compile polymorphic function. For instance, a function relying on @push_back cannot be compiled because @push_back is polymorphic: its first argument can be a tab or a nim.

There are no new entities introduced into the language to specify a type. Instead, we use a value that encodes this type. In the following, we describe these values.

Scalar Types

Scalar types are the types attributed to scalar values. They are encoded by a string or by the equivalent symbol:

Type Encoding Example of a value of this type
undef "undef" or "void" <undef>
bool "bool" or "boolean" <undef>
int "int" or "integer" or "long" 3
float "float" or "double" or "numeric" 3.1415
string "string" "abc"
exe "exe" or "exec" they are no constant of this kind2
proc "proc" ::my_proc
obj "obj" obj::my_obj

When several strings are given in these, they can be used interchangeably (they are aliases to refer to the same type). Instead of a string, one may use a symbol (i.e. the string without its quote), provided that the symbol has no user-defined evaluation rule attached.

The type of a tab

The type of a tab is specified by a tab with only one element: the representation of the type of the elements. Nota Bene that only homogeneous tab can be typed: there is no type corresponding to a tab containing elements of different types.

For instance, the type of a vector of floting point values is represented by TAB["float"] which can also be written ["float"] or simply [float].

The function @typecheck can be used to check if a value is of a given type. So we can check for instance the type of a nested tab of boolean:

          @typecheck([["bool"]], [ [true, false], [false, true], [true, true] ])

must returns true because [["bool"]] describe a tab whose elements are of type ["bool"], i.e., tab of booleans.

The type of a function

The type of a function is represented by a tab with two elements:

  • the first element is a tab that lists the type of the arguments (the empty tab if there is no argument),

  • the second element is the type of the returned value.

Beware that the first element of the signature is a tab, but this tab is used to collect the type of the arguments of the function, and do not denote a tab type.

Here are some examples. We use the classical notation arg_1 \times \dots \times arg_n \rightarrow ret to refer to a function that takes n arguments of type arg_i and returns a value of type ret:

  • int \times int \rightarrow float
    [[int, int], float]
  • () \rightarrow bool
    [[], bool]
  • bool \rightarrow bool
    [[bool], bool]
  • bool \times bool \rightarrow bool
    [[bool, bool], bool]
  • bool^n \times bool \rightarrow bool
    [ [ [bool] ], bool]    here bool^n in the signature denotes a tab of booleans. This type is specified with [bool] in the signature, which is the only argument [ [bool] ] in the signature.
  • float^n \times float^n \rightarrow float^n
    [[ [float], [float] ], bool]
  • float^n \times int \rightarrow float^n
    [[ [float], int ], bool]
  • float^n \times float^n \rightarrow float^n
    [[ [float], [float] ], bool]

Notice that the type of a tab is a tab with only one elements whilst the type of a function is a tab which has two elements. So there is no ambiguity.

The type of an obj

The type of an obj is a map whose keys are fields and methods of the obj. The type can be partial: it may describe only some fields and some methods, not all. By methods here we mean the function associated to the obj and defined by a @fun_def, not a routine.

Fields and methods are specified using a string representing their identifier. The value associated to the key is the type of the key. For instance:

          $sig := MAP { "$count" -> int,
                        "increase" -> [[int], int],
                        "decrease" -> [[int], int]
                      }

Notice that the key of the map are strings while the value associated to the key are strings or symbol or tab of such things.

The map refered by $sig can be used to type the object:

          @obj_def obj::counter()
          {
                   @local $count := 0

                   @fun_def increase($p = 1) {
                          $count += $p
                          return $count
                   }

                   @fun_def decrease($p = 1) {
                          $count -= $p
                          return $count
                   }
          }

The type of an object can be partial: only a subset of of the fields and of the methods can be described.

These signatures are used to type an object definition. They cannot be used to specify the type of a variable or the type of a function parameter or of its return value. If a variable, a function parameter or the return type of a function is an obj or a proc, such type are simply specified using "obj" or obj without further information (that is, we cannot specify which specific kind of object is refered). For instance, with the previous definition, we can have:

    @fun_def @f($o, $p) { return $o.increase($p) }

    _ := @compilation(MAP { @f -> [[obj, int], int],
                            obj::counter -> $sig })
````

but we cannot write


```antescofo
     @compilation(MAP { @f -> [[obj::counter, int], int],
````



#### The Predicate [@typecheck]

The predicate [@typecheck] can be used to check if the first argument is
a type compatible with the second argument. For instance:

```antescofo
          @typecheck("int", 3)      ; returns true
          @typecheck("int", true)   ; returns false
          @typecheck("zzz", true)   ; returns <undef> because "zzz" does not represent a type


Signatures

A signature is not a type: it is an environment which gives the types of some Antescofo entities: variables, functions and objects. It is represented also by a map, as for the type of an object, but the key are different. The key in a a signature can be

  • a string representing the name of the variable or the function or the object,

  • a function denoted by its @-identifier

  • an obj denoted by its identifier obj::xxx.

For example, with the previous definition of obj::counter, the following code

          @fun_def @f($o, $p) { return $o.increase($p) }

          $sig := MAP { "$count" -> "int",
              "increase" -> [["int"], "int"],
              "decrease" -> [["int"], "int"]
          }

          _ := @compilation(MAP { @f -> [["obj", "int"], "int"],
                                  obj::counter -> $sig })

can be used to compile the methods increase, decrease and the function @f.

Notice the type of @f: the first argument is an instance of obj::counter but this type does not exist. What exists, is a less precise type "obj" which is the type of an instance of an object, whatever it is. The type described by $sig is the type of the class obj::counter, not of an instance of this class.


Compilation

In order to be compiled all expressions involved in the function evaluation must be compiled. In particular, it means that all the functions and methods) invoked must be compiled (and the constraint applies recursively). This is why a signature usually lists not only the type of the function to be compiled but also the type of all involved functions.

When a function @f is compiled, the compiler looks for a compiled version of all invoked functions @g. If the compiled version does not exist, it looks in the signature to find the type of @g. If this type is not found, an error is declared and the compilation stops.

A compiled version of a function @g exists if it has been compiled by a previous call to @compilation or because it has been already compiled in the current call or because @g is a predefined function that comes with a predefined compiled version.

If the compilation is successful:

  • a function @f generates a function @f_compiled which is compiled

  • a method f in an object obj::O will be replaced by the compiled version.

This behavior is not homogeneous and is subject to change in the future.

Compilation's restrictions

We already mention that they are severe constraints restricting the set of compilable expressions:

  • The type of a function must be expressible in the above type system. This restrict functions to be monomorphic, or to compile a specific monomorphic version of the function.

  • Accessing a local variable in a function is not allowed. Reading a global variable is permitted. Reading and writing a field in a method is also permitted.

  • Uninitialized local variable may causes problem. They are compilable if the Antescofo compiler or the C++ compiler is able to determine the type of the value referred by the variable. This is not always possible. The workaround is to specify an initialization value.

  • The compilation of a function implies the existence of a compiled version for all functions invoked during the evaluation.

  • Most of predefined functions have no compiled counterpart and then cannot be invoked in a compiled function. Mathematical functions (@sin, {@floor], etc.) are a notable exception.

  • Calling a method on an object is not compilable, at the exception of method called on the same object (i.e. expression $THISOBJ.method(...) or its abbreviation .method(...).

  • Multidimensional tab access T[i, j, ...] is not implemented but can be rewritten T[i][j][...].

  • Labeled arguments in function call and default arguments value are not compilable.

  • Computed function calls (i.e. function application where the function is the value of a non-constant expression) are not compilable.

  • Expressions whose result is a partially applied function, are not compilable.

  • The forall expression on map is not implemented.

We are working to extend the set of compilable expressions. Please reports your specific needs in the Antescofo user's forum.

Compilation Errors

The compilation workflow proceed as follows:

  • The invocation of the @compilation function leads to the generation of a self-contained C++ file (this file requires relies only on the STL). The path of this file is /tmp/tmp.antescofo.compil.N_xxxx.cpp where N is an integer. These files are erased during the first instantiation of an antescofo object (in Max or PD).

  • This file is compiled and the trace of the compilation is accessible in /tmp/tmp.antescofo.compil.N_xxxx.cpp.log. Some errors are reported by the C++ compiler. When the C++ compilation fails, a message is written to the console. If the verbosity is greater than 1, the log of the compilation and the generated C++ file are opened in a text editor. This is only for internal debugging purposes: the generated code is not intended to be read by the Antescofo programmer.

  • The result of the compilation is a dynamically linkable library which is loaded on-the-fly in the Antescofo executable. This operation may fails, for instance if some external references are missing. In this case an error is emitted on the console.

  • When a compiled function/method is called, the application may fail. This is the case if wrong arguments (of a bad type) are provided. Very few checks are done at the interface of the Antescofo and the C++ world. These errors usually lead to a crash without further notice.

The compilation process is not stopped by an error on the compilation of a function, but tries to compile the other functions specified in the signature.



  1. the type system is subject to be enhanced in the future. 

  2. A value of type exe is created by process call or an actor instantiation, the launch of a group or a loop, etc. They are not denotable, meaning that there is no constant of this type in the language, even if such value can be created as the result of the evaluation of some expressions.