Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
FMorschel opened this issue May 9, 2024 · 10 comments
Labels
request Requests to resolve a particular developer problem

Comments

@FMorschel
Copy link

FMorschel commented May 9, 2024

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.

@lrhn
Copy link
Member

lrhn commented May 9, 2024

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

@eernstg
Copy link
Member

eernstg commented May 10, 2024

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

@mraleph
Copy link
Member

mraleph commented May 11, 2024

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

@lrhn
Copy link
Member

lrhn commented May 11, 2024

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.

@FMorschel
Copy link
Author

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

@eernstg
Copy link
Member

eernstg commented May 13, 2024

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 eernstg transferred this issue from dart-lang/sdk May 13, 2024
@eernstg eernstg added the request Requests to resolve a particular developer problem label May 13, 2024
@eernstg
Copy link
Member

eernstg commented May 13, 2024

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 #3797 containing the proposal that I outlined earlier in this thread.

@eernstg
Copy link
Member

eernstg commented May 13, 2024

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.

@FMorschel
Copy link
Author

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.

@eernstg
Copy link
Member

eernstg commented May 13, 2024

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 eernstg reopened this May 13, 2024
@lrhn lrhn changed the title Abstract classes with recurring generics could warn on creation of nullable type-solving impossibility Type inference fails to solve Null <: T? and T <: C<T> to Never, choses non-solution Null. May 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

4 participants