Macro questions

169 views
Skip to first unread message

azrafe7

unread,
Jun 18, 2017, 7:53:54 PM6/18/17
to Haxe

Can you help me understand macros some more?

I’ve just started writing them (/trying-to actually), but quickly got lost and couldn’t find a way to make them work the way I intended to.

So far I’m mainly working with build macros, so my idea was to create a bunch of functions that would serve as building blocks for more complex work.

Here’s some of them:

1. create var field

  #if !macro macro #end
  static public function createFieldVar(name:String, value):Field {
    var field = {
      name: name,
      kind: FieldType.FVar(null, macro $v{value}),
      access: [AStatic, APublic],
      pos: Context.currentPos()
    }

    return field;
  }

From what I can gather, this only works the first time you call it. In the sense that - being non generic - once the compiler infers the type of value you cannot invoke it with a different type parameter.

Ex:

  // inside build macro
  Macros.createFieldVar("one", 1); // ok
  Macros.createFieldVar("two", "two"); // error

Here I thought explicitly typing the function param as value:Expr might work, but that’s not the case, and I don’t understand why.

2. get type from string repr

In the previous example I think the code makes the compiler infer the type when doing FVar(null, value), but sometimes it could be useful to have the type passed in as a function parameter (or better get it examining the value).

Is it possible to get a Type/ComplexType from a string representation of it?

Put it concisely:

  var macroType = macro :Array<Int>;
  var resolvedType = resolveFromString("Array<Int>");

  same(macroType == resolvedType); // true

3. extract fields from anon struct

I have something like this:

  #if !macro macro #end
  static public function extractFieldsFromAnon(anon:ExprOf<{}>):ExprOf<Array<{name:String, value:Expr}>> {
    var pairs = new Array<{name:String, value:Expr}>();

    switch (anon.expr) {
      case EObjectDecl(anonFields):
        for (f in anonFields) {
          switch (f.expr.expr) {
            case EConst(_), EArrayDecl(_):
            default:
              throw "only EConst and EArrayDecl";
          }
          pairs.push({name:f.field, value:f.expr});
        }

      default:
        throw "only EObjectDecl";
    }

    var res = {
      expr: macro $v{pairs},
      pos: Context.currentPos()
    }
    return macro $v{res};
  }

This really confuses me (and makes me think my assumptions are plain wrong! so I’d be glad to have someone explaining to me where and why I’m failing).


PS: code snippets posted have been copy-pasted from different attempts. They should be representative (is that the correct word?), but if they don’t please report back and I’ll come up with better ones.


PPS: I know there’s some useful libs around (notably tink and thx) that make working with macros very easy. Thing is: I feel I don’t know the macros system well enough to just start fiddling with those libs.
So if anyone can clarify these points (to start, as I have many more), I’d really appreciate! :smile:

Thanks

Juraj Kirchheim

unread,
Jun 19, 2017, 3:10:58 AM6/19/17
to haxe...@googlegroups.com
1. I don't really see why that wouldn't work. Can you give a complete minimal example that exhibits the problem you're facing?
2. Well, this would work:

  function parseComplexType(s:String) 
    return switch Context.parseInlineExpr('(_:$s)', (macro _).pos) {
      case macro (_:$ct): ct;
      default: throw "unreachable";
    }

The basic idea is to construct a String with an expression that contains the type (in this case ECheckType - but EVars would also work) and then extract the type from the parsed expressions.
3. Well, you're mixing expressions and values in this case, i.e. the pairs array has objects, where one field is a string constant and the other is an expression, so $v{pairs} will make the compiler convert the constant to an expression that evaluates to said constant and the expression to an expression that evaluates to the original expression (it's in a way like urlencoding a string twice):

  var pairs = new Array<Expr>();
  //then, in the loop:
  pairs.push(macro {name: $v{f.field}, value: ${f.expr} });
  //and in the end:
  return macro $a{pairs}

Simply put: no value that contains an expression should ever go into $v{} (unless you actually want an expression at runtime).

Best,
Juraj

azrafe7

unread,
Jun 19, 2017, 3:03:20 PM6/19/17
to Haxe

Awesome!

back2dos, thanks for the detailed answer!

