How to declare instantiation in Haxe macro function
Asked Answered
S

3

5

I want to create a macro that generates this code for me:

if (myEntity.get(Attack) == null) myEntity.add(new Attack());
if (myEntity.get(Confused) == null) myEntity.add(new Confused());
if (myEntity.get(Defend) == null) myEntity.add(new Defend());
if (myEntity.get(Offense) == null) myEntity.add(new Offense());

In code I'd like to declare/use it like this:

EntityMacroUtils.addComponents(myEntity, Attack, Confused, Defend, Offense);

The current macro function looks like this:

macro public static function addComponents(entity:ExprOf<Entity>, components:Array<ExprOf<Class<Component>>>):Expr
{
    var exprs:Array<Expr> = [];
    for (componentClass in components)
    {
        var instance = macro $e { new $componentClass() }; // problem is here
        var expr = macro if ($entity.get($componentClass) == null) $entity.add(instance);
        exprs.push(expr);
    }
    return macro $b{ exprs };
}

This macro function is incorrect, I get the error:

EntityMacroUtils.hx:17: characters 22-43 : Type not found : $componentClass

The problem is I don't know how to define new $componentClass(). How would I solve this?

I also want to avoid to have Type.createInstance in the output code.

Seibert answered 24/8, 2015 at 9:45 Comment(0)
I
5

One way to programmatically generate instantiation code is by using "old school" enums AST building (compatible Haxe 3.0.1+):

// new pack.age.TheClass()
return {
    expr:ENew({name:"TheClass", pack:["pack", "age"], params:[]}, []),
    pos:Context.currentPos()
};

An improved syntax using reification is possible:

// new pack.age.TheClass()
var typePath = { name:"TheClass", pack:["pack", "age"], params:[] };
return macro new $typePath();

Now, for a convenient "instantiation helper" function syntax, we need to do some contorsions to extract a type path from the expression we receive in the macro function:

// new Foo(), new pack.Bar(), new pack.age.Baz()
instantiate(Foo, pack.Bar, pack.age.Baz);

macro static function instantiate(list:Array<Expr>)
{
    var news = [for (what in list) {
        var tp = makeTypePath(what);
        macro new $tp();
    }];
    return macro $b{news};
}

#if macro
static function makeTypePath(of:Expr, ?path:Array<String>):TypePath 
{
    switch (of.expr)
    {
        case EConst(CIdent(name)):
            if (path != null) {
                path.unshift(name);
                name = path.pop();
            }
            else path = [];
            return { name:name, pack:path, params:[] };

        case EField(e, field):
            if (path == null) path = [field];
            else path.unshift(field);
            return makeTypePath(e, path);

        default:
            throw "nope";
    }
}
#end
Imidazole answered 24/8, 2015 at 20:6 Comment(4)
Thanks, Ill try that. Still trying to find my way with macros and expression reification. Sometimes it feels I understand how it works, until it doesn't :)Seibert
That's one of the only thing I couldn't figure to do with reification.Imidazole
Nice! You need to pass the full path to the macro right?Seibert
Yes this answer doesn't parse type parameters and doesn't resolve types not fully qualified - I believe this can be added.Imidazole
S
2

In case anyone is in need for answers, I got this Thanks to ousado on the Haxe IRC chat:

If you do it in macro alone you can do this:

var ct = macro : pack.age.SomeTypename;
var tp = switch ct { case TPath(tp):tp; case _: throw "nope"; }
var expr = macro new $tp();

..or, if you explicitly construct tp:

var tp = {sub:'SomeTypeName',params:[],pack:['pack','age'],name:"SomeModuleName"}

As you can see, the complex type path is explicitly given here.

Unfortunately, Haxe don't really have a concise syntax for types in expression positions. You can pass ( _ : TypeName ) to provide an expression that contains a ComplexType.

But if you want to pass a type as argument, you could do it like this:

import haxe.macro.Expr;
using haxe.macro.Tools;

class Thing {
    public function new(){}
}
class OtherThing {
    public function new(){}
}

class TMacroNew {

    macro static function instances( arr:Array<Expr> ) {

        var news = [for (e in arr) {
            var ct = switch e.expr { case EParenthesis({expr:ECheckType(_,ct)}):ct; case _: throw "nope"; };
            var tp = switch ct { case TPath(tp):tp; case _: throw "nope"; };
            macro new $tp();
        }];
        trace( (macro $b{news}).toString());
        return macro $b{news};
    }


    static function main(){
        instances( (_:Thing), (_:Thing), (_:OtherThing) );
    }
}

..if you want a list of types, you might want to go for a parameter list like ( _ : L< One,Two,Three> ).

Seibert answered 27/8, 2015 at 14:2 Comment(3)
After getting this information I realized I don't want a macro (if syntax is weirder) but Ill just write it out by hand.Seibert
Ha, so there is a reification way - I updated my response and even found a way to keep the syntax clean!Imidazole
I think the docs can be improved on this, I see the reification ways here haxe.org/manual/macro-reification-expression.html it only says function $name() { } from which you could guess the syntax exist.Seibert
B
1

The accepted answer is problematic because it breaks when type parameters are involved, or when support for non-nominal types should be included.

I updated the example using two alternatives for a more concise notation for the list of types, while still allowing syntax for actual types.

import haxe.macro.Expr;
using haxe.macro.Tools;

class Thing {
    public function new(){}
}
class OtherThing {
    public function new(){}
}

class TPThing<T>{
    public function new(){}
}

class TMacroNew {

    macro static function instances( e:Expr ) {
        var tps = switch e.expr { 
            case EParenthesis({expr:ECheckType(_,TPath({params:tps}))}):tps; 
            case ENew({params:tps},_):tps;
            case _: throw "not supported"; 
        }
        var type_paths = [ for (tp in tps) switch tp { 
            case TPType(TPath(tp)):tp; 
            case _: throw "not supported"; 
        }];
        var news = [for (tp in type_paths) macro new $tp()];
        trace( (macro $b{news}).toString());
        return macro $b{news};
    }


    static function main(){
        instances( (_:L<Thing,Thing,OtherThing,TPThing<Int>> ) );
        instances( new L<Thing,Thing,OtherThing,TPThing<Int>>()  );
    }
}

Edit: The L in L< ... > could be any valid type name. Its only purpose is allowing to write a comma-separated list of types in valid syntax. Since macro functions take expressions as arguments, we have to use an expression that allows/requires a type, like: ( _ :T ), new T(), var v:T, function(_:T):T {}.

Bootie answered 28/8, 2015 at 8:51 Comment(2)
Thanks for this addition! What is L exactly in this case?Seibert
L isn't used - you could use any conceivable valid name for a type. Alternatively, you could enforce the use of a specific name, perhaps something that's descriptive, e.g. new FOR_ALL<T1,T2,T3,...>().Bootie

© 2022 - 2024 — McMap. All rights reserved.