Shared type analysis: add support for `when` clauses and fix label support.

Support for `when` clauses requires flow analysis integration, so that
`when` clauses can promote variables, e.g.:

    f(int x, String? y) {
      switch (x) {
        case 0 when y != null:
          // y is known to be non-null here
      }
    }

Support for labels in switch statements had a small flaw: we weren't
reporting an error in the case where a label shared a case body with a
pattern that tried to bind a variable, e.g.:

    f(int x) {
      switch (x) {

        L: // Error: does not mind the variable `y`
        case var y:
          ...
      }
    }
Change-Id: I0b2bb4721a6b3a8f7898df682b24b75ddb6e44ae
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/256605
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart b/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart
index f7d88b7..e315ad9 100644
--- a/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart
@@ -530,6 +530,17 @@
   @visibleForTesting
   SsaNode<Type>? ssaNodeForTesting(Variable variable);
 
+  /// Call this method just after visiting a `when` part of a case clause.  See
+  /// [switchStatement_expressionEnd] for details.
+  ///
+  /// [when] should be the expression following the `when` keyword.
+  void switchStatement_afterWhen(Expression when);
+
+  /// Call this method just before visiting a sequence of two or more `case` or
+  /// `default` clauses that share a body.  See [switchStatement_expressionEnd]
+  /// for details.`
+  void switchStatement_beginAlternatives();
+
   /// Call this method just before visiting one of the cases in the body of a
   /// switch statement.  See [switchStatement_expressionEnd] for details.
   ///
@@ -547,6 +558,16 @@
   /// were listed in cases.
   void switchStatement_end(bool isExhaustive);
 
+  /// Call this method just after visiting a `case` or `default` clause, if it
+  /// shares a body with at least one other `case` or `default` clause.  See
+  /// [switchStatement_expressionEnd] for details.`
+  void switchStatement_endAlternative();
+
+  /// Call this method just after visiting a sequence of two or more `case` or
+  /// `default` clauses that share a body.  See [switchStatement_expressionEnd]
+  /// for details.`
+  void switchStatement_endAlternatives();
+
   /// Call this method just after visiting the expression part of a switch
   /// statement or expression.  [switchStatement] should be the switch statement
   /// itself (or `null` if this is a switch expression).
@@ -554,9 +575,18 @@
   /// The order of visiting a switch statement should be:
   /// - Visit the switch expression.
   /// - Call [switchStatement_expressionEnd].
-  /// - For each switch case (including the default case, if any):
+  /// - For each case body:
   ///   - Call [switchStatement_beginCase].
-  ///   - Visit the case.
+  ///   - If there is more than one `case` or `default` clause associated with
+  ///     this case body, call [switchStatement_beginAlternatives].
+  ///   - For each `case` or `default` clause associated with this case body:
+  ///     - If a `when` clause is present, visit it and then call
+  ///       [switchStatement_afterWhen].
+  ///     - If there is more than one `case` or `default` clause associated with
+  ///       this case body, call [switchStatement_endAlternative].
+  ///   - If there is more than one `case` or `default` clause associated with
+  ///     this case body, call [switchStatement_endAlternatives].
+  ///   - Visit the case body.
   /// - Call [switchStatement_end].
   void switchStatement_expressionEnd(Statement? switchStatement);
 
@@ -1173,6 +1203,18 @@
   }
 
   @override
+  void switchStatement_afterWhen(Expression when) {
+    _wrap('switchStatement_afterWhen($when)',
+        () => _wrapped.switchStatement_afterWhen(when));
+  }
+
+  @override
+  void switchStatement_beginAlternatives() {
+    _wrap('switchStatement_beginAlternatives()',
+        () => _wrapped.switchStatement_beginAlternatives());
+  }
+
+  @override
   void switchStatement_beginCase(bool hasLabel, Statement? node) {
     _wrap('switchStatement_beginCase($hasLabel, $node)',
         () => _wrapped.switchStatement_beginCase(hasLabel, node));
@@ -1185,6 +1227,18 @@
   }
 
   @override
