Compiler Directives

As the “compile-time” language Ante naturally needs a mechanism for interacting with the compiler and other compile-time constructs. Of course, you can use the ante modifier or any of the builtin compile-time functions in the Ante module but to properly leverage the full power of Ante it is necessary to learn about compiler directives.

In Ante, compiler directives are essentially user-defined modifiers. Just like modifiers, compiler-directives can be used to modify variable declarations, function declarations, or even arbitrary expressions. The semantics of this modification of course depends on the the modifier itself.

A compiler directive is in the form ![name arg1 arg2 ... argn]. As you have probably noticed from this, compiler directives, like functions, can take arguments. Like ante functions, these arguments must all be compile-time constants. There also exists a shorthand for compiler directives that take no arguments, !name.

You may be thinking they sound very similar to functions, and you are right. If we really wanted to, we could define a compiler directive that adds two numbers during compile-time:

![add 1 2]  //=> 3

Although such a function would be better performed by an ante function.

let add = ante (+)

add 1 2  //=> 3

Generally, ante functions should be preferred when possible. The main advantage compiler directives have over ante functions is their ability to precede function/variable/type declarations as modifiers.


As Modifiers

Modifiers can appear before and modify any declaration (function, variable, or type), any type, or any arbitrary expression. Naturally, because compiler directives are modifiers, they can also be used in these places as well.

The most common location for compiler directives is before declarations:

!inline
fun id: x = x

!packed
type T = bool a b, i32 i

!pure
![prereq (x > 0)]
fun fact: Int x =
    if x = 1 then 1
    else x * fact (x-1)

They can also appear before types and arbitrary expressions however:

fun reverse: !gc List l =
    foldl cons empty l


//Like ante functions, compiler directives can modify parse trees:
!class !nametypedecl
type Person =
    id: !unique Nat
    tasks: !gc List Str

    fun doWork: self
        match self.tasks with
        | t::ts -> print "Doing ${pop self.tasks}"
        | [] -> print "Nothing to do."

    fun get_name: self =
        "Bob. Probably."


let ntc = ![reinterpret_as Arr u8] Person(new 3, ["Work"])

//In most cases, compiler directives on arbitrary expressions
//could be better represented with a function.
let ntc = Person(new 3, ["Work"]).reinterpret_as Arr u8

Creating New Compiler Directives

To create your own compiler directive, tag a function with !compiler_directive. As with functions, the parameters of a compiler directive determine what it can be used with. If the newly created compiler directive should be used with function declarations its first parameter should be a FuncDecl, likewise if it is to be used with a type declaration or a variable declaration its first parameter should be a TypeDecl or VarDecl respectively. If it can operate on any declaration, it should take a Decl.

!compiler_directive
fun test: FuncDecl fd, Args a, 't val
    if fd a != val then
        Ante.error "${fd.name} ${a} != ${val}"


![test 1 1]
![test 5 120]
fun fact: i32 x =
    if x = 1 then 1
    else x * fact (x-1)

The following example implements the function typeof on any type that includes !typeid in its definition.

//map types to their type ids
mut type_tbl = Vec Type

!compiler_directive
fun typeid: mut TypeDecl td
    //inject a hidden field with a default value of len type_tbl
    td.inject_hidden "u64 type_id" default:(len type_tbl)
    type_tbl.push td

    td.define "typeof" (fun self =
        type_tbl#td.get_hidden "type_id")


!typeid type Person = Str name job, u8 age

!typeid type Animal = Str name species, u8 age


let p = Person("John", "Programmer", 32)
let a = Animal("Spot", "Dog", 3)
do_thing p  //=> "Do whatever it is Programmers do!"
do_thing a  //=> "Go fetch!"

fun do_thing: 't val
    match typeof val with
    | Person -> "Do whatever it is ${val.job}s do"
    | Animal where val.species = "Dog" -> "Go fetch!"
    | _ -> "Wait, O unknown value"

Builtin Compiler Directives


!inline

Inlines a function at its callsite whenever possible.

!inline
fun add: i32 a b = a + b

!inline
fun print: Str s
    puts s.cStr

!compiler_directive

Creates a new compiler directive with the given function as its implementation.

!compiler_directive
fun debug: 't v = print v

!debug
!compiler_directive
fun test: FuncDecl fd, Args a
    print "test <| ${fd} ${a} = " (fd a)

!implicit

If used on a cast function, marks the function as an implicit cast. If used on a type, enables implicit casts to that type.

type Vec 't = ...
type Arr 't = ...

ext Arr 't
    !implicit fun init: Vec 't v = ...

fun print_arr: !implicit Arr arr
    print "Array" arr "of length" arr.len

let a = Arr[1, 5, 9]
let v = Vec[1, 3, 7]

print_arr a  //Arr is already an Arr
print_arr v  //Vec is converted to Arr

!lazy

Marks a type as lazy. This type is guarenteed not to evaluate its value until forced into a non-lazy type. Furthermore, the value is never double-evaluated.

type ThingNum = One | Two | Three

fun do_thing: ThingNum thing_num, !lazy 't thing1 thing2 thing3 -> 't
    match thing_num with
    | 1 -> thing1
    | 2 -> thing2
    | 3 -> thing3

let t1 = !lazy print "one"

//only print "two"
do_thing Two t1 (print "two") (print "three")

!noinit

Declares a variable without an initialization. Variables declared with noinit will issue an error if they are used in an expression when the compiler cannot guarentee that they have been initialized. Noinit vars are usually used paired with c functions that expect to initialize the values themselves.

type Mpz = void*

//Declare mpz_init to accept possibly uninitialized values
fun mpz_init: !noinit mut Mpz out;

//Create a wrapper around the c-style initialization
fun Mpz.init: -> Mpz
    !noinit mut Mpz t
    mpz_init t
    t

!raw

Explicitly declares an unmanaged pointer. If the GC or Ownership modules are imported this variable or type will remain manually managed.

fun alloc_int: Int i -> !raw Int*
    new i

let !raw mem = malloc 8
let int = alloc_int 10

free mem
free int