language icon indicating copy to clipboard operation
language copied to clipboard

Type inference fails to solve `Null <: T?` and `T <: C<T>` to `Never`, choses non-solution `Null`.

Open FMorschel opened this issue 1 year ago • 10 comments

As stated at dart-lang/sdk#55647 I've created on my project a class structure similar to the following:

abstract class C<T extends C<T>> {}

class D extends C<D> {}

class E extends C<E> {}

This is not an impossible generics since two classes can fit in the constraints.

My point comes when we have some other class/method that requires one of the C subtypes. If we do the following:

class W<T extends C> {}

We then, of course, get the following error on C:

Type parameter bound types must be instantiated.

Doing this solves the issue:

class W<T extends C<T>> {}

The problem begins to appear if T is used solely on a nullable variable inside W.

void main() {
  final w1 = W(D());
  final w2 = W(E());
  final w3 = W(null); // Error
}

abstract class C<T extends C<T>> {}

class D extends C<D> {}

class E extends C<E> {}

class W<T extends C<T>> {
  const W(this.obj);
  
  final T? obj;  
}

We then get:

'Null' doesn't conform to the bound 'C<Null>' of the type parameter 'T'.

This of course is a design problem from whoever created W but my point is that the creator could be warned in some way about that.

If this class has more than one constructor for example (I'm assuming another constructor that constraints obj to non-nullable T), then the author of W may never think/need to instantiate the class with the constructor that allows a nullable value inside its library but from a public library that could cause some issues for the end user that may not know what to place as a generics in a way that doesn't break the implementation (assuming for example that the author tests somewhere the actual value of T like T == E).

I believe that this could warn on all nullable variables if that is the only use of T (no non-null variable). There may be other edge cases where this would apply as well but maybe exposing it here can help others find them since I could not think of other cases.

FMorschel avatar May 09 '24 01:05 FMorschel

So the problem is that W(null) tries to infer a T for W, and given only the constraints Null <: T? and T <: C<T>, it decides that T is Null instead of choosing for example Never.

That seems like just a bad solution, since Null is not actually a solution to T <: C<T>, but Never satisfies both constraints. Can we just improve this? @stereotype441

lrhn avatar May 09 '24 12:05 lrhn

[Edit: This proposal has now been stated as a separate 'small-feature' issue, #3797.]

Perhaps we should add another case to the following rule:

  • If Q is Q0? the match holds under constraint set C:
    • If P is P0? and P0 is a subtype match for Q0 under constraint set C.
    • Or if P is dynamic or void and Object is a subtype match for Q0 under constraint set C.
    • Or if P is Null and Never is a subtype match for Q0 under constraint set C.    <--- New case
    • Or if P is a subtype match for Q0 under non-empty constraint set C.
    • Or if P is a subtype match for Null under constraint set C.
    • Or if P is a subtype match for Q0 under empty constraint set C.

This might be a useful behavior in practice, in spite of the fact that it introduces the type Never. Considering the given example, it does make sense to have an instance of W<Never> (which will be usable as a W<T> for any T), in particular because its obj is allowed to be "absent" by having type T?.

eernstg avatar May 10 '24 13:05 eernstg

@lrhn @eernstg can you label the issue? It is unclear to me if this is tool issue or a language issue. Thanks.

mraleph avatar May 11 '24 08:05 mraleph

I believe this was filed as a request for a warning, so for the analyzer. I'd rather fix the real problem, if possible. So if possible, it's a language issue, if not, it's a tool issue.

Will mark as language for now.

lrhn avatar May 11 '24 08:05 lrhn

Please feel free to rename this into whatever makes more sense.

FMorschel avatar May 13 '24 15:05 FMorschel

I believe the behavior of type inference in this case is as specified, which means that it is not a tool issue.

If it is taken as a problem description ("I'd like to be able to do this in Dart, please fix the language such that it is possible") then it would be a 'request' in the language repository. I'll transfer it there and label it as such, which seems to be at least a reasonable classification.

eernstg avatar May 13 '24 16:05 eernstg

This is now a request issue (that is, a description of a difficulty in the language Dart). Proposed solutions should be stated in separate issues. I created https://github.com/dart-lang/language/issues/3797 containing the proposal that I outlined earlier in this thread.

eernstg avatar May 13 '24 16:05 eernstg

By the way, we don't really have a 'type-solving impossibility' in the given example:

void main() {
  final w1 = W(D());
  final w2 = W(E());
  final w3 = W<Never>(null); // No problem.
}

abstract class C<T extends C<T>> {}

class D extends C<D> {}

class E extends C<E> {}

class W<T extends C<T>> {
  const W(this.obj);
  final T? obj;  
}

So there is a solution to the constraints, it's just that type inference wasn't able to find it. That's also the reason why #3797 describes a small modification of the type inference algorithm which would make it succeed.

eernstg avatar May 13 '24 16:05 eernstg

Yes, I saw that. When I created the issue I completely forgot about the existence of Never.

Since that other issue was created, I don't think there is a need for this one to be opened. I'm going to take the liberty to close it. If you disagree, please reopen.

FMorschel avatar May 13 '24 16:05 FMorschel

I think we should reopen. A description of a difficulty (that is, a 'request' issue) is a valuable resource because it strictly keeps the focus on a situation which isn't very good.

We could have a bunch of different solutions, and each of them would go into a 'feature' or 'small-feature' issue, and they would all refer to the 'request' in order to explain what their purpose is (or, at least which purpose was the initial inspiration for that proposal).

eernstg avatar May 13 '24 16:05 eernstg