bazel syntax: don't use exceptions for control flow

Previously, break, continue, and return were implemented by
throwing and catching exceptions.

Now, the Eval.exec methods return a token to indicate the
continuation: one of PASS, BREAK, CONTINUE, or RETURN.
In addition, the return value is saved in the Eval,
which is good for only one function invocation.

This is simpler (and slightly more efficient).

Also:
- FlowStatement now includes PASS too; PassStatement is gone.
- ReturnException is gone, plus one EvalException constructor.
PiperOrigin-RevId: 255882194
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServer.java b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServer.java
index d87bfda..b546fa6f 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServer.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServer.java
@@ -26,6 +26,7 @@
 import com.google.devtools.build.lib.syntax.Eval;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.Statement;
+import com.google.devtools.build.lib.syntax.TokenKind;
 import java.io.IOException;
 import java.net.ServerSocket;
 import java.util.List;
@@ -254,9 +255,9 @@
     }
 
     @Override
-    public void exec(Statement st) throws EvalException, InterruptedException {
+    protected TokenKind exec(Statement st) throws EvalException, InterruptedException {
       pauseIfNecessary(env, st.getLocation());
-      super.exec(st);
+      return super.exec(st);
     }
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Environment.java b/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
index 4a6d014..8348d60 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
@@ -56,9 +56,7 @@
  * respect to this {@code Environment}.
  *
  * <p>{@link Continuation}-s are explicitly represented, but only partly, with another part being
- * implicit in a series of try-catch statements, to maintain the direct style. One notable trick is
- * how a {@link UserDefinedFunction} implements returning values as the function catching a {@link
- * ReturnStatement.ReturnException} thrown by a {@link ReturnStatement} in the body.
+ * implicit in a series of try-catch statements, to maintain the direct style.
  *
  * <p>Every {@code Environment} has a {@link Mutability} field, and must be used within a function
  * that creates and closes this {@link Mutability} with the try-with-resource pattern. This {@link
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Eval.java b/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
index 7a4273b..f8e1b6d 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
@@ -25,18 +25,7 @@
  */
 public class Eval {
   protected final Environment env;
-
-  /** An exception that signals changes in the control flow (e.g. break or continue) */
-  private static class FlowException extends EvalException {
-    FlowException(String message) {
-      super(null, message);
-    }
-
-    @Override
-    public boolean canBeAddedToStackTrace() {
-      return false;
-    }
-  }
+  private Object result = Runtime.NONE;
 
   public static Eval fromEnvironment(Environment env) {
     return evalSupplier.apply(env);
@@ -54,10 +43,6 @@
   // TODO(bazel-team): remove this static state in favor of storing Eval instances in Environment
   private static Function<Environment, Eval> evalSupplier = Eval::new;
 
-  private static final FlowException breakException = new FlowException("FlowException - break");
-  private static final FlowException continueException =
-      new FlowException("FlowException - continue");
-
   /**
    * This constructor should never be called directly. Call {@link #fromEnvironment(Environment)}
    * instead.
@@ -66,6 +51,11 @@
     this.env = env;
   }
 
+  /** getResult returns the value returned by executing a ReturnStatement. */
+  Object getResult() {
+    return this.result;
+  }
+
   void execAssignment(AssignmentStatement node) throws EvalException, InterruptedException {
     Object rvalue = node.getExpression().eval(env);
     node.getLValue().assign(rvalue, env, node.getLocation());
@@ -77,12 +67,12 @@
         .assignAugmented(node.getOperator(), node.getExpression(), env, node.getLocation());
   }
 
-  void execIfBranch(IfStatement.ConditionalStatements node)
+  TokenKind execIfBranch(IfStatement.ConditionalStatements node)
       throws EvalException, InterruptedException {
-    execStatements(node.getStatements());
+    return execStatements(node.getStatements());
   }
 
-  void execFor(ForStatement node) throws EvalException, InterruptedException {
+  TokenKind execFor(ForStatement node) throws EvalException, InterruptedException {
     Object o = node.getCollection().eval(env);
     Iterable<?> col = EvalUtils.toIterable(o, node.getLocation(), env);
     EvalUtils.lock(o, node.getLocation());
@@ -90,17 +80,25 @@
       for (Object it : col) {
         node.getVariable().assign(it, env, node.getLocation());
 
-        try {
-          execStatements(node.getBlock());
-        } catch (FlowException ex) {
-          if (ex == breakException) {
-            return;
-          }
+        switch (execStatements(node.getBlock())) {
+          case PASS:
+          case CONTINUE:
+            // Stay in loop.
+            continue;
+          case BREAK:
+            // Finish loop, execute next statement after loop.
+            return TokenKind.PASS;
+          case RETURN:
+            // Finish loop, return from function.
+            return TokenKind.RETURN;
+          default:
+            throw new IllegalStateException("unreachable");
         }
       }
     } finally {
       EvalUtils.unlock(o, node.getLocation());
     }
+    return TokenKind.PASS;
   }
 
   void execDef(FunctionDefStatement node) throws EvalException, InterruptedException {
@@ -130,17 +128,16 @@
             env.getGlobals()));
   }
 
-  void execIf(IfStatement node) throws EvalException, InterruptedException {
+  TokenKind execIf(IfStatement node) throws EvalException, InterruptedException {
     ImmutableList<IfStatement.ConditionalStatements> thenBlocks = node.getThenBlocks();
     // Avoid iterator overhead - most of the time there will be one or few "if"s.
     for (int i = 0; i < thenBlocks.size(); i++) {
       IfStatement.ConditionalStatements stmt = thenBlocks.get(i);
       if (EvalUtils.toBoolean(stmt.getCondition().eval(env))) {
-        exec(stmt);
-        return;
+        return exec(stmt);
       }
     }
-    execStatements(node.getElseBlock());
+    return execStatements(node.getElseBlock());
   }
 
   void execLoad(LoadStatement node) throws EvalException, InterruptedException {
@@ -163,12 +160,12 @@
     }
   }
 
-  void execReturn(ReturnStatement node) throws EvalException, InterruptedException {
+  TokenKind execReturn(ReturnStatement node) throws EvalException, InterruptedException {
     Expression ret = node.getReturnExpression();
-    if (ret == null) {
-      throw new ReturnStatement.ReturnException(node.getLocation(), Runtime.NONE);
+    if (ret != null) {
+      this.result = ret.eval(env);
     }
-    throw new ReturnStatement.ReturnException(ret.getLocation(), ret.eval(env));
+    return TokenKind.RETURN;
   }
 
   /**
@@ -177,57 +174,54 @@
    * @throws EvalException if execution of the statement could not be completed.
    * @throws InterruptedException may be thrown in a sub class.
    */
-  public void exec(Statement st) throws EvalException, InterruptedException {
+  protected TokenKind exec(Statement st) throws EvalException, InterruptedException {
     try {
-      execDispatch(st);
+      return execDispatch(st);
     } catch (EvalException ex) {
       throw st.maybeTransformException(ex);
     }
   }
 
-  void execDispatch(Statement st) throws EvalException, InterruptedException {
+  TokenKind execDispatch(Statement st) throws EvalException, InterruptedException {
     switch (st.kind()) {
       case ASSIGNMENT:
         execAssignment((AssignmentStatement) st);
-        break;
+        return TokenKind.PASS;
       case AUGMENTED_ASSIGNMENT:
         execAugmentedAssignment((AugmentedAssignmentStatement) st);
-        break;
+        return TokenKind.PASS;
       case CONDITIONAL:
-        execIfBranch((IfStatement.ConditionalStatements) st);
-        break;
+        return execIfBranch((IfStatement.ConditionalStatements) st);
       case EXPRESSION:
         ((ExpressionStatement) st).getExpression().eval(env);
-        break;
+        return TokenKind.PASS;
       case FLOW:
-        throw ((FlowStatement) st).getKind() == FlowStatement.Kind.BREAK
-            ? breakException
-            : continueException;
+        return ((FlowStatement) st).getKind();
       case FOR:
-        execFor((ForStatement) st);
-        break;
+        return execFor((ForStatement) st);
       case FUNCTION_DEF:
         execDef((FunctionDefStatement) st);
-        break;
+        return TokenKind.PASS;
       case IF:
-        execIf((IfStatement) st);
-        break;
+        return execIf((IfStatement) st);
       case LOAD:
         execLoad((LoadStatement) st);
-        break;
-      case PASS:
-        break;
+        return TokenKind.PASS;
       case RETURN:
-        execReturn((ReturnStatement) st);
-        break;
+        return execReturn((ReturnStatement) st);
     }
+    throw new IllegalArgumentException("unexpected statement: " + st.kind());
   }
 
-  private void execStatements(ImmutableList<Statement> statements)
+  public TokenKind execStatements(ImmutableList<Statement> statements)
       throws EvalException, InterruptedException {
     // Hot code path, good chance of short lists which don't justify the iterator overhead.
     for (int i = 0; i < statements.size(); i++) {
-      exec(statements.get(i));
+      TokenKind flow = exec(statements.get(i));
+      if (flow != TokenKind.PASS) {
+        return flow;
+      }
     }
+    return TokenKind.PASS;
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/EvalException.java b/src/main/java/com/google/devtools/build/lib/syntax/EvalException.java
index d0f94ff..cefb389 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/EvalException.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/EvalException.java
@@ -67,25 +67,7 @@
    * @param dueToIncompleteAST if the error is caused by a previous error, such as parsing.
    */
   public EvalException(Location location, String message, boolean dueToIncompleteAST) {
-    this(location, message, dueToIncompleteAST, true);
-  }
-
-  /**
-   * Create an EvalException with the option to not fill in the java stack trace. An optimization
-   * for ReturnException, and potentially others, which aren't exceptional enough to include a
-   * stack trace.
-   *
-   * @param location the location where evaluation/execution failed.
-   * @param message the error message.
-   * @param dueToIncompleteAST if the error is caused by a previous error, such as parsing.
-   * @param fillInJavaStackTrace whether or not to fill in the java stack trace for this exception
-   */
-  EvalException(
-      Location location,
-      String message,
-      boolean dueToIncompleteAST,
-      boolean fillInJavaStackTrace) {
-    super(null, null, /*enableSuppression=*/ true, fillInJavaStackTrace);
+    super(null, null, /*enableSuppression=*/ true, /*writableStackTrace=*/ true);
     this.location = location;
     this.message = Preconditions.checkNotNull(message);
     this.dueToIncompleteAST = dueToIncompleteAST;
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/FlowStatement.java b/src/main/java/com/google/devtools/build/lib/syntax/FlowStatement.java
index b060b78..4477b91 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/FlowStatement.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/FlowStatement.java
@@ -15,48 +15,29 @@
 
 import java.io.IOException;
 
-/** A class for flow statements (e.g. break and continue) */
+/** A class for flow statements (e.g. break, continue, and pass) */
 public final class FlowStatement extends Statement {
-  // TODO(laurentlb): This conflicts with Statement.Kind, maybe remove it?
-  public enum Kind {
-    BREAK("break"),
-    CONTINUE("continue");
 
-    private final String name;
+  private final TokenKind kind; // BREAK | CONTINUE | PASS
 
-    private Kind(String name) {
-      this.name = name;
-    }
-
-    public String getName() {
-      return name;
-    }
-  }
-
-  private final Kind kind;
-
-  /**
-   *
-   * @param kind The label of the statement (either break or continue)
-   */
-  public FlowStatement(Kind kind) {
+  /** @param kind The label of the statement (break, continue, or pass) */
+  public FlowStatement(TokenKind kind) {
     this.kind = kind;
   }
 
-  public Kind getKind() {
+  public TokenKind getKind() {
     return kind;
   }
 
   @Override
   public void prettyPrint(Appendable buffer, int indentLevel) throws IOException {
     printIndent(buffer, indentLevel);
-    buffer.append(kind.name);
-    buffer.append('\n');
+    buffer.append(kind.getPrettyName()).append('\n');
   }
 
   @Override
   public String toString() {
-    return kind.name + "\n";
+    return kind.getPrettyName() + "\n";
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Parser.java b/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
index fad276c..169b731 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
@@ -1126,26 +1126,16 @@
     }
   }
 
-  // small_stmt | 'pass'
-  private void parseSmallStatementOrPass(List<Statement> list) {
-    if (token.kind == TokenKind.PASS) {
-      list.add(setLocation(new PassStatement(), token.left, token.right));
-      expect(TokenKind.PASS);
-    } else {
-      list.add(parseSmallStatement());
-    }
-  }
-
   // simple_stmt ::= small_stmt (';' small_stmt)* ';'? NEWLINE
   private void parseSimpleStatement(List<Statement> list) {
-    parseSmallStatementOrPass(list);
+    list.add(parseSmallStatement());
 
     while (token.kind == TokenKind.SEMI) {
       nextToken();
       if (token.kind == TokenKind.NEWLINE) {
         break;
       }
-      parseSmallStatementOrPass(list);
+      list.add(parseSmallStatement());
     }
     expectAndRecover(TokenKind.NEWLINE);
   }
@@ -1153,7 +1143,7 @@
   //     small_stmt ::= assign_stmt
   //                  | expr
   //                  | return_stmt
-  //                  | flow_stmt
+  //                  | BREAK | CONTINUE | PASS
   //     assign_stmt ::= expr ('=' | augassign) expr
   //     augassign ::= ('+=' | '-=' | '*=' | '/=' | '%=' | '//=' )
   // Note that these are in Python, but not implemented here (at least for now):
@@ -1162,8 +1152,13 @@
     int start = token.left;
     if (token.kind == TokenKind.RETURN) {
       return parseReturnStatement();
-    } else if (token.kind == TokenKind.BREAK || token.kind == TokenKind.CONTINUE) {
-      return parseFlowStatement(token.kind);
+    } else if (token.kind == TokenKind.BREAK
+        || token.kind == TokenKind.CONTINUE
+        || token.kind == TokenKind.PASS) {
+      TokenKind kind = token.kind;
+      int end = token.right;
+      expect(kind);
+      return setLocation(new FlowStatement(kind), start, end);
     }
     Expression expression = parseExpression();
     if (token.kind == TokenKind.EQUALS) {
@@ -1333,16 +1328,6 @@
     return list;
   }
 
-  // flow_stmt ::= BREAK | CONTINUE
-  private FlowStatement parseFlowStatement(TokenKind kind) {
-    int start = token.left;
-    int end = token.right;
-    expect(kind);
-    FlowStatement.Kind flowKind =
-        kind == TokenKind.BREAK ? FlowStatement.Kind.BREAK : FlowStatement.Kind.CONTINUE;
-    return setLocation(new FlowStatement(flowKind), start, end);
-  }
-
   // return_stmt ::= RETURN [expr]
   private ReturnStatement parseReturnStatement() {
     int start = token.left;
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/PassStatement.java b/src/main/java/com/google/devtools/build/lib/syntax/PassStatement.java
deleted file mode 100644
index e035100..0000000
--- a/src/main/java/com/google/devtools/build/lib/syntax/PassStatement.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright 2017 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.devtools.build.lib.syntax;
-
-import java.io.IOException;
-
-/** Syntax node for a `pass` statement. */
-public class PassStatement extends Statement {
-
-  @Override
-  public void prettyPrint(Appendable buffer, int indentLevel) throws IOException {
-    printIndent(buffer, indentLevel);
-    buffer.append("pass\n");
-  }
-
-  @Override
-  public void accept(SyntaxTreeVisitor visitor) {
-    visitor.visit(this);
-  }
-
-  @Override
-  public Kind kind() {
-    return Kind.PASS;
-  }
-}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ReturnStatement.java b/src/main/java/com/google/devtools/build/lib/syntax/ReturnStatement.java
index 4d9c2ad..6f249cf 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/ReturnStatement.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ReturnStatement.java
@@ -13,38 +13,12 @@
 // limitations under the License.
 package com.google.devtools.build.lib.syntax;
 
-import com.google.devtools.build.lib.events.Location;
 import java.io.IOException;
 import javax.annotation.Nullable;
 
 /** A wrapper Statement class for return expressions. */
 public final class ReturnStatement extends Statement {
 
-  /**
-   * Exception sent by the return statement, to be caught by the function body.
-   */
-  public static class ReturnException extends EvalException {
-    private final Object value;
-
-    public ReturnException(Location location, Object value) {
-      super(
-          location,
-          "return statements must be inside a function",
-          /*dueToIncompleteAST=*/false,
-          /*fillInJavaStackTrace=*/false);
-      this.value = value;
-    }
-
-    public Object getValue() {
-      return value;
-    }
-
-    @Override
-    public boolean canBeAddedToStackTrace() {
-      return false;
-    }
-  }
-
   @Nullable private final Expression returnExpression;
 
   public ReturnStatement(@Nullable Expression returnExpression) {
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Statement.java b/src/main/java/com/google/devtools/build/lib/syntax/Statement.java
index 43363fc..1f08bbb 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Statement.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Statement.java
@@ -33,7 +33,6 @@
     FUNCTION_DEF,
     IF,
     LOAD,
-    PASS,
     RETURN,
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SyntaxTreeVisitor.java b/src/main/java/com/google/devtools/build/lib/syntax/SyntaxTreeVisitor.java
index 117bbf0..523177d 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SyntaxTreeVisitor.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SyntaxTreeVisitor.java
@@ -143,16 +143,13 @@
     visitBlock(node.getStatements());
   }
 
-  public void visit(PassStatement node) {}
-
   public void visit(ReturnStatement node) {
     if (node.getReturnExpression() != null) {
       visit(node.getReturnExpression());
     }
   }
 
-  public void visit(FlowStatement node) {
-  }
+  public void visit(FlowStatement node) {}
 
   public void visit(DictionaryLiteral node) {
     visitAll(node.getEntries());
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java b/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java
index 460bc35..5bc2da9 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java
@@ -77,24 +77,8 @@
       }
 
       Eval eval = Eval.fromEnvironment(env);
-      try {
-        for (Statement stmt : statements) {
-          if (stmt instanceof ReturnStatement) {
-            // Performance optimization.
-            // Executing the statement would throw an exception, which is slow.
-            Expression returnExpr = ((ReturnStatement) stmt).getReturnExpression();
-            if (returnExpr == null) {
-              return Runtime.NONE;
-            }
-            return returnExpr.eval(env);
-          } else {
-            eval.exec(stmt);
-          }
-        }
-      } catch (ReturnStatement.ReturnException e) {
-        return e.getValue();
-      }
-      return Runtime.NONE;
+      eval.execStatements(statements);
+      return eval.getResult();
     } finally {
       env.exitScope();
     }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java b/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
index ada8a87..c410aaa 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
@@ -144,7 +144,6 @@
       case CONDITIONAL:
       case EXPRESSION:
       case FLOW:
-      case PASS:
       case RETURN:
         // nothing to declare
     }
@@ -213,9 +212,10 @@
 
   @Override
   public void visit(FlowStatement node) {
-    if (loopCount <= 0) {
+    if (node.getKind() != TokenKind.PASS && loopCount <= 0) {
       throw new ValidationException(
-          node.getLocation(), node.getKind().getName() + " statement must be inside a for loop");
+          node.getLocation(),
+          node.getKind().getPrettyName() + " statement must be inside a for loop");
     }
     super.visit(node);
   }
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ASTPrettyPrintTest.java b/src/test/java/com/google/devtools/build/lib/syntax/ASTPrettyPrintTest.java
index 262e4e4..91b67da 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/ASTPrettyPrintTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ASTPrettyPrintTest.java
@@ -291,11 +291,11 @@
   @Test
   public void flowStatement() {
     // The parser would complain if we tried to construct them from source.
-    ASTNode breakNode = new FlowStatement(FlowStatement.Kind.BREAK);
+    ASTNode breakNode = new FlowStatement(TokenKind.BREAK);
     assertIndentedPrettyMatches(breakNode, "  break\n");
     assertTostringMatches(breakNode, "break\n");
 
-    ASTNode continueNode = new FlowStatement(FlowStatement.Kind.CONTINUE);
+    ASTNode continueNode = new FlowStatement(TokenKind.CONTINUE);
     assertIndentedPrettyMatches(continueNode, "  continue\n");
     assertTostringMatches(continueNode, "continue\n");
   }
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java b/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
index 15bbe94..28d7a2a 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
@@ -608,28 +608,33 @@
   }
 
   @Test
-  public void testForBreakContinue() throws Exception {
-    List<Statement> file = parseFileForSkylark(
-        "def foo():",
-        "  for i in [1, 2]:",
-        "    break",
-        "    continue",
-        "    break");
+  public void testForBreakContinuePass() throws Exception {
+    List<Statement> file =
+        parseFileForSkylark(
+            "def foo():",
+            "  for i in [1, 2]:",
+            "    break",
+            "    continue",
+            "    pass",
+            "    break");
     assertThat(file).hasSize(1);
     List<Statement> body = ((FunctionDefStatement) file.get(0)).getStatements();
     assertThat(body).hasSize(1);
 
     List<Statement> loop = ((ForStatement) body.get(0)).getBlock();
-    assertThat(loop).hasSize(3);
+    assertThat(loop).hasSize(4);
 
-    assertThat(((FlowStatement) loop.get(0)).getKind()).isEqualTo(FlowStatement.Kind.BREAK);
+    assertThat(((FlowStatement) loop.get(0)).getKind()).isEqualTo(TokenKind.BREAK);
     assertLocation(34, 39, loop.get(0).getLocation());
 
-    assertThat(((FlowStatement) loop.get(1)).getKind()).isEqualTo(FlowStatement.Kind.CONTINUE);
+    assertThat(((FlowStatement) loop.get(1)).getKind()).isEqualTo(TokenKind.CONTINUE);
     assertLocation(44, 52, loop.get(1).getLocation());
 
-    assertThat(((FlowStatement) loop.get(2)).getKind()).isEqualTo(FlowStatement.Kind.BREAK);
-    assertLocation(57, 62, loop.get(2).getLocation());
+    assertThat(((FlowStatement) loop.get(2)).getKind()).isEqualTo(TokenKind.PASS);
+    assertLocation(57, 61, loop.get(2).getLocation());
+
+    assertThat(((FlowStatement) loop.get(3)).getKind()).isEqualTo(TokenKind.BREAK);
+    assertLocation(66, 71, loop.get(3).getLocation());
   }
 
   @Test
@@ -1014,13 +1019,6 @@
   }
 
   @Test
-  public void testPass() throws Exception {
-    List<Statement> statements = parseFileForSkylark("pass\n");
-    assertThat(statements).hasSize(1);
-    assertThat(statements.get(0)).isInstanceOf(PassStatement.class);
-  }
-
-  @Test
   public void testForPass() throws Exception {
     List<Statement> statements = parseFileForSkylark(
         "def foo():",
@@ -1028,7 +1026,7 @@
 
     assertThat(statements).hasSize(1);
     FunctionDefStatement stmt = (FunctionDefStatement) statements.get(0);
-    assertThat(stmt.getStatements().get(0)).isInstanceOf(PassStatement.class);
+    assertThat(stmt.getStatements().get(0)).isInstanceOf(FlowStatement.class);
   }
 
   @Test