Abstract inline set
Inlining an abstract function that modifies this is valid. This is used for example in haxe.EnumFlags.
However, this does not work when the variable is stored in a property. The setter will not be called, as the following example shows:
abstract X(Int) {
public function new(v:Int) { this = v; }
public inline function incr() { this++; }
}
class Test {
static var B(default,set) : X;
static function main() {
var a = new X(3);
a.incr();
trace(a); // 4, ok !
B = new X(3);
B.incr(); // no set_4 !
}
static function set_B(v:X) {
trace("SET:"+v);
return v;
}
}
This is pretty funky because B++ itself isn't legal, and that's what we get after the inlining. What do you actually expect this to generate?
If you remove the abstract wrap and use B++ if should correctly call the setter. I would expect it replaces the ++ call or any other assign done in the inlined method by corresponding set_B calls.
Let's forget about the inlining for now and look at the direct B++ case:
abstract X(Int) {
public function new(v:Int) {
this = v;
}
}
class Main {
static var B(default, set):X;
static function main() {
B = new X(3);
B++;
}
static function set_B(v:X) {
trace("SET:" + v);
return B = v;
}
}
This fails with X should be Int which makes sense because the compiler doesn't know what to do with the unop, because nothing here suggests that the abstract supports arithmetic operations. The var version also fails the same way, so this is not about the setter.
Now with your inlined incr function this becomes more complicated. We can think about this as if it was (cast B : Int)++, which in this form gives us an Invalid assign error. Let's say it didn't, which means that we now have a defined arithmetic meaning for the unop itself and allows us to think about the setter situation.
If B was an Int, the operation would generate Main.set_B(Main.B + 1), which is fine. However, with the abstract we're looking at an Int value from the arithmetic operation, while the setter wants something typed as the abstract X. Which means that we would have to break the abstraction and generate something akin to set_B(((B : Int) + 1) : X).
This suggests to me that your example should only work if the abstract has both from Int and to Int. It still doesn't, but that's another story...
That part sounds similar to https://github.com/HaxeFoundation/haxe/issues/11605#issuecomment-2026967963 ?
Well, @:op isn't involved here. The problem is that we currently don't re-type field access after inlining. I'm not even sure if our architecture allows this because access is generally determined during the initial typing, i.e. the expr to texpr step, whereas here we already have a texpr.
I understand this is tricky. OTOH we already allow setting the value of this in the abstract new. I guess then any abstract member function (inline or not) that assigns this should not be allowed to return something else then Void, then we would force a return this in the Impl version, and tag this member function with @:abstractAssign, so when calling it it would do value = AbsImpl.method(value)
(this is just a generalisation of what we do already for abstract new)