Skip to content

#3057. Add switch statements/expressions patterns matching tests #3169

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

sgrekhov
Copy link
Contributor

@sgrekhov sgrekhov commented May 6, 2025

No description provided.

@sgrekhov sgrekhov marked this pull request as draft May 6, 2025 19:56
@sgrekhov sgrekhov marked this pull request as ready for review May 7, 2025 08:47
@eernstg
Copy link
Member

eernstg commented May 9, 2025

This is again an area where the specification needs to be updated and/or completed.

There is a notion of 'values' which are being promoted, in addition to local variables or instance variables (being private and satisfying some other constraints).

In particular, if the 'matched value' during a pattern matching process is promoted then this can give rise to a promotion to the scrutinee expression, if it is a promotable variable:

void main() {
  Object? o = 42 as dynamic;
  switch (o) {
    case num n when n is int:
      n.isEven;
      o.floor();
  }
}

This is currently accepted in DartPad (any channel), and it illustrates that n can be declared with a type like num and then promoted in the when clause (as usual, you might say), but o is also promoted when it is known that the match with num n has succeeded, but not when n is tested in the when clause.

An interesting case is records:

void main() {
  Object? o1 = 42 as dynamic, o2 = "Hello" as dynamic;
  switch ((o1, o2)) {
    case (int, String) _:
      o1.isEven; // Error.
      o2.substring(0); // Error.
  }
}

This shows that the promotion which was propagated from the matched value to the variable in the first example does not occur in the second example.

So we'd need to come up with a set of useful assumptions outlining the precise rules, or we'd need to block tests on this particular topic until we know more.

@stereotype441, WDYT?

@stereotype441
Copy link
Member

This is again an area where the specification needs to be updated and/or completed.

There is a notion of 'values' which are being promoted, in addition to local variables or instance variables (being private and satisfying some other constraints).

In particular, if the 'matched value' during a pattern matching process is promoted then this can give rise to a promotion to the scrutinee expression, if it is a promotable variable:

void main() {
  Object? o = 42 as dynamic;
  switch (o) {
    case num n when n is int:
      n.isEven;
      o.floor();
  }
}

This is currently accepted in DartPad (any channel), and it illustrates that n can be declared with a type like num and then promoted in the when clause (as usual, you might say), but o is also promoted when it is known that the match with num n has succeeded, but not when n is tested in the when clause.

An interesting case is records:

void main() {
  Object? o1 = 42 as dynamic, o2 = "Hello" as dynamic;
  switch ((o1, o2)) {
    case (int, String) _:
      o1.isEven; // Error.
      o2.substring(0); // Error.
  }
}

This shows that the promotion which was propagated from the matched value to the variable in the first example does not occur in the second example.

So we'd need to come up with a set of useful assumptions outlining the precise rules, or we'd need to block tests on this particular topic until we know more.

@stereotype441, WDYT?

@eernstg and I discussed this a bit on Monday, and decided that what makes the most sense is for me to continue replying to questions like this one with informal descriptions of flow analysis behavior, and in the background he and I will work together to make those descriptions more formal and integrate them into the spec.

