dart-lang / dart-lang/language

Decide on dynamic type checks that may be stronger than required

Open
#3,758 0 comments 0 reactions 1 assignee Claimed by @eernstg View on GitHub
question
Dominant language
TeX
Stars
2.9k
Forks
239
Avg merge
2d 18h
Merged PRs (30d)
14

Description

This issue is concerned with the discussion about how strong our dynamic type checks should be, in particular: Whether we want to apply statically based type checks in some situations where they may be stronger than the actual requirement at run time, which may cause dynamic errors that we would not have if we were to check for the type which is actually required.

Consider the following example of declaring and using the "most general function type for a given argument list shape":

```dart
typedef Arity1 = dynamic Function(Null);

int f1(num n) => 42;
void f2([int i, String s = 'Hello']) => print(s);
Future f3(void ignore, {bool b}) async {}

main() {
Arity1 f;
f = ... ? f1 : (... ? f2 : f3);
f(42); // (*)
}
```
With the sound approach to co/contravariance in function types which is used in Dart 2, such a "most general function type" should use parameter type `Null` in order to be a supertype of the relevant set of function types, so we can't just change that to `dynamic` or anything else.

At (*) we may generate code for a type check that the actual argument has type `Null`, derived from the statically known type of `f`, or we may check that the function gets a value that satisfies the actual requirement. We may achieve the latter because we have generated code in the body of all possible values of `f` such that the argument is guaranteed to be checked, or we might invoke a special entry point of the value of `f` in order to select a variant of the function body that performs these checks, or we could "look up" the parameter type in the dynamic value of `f` in order to find the type that is actually required of this actual argument.

The statically based check is obviously likely to be more performant, but it also gives rise to some failures at run time in situations where the given dynamic types _do_ satisfy all requirements needed for soundness, so the more performant approach is less powerful, which makes the trade-off difficult. (In the example above, the statically based check will fail at run time, but the dynamically based checks will succeed for all possible values of `f`).

Of course, we could perform the invocation using `(f as dynamic)(42)` in which case we will get the check that is actually associated with the dynamic value of `f`, but that would discard the static guarantees that we have about the signature of `f` (so we would "forget" that it is actually guaranteed that `f` will accept one positional argument, so the performance loss is likely to be significantly worse than necessary).

Also, it seems silly to tell developers that they should use `Function(Null)` as "the most general function type" with this shape, and then they can't call it!

Another example (which may have been landed as `tests/language_2/implicit_downcast_tricky_test.dart` at this point) shows that a very similar situation may arise when we use non-trivial contravariance in a regular class (no `covariant` needed, no type arguments needed, just regular instance method overrides):

```dart
import "package:expect/expect.dart";

main() {
void Function(String arg) fun = (Object o) {};
Object obj = 42;

// Downcast even when dynamic types are compatible.
Expect.throws(() {
fun(obj); // Implicit downcast from of obj to String.
}, null, "function-type");

dynamic dyn = C();

// Downcast on correct-arity dynamic invocation of Object method.
Expect.throws(() {
dyn.noSuchMethod(obj); // Implicit downcast from obj to Invocation.
}, null, "dyn-valid");

// No downcast on a wrong-arity dynamic invocation of Object method.
Expect.equals(obj, dyn.noSuchMethod(obj, null), "dyn-invalid");

// Downcast even if subclasses are more permissive.

D d = E();
Expect.throws(() {
d.foo(obj);
}, null, "override");

F f = G();
Expect.throws(() {
f.foo(obj);
}, null, "override-generic");
}

class C {
Object noSuchMethod(Object o, [Object o2]) => o;
}

class D {
void foo(String x) {}
}

class E implements D {
void foo(Object x) {}
}

class F {
void foo(T x) {}
}

class G implements F {
void foo(Object x) {}
}
```

In all these cases, the statically known type requirement on an actual argument in a function invocation is more strict than the actual requirement stated for the corresponding parameter in the callee, just like we had it with "most general function type".

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.