Macros

You are viewing an old version of this entry, click here to see latest version.

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 it 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.

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 has code that needs to be run but as code that creates an expression. It will also replace all $-prefixed identifiers by the corresponding variable.

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

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 #15089, modified 2012-07-16 18:14:54 by ncannasse