haxe icon indicating copy to clipboard operation
haxe copied to clipboard

Abstract inline set

Open ncannasse opened this issue 3 weeks ago • 7 comments

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;
  }
  
}

ncannasse avatar Dec 03 '25 15:12 ncannasse

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?

Simn avatar Dec 03 '25 15:12 Simn

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.

ncannasse avatar Dec 03 '25 16:12 ncannasse

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...

Simn avatar Dec 03 '25 16:12 Simn

That part sounds similar to https://github.com/HaxeFoundation/haxe/issues/11605#issuecomment-2026967963 ?

kLabz avatar Dec 03 '25 17:12 kLabz

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.

Simn avatar Dec 03 '25 17:12 Simn

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)

ncannasse avatar Dec 03 '25 21:12 ncannasse

(this is just a generalisation of what we do already for abstract new)

ncannasse avatar Dec 03 '25 21:12 ncannasse