So, with that in mind, here's an informal description of how promotion in a switch works:

  • After visiting the "scrutinee" of a switch (the expression in parentheses after the switch keyword), flow analysis creates a "shadow variable" to represent the matched value.
    • I haven't formally defined the notion of "shadow variable" in the spec yet, but informally, a shadow variable is a synthetic variable used by flow analysis to represent a value that the compiler could, in principle, decide to cache in a local variable as part of the compilation process.
    • All shadow variables are final.
    • The type of the scrutinee shadow variable is the static type of the scrutinee expression.
    • If the scrutinee of the switch refers to something promotable (a local variable or a promotable field), then flow analysis also makes a note of what the scrutinee refers to, as well as the current "version" of that thing.
      • I haven't formally defined the notion of "version" either. Informally, a version represents a stable value in the program. At any given point in control flow, each promotable thing has an associated version; assigning to a local variable causes that variable (and all of its promotable fields, and all of their promotable fields, etc.) to take on new versions that are distinct from previously assigned versions. Control flow joins also cause promotable things to take on new versions.
  • When flow analysis is visiting a pattern, there is always a shadow variable representing the "matched value" (the value that the pattern is being matched against).
  • When flow analysis visits a pattern that implicitly performs a type check, flow analysis promotes the shadow variable that represents the matched value, in much the same way that it would promote the target of an is expression. The set of patterns that implicitly perform a type check are based on the Matching (refuting and destructuring) section of the patterns spec. For example, the pattern T x implicitly checks that the matched value has type T, and the pattern <int>[1, 2, ...var x] implicitly checks that the matched value has type List<int>.
    • Any time one of these type checks occurs, flow analysis also checks whether the matched value is the scrutinee or a subpattern; if it's the scrutinee, and the scrutinee referred to something promotable, and the current version of the scrutinee hasn't changed since the start of the switch, then flow analysis also promotes the scrutinee.
  • When flow analysis visits a subpattern of another pattern, it usually creates a fresh shadow variable to represent the subpattern. Exceptions: || and && patterns.
  • Record patterns are special:
    • When flow analysis visits a record pattern, it first looks at the shape of the record (the number of unnamed fields and the names of the named fields), converting that to a type using Object? for the type of every field. Then it performs a type check based on the resulting type. So for example, the first step in matching the pattern (int x, <String>[...var y]) is for the value to be type checked against the type (Object?, Object?).
    • Then flow analysis visits all the subpatterns of the record pattern. As it does so, it records the "demonstrated type" of each subpattern. "demonstated type" hasn't been formally defined yet either, but informally, it's the type that the field can be reasonably be assumed to have assuming the field's subpattern matches. So for instance, the demonstrated type of int x is int, and the demonstrated type of <String>[...var y] is List<String>.
    • Finally, flow analysis performs a synthetic type check based on the shape of the record and the demonstrated types of subpatterns. (Again, considering the example of (int x, <String>[...var y]), this check uses the type (int, List<String>). This is a synthetic type check performed by flow analysis for the purpose of promoting the matched value; it doesn't correspond to a real type check performed by the compiled code. Accordingly, flow analysis knows that this synthetic type check is guaranteed to succeed.

Tying that all together, here is what happens for:

void main() {
  Object? o = 42 as dynamic;
  switch (o) {
    case num n when n is int:
      n.isEven;
      o.floor();
  }
}
  • A shadow variable is allocated for the scrutinee; let's call it $scrutinee. It has type Object?.
  • Flow analysis notes that the scrutinee is o, and the current version of o is o#1.
  • The pattern num n implicitly performs a type check using the type num, so $scrutinee is promoted from Object? to num. Since the current version of o is still o#1, o is also promoted from Object? to num.
  • The variable n has declared type num.
  • The clause when n is int promotes the variable n from num to int.
  • Since n has been promoted to int, n.isEven is valid.
  • Since o has been promoted to num, o.floor() is valid.

For this code:

void main() {
  Object? o1 = 42 as dynamic, o2 = "Hello" as dynamic;
  switch ((o1, o2)) {
    case (int, String) _:
      o1.isEven; // Error.
      o2.substring(0); // Error.
  }
}
  • A shadow variable is allocated for the scrutinee; let's call it $scrutinee again. It has type (Object?, Object?).
  • But this time the scrutinee expression isn't something promotable.
  • The pattern (int, String) _ implicitly performs a type check using the type (int, String), so $scrutinee is promoted from (Object?, Object?) to (int, String). But no promotion is applied to o1 or o2.
  • Hence o1.isEven and o2.substring(0) are errors.

And here's one more example:

void main() {
  Object? o = 42 as dynamic;
  switch (o) {
    case int _ && var x when (o = 'foo') == 'bar':
      x.isEven;
    case int _ && var y:
      y.isEven;
      o.isEven; // error
  }
}
  • A shadow variable is allocated for the scrutinee ($scrutinee), with type Object?.
  • Flow analysis notes that the scrutinee corresponds to the promotable variable o, with version o#1.
  • In the first case clause, the pattern int _, implicitly type checks $scrutinee against int, causing it to be promoted. Since the version of o is still o#1, o is also promoted to int.
  • The pattern var x is matched against $scrutinee, which has been promoted to int, so the declared type of x is int.
  • The clause when (o = 'foo') == 'bar' assigns to o, so o is demoted back to Object?. Also, the version of o is now o#2.
  • We know that (o = 'foo') == 'bar' is impossible, but flow analysis can't tell that, so it considers x.isEven to be reachable.
  • x has declared type int, so x.isEven is allowed.
  • Now, before reaching the second case clause, flow analysis has to join two flow control paths: the flow control path in which int _ failed to match, and the one in which (o = 'foo') == 'bar' evaluated to false.
    • It doesn't have to join with the control flow path where var x failed to match, because var x is guaranteed to always match.
    • $scrutinee was promoted on one of these two control flow paths but not the other, so the type of $scrutinee is demoted back to Object?.
    • o has version o#1 in one of these two control flow paths, and version o#2 in the other, so o is assigned a new version of o#3.
  • In the second case clause, the pattern int _ implicitly type checks $scrutinee against int, causing it to be promoted. Since the version of o is no longer o#1, o is not promoted.
  • The pattern var y is matched against $scrutinee, which has been promoted to int, so the declared type of y is int.
  • y has declared type int, so y.isEven is allowed.
  • o has not been promoted, so o.isEven is an error.

I hope this helps! Please feel free to file issues if you have follow-up questions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants