Skip to content

Type inferrence of generic to Never #53668

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

Closed
Leptopoda opened this issue Oct 2, 2023 · 5 comments
Closed

Type inferrence of generic to Never #53668

Leptopoda opened this issue Oct 2, 2023 · 5 comments
Labels
closed-as-intended Closed as the reported issue is expected behavior

Comments

@Leptopoda
Copy link

Leptopoda commented Oct 2, 2023

The following code infers const A() to A<Never> when assigned using the nullcheck operator.
This is unexpected as I'd either think A<dynamic> or better A<T> is inferred.
The code throws at runtime which is bad.

class A<T> {
  const A();

  A<T> copyWith({
    T? newValue,
  }) =>
      A();
}

A<T> method<T>(final A<T>? subject, T valuet) {
  final value = subject ?? const A(); // const A() is inferred to A<Never> while value is A<T>.
  value.copyWith(valuet); // throws at runtime when value actually is A<Never>
  return value;
}

A<T> method2<T>(final A<T>? subject, T valuet) {
  const loading = A(); // inferred to A<dynamic>, inference_failure_on_instance_creation
  final value = subject ?? loading; // inferred to A<dynamic>
  value.copyWith(valuet);
  return value; // not possible as value is A<dynymic>
}

General info

  • Dart 3.1.3 (stable) (Tue Sep 26 14:25:13 2023 +0000) on "linux_x64"
  • on linux / Linux 6.5.5-arch1-1 # 1 SMP PREEMPT_DYNAMIC Sat, 23 Sep 2023 22:55:13 +0000
  • locale is de_DE.UTF-8

Project info

  • sdk constraint: '>=3.1.3 <4.0.0'
  • dependencies:
  • dev_dependencies: commitlint_cli, fvm, husky, melos

Process info

Memory CPU Elapsed time Command line
74 MB 0.0% 15:13:04 dart devtools --machine --try-ports 10 --allow-embedding
1897 MB 1.0% 15:13:04 dart language-server --protocol=lsp --client-id=VS-Code --client-version=3.72.2
92 MB 0.0% 15:13:04 flutter_tools.snapshot daemon
42 MB 91.3% 00:00 main.dart-3.1.3.snapshot dart info
@lrhn
Copy link
Member

lrhn commented Oct 2, 2023

Working as intended.

The context type for the second operand of e1 ?? e2 is either the context type of the entire e1 ?? e2 expression, if the is one, or the non-null static type of e1 if there is not surrounding context type. The latter case applies here for final value = subject ?? const A();

The static type of subject is A<T>?, the non-null version of that is A<T>.
Since T is a type variable, which can be any type at runtime, and const A() must evaluate to one value at compile-time, the inferred type is the only one which is assignable to any A<T>, which is const A<Never>().

You'll see the same behavior in any place where a generic constant expression is assigned to a type with a type variable, including List<T> list = const [];, which infers const <Never>[].

Using A<T> is impossible in a constant expression. There is only one value of any const expression, so it cannot depend on any information which can vary at runtime.

Choosing A<dynamic> would actually work in this case, because there is no context type to be incompatible with.
The choice of using the first operand's static type as type context for the second operand, was chosen because it is often useful.

People would write things like var list = maybeListInt ?? []; and expect to get <int>[], or var x = maybeDouble ?? 0; and expect 0 to mean 0.0, because "obviously" they'd want a List<int> or double in that case.
It's correct more often than its wrong.

So if you don't want that, you can write either:

 final A value = subject ?? const A(); // because `A` means `A<dynamic>` as a raw type

or

final value = subject ?? const A<dynamic>();

The throwing is bad, but it's a consequence of covariant generics being unsafe. You'll get the same error with:

List<int> list = <Never>[]; // Generic up-cast, Never <: int => List<Never> <: List<int>
list.add(1); // Calling method which uses type variable contravariantly.

Any time you up-cast a generic type to a supertype of the type variable, you risk errors if calling a method which has a contravariant occurrence of the type variable. The compiler needs to throw in that case, because continuing would be unsound.

@lrhn lrhn closed this as completed Oct 2, 2023
@lrhn lrhn added the closed-as-intended Closed as the reported issue is expected behavior label Oct 2, 2023
@Leptopoda
Copy link
Author

Thanks for the insightful explanation.

Using A is impossible in a constant expression.

Totally makes sense.

This is probably a feature request for the analyzer team but I somehow expected either strict-inference or strict-raw-types should catch this.
Probably related to #34058

@lrhn
Copy link
Member

lrhn commented Oct 2, 2023

It's related to #34058, but mainly in the contravariance issue. That other issue is about under-constrained type arguments being solved to extreme types (top types like dynamic in covariant positions, bottom types like Null then and Never today in contravariant positions). That's about being the most permissive absent of any limiting constraint.
The Never inferred here is the opposite, it's inferred in covariant position because of a maximally limiting constraint (the inferred type must be subtype of any possible type that T can be bound to at runtime, which is only satisfiable by a bottom type).

I don't know if there is a lint for this. The strict-raw-types lint only applies to type terms, this is an instance creation expression, and strict-inference (I think) only warns about inferring dynamic, not Never.

@Leptopoda
Copy link
Author

and strict-inference (I think) only warns about inferring dynamic, not Never.

Yes, it doesn't. None of these language features cover Never.
Should I open a new feature request for a lint or expanding one of the language features regarding the Never case?

@lrhn
Copy link
Member

lrhn commented Oct 2, 2023

If you want a lint, you should ask for one. Defintely won't happen otherwise.

What would it warn about?

Whenever a Never is inferred as a solution to <: T for some type variable in a constant context, where a non-constant solution would have been T?
Or only if it happens in the second operand of ?? with no typing context?
And only if the class being instantiated has a contravariant occurrence of its type variable in a public member?

And considering that a var list = maybeIntList ?? const []; is likely not a problem, because the methods of List where the element type occurs contravariantly are those that modify the list, and a constant list would throw for those anyway, so the author of that code is like perfectly happy with const <Never>[].)

That suggests that there could be a somewhat large number of false positives for such a lint. That's not inherently an argument against a lint, they are allowed to have false positives. That's why lints are optional. But it can mean that there will not be a lot of users of the lint, if it gets in the way of their code.

If the real issue is the unsafe upcast, it may be better to put resources into variance annotations. If the type parameter of A had been invariant, instead of unsafely covariant, then inference would have given up and told you that there is no constant type that would make const A<ThatType>() have type A<T> for any type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-as-intended Closed as the reported issue is expected behavior
Projects
None yet
Development

No branches or pull requests

2 participants