[dartdevc] Adding support for labels in switch statements

Issue: https://github.com/dart-lang/sdk/issues/29352
Issue: https://github.com/dart-lang/sdk/issues/36345
Change-Id: Ic811d0b54784483c1ff642909acf0df1609cf407
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/100570
Commit-Queue: Mark Zhou <markzipan@google.com>
Reviewed-by: Nicholas Shahan <nshahan@google.com>
diff --git a/pkg/dev_compiler/lib/src/analyzer/side_effect_analysis.dart b/pkg/dev_compiler/lib/src/analyzer/side_effect_analysis.dart
index 145219f..44517f2 100644
--- a/pkg/dev_compiler/lib/src/analyzer/side_effect_analysis.dart
+++ b/pkg/dev_compiler/lib/src/analyzer/side_effect_analysis.dart
@@ -132,3 +132,39 @@
     return initializer.accept(constantVisitor);
   }
 }
+
+class LabelContinueFinder extends SimpleAstVisitor {
+  var found = false;
+  visit(Statement s) {
+    if (!found && s != null) s.accept(this);
+  }
+
+  @override
+  visitBlock(Block node) => node.statements.forEach(visit);
+  @override
+  visitWhileStatement(WhileStatement node) => visit(node.body);
+  @override
+  visitDoStatement(DoStatement node) => visit(node.body);
+  @override
+  visitForStatement(ForStatement node) => visit(node.body);
+  @override
+  visitLabeledStatement(LabeledStatement node) => visit(node.statement);
+  @override
+  visitContinueStatement(ContinueStatement node) => found = node.label != null;
+  @override
+  visitSwitchStatement(SwitchStatement node) {
+    node.members.forEach((m) => m.statements.forEach(visit));
+  }
+
+  @override
+  visitIfStatement(IfStatement node) {
+    visit(node.thenStatement);
+    visit(node.elseStatement);
+  }
+
+  @override
+  visitTryStatement(TryStatement node) {
+    node.body.accept(this);
+    node.finallyBlock.accept(this);
+  }
+}
diff --git a/pkg/dev_compiler/lib/src/kernel/compiler.dart b/pkg/dev_compiler/lib/src/kernel/compiler.dart
index b6c1925..3267966 100644
--- a/pkg/dev_compiler/lib/src/kernel/compiler.dart
+++ b/pkg/dev_compiler/lib/src/kernel/compiler.dart
@@ -166,6 +166,24 @@
   /// label name that was assigned to it.
   final _labelNames = HashMap<Statement, String>.identity();
 
+  /// Indicates that the current context exists within a switch statement that
+  /// uses at least one continue statement with a target label.
+  ///
+  /// JS forbids labels at case statement boundaries, so these switch
+  /// statements must be generated less directly.
+  /// Updated from the method 'visitSwitchStatement'.
+  bool _inLabeledContinueSwitch = false;
+
+  /// A map from switch statements to their state information.
+  /// State information includes the names of the switch statement's implicit
+  /// label name and implicit state variable name.
+  ///
+  /// Entries are only created for switch statements that contain labeled
+  /// continue statements and are used to simulate "jumping" to case statements.
+  /// State variables hold the next constant case expression, while labels act
+  /// as targets for continue and break.
+  final _switchLabelStates = HashMap<Statement, SwitchLabelState>();
+
   final Class _jsArrayClass;
   final Class privateSymbolClass;
   final Class linkedHashMapImplClass;
