Skip to content

Inconsistent promotion behaviour between assignments inside and outside of patterns #2857

@stereotype441

Description

@stereotype441

In my work on flow analysis for patterns, I've made the decision that any pattern match with a required type should promote the type of the scrutinee. That means that, for instance, the following pattern match promotes x:

num x = ...;
switch (x) {
  case int():
    x.isEven; // OK: `x` has been promoted to `int`, and `int` supports `.isEven`
}

And so does this:

num x = ...;
switch (x) {
  case int y:
    x.isEven; // also OK
}

To be consistent, I've made pattern variable declarations and pattern assignments behave the same way. It's only observable when the scrutinee is dynamic, because otherwise the assignment would be invalid. So, for example:

dynamic x = ...;
var (int y) = x; // OK; `x` is downcast to `int`
x.foo(); // ERROR: `x` has been promoted to `int`, and there is no such method `int.foo`

and:

dynamic x = ...;
int y;
(y) = x; // OK; `x` is downcast to `int`
x.foo(); // ERROR: `x` has been promoted to `int`, and there is no such method `int.foo`

However, this feels a little weird, because we don't promote the RHS of ordinary assignments and variable declarations, e.g.:

dynamic x = ...;
int y = x; // OK; `x` is downcast to `int`
x.foo(); // OK; the call to `foo` is dynamically dispatched

and:

dynamic x = ...;
int y;
y = x; // OK; `x` is downcast to `int`
x.foo(); // OK; the call to `foo` is dynamically dispatched

A related problem is that with ordinary assignment expressions, a coercion such as an implicit tearoff of .call affects the value of the assignment expression, e.g.:

class C {
  void call() {}
  void m() {}
}
f(C c) {
  void Function() g;
  var x = g = c; // `g = c` is interpreted as `g = c.call`, so `x` has inferred type `void Function()`
  x.m(); // ERROR: type `void Function()` has no such method `m`
}

But for pattern matches it doesn't really make sense to apply coercions to the right hand side of the assignment, e.g.:

class C {
  void call() {}
  void m() {}
}
f((C, int) x) {
  void Function() g;
  var y = (g, _) = x;
  // tearoff should probably happen as part of the subpattern match of `g`, so `y` should get inferred type
  // `(C, int)`, and thus this should be valid:
  y.$1.m();
}

In which case how do we want this code to behave?

class C {
  void call() {}
  void m() {}
}
f(C c) {
  void Function() g;
  var x = (g) = c;
  x.m(); // ???
}

For consistency with non-pattern assignments, it would make sense to have an error (because the value of (g) = c is the coerced value); for consistency with pattern assignments, it would make sense to have no error (because the coercion happened as part of the subpattern match of g).

Personally, my preference is to say that all pattern assignments and pattern variable declarations should follow the new rules for patterns, and we shouldn't worry about these subtle inconsistencies with old non-pattern behaviour. (In fact, in the long term, I would love to change the behaviour of old-style assignments and variable declarations to match the new pattern behaviour, but that's another story). In which case, in (g) = c, the coercion would happen as part of the subpattern match of g, and therefore x would have type C, so x.m() would be valid.

But I would love to hear other people's thoughts.
CC @dart-lang/language-team

Metadata

Metadata

Assignees

No one assigned

    Labels

    flow-analysisDiscussions about possible future improvements to flow analysispatternsIssues related to pattern matching.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions