HaxeFoundation / haxe

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

[cs] type params, constraints and Map implementation selection. #3777

Closed nadako closed 1 year ago

nadako commented 9 years ago

I'm not sure if it's gencommon issue or general (ping @Simn), but it's reproduced easily with C# target:

class A<T:String> {
    function f():Map<T,Int> throw "not implemented";
}

class B extends A<String> {
    override function f() return ["a" => 1];
}

this fails with src\Main.cs(140,12): error CS0029: Cannot implicitly convert type "haxe.ds.StringMap<int>" to "haxe.ds.ObjectMap<object,int>" and that's because f is typed as ObjectMap<T,int>:

public class A<T> : global::haxe.lang.HxObject, global::A {
    public virtual global::haxe.ds.ObjectMap<T, int> f() {
        throw global::haxe.lang.HaxeException.wrap("not implemented");
    }
}

public class B : global::A<object> {
    public override global::haxe.ds.ObjectMap<object, int> f() {
        unchecked {
            {
                global::haxe.ds.StringMap<int> _g = new global::haxe.ds.StringMap<int>();
                _g.@set("a", 1);
                return _g;
            }

        }
    }
}
nadako commented 9 years ago

Note that if we remove the String constraint, f will be outputted as:

public virtual global::haxe.IMap<T, int> f() {
waneck commented 9 years ago

@Simn this fails too on cpp. It seems that Map<T,Int> when T : String is inferred as ObjectMap instead of StringMap

nadako commented 9 years ago

I reduced example to a small @:multiType abstract and it seems it's somehow related to type param/constraint being used in the @:to function:

interface Base<T> {}

@:multiType(T)
abstract A<T>(Base<T>) {
    public function new();
    @:to static function toS(t:Base<String>):S return null;
    @:to static function toO<T:{}>(t:Base<T>):O return null;
}

class S implements Base<String> {}
class O implements Base<{}> {}

class B<T:String> {
    var v:A<T>;
}

generates:

public class B<T> : global::haxe.lang.HxObject, global::B {
    public global::O v;
}
Simn commented 9 years ago

That might be the most confusing issue reduction I have ever seen. :P

But yes, the @:to resolution has always used type_eq instead of unify so T:String is not considered compatible with String. I don't want to change that either because it would allow using IntMap with Float and other horrible things.

nadako commented 9 years ago

But why does it ends up using ObjectMap instead of IMap when dealing with constrained type param?

Simn commented 9 years ago

Because String can be assigned to {}.

nadako commented 9 years ago

right...

waneck commented 9 years ago

But even if it can be assigned to {}, type_eq shouldn't be true. Should it? I feel like if we had a better variance idiom, these kinds of issues would be more clear.

Simn commented 9 years ago

Here's what happens inside unify_to_field:

toStringMap
type_eq IMap<test.T, String> IMap<String, Unknown<0>>
type_eq test.T String
fail
toIntMap
type_eq IMap<test.T, String> IMap<Int, Unknown<0>>
type_eq test.T Int
fail
toEnumValueMapMap
type_eq IMap<test.T, String> IMap<Unknown<0>, Unknown<1>>
beginning constraint checks
unify test.T EnumValue
fail
toObjectMap
type_eq IMap<test.T, String> IMap<Unknown<0>, Unknown<1>>
beginning constraint checks
unify test.T { }
done constraint checks

As you see the constraints are checked using unify.

Simn commented 9 years ago

We could try changing toStringMap to toStringMap<K:String, V>(t:IMap<K,V>):StringMap<V>.

waneck commented 9 years ago

I guess that solves it. Which makes me think - why do you have T:String anyway, @nadako ? :P

Simn commented 9 years ago

I've been wondering about the semantics of T:String as well...

nadako commented 9 years ago

I have a base class for a functionality that works with strings, but i wanted to be able to constraint those strings in a subclass using @:enum abstract.

nadako commented 9 years ago

E.g.

typedef ServiceHandler = {} -> Void;

class ServicesBase<TServiceType:String> {
    var handlers:Map<TServiceType,ServiceHandler>;

    public function new() {
        handlers = initHandlers();
    }

    function initHandlers():Map<TServiceType,ServiceHandler> {
        throw "not implemented";
    }

    public function useService(type:TServiceType, data:{}) {
        handlers[type](data);
    }
}

@:enum abstract MyServiceType(String) to String {
    var Gift = "gift";
    var Matchmaking = "matchmaking";
}

class MyServices extends ServicesBase<MyServiceType> {
    override function initHandlers() {
        return [
            Gift => handleGifts,
            Matchmaking => handleMatchMaking
        ];
    }

    function handleGifts(data:{}) {}
    function handleMatchMaking(data:{}) {}
}
Simn commented 9 years ago

Anyway, feel free to make the change, I don't want to be responsible. :P

Simn commented 7 years ago

The original test for this started failing when I moved Map to haxe.ds and aliased it from toplevel. I "fixed" the test by using haxe.ds.Map explicitly, but there must be something wrong here. Maybe a missing follow somewhere in the C# area or something...

Simn commented 1 year ago

Closing old C# issue that won't be addressed.