HaxeFoundation / haxe

Haxe - The Cross-Platform Toolkit
https://haxe.org
6.19k stars 655 forks source link

Inconsistent interface type inference for getters/setters #11774

Open RblSb opened 2 months ago

RblSb commented 2 months ago
interface IObj {
    var width(default, null):Float;
    var height(get, set):Float;
}

class Main implements IObj {
    public var width(default, null) = 0; // Int in this implementation

    @:isVar public var height(get, set) = 0.0; // cannot be Int here, but

    function get_height():Int { // no signature error
        return Std.int(height);
    }

    function set_height(height:Float):Int { // fine too
        return Std.int(height);
    }

    public function new() {}

    static function main() {
        final m = new Main();
        int(m.width); // fine int
        int(m.height); // Error: Float should be Int
    }

    static function int(v:Int):Void {
        trace(v);
    }
}
pecheny commented 1 month ago

What's wrong with that? Getter and setter are implementation details compatible with the contract as long as Void->Int is a subtype of Void->Float, covariance stuff and all...

class Test {
  static function main() {
       var myFunc:Void->Float = foo;
  }

  static function foo():Int {
    return 0;
  }
}

Main is also a subtype of IObj with its own width:Int, as long as read-only access is covariant on the return type.

RblSb commented 1 month ago

Yes, i think implementing Float as Int should be allowed, this is pretty handy for subclasses to restrict Float a little. But it doesn't work in all cases, only for (default, ...), but not for get, set or simple fields:

interface IMain {
    var a(default, never):Float;
    var b(default, null):Float;
    var c:Float;
    var d(get, set):Float;
}

class Main implements IMain {
    public var a:Int = 0; // fine, good
    public var b:Int = 0; // fine, good
    public var c:Int = 0; // Field c has different type than in IMain
    public var d(get, set):Int; // Field d has different type than in IMain

    function get_d():Int
        return 0;

    function set_d(d:Float):Int
        return 0;

    static function main() {
        final main = new Main();
        // Allowed because there is no `a(default, never)` in Main, bad
        main.a = 1;
    }

    public function new() {}
}

And there is another bug, you can have more restricted type in interface with (default, never), but compiler will not inference (default, never) to your implementation.

pecheny commented 1 month ago

It should work for read access only. In case of write access relation is also a contravariant so subtyping is impossible. public var d(get, set):Int should accept Float for writing by contract so compilation error is correct for this case. The same for second case: interface may and should be more restricted than implementation but not vice versa.