+  void switchStatement_endAlternative() {
+    _wrap('switchStatement_endAlternative()',
+        () => _wrapped.switchStatement_endAlternative());
+  }
+
+  @override
+  void switchStatement_endAlternatives() {
+    _wrap('switchStatement_endAlternatives()',
+        () => _wrapped.switchStatement_endAlternatives());
+  }
+
+  @override
   void switchStatement_expressionEnd(Statement? switchStatement) {
     _wrap('switchStatement_expressionEnd($switchStatement)',
         () => _wrapped.switchStatement_expressionEnd(switchStatement));
@@ -3686,6 +3740,22 @@
       .variableInfo[promotionKeyStore.keyForVariable(variable)]?.ssaNode;
 
   @override
+  void switchStatement_afterWhen(Expression when) {
+    ExpressionInfo<Type>? expressionInfo = _getExpressionInfo(when);
+    if (expressionInfo != null) {
+      _current = expressionInfo.ifTrue;
+    }
+  }
+
+  @override
+  void switchStatement_beginAlternatives() {
+    _current = _current.split();
+    _SwitchAlternativesContext<Type> context =
+        new _SwitchAlternativesContext<Type>(_current);
+    _stack.add(context);
+  }
+
+  @override
   void switchStatement_beginCase(bool hasLabel, Statement? node) {
     _SimpleStatementContext<Type> context =
         _stack.last as _SimpleStatementContext<Type>;
@@ -3715,6 +3785,21 @@
   }
 
   @override
+  void switchStatement_endAlternative() {
+    _SwitchAlternativesContext<Type> context =
+        _stack.last as _SwitchAlternativesContext<Type>;
+    context._combinedModel = _join(context._combinedModel, _current);
+    _current = context._previous;
+  }
+
+  @override
+  void switchStatement_endAlternatives() {
+    _SwitchAlternativesContext<Type> context =
+        _stack.removeLast() as _SwitchAlternativesContext<Type>;
+    _current = context._combinedModel!.unsplit();
+  }
+
+  @override
   void switchStatement_expressionEnd(Statement? switchStatement) {
     _current = _current.split();
     _SimpleStatementContext<Type> context =
@@ -4515,12 +4600,24 @@
   }
 
   @override
+  void switchStatement_afterWhen(Expression when) {}
+
+  @override
+  void switchStatement_beginAlternatives() {}
+
+  @override
   void switchStatement_beginCase(bool hasLabel, Statement? node) {}
 
   @override
   void switchStatement_end(bool isExhaustive) {}
 
   @override
+  void switchStatement_endAlternative() {}
+
+  @override
+  void switchStatement_endAlternatives() {}
+
+  @override
   void switchStatement_expressionEnd(Statement? switchStatement) {}
 
   @override
@@ -4767,6 +4864,14 @@
       'checkpoint: $_checkpoint)';
 }
 
+class _SwitchAlternativesContext<Type extends Object> extends _FlowContext {
+  final FlowModel<Type> _previous;
+
+  FlowModel<Type>? _combinedModel;
+
+  _SwitchAlternativesContext(this._previous);
+}
+
 /// Specialization of [ExpressionInfo] for the case where the information we
 /// have about the expression is trivial (meaning we know by construction that
 /// the expression's [after], [ifTrue], and [ifFalse] models are all the same).
diff --git a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart
index cdec2a3..a84e2dc 100644
--- a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart
@@ -14,29 +14,36 @@
   /// For a `case` clause, the case pattern.  For a `default` clause, `null`.
   final Node? pattern;
 
+  /// For a `case` clause that has a `when` part, the expression following
+  /// `when`.  Otherwise `null`.
+  final Expression? when;
+
   /// The body of the `case` or `default` clause.
   final Expression body;
 
-  ExpressionCaseInfo(this.pattern, this.body);
+  ExpressionCaseInfo({required this.pattern, this.when, required this.body});
 }
 
 /// Information supplied by the client to [TypeAnalyzer.analyzeSwitchStatement]
 /// about an individual `case` or `default` clause.
 ///
 /// The client is free to `implement` or `extend` this class.
