dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.21k stars 1.57k forks source link

unable to infer non nullable type in case of generics #56954

Open bar4488 opened 2 hours ago

bar4488 commented 2 hours ago

In these cases we should be able to infer that the type cannot be null, however the current implementation raises a compile error:

class A<T> {
  final T value;

  A(this.value);
}

void main() {
  int? n = 3;
  int n2 = n == null ? 3 : n + 2; // as intended

  if (n != null) {
    int n2 = n; // as intended
  }

  A<int?> a = A(3);
  int b = a.value == null ? 3 : a.value + 2; // error: Operator '+' cannot be called on 'int?' because it is potentially null

  if (a.value != null) {
    int b = a.value; // error: A value of type 'int?' can't be assigned to a variable of type 'int'.
  }
}
dart-github-bot commented 2 hours ago

Summary: The issue is that Dart's type inference fails to recognize that a generic type parameter T cannot be null when it's initialized with a non-null value, leading to compile errors when attempting to use the value in operations that require non-nullable types.

eernstg commented 2 hours ago

This is working as intended. You expect a.value to be promoted by the test a.value == null, but this would not be sound. That is so because value is an instance member of of a, and it could be overridden by a getter whose value differs each time you call it. (So it wouldn't be sound to assume that a.value is non-null just because it was non-null the previous time you called it).

We do actually support a special case: If you change the name of value to _value (such that it's a private member of A) then it can be promoted. The point is that private members cannot be overridden in a different library, and this means that we can safely rely on the information which can be obtained by investigating the current library.

Another possible way ahead would be to adopt 'stable getters', dart-lang/language#1518, which would establish a guarantee that a certain getter will return the same value if you call it multiple times. However, that's a proposal rather than an actual language mechanism, so we can't do that today. (You can vote for it, though ;-)

bar4488 commented 2 hours ago

ok thanks! seems about right.
@eernstg is there a reason we do not support it in the case of final classes?

final class A {
  final int? value;

  A(this.value);
}

void main() {
  int? n = 3;
  int n2 = n == null ? 3 : n + 2; // as intended

  if (n != null) {
    int n2 = n; // as intended
  }

  A a = A(3);
  int b = a.value == null
      ? 3
      : a.value +
          2; // error Operator '+' cannot be called on 'int?' because it is potentially null

  if (a.value != null) {
    int b = a.value; // error
  }
}
eernstg commented 2 hours ago
// --- Library 'a.dart'.

final class A1 {
  final int value;
  A1(this.value);
}

base class A2 extends A1 {
  A2(super.value);
}

// --- Library 'b.dart'.
import 'a.dart';

var _sillyCounter = 0;

base class B extends A2 {
  B(): super(0);
  int get value => ++_sillyCounter;  
}

// --- Library 'main.dart'.
import 'a.dart';
import 'b.dart';

void main() {
  A1 a = B();
  print('The value is ${a.value}, then ${a.value}');
}

Just like promotion, superinterface relationships also have special privileges in the same library (in this case: a subclass of a final class, in the same library, can be base, and this means that we can create a subclass in a different library).