Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to pass an Array of Expr (expressions) to Haxe Macro?

I'm basically trying to create a macro that automatically generates an if/else if chain by supplying one common outcome statement for all conditions.

This is what I've tried so far (modified the code just to show as an example):

import haxe.macro.Expr;

class LazyUtils {

    public macro static function tryUntilFalse( xBool:Expr, xConds:Array<Expr> ) {
        var con1, con2, con3, con4, con5;

        /*
         * Here's a switch to handle specific # of conditions, because for-loops
         * don't seem to be allowed here (at least in the ways I've tried so far).
         * 
         * If you know how to use for-loop for this, PLEASE do tell!
         */
        switch(xConds.length) {
            case 1: {
                con1 = conds[0];
                return macro {
                    if (!$con1) $xBool;
                }
            }
            case 2: {
                con1 = conds[0];
                con2 = conds[1];
                return macro {
                    if (!$con1) $xBool;
                    else if (!$con2) $xBool;
                }
            }
            case 3: {
                con1 = conds[0];
                con2 = conds[1];
                con3 = conds[2];
                return macro {
                    if (!$con1) $xBool;
                    else if (!$con2) $xBool;
                    else if (!$con3) $xBool;
                }
            }
            // ... so on and so forth
        }

        return macro { trace("Unhandled length of conditions :("); };
    }
}

Then, in theory it could be used like this:

class Main {
    static function main() {
        var isOK = true;
        LazyUtils.tryUntilFalse( isOK = false, [
            doSomething(),
            doSomethingElse(), //Returns false, so should stop here.
            doFinalThing()
        ]);
    }

    static function doSomething():Bool {
        // ???
        return true;
    }

    static function doSomethingElse():Bool {
        // ???
        return false;
    }

    static function doFinalThing():Bool {
        return true;
    }
}

Which should generate this condition tree:

if (!doSomething()) isOK = false;
else if (!doSomethingElse()) isOK = false;
else if (!doFinalThing()) isOK = false;

Alternatively, I suppose it could output this instead:

if(!doSomething() || !doSomethingElse() || !doFinalThing()) isOK = false;

Looking back at this now, true - it may not make much sense to write a whole macro to generate code that would be easier to type out in it's raw format.

But for the sake of learning about macros, does anyone know if multiple expressions can be passed in an Array<Expr> like I tried in the above code sample?

like image 827
chamberlainpi Avatar asked Apr 01 '16 17:04

chamberlainpi


1 Answers

You probably couldn't get the xConds argument to behave like you expected because the final argument of an expression macro with the type Array<Expr> is implicitly a rest argument. That means you ended up with an array that contained a single EArrayDecl expression. This can be fixed by simply omitting the [].

Regarding generating the if-else-chain - let's take a look at EIf:

/**
    An `if(econd) eif` or `if(econd) eif else eelse` expression.
**/
EIf( econd : Expr, eif : Expr, eelse : Null<Expr> );

The chain can be thought of as a singly linked list - the eelse if the first EIf should reference the next EIf and so forth, until we stop with eelse = null for the last EIf. So we want to generate this for your example (pseudo-code):

EIf(doSomething(), isOk = false, /* else */
    EIf(doSomethingElse, isOk = false, /* else */
        EIf(doFinalThing(), isOk = false, null)
    )
)

Recursion works well for this.

Typically it's more convenient to work with reification than raw expressions like I do here, but I'm not sure the former is really possible when dynamically generating expressions like this.

import haxe.macro.Context;
import haxe.macro.Expr;

class LazyUtils {

    public macro static function tryUntilFalse(setBool:Expr, conditions:Array<Expr>):Expr {
        return generateIfChain(setBool, conditions);
    }

    private static function generateIfChain(eif:Expr, conditions:Array<Expr>):Expr {
        // get the next condition
        var condition = conditions.shift();
        if (condition == null) {
            return null; // no more conditions
        }

        // recurse deeper to generate the next if
        var nextIf = generateIfChain(eif, conditions);
        return {
            expr: EIf(condition, eif, nextIf),
            pos: Context.currentPos()
        };
    }
}

And Main.hx (mostly unchanged):

class Main {

    static function main() {
        var isOK = true;
        LazyUtils.tryUntilFalse(isOK = false,
            !doSomething(),
            !doSomethingElse(), //Returns false, so should stop here.
            !doFinalThing()
        );
    }

    static function doSomething():Bool {
        trace("doSomething");
        return true;
    }

    static function doSomethingElse():Bool {
        trace("doSomethingElse");
        return false;
    }

    static function doFinalThing():Bool {
        trace("doFinalThing");
        return true;
    }
}

To keep things simple I inverted the function call arguments with ! at the call site instead of handling that in the macro.

You can use -D dump=pretty to generate AST dumps and check what code is being generated. Here's the result:

if ((! Main.doSomething()))isOK = false else if ((! Main.doSomethingElse()))isOK = false else if ((! Main.doFinalThing()))isOK = false;
like image 183
Gama11 Avatar answered Oct 16 '22 14:10

Gama11