Haxe advanced conditional compilation

Conditional Compilation (AKA preprocessor macros) in Haxe is pretty useful but limited to boolean values. We can make use of macros to use compiler flag of non-boolean type.

The traditional way

Usually we can define a flag mydebug in hxml:

-D mydebug

And use it in Haxe code in this way:

#if mydebug
trace("some debug trace")
#end

If we want to conditionally include browser support, we need to use a scheme of flags.

-D supportIE8 #define we support IE8 and above
#if (supportIE6 || supportIE7) //assume 6 is the min version
trace("some code for IE7");
#end

As shown above we need to specify every version before 7 for a piece of code that is for IE7.
It is problematic because for now we at most put #if (supportIE6 || supportIE7 || supportIE8 || supportIE9 || supportIE10 || supportIE11) for a piece of IE11-specific code. But we don’t know if IE will have version 99 and definitely we do not care IE users do not want to write #if (supportIE6 || supportIE7 || ... || supportIE99) for a piece of IE99-specific code.

The macros way

With the help of macros, we will be allowed to code something like:

-js bin/Test.js
-main Test
-cp src
--macro CompilationOption.set('supportIE', 8)
class Test 
{
    public static function main() 
    {
        if (CompilationOption.exists("supportIE") && 
            CompilationOption.get("supportIE") <= 7) {
            trace("some code for IE7");
        }
    }
}

The JavaScript output of the main function:

Test.main = function() {
    console.log("some code for IE7");
}

And here is my CompilationOption class:

#if macro
import haxe.macro.Context;
import haxe.macro.Expr;
#end

/**
* CompilationOption allows us to use compile-time variables for advanced conditional compilation.
*/
class CompilationOption {
    #if macro
    /**
    * Internal storage of the options.
    */
    static var storage = new Hash();
    #end

    /**
    * Set `key` to `value`.
    * 
    * For simplicity `value` can only be constant Bool/Int/Float/String or null.
    * Array and structures are also possible, check:
    * http://haxe.org/manual/macros#constant-arguments
    * 
    * Set `force` to true in order to override an option. 
    * But be careful overriding will influenced by compilation order which may be hard to predict.
    */
    @:overload(function (key:String, value:Bool, ?force:Bool = false):Void{})
    @:overload(function (key:String, value:Int, ?force:Bool = false):Void{})
    @:overload(function (key:String, value:Float, ?force:Bool = false):Void{})
    @:overload(function (key:String, value:String, ?force:Bool = false):Void{})
    @:macro static public function set(key:String, value:Void, ?force:Bool = false) {
        if (!force && storage.exists(key))
            throw key + " has already been set to " + storage.get(key);

        storage.set(key, value);

        return macro {}; //an empty block, which means nothing
    }

    /**
    * Return the option as a constant.
    */
    @:macro static public function get(key:String):Expr {
        return Context.makeExpr(storage.get(key), Context.currentPos());
    }

    /**
    * Tell if `key` was set.
    */
    @:macro static public function exists(key:String):ExprOf {
        return Context.makeExpr(storage.exists(key), Context.currentPos());
    }

    /**
    * Removes an option. Returns true if there was such option.
    */
    @:macro static public function remove(key:String):ExprOf {
        return Context.makeExpr(storage.remove(key), Context.currentPos());
    }

    /**
    * Dump the options as an object with keys as fields.
    * eg. trace(CompilationOption.dump());
    */
    @:macro static public function dump():ExprOf {
        var obj = {};
        for (key in storage.keys()) {
            Reflect.setField(obj, key, storage.get(key));
        }
        return Context.makeExpr(obj, Context.currentPos());
    }
}

Alternative

One alternative to the above is to have a macro that define all the supportIE7supportIE11 when supportIE7 is passed.
The source code can now be:

#if supportIE7
trace("some code for IE7");
#end

It is very similar to how the Haxe compiler handles Haxe version currently (haxe_208, haxe_209, haxe_210 are defined when using Haxe 2.10).

Improvement

Generally it is better to integrate the macro into specific class when needed, instead of using the CompilationOption class above. Because:

Firstly, the value type is unspecified. Although it is still typed after one specified a value, but user may have to guess or look it up before using it.

Secondly, the key(name) of an option is a String, and we don’t like “stringly typed code” because again people have to look up or remember.

The below API will be much better:

-js bin/Test.js
-main Test
-cp src
--macro SupportIE.equalsOrAbove(7)
class Test 
{
    public static function main() 
    {       
        if (SupportIE.minVersion() <= 7) {
            trace("some code for IE7");
        }
    }
}
comments powered by Disqus