dart-lang / dart-lang/language

Unusable inference in list pattern with `dynamic` value.

Open
#3,956 7 comments 4 reactions 0 assignees View on GitHub
patterns
Dominant language
TeX
Stars
2.9k
Forks
239
Avg merge
2d 18h
Merged PRs (30d)
14

Description

Example code:
```dart
void main() {
var [int a, String b] = [2, "a"] as dynamic;
print("$a: $b");
}
```
The inference (or the implementation of it at least) infers a type argument for the list pattern of ``.
That is, the line becomes:
```dart
var [int a, String b] = [2, "a"] as dynamic;
```
and this type check *fails*.

If I write it as
```dart
var [int a, String b] = [2, "a"] as List;
```
then the inference becomes
```dart
var [int a, String b] = [2, "a"] as List;
```
and it works. `List` is OK because `dynamic` is assignable to each sub-pattern.
The top-level `dynamic` is accepted statically because `dynamic` is assignable to any list pattern, but the inferrence of `` is *always* going to get in the way.

**Suggestion**: If the matched value type of a list pattern (and probably map pattern) with no explicit type arguments,
is `dynamic`, then infer `` as the type argument type. Otherwise, if the matched value type is not `dynamic`, do what we do today.

This only occurs at the top-level of the pattern. If I change it to:
```dart
var [int a, [int c, String b]] = [2, [2, "a"]] as List;
```
the match succeeds, even though the inner `[int c, String b]` list pattern should also have a matched value type of `dynamic`.
Maybe it's just a bug. (If it's just a bug in the implementation, we should fix it.)

----

I have checked more. I believe it is a bug, with implementations not matching the specified behavior.
It's likely due to an implicit downcast being done too soon. (Someone please verify).

In ([step three](https://github.com/dart-lang/language/blob/main/accepted/3.0/patterns/feature-specification.md#type-checking-and-pattern-required-type) of) type inference of patterns, for a `List` pattern *p* with matched value type `M`, it infers an element type for the list pattern as:

> 1. Calculate the value's element type `E`:
> 1. If `p` has a type argument `T`, then `E` is the type `T`.
> 2. Else if `M` implements `List` for some `T` then `E` is `T`.
> 3. Else if `M` is `dynamic` then `E` is `dynamic`.
> 4. Else `E` is `Object?`.

Number 3 means that `var [int a, String b] = [1, "a"] as dynamic;` should infer a list element type of `dynamic`.

Instead it uses `Never`. (And/or it casts `[1, "a"]` to `List` before getting there.)

That `Never` is likely not taken directly from the [context type schema](https://github.com/dart-lang/language/blob/main/accepted/3.0/patterns/feature-specification.md#pattern-context-type-schema) of the pattern, computed in step one to be `List` where `Never` is **Down** of `int` and `String`, because that schema is only used for inferring the static type of the matched value.

More likely there is an implicit downcast from `dynamic` to the context type `List`, which means that the RHS gets a static matched value type of `List` instead of retaining the `dynamic`, which then goes back and makes the pattern infer `` as its required element type (`M implements List for some `T``), and `Never` is (trivially) a valid element type for all its sub-patterns, so type checking succeeds.

That's somewhat consistent with what we do for other assignments, inserting the implicit downcast *during* inference of the RHS, but it's incorrect for pattern declarations, where the downcast is specified to happen later, after we have found the _required type_ of the pattern.

The spec stats, *after* step 3 has inferred a required type, `T`, from the pattern `p` and the matched value type `M`, that:
> If `p` with required type `T` is in an irrefutable context:
> * If `M` is `dynamic` and `T` is not `dynamic`, then an implicit cast from `dynamic` to `T` is made before the pattern binds the value, tests the value's type, destructures the value, or invokes a function with the value as a target or argument. During destructuring, an implicit cast from `dynamic` is allowed, which may fail and throw an exception at runtime.

A pattern declaration is an irrefutable context, so the downcast from `dynamic` must be done to the *required* type from step 3, not the context type schema from step 1, and the type checking in step 3 should use the un-coerced type of the initializer expression (`dynamic`) here.

So, inference for:
```dart
var [int a, String b] = [1, "a"] as dynamic;
```
*should* proceed as:
* Step 1: context type schema of pattern is `List`, with `Never` being the lower bound of `int` and `String`.
* Step 2: `as` ignores context type schema, so RHS becomes `[1, "a"] as dynamic` with static type `dynamic`.
* Step 3: element type `E` is `dynamic` because matched value type is `dynamic`, so `var [int a, String b] = ...`.

*Then* insert required downcast from `dynamic`, so the result after inference is:
```dart
var [int a, String b] = ([1, "a"] as dynamic) as List;
```

The current implementation probably inserts the `as List` during inference of the RHS, which it shouldn't.
(So definitely @stereotype441!)

More generally, we could assume that all declaration assignments has a third step which infers the required type of the variable, which happens before coercions:
* For `int x = ...` the required type is the declared type (and the same as the context type), so when we downcast to the required type, we downcast is to (the same type as) the context type.
* For `var x = ...` the required type is the inferred declared type, which is the type of the RHS (unless `Null`), so no downcast is ever required.
* But for `var pattern = ...`, the required type inferred from the pattern and matched value type may differ from both the context type and the static matched value type, and that is what we downcast to.

(One could then hope that a raw types could one day be inferred that way. `List x = [1];` would introduce a context type of `List<_>`, the RHS would have a static type of `List`, and the third step to infer the declared/required type as `List`. Which you can effectively do today as `var ([..._] && x) = [1];`, which requires a `List`, but infers its element type. Shouldn't `var (List() && x)` = [1];` also work? It seems object patterns use I2B instead of `_` for the schema, should we change that? "If the type the object name resolves to is generic, and no type arguments are specified, then instantiate to bounds is used to fill in provisional type arguments for the purpose of determining the context type schema.")

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.