Multi-pass Macros/Macro order?

I’ve just got into using macros to try to get this one weird bit of functionality working, and I think I’ve almost gotten what I want. What I’m trying to do is build a class at compile time that combines a set of input classes, so for example, these two classes:

class TestClass
{
    public var floatField:Float;
    public var intField:Int;
}

class TestClass2
{
    public var floatField:Float;

    // this would compile to an error as the types don't match 
    // public var intField:String;

    public var stringField:String;
}

would become:

class CombinedClass
{
    public var floatField:Float;
    public var intField:Int;
    public var stringField:String;
}

What I want to use this for looks like this:

var combinedObj:CombinedClass;

var testObj:TestClass = cast combinedObj;
testObj.intField = 4; //type safe!

trace(combinedObj.intField); //4

I currently have this implemented using an autoBuild macro that adds all of the class fields to a static list, and then at some point generateClass is called that puts it all together and defines the type. I’ve put the macro source here.

However, the execution order of the macros is obviously really important, and I’ve found that I can guarantee the macro order like this:

TestClass2;
TestClass;
TestMacro.generateClass();

But that’s not so ideal. Having to state the classes used in a specific place undermines the benefits of having this API. Reading this, it states quite ambiguously that the order of build macros specifically is undefined, and that multi-pass macros should happen in the macro source. This left me under the impression that expression macros would be delayed until after the build macros are all run, but in testing I’ve found that this isn’t true.

My question is, essentially, how do I get the right behavior? If I could call macro.context.defineType multiple times and overwrite the class in each call to the build macro, that would do it. If I could delay a macro execution until after all of the build macros, that would also do it. If I could access the list of class fields at runtime, I could build anonymous structures instead, and that would work in the same way. The reason I feel like this should be possible is that the API will not be exposing the combined class to the user, so any solution that doesn’t require the user to jump through hoops would be enough.

Thanks in advance!

If you don’t define the type elsewhere, the compiler will call the callback(s) set using onTypeNotFound(). There’s a small chance that this call will happen after all the other macros are done.

See getInstanceFields().

I didn’t think of that actually. In testing however, it is still run in place as if I had called a macro where the type is not found.

Also, after some more testing, I found that unsafe casts don’t work at all on static targets, so really all of these approaches were doomed to fail. I think I’m just gonna redesign what I want to do entirely.

Thanks for the help.

Typedefs don’t help you automatically generate the class, but otherwise they might be what you want.

Interfaces would help, but then you have to list everything in one place. You could build the class using something like this, but I didn’t test it because try.haxe.org doesn’t allow macros.

class CombinedClassBuilder {
    public static macro function build():Array<Field> {
        var fields:Array<Field> = Context.getBuildFields();
        
        for(i in Context.getLocalClass().get().interfaces) {
            for(interfaceField in i.t.get().fields.get()) {
                var alreadyExists:Bool = false;
                for(f in fields) {
                    if(interfaceField.name == f.name) {
                        alreadyExists = true;
                        break;
                    }
                }
                if(!alreadyExists) {
                    fields.push(interfaceField);
                }
            }
        }
        return fields;
    }
}