-class StatementCaseInfo<Statement, Node> {
+class StatementCaseInfo<Statement, Expression, Node> {
   /// The AST node for this `case` or `default` clause.  This is used for error
   /// reporting, in case errors arise from mismatch among the variables bound by
   /// various cases that share a body.
   final Node node;
 
-  /// Indicates whether this `case` or `default` clause is preceded by one or
-  /// more `goto` labels.
-  final bool hasLabel;
+  /// The labels preceding this `case` or `default` clause, if any.
+  final List<Node> labels;
 
   /// For a `case` clause, the case pattern.  For a `default` clause, `null`.
   final Node? pattern;
 
+  /// For a `case` clause that has a `when` part, the expression following
+  /// `when`.  Otherwise `null`.
+  final Expression? when;
+
   /// The statements following this `case` or `default` clause.  If this list is
   /// empty, and this is not the last `case` or `default` clause, this clause
   /// will be considered to share a body with the `case` or `default` clause
@@ -45,8 +52,9 @@
 
   StatementCaseInfo(
       {required this.node,
-      required this.hasLabel,
+      this.labels = const [],
       required this.pattern,
+      this.when,
       required this.body});
 }
 
@@ -70,6 +78,9 @@
 mixin TypeAnalyzer<Node extends Object, Statement extends Node,
         Expression extends Node, Variable extends Object, Type extends Object>
     implements VariableBindingCallbacks<Node, Variable, Type> {
+  /// Returns the type `bool`.
+  Type get boolType;
+
   /// Returns the type `double`.
   Type get doubleType;
 
@@ -175,6 +186,8 @@
   /// - [analyzeExpression]
   /// - For each `case` or `default` clause:
   ///   - [dispatchPattern] if this is a `case` clause
+  ///   - [analyzeExpression] if this is a `case` clause with a `when` part
+  ///   - [handleCaseHead] if this is a `case` clause
   ///   - [handleDefault] if this is a `default` clause
   ///   - [handleCase_afterCaseHeads]
   ///   - [analyzeExpression]
@@ -196,10 +209,17 @@
       if (pattern != null) {
         dispatchPattern(pattern)
             .match(expressionType, bindings, isFinal: true, isLate: false);
+        Expression? when = caseInfo.when;
+        bool hasWhen = when != null;
+        if (hasWhen) {
+          analyzeExpression(when, boolType);
+          flow?.switchStatement_afterWhen(when);
+        }
+        handleCaseHead(hasWhen: hasWhen);
       } else {
         handleDefault();
       }
-      handleCase_afterCaseHeads(1);
+      handleCase_afterCaseHeads(const [], 1);
       Type type = analyzeExpression(caseInfo.body, context);
       if (lubType == null) {
         lubType = type;
@@ -217,8 +237,11 @@
   /// Invokes the following [TypeAnalyzer] methods (in order):
   /// - [dispatchExpression]
   /// - For each `case` or `default` body:
-  ///   - [dispatchPattern] for each `case` pattern associated with the body
-  ///   - [handleDefault] if a `default` clause is associated with the body
+  ///   - For each `case` or `default` clause associated with the body:
+  ///     - [dispatchPattern] if this is a `case` clause
+  ///     - [analyzeExpression] if this is a `case` clause with a `when` part
+  ///     - [handleCaseHead] if this is a `case` clause
+  ///     - [handleDefault] if this is a `default` clause
   ///   - [handleCase_afterCaseHeads]
   ///   - [dispatchStatement] for each statement in the body
   ///   - [finishStatementCase]
@@ -228,42 +251,65 @@
   /// length of [cases] because a case with no statements get merged into the
   /// case that follows).
   int analyzeSwitchStatement(Statement node, Expression scrutinee,
-      List<StatementCaseInfo<Statement, Node>> cases) {
+      List<StatementCaseInfo<Statement, Expression, Node>> cases) {
     Type expressionType = analyzeExpression(scrutinee, unknownType);
     flow?.switchStatement_expressionEnd(node);
-    bool hasLabel = false;
-    List<StatementCaseInfo<Statement, Node>>? casesInThisExecutionPath;
+    List<Node> labels = [];
+    List<StatementCaseInfo<Statement, Expression, Node>>?
+        casesInThisExecutionPath;
     int numExecutionPaths = 0;
     for (int i = 0; i < cases.length; i++) {
-      StatementCaseInfo<Statement, Node> caseInfo = cases[i];
-      hasLabel = hasLabel || caseInfo.hasLabel;
+      StatementCaseInfo<Statement, Expression, Node> caseInfo = cases[i];
+      labels.addAll(caseInfo.labels);
       (casesInThisExecutionPath ??= []).add(caseInfo);
       if (i == cases.length - 1 || caseInfo.body.isNotEmpty) {
         numExecutionPaths++;
-        flow?.switchStatement_beginCase(hasLabel, node);
+        flow?.switchStatement_beginCase(labels.isNotEmpty, node);
         VariableBindings<Node, Variable, Type> bindings =
             new VariableBindings(this);
         bindings.startAlternatives();
-        for (int i = 0; i < casesInThisExecutionPath.length; i++) {
-          StatementCaseInfo<Statement, Node> caseInfo =
+        // Labels count as empty patterns for the purposes of bindings.
+        for (Node label in labels) {
+          bindings.startAlternative(label);
+          bindings.finishAlternative();
+        }
+        int numCasesInThisExecutionPath = casesInThisExecutionPath.length;
+        if (numCasesInThisExecutionPath > 1) {
+          flow?.switchStatement_beginAlternatives();
+        }
+        for (int i = 0; i < numCasesInThisExecutionPath; i++) {
+          StatementCaseInfo<Statement, Expression, Node> caseInfo =
               casesInThisExecutionPath[i];
           bindings.startAlternative(caseInfo.node);
           Node? pattern = caseInfo.pattern;
           if (pattern != null) {
             dispatchPattern(pattern)
                 .match(expressionType, bindings, isFinal: true, isLate: false);
+            Expression? when = caseInfo.when;
+            bool hasWhen = when != null;
+            if (hasWhen) {
+              analyzeExpression(when, boolType);
+              flow?.switchStatement_afterWhen(when);
+            }
+            handleCaseHead(hasWhen: hasWhen);
           } else {
             handleDefault();
           }
           bindings.finishAlternative();
+          if (numCasesInThisExecutionPath > 1) {
+            flow?.switchStatement_endAlternative();
+          }
         }
         bindings.finishAlternatives();
-        handleCase_afterCaseHeads(casesInThisExecutionPath.length);
+        if (numCasesInThisExecutionPath > 1) {
+          flow?.switchStatement_endAlternatives();
+        }
+        handleCase_afterCaseHeads(labels, numCasesInThisExecutionPath);
         for (Statement statement in caseInfo.body) {
           dispatchStatement(statement);
         }
         finishStatementCase(node, i, caseInfo.body.length);
-        hasLabel = false;
+        labels.clear();
         casesInThisExecutionPath = null;
       }
     }
@@ -334,7 +380,10 @@
   void finishStatementCase(Statement node, int caseIndex, int numStatements);
 
   /// See [analyzeSwitchStatement] and [analyzeSwitchExpression].
-  void handleCase_afterCaseHeads(int numHeads);
+  void handleCase_afterCaseHeads(List<Node> labels, int numHeads);
+
+  /// See [analyzeSwitchStatement] and [analyzeSwitchExpression].
+  void handleCaseHead({required bool hasWhen});
 
   /// See [analyzeConstOrLiteralPattern].
   void handleConstOrLiteralPattern();
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
index 35d1050..7a806c4 100644
--- a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
@@ -1628,10 +1628,11 @@
 
     test('labeledBlock without break', () {
       var x = Var('x');
+      var l = Label('l');
       h.run([
         declare(x, type: 'int?', initializer: expr('int?')),
         if_(x.expr.isNot('int'), [
-          labeled((_) => return_()),
+          l.thenStmt(return_()),
         ]),
         checkPromoted(x, 'int'),
       ]);
@@ -1639,15 +1640,16 @@
 
     test('labeledBlock with break joins', () {
       var x = Var('x');
+      var l = Label('l');
       h.run([
         declare(x, type: 'int?', initializer: expr('int?')),
         if_(x.expr.isNot('int'), [
-          labeled((t) => block([
-                if_(expr('bool'), [
-                  break_(t),
-                ]),
-                return_(),
-              ])),
+          l.thenStmt(block([
+            if_(expr('bool'), [
+              break_(l),
+            ]),
+            return_(),
+          ])),
         ]),
         checkNotPromoted(x),
       ]);
@@ -1914,8 +1916,8 @@
       h.run([
         switchExpr(throw_(expr('C')), [
           caseExpr(intLiteral(0).pattern,
-              checkReachable(false).thenExpr(intLiteral(1))),
-          defaultExpr(checkReachable(false).thenExpr(intLiteral(2))),
+              body: checkReachable(false).thenExpr(intLiteral(1))),
+          defaultExpr(body: checkReachable(false).thenExpr(intLiteral(2))),
         ]).stmt,
         checkReachable(false),
       ]);
@@ -1924,8 +1926,8 @@
     test('switchExpression throw in case body has isolated effect', () {
       h.run([
         switchExpr(expr('int'), [
-          caseExpr(intLiteral(0).pattern, throw_(expr('C'))),
-          defaultExpr(checkReachable(true).thenExpr(intLiteral(2))),
+          caseExpr(intLiteral(0).pattern, body: throw_(expr('C'))),
+          defaultExpr(body: checkReachable(true).thenExpr(intLiteral(2))),
         ]).stmt,
         checkReachable(true),
       ]);
@@ -1934,8 +1936,8 @@
     test('switchExpression throw in all case bodies affects flow after', () {
       h.run([
         switchExpr(expr('int'), [
-          caseExpr(intLiteral(0).pattern, throw_(expr('C'))),
-          defaultExpr(throw_(expr('C'))),
+          caseExpr(intLiteral(0).pattern, body: throw_(expr('C'))),
+          defaultExpr(body: throw_(expr('C'))),
         ]).stmt,
         checkReachable(false),
       ]);
@@ -1952,7 +1954,7 @@
       h.run([
         switchExpr(expr('int'), [
           caseExpr(x.pattern(type: 'int?'),
-              checkNotPromoted(x).thenExpr(nullLiteral)),
+              body: checkNotPromoted(x).thenExpr(nullLiteral)),
         ]).stmt,
       ]);
     });
@@ -1962,10 +1964,10 @@
         switch_(
             throw_(expr('C')),
             [
-              case_(intLiteral(0).pattern, [
+              case_(intLiteral(0).pattern, body: [
                 checkReachable(false),
               ]),
-              case_(intLiteral(1).pattern, [
+              case_(intLiteral(1).pattern, body: [
                 checkReachable(false),
               ]),
             ],
@@ -1985,7 +1987,7 @@
         switch_(
             expr('int'),
             [
-              case_(x.pattern(type: 'int?'), [
+              case_(x.pattern(type: 'int?'), body: [
                 checkNotPromoted(x),
               ]),
             ],
@@ -1993,6 +1995,31 @@
       ]);
     });
 
+    test('switchStatement_afterWhen() promotes', () {
+      var x = Var('x');
+      h.run([
+        switch_(
+            expr('num'),
+            [
+              case_(x.pattern(), when: x.expr.is_('int'), body: [
+                checkPromoted(x, 'int'),
+              ]),
+            ],
+            isExhaustive: true),
+      ]);
+    });
+
+    test('switchStatement_afterWhen() called for switch expressions', () {
+      var x = Var('x');
+      h.run([
+        switchExpr(expr('num'), [
+          caseExpr(x.pattern(),
+              when: x.expr.is_('int'),
+              body: checkPromoted(x, 'int').thenExpr(expr('String'))),
+        ]).stmt,
+      ]);
+    });
+
     test('switchStatement_beginCase(false) restores previous promotions', () {
       var x = Var('x');
       h.run([
@@ -2001,12 +2028,12 @@
         switch_(
             expr('int'),
             [
-              case_(intLiteral(0).pattern, [
+              case_(intLiteral(0).pattern, body: [
                 checkPromoted(x, 'int'),
                 x.write(expr('int?')).stmt,
                 checkNotPromoted(x),
               ]),
-              case_(intLiteral(1).pattern, [
+              case_(intLiteral(1).pattern, body: [
                 checkPromoted(x, 'int'),
                 x.write(expr('int?')).stmt,
                 checkNotPromoted(x),
@@ -2024,7 +2051,7 @@
         switch_(
             expr('int'),
             [
-              case_(intLiteral(0).pattern, [
+              case_(intLiteral(0).pattern, body: [
                 checkPromoted(x, 'int'),
                 x.write(expr('int?')).stmt,
                 checkNotPromoted(x),
@@ -2043,7 +2070,7 @@
         switch_(
             expr('int'),
             [
-              case_(intLiteral(0).pattern, [
+              case_(intLiteral(0).pattern, body: [
                 checkPromoted(x, 'int'),
                 localFunction([
                   x.write(expr('int?')).stmt,
@@ -2057,6 +2084,7 @@
 
     test('switchStatement_beginCase(true) un-promotes', () {
       var x = Var('x');
+      var l = Label('l');
       late SsaNode<Type> ssaBeforeSwitch;
       h.run([
         declare(x, type: 'int?', initializer: expr('int?')),
@@ -2067,16 +2095,13 @@
               getSsaNodes((nodes) => ssaBeforeSwitch = nodes[x]!),
             ])),
             [
-              case_(
-                  intLiteral(0).pattern,
-                  [
-                    checkNotPromoted(x),
-                    getSsaNodes(
-                        (nodes) => expect(nodes[x], isNot(ssaBeforeSwitch))),
-                    x.write(expr('int?')).stmt,
-                    checkNotPromoted(x),
-                  ],
-                  hasLabel: true),
+              l.thenCase(case_(intLiteral(0).pattern, body: [
+                checkNotPromoted(x),
+                getSsaNodes(
+                    (nodes) => expect(nodes[x], isNot(ssaBeforeSwitch))),
+                x.write(expr('int?')).stmt,
+                checkNotPromoted(x),
+              ])),
             ],
             isExhaustive: false),
       ]);
@@ -2084,23 +2109,21 @@
 
     test('switchStatement_beginCase(true) handles write captures in cases', () {
       var x = Var('x');
+      var l = Label('l');
       h.run([
         declare(x, type: 'int?', initializer: expr('int?')),
         x.expr.as_('int').stmt,
         switch_(
             expr('int'),
             [
-              case_(
-                  intLiteral(0).pattern,
-                  [
-                    x.expr.as_('int').stmt,
-                    checkNotPromoted(x),
-                    localFunction([
-                      x.write(expr('int?')).stmt,
-                    ]),
-                    checkNotPromoted(x),
-                  ],
-                  hasLabel: true),
+              l.thenCase(case_(intLiteral(0).pattern, body: [
+                x.expr.as_('int').stmt,
+                checkNotPromoted(x),
+                localFunction([
+                  x.write(expr('int?')).stmt,
+                ]),
+                checkNotPromoted(x),
+              ])),
             ],
             isExhaustive: false),
       ]);
@@ -2119,7 +2142,7 @@
         switch_(
             expr('int'),
             [
-              case_(intLiteral(0).pattern, [
+              case_(intLiteral(0).pattern, body: [
                 x.expr.as_('int').stmt,
                 y.write(expr('int?')).stmt,
                 break_(),
@@ -2148,13 +2171,13 @@
         switch_(
             expr('int'),
             [
-              case_(intLiteral(0).pattern, [
+              case_(intLiteral(0).pattern, body: [
                 w.expr.as_('int').stmt,
                 y.expr.as_('int').stmt,
                 x.write(expr('int?')).stmt,
                 break_(),
               ]),
-              default_([
+              default_(body: [
                 w.expr.as_('int').stmt,
                 x.expr.as_('int').stmt,
                 y.write(expr('int?')).stmt,
@@ -2176,17 +2199,41 @@
         switch_(
             expr('int'),
             [
-              case_(intLiteral(0).pattern, [
+              case_(intLiteral(0).pattern, body: [
                 x.expr.as_('int').stmt,
                 break_(),
               ]),
-              default_([]),
+              default_(body: []),
             ],
             isExhaustive: true),
         checkNotPromoted(x),
       ]);
     });
 
+    test('switchStatement_endAlternative() joins branches', () {
+      var x = Var('x');
+      var y = Var('y');
+      var z = Var('z');
+      h.run([
+        declare(y, type: 'num'),
+        declare(z, type: 'num'),
+        switch_(
+            expr('num'),
+            [
+              case_(x.pattern(),
+                  when: x.expr.is_('int').and(y.expr.is_('int')), body: []),
+              case_(x.pattern(),
+                  when: y.expr.is_('int').and(z.expr.is_('int')),
+                  body: [
+                    checkNotPromoted(x),
+                    checkPromoted(y, 'int'),
+                    checkNotPromoted(z),
+                  ]),
+            ],
+            isExhaustive: true),
+      ]);
+    });
+
     test('tryCatchStatement_bodyEnd() restores pre-try state', () {
       var x = Var('x');
       var y = Var('y');
diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart
index 5fe88dd..5cbd243 100644
--- a/pkg/_fe_analyzer_shared/test/mini_ast.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart
@@ -28,14 +28,15 @@
 
 Expression booleanLiteral(bool value) => _BooleanLiteral(value);
 
-Statement break_([LabeledStatement? target]) => new _Break(target);
+Statement break_([Label? target]) => new _Break(target);
 
-StatementCase case_(Pattern pattern, List<Statement> body,
-        {bool hasLabel = false}) =>
-    StatementCase._(hasLabel, pattern, _Block(body));
+StatementCase case_(Pattern pattern,
+        {Expression? when, required List<Statement> body}) =>
+    StatementCase._(pattern, when, _Block(body));
 
-ExpressionCase caseExpr(Pattern pattern, Expression expression) =>
-    ExpressionCase._(pattern, expression);
+ExpressionCase caseExpr(Pattern pattern,
+        {Expression? when, required Expression body}) =>
+    ExpressionCase._(pattern, when, body);
 
 /// Creates a pseudo-statement whose function is to verify that flow analysis
 /// considers [variable]'s assigned state to be [expectedAssignedState].
@@ -77,11 +78,11 @@
         isLate: isLate,
         isFinal: isFinal);
 
-StatementCase default_(List<Statement> body, {bool hasLabel = false}) =>
-    StatementCase._(hasLabel, null, _Block(body));
+StatementCase default_({required List<Statement> body}) =>
+    StatementCase._(null, null, _Block(body));
 
-ExpressionCase defaultExpr(Expression expression) =>
-    ExpressionCase._(null, expression);
+ExpressionCase defaultExpr({required Expression body}) =>
+    ExpressionCase._(null, null, body);
 
 Statement do_(List<Statement> body, Expression condition) =>
     _Do(block(body), condition);
@@ -146,12 +147,6 @@
 Literal intLiteral(int value, {bool? expectConversionToDouble}) =>
     new _IntLiteral(value, expectConversionToDouble: expectConversionToDouble);
 
-Statement labeled(Statement Function(LabeledStatement) callback) {
-  var labeledStatement = LabeledStatement._();
-  labeledStatement._body = callback(labeledStatement);
-  return labeledStatement;
-}
-
 Statement localFunction(List<Statement> body) => _LocalFunction(block(body));
 
 Statement match(Pattern pattern, Expression initializer,
@@ -280,12 +275,18 @@
   final Pattern? pattern;
 
   @override
+  final Expression? when;
+
+  @override
   final Expression body;
 
-  ExpressionCase._(this.pattern, this.body) : super._();
+  ExpressionCase._(this.pattern, this.when, this.body) : super._();
 
-  String toString() =>
-      [pattern == null ? 'default:' : 'case $pattern:', '$body'].join(' ');
+  String toString() => [
+        pattern == null ? 'default' : 'case $pattern',
+        if (when != null) ' when $when',
+        ': $body'
+      ].join('');
 
   void _preVisit(AssignedVariables<Node, Var> assignedVariables) {
     pattern?.preVisit(assignedVariables);
@@ -642,23 +643,29 @@
   }
 }
 
-class LabeledStatement extends Statement {
-  late final Statement _body;
+class Label extends Node {
+  final String _name;
 
-  LabeledStatement._();
+  late final Node _binding;
 
-  @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    _body.preVisit(assignedVariables);
+  Label(this._name) : super._();
+
+  StatementCase thenCase(StatementCase case_) {
+    case_.labels.insert(0, this);
+    return case_;
+  }
+
+  Statement thenStmt(Statement statement) {
+    if (statement is! _LabeledStatement) {
+      statement = _LabeledStatement(statement);
+    }
+    statement._labels.insert(0, this);
+    _binding = statement;
+    return statement;
   }
 
   @override
-  String toString() => 'labeled: $_body';
-
-  @override
-  void visit(Harness h) {
-    h.typeAnalyzer.analyzeLabeledStatement(this, _body);
-  }
+  String toString() => _name;
 }
 
 abstract class Literal extends Expression {
@@ -765,16 +772,20 @@
 
 /// Representation of a single case clause in a switch statement.  Use [case_]
 /// to create instances of this class.
-class StatementCase extends Node implements StatementCaseInfo<Statement, Node> {
+class StatementCase extends Node
+    implements StatementCaseInfo<Statement, Expression, Node> {
   @override
-  final bool hasLabel;
+  final List<Label> labels = [];
 
   @override
   final Pattern? pattern;
 
+  @override
+  final Expression? when;
+
   final _Block _statements;
 
-  StatementCase._(this.hasLabel, this.pattern, this._statements) : super._();
+  StatementCase._(this.pattern, this.when, this._statements) : super._();
 
   @override
   List<Statement> get body => _statements.statements;
@@ -783,7 +794,7 @@
   Node get node => this;
 
   String toString() => [
-        if (hasLabel) '<label>:',
+        for (var label in labels) '$label:',
         pattern == null ? 'default:' : 'case $pattern:',
         ...body
       ].join(' ');
@@ -945,7 +956,7 @@
 }
 
 class _Break extends Statement {
-  final LabeledStatement? target;
+  final Label? target;
 
   _Break(this.target);
 
@@ -957,7 +968,7 @@
 
   @override
   void visit(Harness h) {
-    h.typeAnalyzer.analyzeBreakStatement(target);
+    h.typeAnalyzer.analyzeBreakStatement(target?._binding as Statement?);
     h.irBuilder.apply('break', 0);
   }
 }
@@ -1619,6 +1630,27 @@
   }
 }
 
+class _LabeledStatement extends Statement {
+  final List<Label> _labels = [];
+
+  final Statement _body;
+
+  _LabeledStatement(this._body);
+
+  @override
+  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
+    _body.preVisit(assignedVariables);
+  }
+
+  @override
+  String toString() => [..._labels, _body].join(': ');
+
+  @override
+  void visit(Harness h) {
+    h.typeAnalyzer.analyzeLabeledStatement(this, _body);
+  }
+}
+
 class _LocalFunction extends Statement {
   final Statement body;
 
@@ -1752,6 +1784,7 @@
 
   final _irBuilder = MiniIrBuilder();
 
+  @override
   late final Type boolType = Type('bool');
 
   @override
@@ -2083,8 +2116,16 @@
   }
 
   @override
-  void handleCase_afterCaseHeads(int numHeads) {
-    _irBuilder.apply('heads', numHeads);
+  void handleCase_afterCaseHeads(List<Node> labels, int numHeads) {
+    for (var label in labels) {
+      _irBuilder.atom((label as Label)._name);
+    }
+    _irBuilder.apply('heads', numHeads + labels.length);
+  }
+
+  @override
+  void handleCaseHead({required bool hasWhen}) {
+    _irBuilder.apply('head', hasWhen ? 2 : 1);
   }
 
   @override
diff --git a/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart b/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart
index c539b43..241f8f5e 100644
--- a/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart
+++ b/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart
@@ -90,7 +90,7 @@
       test('IR', () {
         h.run([
           switchExpr(expr('int'), [
-            defaultExpr(intLiteral(0)),
+            defaultExpr(body: intLiteral(0)),
           ]).checkIr('switchExpr(expr(int), case(heads(default), 0))').stmt,
         ]);
       });
@@ -98,7 +98,7 @@
       test('scrutinee expression context', () {
         h.run([
           switchExpr(expr('int').checkContext('?'), [
-            defaultExpr(intLiteral(0)),
+            defaultExpr(body: intLiteral(0)),
           ]).inContext('num'),
         ]);
       });
@@ -106,7 +106,7 @@
       test('body expression context', () {
         h.run([
           switchExpr(expr('int'), [
-            defaultExpr(nullLiteral.checkContext('C?')),
+            defaultExpr(body: nullLiteral.checkContext('C?')),
           ]).inContext('C?'),
         ]);
       });
@@ -114,11 +114,29 @@
       test('least upper bound behavior', () {
         h.run([
           switchExpr(expr('int'), [
-            caseExpr(intLiteral(0).pattern, expr('int')),
-            defaultExpr(expr('double')),
+            caseExpr(intLiteral(0).pattern, body: expr('int')),
+            defaultExpr(body: expr('double')),
           ]).checkType('num').stmt
         ]);
       });
+
+      test('when clause', () {
+        var i = Var('i');
+        h.run([
+          switchExpr(expr('int'), [
+            caseExpr(i.pattern(),
+                when: i.expr
+                    .checkType('int')
+                    .eq(expr('num'))
+                    .checkContext('bool'),
+                body: expr('String')),
+          ])
+              .checkIr('switchExpr(expr(int), '
+                  'case(heads(head(varPattern(i, int), ==(i, expr(num)))), '
+                  'expr(String)))')
+              .stmt,
+        ]);
+      });
     });
   });
 
@@ -129,13 +147,13 @@
           switch_(
                   expr('int').checkContext('?'),
                   [
-                    case_(intLiteral(0).pattern, [
+                    case_(intLiteral(0).pattern, body: [
                       break_(),
                     ]),
                   ],
                   isExhaustive: false)
               .checkIr('switch(expr(int), '
-                  'case(heads(const(0)), block(break())))'),
+                  'case(heads(head(const(0))), block(break())))'),
         ]);
       });
 
@@ -146,13 +164,13 @@
             switch_(
                     expr('int').checkContext('?'),
                     [
-                      case_(x.pattern(), [
+                      case_(x.pattern(), body: [
                         break_(),
                       ]),
                     ],
                     isExhaustive: false)
                 .checkIr('switch(expr(int), '
-                    'case(heads(varPattern(x, int)), '
+                    'case(heads(head(varPattern(x, int))), '
                     'block(break())))'),
           ]);
         });
@@ -163,13 +181,13 @@
             switch_(
                     expr('int').checkContext('?'),
                     [
-                      case_(x.pattern(type: 'num'), [
+                      case_(x.pattern(type: 'num'), body: [
                         break_(),
                       ]),
                     ],
                     isExhaustive: false)
                 .checkIr('switch(expr(int), '
-                    'case(heads(varPattern(x, num)), '
+                    'case(heads(head(varPattern(x, num))), '
                     'block(break())))'),
           ]);
         });
@@ -180,7 +198,7 @@
           switch_(
               expr('int').checkContext('?'),
               [
-                case_(intLiteral(0).pattern, [
+                case_(intLiteral(0).pattern, body: [
                   break_(),
                 ]),
               ],
@@ -193,37 +211,43 @@
           switch_(
                   expr('int'),
                   [
-                    case_(intLiteral(0).pattern, []),
-                    case_(intLiteral(1).pattern, [
+                    case_(intLiteral(0).pattern, body: []),
+                    case_(intLiteral(1).pattern, body: [
                       break_(),
                     ]),
                   ],
                   isExhaustive: false)
               .checkIr('switch(expr(int), '
-                  'case(heads(const(0), const(1)), block(break())))'),
+                  'case(heads(head(const(0)), head(const(1))), '
+                  'block(break())))'),
         ]);
       });
 
       test('merge labels', () {
         var x = Var('x');
+        var l = Label('l');
         h.run([
           declare(x, type: 'int?', initializer: expr('int?')),
           x.expr.as_('int').stmt,
           switch_(
-              expr('int'),
-              [
-                case_(intLiteral(0).pattern, [], hasLabel: true),
-                case_(intLiteral(1).pattern, [
-                  x.expr.checkType('int?').stmt,
-                  break_(),
-                ]),
-                case_(intLiteral(2).pattern, [
-                  x.expr.checkType('int').stmt,
-                  x.write(nullLiteral).stmt,
-                  continue_(),
-                ])
-              ],
-              isExhaustive: false),
+                  expr('int'),
+                  [
+                    l.thenCase(case_(intLiteral(0).pattern, body: [])),
+                    case_(intLiteral(1).pattern, body: [
+                      x.expr.checkType('int?').stmt,
+                      break_(),
+                    ]),
+                    case_(intLiteral(2).pattern, body: [
+                      x.expr.checkType('int').stmt,
+                      x.write(nullLiteral).stmt,
+                      continue_(),
+                    ])
+                  ],
+                  isExhaustive: false)
+              .checkIr('switch(expr(int), '
+                  'case(heads(head(const(0)), head(const(1)), l), '
+                  'block(x, break())), '
+                  'case(heads(head(const(2))), block(x, null, continue())))'),
         ]);
       });
 
@@ -232,15 +256,37 @@
           switch_(
                   expr('int'),
                   [
-                    case_(intLiteral(0).pattern, [
+                    case_(intLiteral(0).pattern, body: [
                       break_(),
                     ]),
-                    case_(intLiteral(1).pattern, []),
+                    case_(intLiteral(1).pattern, body: []),
                   ],
                   isExhaustive: false)
               .checkIr('switch(expr(int), '
-                  'case(heads(const(0)), block(break())), '
-                  'case(heads(const(1)), block()))'),
+                  'case(heads(head(const(0))), block(break())), '
+                  'case(heads(head(const(1))), block()))'),
+        ]);
+      });
+
+      test('when clause', () {
+        var i = Var('i');
+        h.run([
+          switch_(
+                  expr('int'),
+                  [
+                    case_(i.pattern(),
+                        when: i.expr
+                            .checkType('int')
+                            .eq(expr('num'))
+                            .checkContext('bool'),
+                        body: [
+                          break_(),
+                        ]),
+                  ],
+                  isExhaustive: true)
+              .checkIr('switch(expr(int), '
+                  'case(heads(head(varPattern(i, int), ==(i, expr(num)))), '
+                  'block(break())))'),
         ]);
       });
 
@@ -251,8 +297,8 @@
             switch_(
                     expr('int'),
                     [
-                      case_(x.pattern(), []),
-                      default_([])..errorId = 'DEFAULT',
+                      case_(x.pattern(), body: []),
+                      default_(body: [])..errorId = 'DEFAULT',
                     ],
                     isExhaustive: true)
                 .expectErrors({'missingMatchVar(DEFAULT, x)'}),
@@ -265,13 +311,28 @@
             switch_(
                     expr('int'),
                     [
-                      case_(intLiteral(0).pattern, [])..errorId = 'CASE(0)',
-                      case_(x.pattern(), []),
+                      case_(intLiteral(0).pattern, body: [])
+                        ..errorId = 'CASE(0)',
+                      case_(x.pattern(), body: []),
                     ],
                     isExhaustive: true)
                 .expectErrors({'missingMatchVar(CASE(0), x)'}),
           ]);
         });
+
+        test('label', () {
+          var x = Var('x');
+          var l = Label('l')..errorId = 'LABEL';
+          h.run([
+            switch_(
+                    expr('int'),
+                    [
+                      l.thenCase(case_(x.pattern(), body: [])),
+                    ],
+                    isExhaustive: true)
+                .expectErrors({'missingMatchVar(LABEL, x)'}),
+          ]);
+        });
       });
 
       group('conflicting var:', () {
@@ -282,9 +343,9 @@
                     expr('num'),
                     [
                       case_(x.pattern(type: 'int')..errorId = 'PATTERN(int x)',
-                          []),
+                          body: []),
                       case_(x.pattern(type: 'num')..errorId = 'PATTERN(num x)',
-                          []),
+                          body: []),
                     ],
                     isExhaustive: true)
                 .expectErrors({
@@ -302,9 +363,9 @@
             switch_(
                     expr('int'),
                     [
-                      case_(x.pattern()..errorId = 'PATTERN(x)', []),
+                      case_(x.pattern()..errorId = 'PATTERN(x)', body: []),
                       case_(x.pattern(type: 'int')..errorId = 'PATTERN(int x)',
-                          []),
+                          body: []),
                     ],
                     isExhaustive: true)
                 .expectErrors({
diff --git a/pkg/front_end/test/spell_checking_list_code.txt b/pkg/front_end/test/spell_checking_list_code.txt
index 012784c..c3e7532 100644
--- a/pkg/front_end/test/spell_checking_list_code.txt
+++ b/pkg/front_end/test/spell_checking_list_code.txt
@@ -1225,6 +1225,7 @@
 sha
 shadowed
 shallow
+shares
 shas
 shelf
 shifts