Skip to content

Commit

Permalink
Additional tests of ??= coercion behaviors for inference-update-3.
Browse files Browse the repository at this point in the history
The tests for the language feature `inference-update-3` are adjusted
to verify that the following hold true for an if-null expression of
the form `e1 ??= e2`:

- If the static type of `e2` is not a subtype of the write type of
  `e1`, but it is assignable via a coercion, then the coercion is
  performed, and the coerced type of `e2` is used to compute the
  static type of the whole `??=` expression.

- If `e1` is a promoted local variable, then coercions are performed
  based solely on the declared (unpromoted) type of `e1`.

These behaviors apply regardless of whether feature
`inference-update-3` is enabled; accordingly, this commit updates both
the `_test.dart` and `_disabled_test.dart` variants of the tests. I've
manually verified that even with the work on `inference-update-3`
reverted, the `_disabled_test.dart` tests continue to pass, so we can
be reasonably certain that these behaviors pre-date the work on the
`inference-update-3` feature.

Note: the diff is large due to the fact that the front end has 6
different code paths for handling `??=`, depending on the form of the
LHS, so to make sure that we have adequate test coverage, there are
tests for every possible LHS form. However, the diffs for all the
tests are pretty much the same except for
`if_null_assignment_local_disabled_test.dart` and
`if_null_assignment_local_test.dart`, which have extra test cases to
cover promotion behaviors.

Bug: dart-lang/language#1618
Change-Id: I711d62d9dc00fc20a2efd3967d60066d9bfaec03
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/356303
Reviewed-by: Lasse Nielsen <lrn@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
  • Loading branch information
stereotype441 authored and Commit Queue committed Mar 12, 2024
1 parent 1285976 commit 6d12146
Show file tree
Hide file tree
Showing 46 changed files with 5,091 additions and 1,199 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ import '../static_type_helper.dart';
/// no type argument is supplied.
Object? contextIterable<T>(Iterable<T> x) => x;

class A {}

class B1<T> implements A {}

class B2<T> implements A {}

class C1<T> implements B1<T>, B2<T> {}

class C2<T> implements B1<T>, B2<T> {}

class CallableClass<T> {
T call() => throw '';
}

/// Class that can be the target of `[]` and `[]=` operations. [ReadType] and
/// [WriteType] are the read and write types of the `[]` and `[]=` operators,
/// respectively.
Expand All @@ -34,14 +48,18 @@ extension Extension<ReadType, WriteType> on Indexable<ReadType, WriteType> {
}

main() {
// - An if-null assignment `E` of the form `lvalue ??= e` with context type
// `K` is analyzed as follows:
// - An if-null assignment `E` of the form `e1 ??= e2` with context type `K`
// is analyzed as follows:
//
// - Let `T1` be the read type of the lvalue.
// - Let `T2` be the type of `e` inferred with context type `J`, where:
// - If the lvalue is a local variable, `J` is the promoted type of the
// variable.
// - Otherwise, `J` is the write type of the lvalue.
// - Let `T1` be the read type of `e1`. This is the static type that `e1`
// would have as an expression with a context type schema of `_`.
// - Let `T2` be the type of `e2` inferred with context type `J`, where:
// - If the lvalue is a local variable, `J` is the current (possibly
// promoted) type of the variable.
// - Otherwise, `J` is the write type `e1`. This is the type schema that
// the setter associated with `e1` imposes on its single argument (or,
// for the case of indexed assignment, the type schema that
// `operator[]=` imposes on its second argument).
{
// Check the context type of `e`.
// ignore: dead_null_aware_expression
Expand All @@ -52,30 +70,49 @@ main() {
..expectStaticType<Exactly<String?>>();
}

// - Let `T` be `UP(NonNull(T1), T2)`.
// - Let `J'` be the unpromoted write type of `e1`, defined as follows:
// - If `e1` is a local variable, `J'` is the declared (unpromoted) type
// of `e1`.
// - Otherwise `J' = J`.
// - Let `T2'` be the coerced type of `e2`, defined as follows:
// - If `T2` is a subtype of `J'`, then `T2' = T2` (no coercion is
// needed).
// - Otherwise, if `T2` can be coerced to a some other type which *is* a
// subtype of `J'`, then apply that coercion and let `T2'` be the type
// resulting from the coercion.
// - Otherwise, it is a compile-time error.
// - Let `T` be `UP(NonNull(T1), T2')`.
// - Let `S` be the greatest closure of `K`.
// - If `T <: S`, then the type of `E` is `T`.
{
// K=Object, T1=int?, and T2=double, therefore T=num and S=Object, so T <:
// K=Object, T1=int?, and T2'=double, therefore T=num and S=Object, so T <:
// S, and hence the type of E is num.
var d = 2.0;
context<Object>((Extension(Indexable<int?, Object?>(null))[0] ??= d)
..expectStaticType<Exactly<num>>());

// K=Iterable<_>, T1=Iterable<int>?, and T2=Iterable<double>, therefore
// K=Iterable<_>, T1=Iterable<int>?, and T2'=Iterable<double>, therefore
// T=Iterable<num> and S=Iterable<Object?>, so T <: S, and hence the type of
// E is Iterable<num>.
var iterableDouble = <double>[] as Iterable<double>;
contextIterable((Extension(Indexable<Iterable<int>?, Object?>(null))[0] ??=
iterableDouble)
..expectStaticType<Exactly<Iterable<num>>>());

// K=Function, T1=Function?, and T2'=int Function() (coerced from
// T2=CallableClass<int>), therefore T=Function and S=Function, so T <: S,
// and hence the type of E is Function.
var callableClassInt = CallableClass<int>();
context<Function>((Extension(Indexable<Function?, Function?>(null))[0] ??=
callableClassInt)
..expectStaticType<Exactly<Function>>());
}

// - Otherwise, if `NonNull(T1) <: S` and `T2 <: S`, then the type of `E` is
// `S` if `inference-update-3` is enabled, else the type of `E` is `T`.
// - Otherwise, if `NonNull(T1) <: S` and `T2' <: S`, then the type of `E`
// is `S` if `inference-update-3` is enabled, else the type of `E` is `T`.
{
// K=Iterable<num>, T1=Iterable<int>?, and T2=List<num>, therefore T=Object
// and S=Iterable<num>, so T is not <: S, but NonNull(T1) <: S and T2 <: S,
// K=Iterable<num>, T1=Iterable<int>?, and T2'=List<num>, therefore T=Object
// and S=Iterable<num>, so T is not <: S, but NonNull(T1) <: S and T2' <: S,
// hence the type of E is Object.
var listNum = <num>[];
var o = [0] as Object?;
Expand All @@ -84,6 +121,19 @@ main() {
o = (Extension(Indexable<Iterable<int>?, Object?>(null))[0] ??= listNum)
..expectStaticType<Exactly<Object>>();
}

// K=B1<int> Function(), T1=C1<int> Function()?, and T2'=C2<int> Function()
// (coerced from T2=CallableClass<C2<int>>), therefore T=A Function() and
// S=B1<int> Function(), so T is not <: S, but NonNull(T1) <: S and T2' <:
// S, hence the type of E is A Function().
var callableClassC2Int = CallableClass<C2<int>>();
o = (() => B1<int>()) as Object?;
if (o is B1<int> Function()) {
// We avoid having a compile-time error because `o` can be demoted.
o = (Extension(Indexable<C1<int> Function()?, Function?>(null))[0] ??=
callableClassC2Int)
..expectStaticType<Exactly<A Function()>>();
}
}

// - Otherwise, the type of `E` is `T`.
Expand All @@ -92,28 +142,66 @@ main() {
var o = 0 as Object?;
var intQuestion = null as int?;
if (o is int?) {
// K=int?, T1=int?, and T2=double, therefore T=num and S=int?, so T is not
// <: S. NonNull(T1) <: S, but T2 is not <: S. Hence the type of E is num.
// K=int?, T1=int?, and T2'=double, therefore T=num and S=int?, so T is
// not <: S. NonNull(T1) <: S, but T2' is not <: S. Hence the type of E is
// num.
// We avoid having a compile-time error because `o` can be demoted.
o = (Extension(Indexable<int?, Object?>(null))[0] ??= d)
..expectStaticType<Exactly<num>>();
}
o = 0 as Object?;
if (o is int?) {
// K=int?, T1=double?, and T2=int?, therefore T=num? and S=int?, so T is
// not <: S. T2 <: S, but NonNull(T1) is not <: S. Hence the type of E is
// K=int?, T1=double?, and T2'=int?, therefore T=num? and S=int?, so T is
// not <: S. T2' <: S, but NonNull(T1) is not <: S. Hence the type of E is
// num?.
// We avoid having a compile-time error because `o` can be demoted.
o = (Extension(Indexable<double?, Object?>(null))[0] ??= intQuestion)
..expectStaticType<Exactly<num?>>();
}
o = '' as Object?;
if (o is String?) {
// K=String?, T1=int?, and T2=double, therefore T=num and S=String?, so
// none of T, NonNull(T1), nor T2 are <: S. Hence the type of E is num.
// K=String?, T1=int?, and T2'=double, therefore T=num and S=String?, so
// none of T, NonNull(T1), nor T2' are <: S. Hence the type of E is num.
// We avoid having a compile-time error because `o` can be demoted.
o = (Extension(Indexable<int?, Object?>(null))[0] ??= d)
..expectStaticType<Exactly<num>>();
}

var callableClassC2Int = CallableClass<C2<int>>();
o = (() => C1<int>()) as Object?;
if (o is C1<int> Function()) {
// K=C1<int> Function(), T1=C1<int> Function()?, and T2'=C2<int>
// Function() (coerced from T2=CallableClass<C2<int>>), therefore T=A
// Function() and S=C1<int> Function(), so T is not <: S. NonNull(T1) <:
// S, but T2' is not <: S. Hence the type of E is A Function().
// We avoid having a compile-time error because `o` can be demoted.
o = (Extension(Indexable<C1<int> Function()?, Function?>(null))[0] ??=
callableClassC2Int)
..expectStaticType<Exactly<A Function()>>();
}

o = (() => C2<int>()) as Object?;
if (o is C2<int> Function()) {
// K=C2<int> Function(), T1=C1<int> Function()?, and T2'=C2<int>
// Function() (coerced from T2=CallableClass<C2<int>>), therefore T=A
// Function() and S=C2<int> Function(), so T is not <: S. T2' <: S, but
// NonNull(T1) is not <: S. Hence the type of E is A Function().
// We avoid having a compile-time error because `o` can be demoted.
o = (Extension(Indexable<C1<int> Function()?, Function?>(null))[0] ??=
callableClassC2Int)
..expectStaticType<Exactly<A Function()>>();
}

o = 0 as Object?;
if (o is int) {
// K=int, T1=C1<int> Function()?, and T2'=C2<int> Function() (coerced from
// T2=CallableClass<C2<int>>), therefore T=A Function() and S=int, so T is
// not <: S. T2' <: S, but NonNull(T1) is not <: S. Hence the type of E is
// A Function().
// We avoid having a compile-time error because `o` can be demoted.
o = (Extension(Indexable<C1<int> Function()?, Function?>(null))[0] ??=
callableClassC2Int)
..expectStaticType<Exactly<A Function()>>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class C1<T> implements B1<T>, B2<T> {}

class C2<T> implements B1<T>, B2<T> {}

class CallableClass<T> {
T call() => throw '';
}

/// Ensures a context type of `B1<T>` for the operand, or `B1<_>` if no type
/// argument is supplied.
Object? contextB1<T>(B1<T> x) => x;
Expand All @@ -47,14 +51,18 @@ extension Extension<ReadType, WriteType> on Indexable<ReadType, WriteType> {
}

main() {
// - An if-null assignment `E` of the form `lvalue ??= e` with context type
// `K` is analyzed as follows:
// - An if-null assignment `E` of the form `e1 ??= e2` with context type `K`
// is analyzed as follows:
//
// - Let `T1` be the read type of the lvalue.
// - Let `T2` be the type of `e` inferred with context type `J`, where:
// - If the lvalue is a local variable, `J` is the promoted type of the
// variable.
// - Otherwise, `J` is the write type of the lvalue.
// - Let `T1` be the read type of `e1`. This is the static type that `e1`
// would have as an expression with a context type schema of `_`.
// - Let `T2` be the type of `e2` inferred with context type `J`, where:
// - If the lvalue is a local variable, `J` is the current (possibly
// promoted) type of the variable.
// - Otherwise, `J` is the write type `e1`. This is the type schema that
// the setter associated with `e1` imposes on its single argument (or,
// for the case of indexed assignment, the type schema that
// `operator[]=` imposes on its second argument).
{
// Check the context type of `e`.
// ignore: dead_null_aware_expression
Expand All @@ -65,49 +73,78 @@ main() {
..expectStaticType<Exactly<String?>>();
}

// - Let `T` be `UP(NonNull(T1), T2)`.
// - Let `J'` be the unpromoted write type of `e1`, defined as follows:
// - If `e1` is a local variable, `J'` is the declared (unpromoted) type
// of `e1`.
// - Otherwise `J' = J`.
// - Let `T2'` be the coerced type of `e2`, defined as follows:
// - If `T2` is a subtype of `J'`, then `T2' = T2` (no coercion is
// needed).
// - Otherwise, if `T2` can be coerced to a some other type which *is* a
// subtype of `J'`, then apply that coercion and let `T2'` be the type
// resulting from the coercion.
// - Otherwise, it is a compile-time error.
// - Let `T` be `UP(NonNull(T1), T2')`.
// - Let `S` be the greatest closure of `K`.
// - If `T <: S`, then the type of `E` is `T`.
{
// K=Object, T1=int?, and T2=double, therefore T=num and S=Object, so T <:
// K=Object, T1=int?, and T2'=double, therefore T=num and S=Object, so T <:
// S, and hence the type of E is num.
var d = 2.0;
context<Object>((Extension(Indexable<int?, Object?>(null))[0] ??= d)
..expectStaticType<Exactly<num>>());

// K=Iterable<_>, T1=Iterable<int>?, and T2=Iterable<double>, therefore
// K=Iterable<_>, T1=Iterable<int>?, and T2'=Iterable<double>, therefore
// T=Iterable<num> and S=Iterable<Object?>, so T <: S, and hence the type of
// E is Iterable<num>.
var iterableDouble = <double>[] as Iterable<double>;
contextIterable((Extension(Indexable<Iterable<int>?, Object?>(null))[0] ??=
iterableDouble)
..expectStaticType<Exactly<Iterable<num>>>());

// K=Function, T1=Function?, and T2'=int Function() (coerced from
// T2=CallableClass<int>), therefore T=Function and S=Function, so T <: S,
// and hence the type of E is Function.
var callableClassInt = CallableClass<int>();
context<Function>((Extension(Indexable<Function?, Function?>(null))[0] ??=
callableClassInt)
..expectStaticType<Exactly<Function>>());
}

// - Otherwise, if `NonNull(T1) <: S` and `T2 <: S`, then the type of `E` is
// `S`.
// - Otherwise, if `NonNull(T1) <: S` and `T2' <: S`, then the type of `E`
// is `S`.
{
// K=B1<_>, T1=C1<int>?, and T2=C2<double>, therefore T=A and S=B1<Object?>,
// so T is not <: S, but NonNull(T1) <: S and T2 <: S, hence the type of E
// is B1<Object?>.
// K=B1<_>, T1=C1<int>?, and T2'=C2<double>, therefore T=A and
// S=B1<Object?>, so T is not <: S, but NonNull(T1) <: S and T2' <: S, hence
// the type of E is B1<Object?>.
var c2Double = C2<double>();
contextB1((Extension(Indexable<C1<int>?, Object?>(null))[0] ??= c2Double)
..expectStaticType<Exactly<B1<Object?>>>());

// K=B1<Object>, T1=C1<int>?, and T2=C2<double>, therefore T=A and
// S=B1<Object>, so T is not <: S, but NonNull(T1) <: S and T2 <: S, hence
// K=B1<Object>, T1=C1<int>?, and T2'=C2<double>, therefore T=A and
// S=B1<Object>, so T is not <: S, but NonNull(T1) <: S and T2' <: S, hence
// the type of E is B1<Object>.
contextB1<Object>((Extension(Indexable<C1<int>?, Object?>(null))[0] ??=
c2Double)
..expectStaticType<Exactly<B1<Object>>>());

// K=Iterable<num>, T1=Iterable<int>?, and T2=List<num>, therefore T=Object
// and S=Iterable<num>, so T is not <: S, but NonNull(T1) <: S and T2 <: S,
// K=Iterable<num>, T1=Iterable<int>?, and T2'=List<num>, therefore T=Object
// and S=Iterable<num>, so T is not <: S, but NonNull(T1) <: S and T2' <: S,
// hence the type of E is Iterable<num>.
var listNum = <num>[];
context<Iterable<num>>(
(Extension(Indexable<Iterable<int>?, Object?>(null))[0] ??= listNum)
..expectStaticType<Exactly<Iterable<num>>>());

// K=B1<int> Function(), T1=C1<int> Function()?, and T2'=C2<int> Function()
// (coerced from T2=CallableClass<C2<int>>), therefore T=A Function() and
// S=B1<int> Function(), so T is not <: S, but NonNull(T1) <: S and T2' <:
// S, hence the type of E is B1<int> Function().
var callableClassC2Int = CallableClass<C2<int>>();
context<B1<int> Function()>(
(Extension(Indexable<C1<int> Function()?, Function?>(null))[0] ??=
callableClassC2Int)
..expectStaticType<Exactly<B1<int> Function()>>());
}

// - Otherwise, the type of `E` is `T`.
Expand All @@ -116,28 +153,66 @@ main() {
var o = 0 as Object?;
var intQuestion = null as int?;
if (o is int?) {
// K=int?, T1=int?, and T2=double, therefore T=num and S=int?, so T is not
// <: S. NonNull(T1) <: S, but T2 is not <: S. Hence the type of E is num.
// K=int?, T1=int?, and T2'=double, therefore T=num and S=int?, so T is
// not <: S. NonNull(T1) <: S, but T2' is not <: S. Hence the type of E is
// num.
// We avoid having a compile-time error because `o` can be demoted.
o = (Extension(Indexable<int?, Object?>(null))[0] ??= d)
..expectStaticType<Exactly<num>>();
}
o = 0 as Object?;
if (o is int?) {
// K=int?, T1=double?, and T2=int?, therefore T=num? and S=int?, so T is
// not <: S. T2 <: S, but NonNull(T1) is not <: S. Hence the type of E is
// K=int?, T1=double?, and T2'=int?, therefore T=num? and S=int?, so T is
// not <: S. T2' <: S, but NonNull(T1) is not <: S. Hence the type of E is
// num?.
// We avoid having a compile-time error because `o` can be demoted.
o = (Extension(Indexable<double?, Object?>(null))[0] ??= intQuestion)
..expectStaticType<Exactly<num?>>();
}
o = '' as Object?;
if (o is String?) {
// K=String?, T1=int?, and T2=double, therefore T=num and S=String?, so
// none of T, NonNull(T1), nor T2 are <: S. Hence the type of E is num.
// K=String?, T1=int?, and T2'=double, therefore T=num and S=String?, so
// none of T, NonNull(T1), nor T2' are <: S. Hence the type of E is num.
// We avoid having a compile-time error because `o` can be demoted.
o = (Extension(Indexable<int?, Object?>(null))[0] ??= d)
..expectStaticType<Exactly<num>>();
}

var callableClassC2Int = CallableClass<C2<int>>();
o = (() => C1<int>()) as Object?;
if (o is C1<int> Function()) {
// K=C1<int> Function(), T1=C1<int> Function()?, and T2'=C2<int>
// Function() (coerced from T2=CallableClass<C2<int>>), therefore T=A
// Function() and S=C1<int> Function(), so T is not <: S. NonNull(T1) <:
// S, but T2' is not <: S. Hence the type of E is A Function().
// We avoid having a compile-time error because `o` can be demoted.
o = (Extension(Indexable<C1<int> Function()?, Function?>(null))[0] ??=
callableClassC2Int)
..expectStaticType<Exactly<A Function()>>();
}

o = (() => C2<int>()) as Object?;
if (o is C2<int> Function()) {
// K=C2<int> Function(), T1=C1<int> Function()?, and T2'=C2<int>
// Function() (coerced from T2=CallableClass<C2<int>>), therefore T=A
// Function() and S=C2<int> Function(), so T is not <: S. T2' <: S, but
// NonNull(T1) is not <: S. Hence the type of E is A Function().
// We avoid having a compile-time error because `o` can be demoted.
o = (Extension(Indexable<C1<int> Function()?, Function?>(null))[0] ??=
callableClassC2Int)
..expectStaticType<Exactly<A Function()>>();
}

o = 0 as Object?;
if (o is int) {
// K=int, T1=C1<int> Function()?, and T2'=C2<int> Function() (coerced from
// T2=CallableClass<C2<int>>), therefore T=A Function() and S=int, so T is
// not <: S. T2' <: S, but NonNull(T1) is not <: S. Hence the type of E is
// A Function().
// We avoid having a compile-time error because `o` can be demoted.
o = (Extension(Indexable<C1<int> Function()?, Function?>(null))[0] ??=
callableClassC2Int)
..expectStaticType<Exactly<A Function()>>();
}
}
}

0 comments on commit 6d12146

Please sign in to comment.