Macros

Some languages features such as C #define enable the user to define syntax shortcuts. They are useful for performing some pseudo-code-generation, but at the same time they allow you to modify the syntax of the language, making the code unreadable for other developers.

The Haxe macro system allows powerful compile-time code-generation without modifying the Haxe syntax.

Macro functions

The principle of a macro is that it is executed at compile time and instead of returning a value it will return some pieces of Haxe code that will be compiled.

A function can be defined as a macro function by using the @:macro Metadata.

This is a macro example that will compile the build date. Please note that since it's a macro, it is run at compilation time, which means it will always give the date at which the compilation was made and not the "current date" at which the program is run.

import haxe.macro.Context;
class Test {
    @:macro public static function getBuildDate() {
        var date = Date.now().toString();
        return Context.makeExpr(date, Context.currentPos());
    }
    static function main() {
        trace(getBuildDate());
    }
}

Since each macro function must return an expression which corresponds to the block of code that will replace the macro call, it is necessary to be able to convert the String value stored in the date variable into the corresponding string-expression. This is done by Context.makeExpr.

Since each expression needs also a position which will tell at which file/line it is declared (for error reporting and debugging purposes), we will use in this example the Context.currentPos() position which is the position where the getBuildDate() macro call has been made.

You cannot have platform specific imports in a file that has @:macro, unless you use some #if !macro ... #end wrappers around them. So put your macros in a specific macro class and do not try to create them next to your regular code unless you sure your not using platform specific code which I guess is quite rare, so simpler to always keep them separate.

Macro Reification

You can of course do much more than converting a simple value to an expression. You can actually generate and manipulate expressions, by using the macro reification :

import haxe.macro.Expr;
class Test {
    @:macro public static function repeat(cond:Expr,e:Expr) : Expr {
        return macro while( $cond ) trace($e);
    }
    static function main() {
        var x = 0;
        repeat(x < 10, x++);
    }
}

This macro will generate the same code as if the user has written while( x < 10 ) trace(x++).

A few explanations :

  • the macro repeat takes expressions as arguments, you can then pass it any expression before it is even typed. This expression has to be valid Haxe syntax but it can still reference unknown variables/types etc.
  • the macro can then manipulate these expressions (see below) and reuse them by generating some wrapping code.
  • the macro keyword will treat the following expression not as code that needs to be run but as code that creates an expression. It will also replace all $-prefixed identifiers by the corresponding variable.

Reification Escaping

new in haxe 3.0

When you use macro reification, you still want to inject some values into the written expressions, this can be done in several ways :

  • Using ${value}, will replace the expression at this place by the corresponding value. The value needs to be an actual haxe.macro.Expr
        var v = macro "Hello";
        var e = macro ${v}.toLowerCase();
        // is the same as :
        var e = macro "Hello".toLowerCase();
  • In some case where the value can only be an identifier and not an expression, $ident' is replaced by the value of the identifier :

With var:

    var myVar = "i";
    var e = macro var $myVar = 0;
    // is the same as :
    var e = macro var i = 0;

With field :

    var myField = "f";
    var e = macro o.$myField;
    // is the same as :
    var e = macro o.f;

With object fields :

    var myField = "f";
    var e = macro { $myField : 0 };
    // is the same as :
    var e = macro { f : 0 };

  • Using $i{ident}, will create an identifier which value is ident
        var varName = "myVar";
        var e = macro $i{varName}++;
        // is the same as :
        var e = macro myVar++;
  • Using $v{value} will tranform the value into the corresponding expression, in a similar way to Context.makeExpr :
        var myStr = "some string";
        var e = macro $v{myStr};
        // is the same as :
        var e = macro "some string";

    And for a more complex case :
        var o = { x : 5 * 20 };
        var e = macro $v{o};
        // is the same as :
        var e = macro { x : 100 };
  • Using $a{exprs}, will substitute the expression Array in a {..} block, a [ ... ] constant array or a call parameters :
        var args = [macro "sub", macro 3];
        var e = macro "Hello".toLowerCase($a{args});
        // is the same as :
        var e = macro "Hello".toLowerCase("sub",3);

