Help creating a macro to forward operator overloads [SOLVED I GUESS?]

Consider the following code

package;

@:transitive
abstract A(Int) from Int to Int {
    @:op(A + B)
    private function add (other: A): A {
        return (this: Int) + (other: Int);
    }
}

// here something like @:forwardOps(A) ?
abstract B(A) from A to A {
    
}

class Main {
	static function main() {
		var a: B = 3;
        var b: B = 6;
        var c: B = a + b; // error !!!
	}
}

How would I create a macro to automatically generate operator overloads for class B that call operator overloads of class A?
For example, to achieve this effect without a macro I would have to copy & paste the following over my entire codebase

abstract B(A) from A to A {
    @:op(A + B)
    private function add (other: B): B
        return (this: A) + (other: A);
}

The above code works, but this all just comes down to type casting.
So I’m wondering… is there a way to automate this process with a macro?

Ok i did it

// ForwardOpsMacro.hx

package;

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

class ForwardOpsMacro {
    macro static public function build (localTypePath: String, baseTypePath: String): Array<Field> {
        function assertAbstract (type: Type): Void {
            if (!type.match(TAbstract(_,_)))
                Context.error('type $type expected to be an Abstract', Context.currentPos());
        }

        function getAbstractRef (type: Type): Ref<AbstractType> {
            assertAbstract(type);
            return switch (type) {
                case TAbstract(ref, _):
                    ref;
                case _: throw "Error"; // should be an abstract type by now
            }
        }

        var localComplexType: ComplexType = {
            var localType: Type = Context.getType(localTypePath);
            assertAbstract(localType);
            Context.toComplexType(localType);
        }

        var baseType: Type = Context.getType(baseTypePath);
        var baseComplexType: ComplexType = {
            assertAbstract(baseType);
            Context.toComplexType(baseType);
        }
        var baseAbstractRef: Ref<AbstractType> = getAbstractRef(baseType);

        var fields = Context.getBuildFields();

        for (staticField in baseAbstractRef.get().impl.get().statics.get()) {
            if (!staticField.meta.has(":op"))
                continue;

            var newArgs = new Array<FunctionArg>();

            var type: Type = staticField.type;
            // Extract the actual type if the type is TLazy
            type = switch(type) {
                case TLazy(_ => _() => actualType):
                    actualType;
                case _: type;
            }

            var args = switch (type) {
                case TFun(__args, _):
                    __args;
                case _: throw "Error"; // static fields with @:op should always be functions
            }

            // skip the first argument - this
            for (i in 1 ... args.length) {
                var arg = args[i];
                var isBaseType: Bool = (getAbstractRef(arg.t).toString() == baseAbstractRef.toString());

                var newArgType: ComplexType = if (isBaseType) {
                    localComplexType;
                } else {
                    Context.toComplexType(arg.t);
                }

                newArgs.push({
                    name: arg.name,
                    opt: arg.opt,
                    type: newArgType
                    // value?
                    // meta?
                });
            }

            var name = staticField.name;
            var newFunc: Function = {
                args: newArgs,
                ret: localComplexType,
                expr: macro return this.$name(other)
                // params?
            }

            fields.push({
                name: staticField.name,
                doc: staticField.doc,
                access: [staticField.isPublic ? APublic : APrivate], // inherit access modifier
                kind: FFun(newFunc),
                pos: staticField.pos,
                meta: staticField.meta.get()
            });
        }

        return fields;
    }
}

Usage:

// Main.hx
package;

@:transitive
abstract A(Int) from Int to Int {
    @:op(A + B)
    public function add (other: A): A {
        return (this: Int) + (other: Int);
    }
    
    @:op(A - B)
    public function sub (other: A): A {
        return (this: Int) - (other: Int);
    }
}

@:transitive
@:build(ForwardOpsMacro.build("B", "A"))
abstract B(A) from A to A {}

@:build(ForwardOpsMacro.build("C", "B"))
abstract C(B) from B to B {}

class Main {
	static function main() {
        var a: C = 68;
        var b: C = 1;
        var c: C = a + b;
        trace(c);
	}
}

Well, as it turns out, it’s not that simple.
The build order is unspecified, so… that’s a problem.
But I’ll be updating the macro here GitHub - kvbc/haxe-macros: my haxe macros library

Yeah, neither macro should rely on the other having run. If C happens to be built before B, drill down all the way to A and get the data from there.

By the way, you can simplify a bit. Haxe already knows how to add two integers, so all you need to do is tell it it’s allowed to do so.

@:transitive
abstract A(Int) from Int to Int {
    @:op(A + B)
    private function add (other: A): A;
}

abstract B(A) from A to A {
    @:op(A + B)
    private function add (other: B): B;
}

Yup, “drilling” is exactly what I did in my updated version.

  • If B has been built before C ===> There’s no problem. B took from A before C could build and take from B, so B already has A. In the end C has both B and A
  • Otherwise, if C has been built before B ===> C takes from B, and since it knows B’s underlying type (A) and B has not yet been built - it takes from B’s underlying type (A)

Examples:

  • Build order: A, B, C (most desirable)
    • A < Int
    • B < A
    • C < B
  • Build order: D, C, B, A (insanity)
    • D < C — “drills” through its underlying types, since C has not yet been built
      • D < B
      • D < A
    • C < B
      • C < A
    • B < A
    • A < Int

I’ve built-in the visualization of this whole process in the updated macro in the github repository I’ve posted right above

Try it out online

Example compiler output for the build order of C, B, A
obraz