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?
// 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);
}
}
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