Creating Expressions

As we see in the two last examples, we have several ways of creating expressions :

  • using Context.makeExpr to convert a value into the corresponding expression, that - when run - will produce the same value
  • using macro to convert some Haxe code into an expression
  • expressions can also be created "by-hand", since they are just plain Haxe enums :

// manual creation by using enums :
var e : Expr = {
    expr : EConst(CString("Hello World")), 
    pos : Context.currentPos()
};
// is actually the same as :
var e : Expr = macro "Hello World !";

Manipulating expressions

Since expressions are just a small structure with a position and an enum, you can easily match them.

For instance the following example make sure that the expression passed as argument is a constant String, and generates a string constant based on the file content :

import haxe.macro.Expr;
import haxe.macro.Context;
class Test {
    @:macro static function getFileContent( fileName : Expr ) {
        var fileStr = null;
        switch( fileName.expr ) {
        case EConst(c):
            switch( c ) {
            case CString(s): fileStr = s;
            default:
            }
        default:
        };
        if( fileStr == null )
            Context.error("Constant string expected",fileName.pos);
        return Context.makeExpr(sys.io.File.getContent(fileStr),fileName.pos);
    }
    static function main() {
        trace(getFileContent("myFile.txt"));
    }
}

Please note that since macro execute at compile-time, the following example will not work :

var file = "myFile.txt";
getFileContent(file);

Because it that case the macro fileName argument expression will be the identifier file, and there is no way to know its value without actually running the code, which is not possible since the code might use some platform-specific API that the macro compiler cannot emulate.

Constant arguments

The above example can be greatly simplified by telling that your macro only accept constant strings :

@:macro static function getFileContent( fileName : String ) {
    var content = sys.io.File.getContent(fileName);
    return Context.makeExpr(content,Context.currentPos());
}

Again - same as above - you will have to pass a constant expression, it cannot be a value of type String.

The following types are supported for constant arguments :

  • Int, Bool, String, Float
  • arrays of constants
  • structures of constants
  • null value

Context API and macro Context

The Context class gives you access to a lot of informations, such as compilation parameters, but also the ability to load type or even create new classes.

The Context API operates on the "application context", which is the unit in which the compilation of your code occurs. There is another context which is the "macro context" which compiles and run the macro code. Please note that these two contexts are separated.

For instance, if you compile a Javascript file :

  • the application context will contain your Haxe/Javascript code, it will have access to the Javascript API
  • the macro context will contain your macro code (the classes in which @:macro methods are declared) and it will not be able to access the Javascript API. It can however access the Sys API, the sys package and the neko API as well. It can still interact and modify the application context by using the Context class.

It is important to understand that some code sometimes get compiled twice : once inside the application context and once inside the macro context.

In general, it is recommended to completely separate your macro code (classes contaiting @:macro statements) from your application code, in order to prevent expected issues due to some code being included in the wrong context.

Type Reification

It is possible to use macro : Type to build a type instead of an expression.

Type reification also support escaping (see below).

The following example will declare a new typed variable when called :

import haxe.macro.Expr;
class Test {
    @:macro static function decl( vname : String ) {
        var str : ComplexType = macro : String;
        var arr : ComplexType = macro : Array<Array<$str>>;
        return macro var $vname : $arr = [];
    }
    #if !macro
    static function main() {
        decl("table");
        trace(table);
    }
    #end
}

Please note that in that case we need to make sure that the main() code is not compiled as part of the macro context.

Building types

You can also generate and manipulate types declarations content with macros, see Building Types with Macros

Advanced Features

Read on to know if you want to learn every bit about Haxe macro possibilities : Advanced Macro Features

version #19729, modified 2013-09-06 14:32:18 by caribou