haxe icon indicating copy to clipboard operation
haxe copied to clipboard

Null<T> should not satisfy type constraint T

Open tobil4sk opened this issue 1 year ago • 4 comments

This compiles fine and leads to a null access error:

function getLength<T:String>(v:T) {
	return v.length;
}

function main() {
	final nullable:Null<String> = null;
	trace(getLength(nullable));
}

tobil4sk avatar Feb 19 '25 13:02 tobil4sk

That would likely break a lot of things.. 🤔 However I do think this shouldn't be allowed with null safety enabled

kLabz avatar Feb 20 '25 05:02 kLabz

A similar example where at least this should not trigger double Null<> wrapping:

function main() {
	var x:Null<String> = "";
	var zz = testfn(x);
	$type(zz); // Null<Null<String>>
}

function testfn<T>(x:T) {
	var arr = [x];
	return arr.pop();
}

(I thought we already did some things to avoid Null<Null<T>>, but that one might be due to this issue)

kLabz avatar Feb 20 '25 06:02 kLabz

That would likely break a lot of things.. 🤔 However I do think this shouldn't be allowed with null safety enabled

Could we make it so that Null<String> never satisfies String, but with null safety disabled we can automatically convert Null<String> to String and instantiate T as String instead of Null<String>? This is consistent with how the type hierarchy should work (Null<String> is not a subtype of String), but still allows the current flexibility if null safety is not desired.

function wrap<T:String>(v:T) {
	return v;
}

function main() {
	final nullable:Null<String> = null;
	$type(wrap(nullable)); // currently Null<String>, would be String
}

This solution would also solve our problem at #12019.

A similar example where at least this should not trigger double Null<> wrapping:

Also, your sample does not require the testfn function to reproduce:

function main() {
	var x:Null<String> = "";
	var arr = [x];
	$type(arr.pop());
}

(I also don't think it's related to this problem, since the T in testfn<T> and Array<T> is unconstrained.)

tobil4sk avatar Feb 20 '25 09:02 tobil4sk

I've come across something similar related to the nullability of basic types:

function f<T:Int>(a:T) {
	trace(a);
}

@:nullSafety(Strict)
@:debug.mono
function main() {
	var x = null;
	f(x);
}

This doesn't complain at all and traces 0. The mono debug shows us that we have a 0: Null<Unknown<0> : Int> (8:2), which means it's a monomorph constrained to be nullable and to be Int, which in my mind is a conflict on static targets.

This is also very likely the root problem of #6762.

Edit: Maybe it actually isn't a conflict because Null<Int> satisfies both constraints...

Simn avatar Apr 06 '25 09:04 Simn