@@ -3226,6 +3244,12 @@
 
   @override
   visitBreakStatement(BreakStatement node) {
+    // Switch statements with continue labels must explicitly break to their
+    // implicit label due to their being wrapped in a loop.
+    if (_inLabeledContinueSwitch &&
+        _switchLabelStates.containsKey(node.target.body)) {
+      return JS.Break(_switchLabelStates[node.target.body].label);
+    }
     // Can it be compiled to a break without a label?
     if (_currentBreakTargets.contains(node.target)) {
       return JS.Break(null);
@@ -3238,7 +3262,7 @@
     // Ensure the effective target is labeled.  Labels are named globally per
     // Kernel binary.
     //
-    // TODO(kmillikin): Preserve Dart label names in Kernel and here.
+    // TODO(markzipan): Retrieve the real label name with source offsets
     var target = _effectiveTargets[node.target];
     var name = _labelNames[target];
     if (name == null) _labelNames[target] = name = 'L${_labelNames.length}';
@@ -3407,43 +3431,102 @@
 
   @override
   visitSwitchStatement(SwitchStatement node) {
+    // Switches with labeled continues are generated as an infinite loop with
+    // an explicit variable for holding the switch's next case state and an
+    // explicit label. Any implicit breaks are made explicit (e.g., when break
+    // is omitted for the final case statement).
+    var previous = _inLabeledContinueSwitch;
+    _inLabeledContinueSwitch = hasLabeledContinue(node);
+
     var cases = <JS.SwitchCase>[];
-    var emptyBlock = JS.Block.empty();
-    for (var c in node.cases) {
-      // TODO(jmesserly): make sure we are statically checking fall through
-      var body = _visitStatement(c.body).toBlock();
-      var expressions = c.expressions;
-      var last =
-          expressions.isNotEmpty && !c.isDefault ? expressions.last : null;
-      for (var e in expressions) {
-        var jsExpr = _visitExpression(e);
-        cases.add(JS.SwitchCase(jsExpr, e == last ? body : emptyBlock));
+
+    if (_inLabeledContinueSwitch) {
+      var labelState = JS.TemporaryId("labelState");
+      // TODO(markzipan): Retrieve the real label name with source offsets
+      var labelName = 'SL${_switchLabelStates.length}';
+      _switchLabelStates[node] = SwitchLabelState(labelName, labelState);
+
+      for (var c in node.cases) {
+        var subcases =
+            _visitSwitchCase(c, lastSwitchCase: c == node.cases.last);
+        if (subcases.isNotEmpty) cases.addAll(subcases);
       }
-      if (c.isDefault) cases.add(JS.SwitchCase.defaultCase(body));
+
+      var switchExpr = _visitExpression(node.expression);
+      var switchStmt = JS.Switch(labelState, cases);
+      var loopBody = JS.Block([switchStmt, JS.Break(null)]);
+      var loopStmt = JS.While(js.boolean(true), loopBody);
+      // Note: Cannot use _labelNames, as the label must be on the loop.
+      // not the block surrounding the switch statement.
+      var labeledStmt = JS.LabeledStatement(labelName, loopStmt);
+      var block = JS.Block([
+        js.statement('let # = #', [labelState, switchExpr]),
+        labeledStmt
+      ]);
+      _inLabeledContinueSwitch = previous;
+      return block;
     }
 
-    return JS.Switch(_visitExpression(node.expression), cases);
+    for (var c in node.cases) {
+      var subcases = _visitSwitchCase(c);
+      if (subcases.isNotEmpty) cases.addAll(subcases);
+    }
+
+    var stmt = JS.Switch(_visitExpression(node.expression), cases);
+    _inLabeledContinueSwitch = previous;
+    return stmt;
+  }
+
+  /// Helper for visiting a SwitchCase statement.
+  ///
+  /// lastSwitchCase is only used when the current switch statement contains
+  /// labeled continues. Dart permits the final case to implicitly break, but
+  /// switch statements with labeled continues must explicitly break/continue
+  /// to escape the surrounding infinite loop.
+  List<JS.SwitchCase> _visitSwitchCase(SwitchCase node,
+      {bool lastSwitchCase: false}) {
+    var cases = <JS.SwitchCase>[];
+    var emptyBlock = JS.Block.empty();
+    // TODO(jmesserly): make sure we are statically checking fall through
+    var body = _visitStatement(node.body).toBlock();
+    var expressions = node.expressions;
+    var lastExpr =
+        expressions.isNotEmpty && !node.isDefault ? expressions.last : null;
+    for (var e in expressions) {
+      var jsExpr = _visitExpression(e);
+      cases.add(JS.SwitchCase(jsExpr, e == lastExpr ? body : emptyBlock));
+    }
+    if (node.isDefault) {
+      cases.add(JS.SwitchCase.defaultCase(body));
+    }
+    // Switch statements with continue labels must explicitly break from their
+    // last case to escape the additional loop around the switch.
+    if (lastSwitchCase && _inLabeledContinueSwitch && cases.isNotEmpty) {
+      // TODO(markzipan): avoid generating unreachable breaks
+      assert(_switchLabelStates.containsKey(node.parent));
+      var breakStmt = JS.Break(_switchLabelStates[node.parent].label);
+      var switchBody = JS.Block(cases.last.body.statements..add(breakStmt));
+      var updatedSwitch = JS.SwitchCase(cases.last.expression, switchBody);
+      cases.removeLast();
+      cases.add(updatedSwitch);
+    }
+    return cases;
   }
 
   @override
   visitContinueSwitchStatement(ContinueSwitchStatement node) {
-    SwitchCase switchCase;
-    for (Statement current = node;;) {
-      var parent = current.parent;
-      if (parent is Block && parent.statements.last == current) {
-        current = parent;
-        continue;
-      }
-      if (parent is SwitchCase) switchCase = parent;
-      break;
-    }
-    if (switchCase != null) {
-      var switchCases = (switchCase.parent as SwitchStatement).cases;
-      var fromIndex = switchCases.indexOf(switchCase);
-      var toIndex = switchCases.indexOf(node.target);
-      if (toIndex == fromIndex + 1) {
-        return JS.Comment('continue to next case');
-      }
+    var switchStmt = node.target.parent;
+    if (_inLabeledContinueSwitch &&
+        _switchLabelStates.containsKey(switchStmt)) {
+      var switchState = _switchLabelStates[switchStmt];
+      // Use the first constant expression that can match the collated switch
+      // case. Use an unused symbol otherwise to force the default case.
+      var jsExpr = node.target.expressions.isEmpty
+          ? js.call("Symbol('_default')", [])
+          : _visitExpression(node.target.expressions[0]);
+      var setStateStmt = js.statement("# = #", [switchState.variable, jsExpr]);
+      var continueStmt = JS.Continue(switchState.label);
+      return JS.Block([setStateStmt, continueStmt]);
     }
     return _emitInvalidNode(
             node, 'see https://github.com/dart-lang/sdk/issues/29352')
@@ -5261,3 +5344,10 @@
   }
   return false;
 }
+
+class SwitchLabelState {
+  String label;
+  JS.Identifier variable;
+
+  SwitchLabelState(this.label, this.variable);
+}
diff --git a/pkg/dev_compiler/lib/src/kernel/kernel_helpers.dart b/pkg/dev_compiler/lib/src/kernel/kernel_helpers.dart
index bca5ea6..3b3b66a 100644
--- a/pkg/dev_compiler/lib/src/kernel/kernel_helpers.dart
+++ b/pkg/dev_compiler/lib/src/kernel/kernel_helpers.dart
@@ -276,3 +276,56 @@
   }
   return sc;
 }
+
+/// Returns true if a switch statement contains any continues with a label.
+bool hasLabeledContinue(SwitchStatement node) {
+  var visitor = LabelContinueFinder();
+  node.accept(visitor);
+  return visitor.found;
+}
+
+class LabelContinueFinder extends StatementVisitor {
+  var found = false;
+
+  visit(Statement s) {
+    if (!found && s != null) s.accept(this);
+  }
+
+  @override
+  visitBlock(Block node) => node.statements.forEach(visit);
+  @override
+  visitAssertBlock(AssertBlock node) => node.statements.forEach(visit);
+  @override
+  visitWhileStatement(WhileStatement node) => visit(node.body);
+  @override
+  visitDoStatement(DoStatement node) => visit(node.body);
+  @override
+  visitForStatement(ForStatement node) => visit(node.body);
+  @override
+  visitForInStatement(ForInStatement node) => visit(node.body);
+  @override
+  visitContinueSwitchStatement(ContinueSwitchStatement node) => found = true;
+
+  @override
+  visitSwitchStatement(SwitchStatement node) {
+    node.cases.forEach((c) => visit(c.body));
+  }
+
+  @override
+  visitIfStatement(IfStatement node) {
+    visit(node.then);
+    visit(node.otherwise);
+  }
+
+  @override
+  visitTryCatch(TryCatch node) {
+    visit(node.body);
+    node.catches.forEach((c) => visit(c.body));
+  }
+
+  @override
+  visitTryFinally(TryFinally node) {
+    visit(node.body);
+    visit(node.finalizer);
+  }
+}