Now I have 2. and 3. working following your directions.

Still can’t make 1. to work though, so I made the minimal example requested: http://try-haxe.mrcdk.com/#70306.

(I’ve also tried to type value as Expr, but that fails too, though it seemed the correct thing to do to me o.O)

azrafe7

unread,
Jun 19, 2017, 3:26:00 PM6/19/17
to Haxe

Oh, silly me! ;p

I can just parameterize it.
This works:

  #if !macro macro #end
  static public function makeVarField<T>(name:String, value:T):Field {
    var field = {
      name: name,
      kind: FieldType.FVar(null, macro $v{value}),
      access: [AStatic, APublic],
      pos: Context.currentPos()
    }

    return field;
  }

(is that the correct way to do it?)

azrafe7

unread,
Jun 21, 2017, 5:04:54 PM6/21/17
to Haxe

More macro troubles, hope you can help on this one too… (:

4. find anon field

I’m trying to write a macro that searches a nested anon for a specified field name (returning null if it’s not there).

I’ve written a non-macro version of it, and then tried to transform that into a macro, unsuccessfully.

Non-macro (using Reflection):

  static public function reflectFindAnonField(dotPath:String, object:{}):Null<{value:Dynamic}> {
    var parts = dotPath.split(".");
    var first = parts.shift();
    var res = null;

    if (!Reflect.isObject(object)) return null;

    var fields = Reflect.fields(object);
    for (fName in fields) {
      var value = Reflect.field(object, fName);
      if (fName == first) {
        if (parts.length == 0) {
          res = {value:value};
          break;
        } else {
          if (Reflect.isObject(value)) {
            return reflectFindAnonField(parts.join("."), value);
          }
        }
      }
    }

    return res;
  }

And this is my (incomplete/non-working) macro attempt:

  #if !macro macro #end
  static public function findAnonField(dotName:String, anon:Expr) {
    var parts = dotName.split(".");
    var first = parts.shift();
    var res = null;

    var t = Context.typeof(anon);
    switch (t) {
      case TAnonymous(_.get() => anonType):

        for (f in anonType.fields) {
          if (f.name == first) {
            if (parts.length == 0) {
              trace("found: " + f.name + "  " + f);
              res = f;
              break;
            } /*else if (f.type.match(TAnonymous(_))) {
              trace(f.type);
              return findAnonField(parts.join("."), macro $v{f});
            }*/
          }
        }
      default:
        throw "only TAnonymous, was " + anon.expr;
    }

    return macro null; // macro $v{res}??
  }

What am I misunderstanding there?

Should I use & switch on EObjectDecl instead of TAnonymous? How?

Side-question: is there a way to make the non-macro version type-safe (i.e. avoid Dynamic)?

Thanks again.

azrafe7

unread,
Jun 22, 2017, 6:37:42 PM6/22/17
to Haxe

Spent quite some time fiddling with my previous code, and maybe I’ve found a solution for 4..

Here’s what I came up with:

  #if !macro macro #end
  static public function findAnonField(dotPath:String, anon:ExprOf<{}>):Null<{field:String, expr:Expr}> {
    var parts = dotPath.split(".");
    var first = parts.shift();
    var res = null;

    switch (anon.expr) {
      case EObjectDecl(fields):
        for (f in fields) {
          if (f.field == first) {
            if (parts.length == 0) {
              res = f;
              break;
            } else {
              switch (f.expr.expr) {
                case EObjectDecl(innerAnon):
                  return findAnonField(parts.join("."), $v{f.expr});
                default:
                  throw "unreachable";
              }
            }
          }
        }
      default:
        throw "only EObjectDecl, was " + anon.expr;
    }

    return $v{res};
  }

Can you please comment on this?

Does it restrain the usage to build macros (or how can I change it to be used from expr macros)?

Juraj Kirchheim

unread,
Jun 23, 2017, 6:36:03 AM6/23/17
to haxe...@googlegroups.com
Hmm, I don't quite get where you're going with this. Maybe a few usage examples would help to clarify that.

Best,
Juraj

--
To post to this group haxe...@googlegroups.com
http://groups.google.com/group/haxelang?hl=en
---
You received this message because you are subscribed to the Google Groups "Haxe" group.
For more options, visit https://groups.google.com/d/optout.

azrafe7

unread,
Jul 4, 2017, 9:46:57 PM7/4/17
to Haxe

5.

Ok. Let me take a step back, and also explain what was my original intent…

@back2dos (and anyone wishing to contribute/help),
I was tempted to post my intentions at the beginning, but hoped to get there step-by-step (still possible, right?).

Anyway, my macro-related-learning-project can basicly be summed up with this:

  1. “Store” assets/resources at compile time (many have started with this, and you’ll probably agree that it’s very suitable for macros), via Context.addResource().
  2. have some utility functions to store/retrieve (like openfl/nme do f.e.) in popular ways (i.e.: build macro + .get("assetName"))
  3. also give the possibility to store/retrieve them in a custom way

Now 5.2 and 5.3 are my main target: write a build macro (along with a suite of utility macros), that give users the power of writing something like this:

// Assets.hx
import UserMacro;

@:build(UserMacro.build("assetsAnon", ["assetsDir1"], ["png,jpg"]))
@:build(UserMacro.build("assetsAnon", ["assetsDir2"], ["txt"]))
class Assets { } 

// UserMacro.hx
...
import az.Utils;

UserMacro {
  static public function build(varName:String, dirs:Array<String>, extFilter:Array<String>):Array<Field> {
    ...
    // create a `varName` field on local building class (if not exisiting)
    // find files in specified dirs (matching the filter or any other custom criteria)
    // add each file's content as a haxe resource
    // update the anon (varName) to match the filesys tree structure and point to the added resource
    ...
  }
...
}

Now, in my use case, the build macro is called twice, as I want multiple calls to do a merge on the same anon object (think of jQuery.extend).

To sum it up the goal is to end up with something like (might forget something but this is the gist of it):

UserMacro {
  static public var assetsAnon: {
    assetsDir1: {
      head: ref to haxe resource,
      button: ref to haxe resource,
      frame: ref to haxe resource,
      nestedDir: {
        cursor: ref to haxe resource,
      }
    },
    assetsDir2: {
      lorem: ref to haxe resource,
      introText: ref to haxe resource,
    }
  }
}

Hope you’re still with me at this point, and that what I’ve written makes some kind of sense. Does it? ;D

To make this work I’ll need some more missing pieces that have eluded me till now:

  1. find if/how I can transform an anon in place (both field names and values - which if I’m following correctly means changing both the Type and the Expr)
  2. a macro to merge anons recursively (I have a reflection-based solution working, but that would provide no completion - which is what I want from the start)

I’d guess 1 leads to 2, but my current code has some problems. This:

  //#if !macro macro #end
  macro static public function transform(anon:ExprOf<{}>):Expr {

    function _transform(e:Expr) {
      switch(e.expr) {
        case EConst(CString(s)):
          e.expr = EConst(CString(s + "_xformed"));
        case EObjectDecl(fields):
          for (f in fields) {
            f.field = f.field + "_xformed";
          }
          ExprTools.iter(e, _transform);
        case _:
          ExprTools.iter(e, _transform);
      }
    }

    //trace(ExprTools.toString(anon));
    _transform(anon);
    return anon;
  }

used like this

  var nested1 = { three: { inner:"deep", first:"fssfsfsf" }};
  var expr_xformed = Macro.transform({ three: { inner:"deep", first:"fssfsfsf" }}); // working as expr is inline
  var ident_xformed = Macro.transform(nested1); // not working

gets what I want for expr_xformed, but not for ident_formed (so a side-question is how to extract fields from a CIdent, but I’m starting to feel this rabbit hole is deeper than I thought :P).

Yeah, I know, quite a long post!
If you can help out I’d really appreciate it.

PS: if I haven't explained my intentions clearly enough please follow up, and I'll get back to you. Thanks in advance.

Cheers, azrafe7

azrafe7

unread,
Jul 4, 2017, 10:04:46 PM7/4/17
to Haxe

6.

PPS: Oh… and another great piece of advice from you would be how to properly use #if macro ... vs/with macro keyword. That would be really helpful!

Reply all
Reply to author
Forward
0 new messages