Skylark: implemented 'break' and 'continue'

Fixes #233.
https://github.com/google/bazel/issues/233

--
MOS_MIGRATED_REVID=95632067
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
new file mode 100644
index 0000000..127b366
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/FlowStatement.java
@@ -0,0 +1,86 @@
+// Copyright 2014 Google Inc. 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;
+
+/**
+ * A class for flow statements (e.g. break and continue)
+ */
+public final class FlowStatement extends Statement {
+
+  public static final FlowStatement BREAK = new FlowStatement("break", true);
+  public static final FlowStatement CONTINUE = new FlowStatement("continue", false);
+
+  private final String name;
+  private final FlowException ex;
+
+  /**
+   *
+   * @param name The label of the statement (either break or continue)
+   * @param terminateLoop Determines whether the enclosing loop should be terminated completely
+   *        (break)
+   */
+  protected FlowStatement(String name, boolean terminateLoop) {
+    this.name = name;
+    this.ex = new FlowException(terminateLoop);
+  }
+
+  @Override
+  void exec(Environment env) throws EvalException {
+    throw ex;
+  }
+
+  @Override
+  void validate(ValidationEnvironment env) throws EvalException {
+    if (!env.isInsideLoop()) {
+      throw new EvalException(getLocation(), name + " statement must be inside a for loop");
+    }
+  }
+
+  @Override
+  public String toString() {
+    return name;
+  }
+
+  @Override
+  public void accept(SyntaxTreeVisitor visitor) {
+    visitor.visit(this);
+  }
+
+  /**
+   * An exception that signals changes in the control flow (e.g. break or continue)
+   */
+  class FlowException extends EvalException {
+    private final boolean terminateLoop;
+
+    /**
+     *
+     * @param terminateLoop Determines whether the enclosing loop should be terminated completely
+     *        (break)
+     */
+    public FlowException(boolean terminateLoop) {
+      super(FlowStatement.this.getLocation(), "FlowException with terminateLoop = "
+          + terminateLoop);
+      this.terminateLoop = terminateLoop;
+    }
+
+    /**
+     * Returns whether the enclosing loop should be terminated completely (break)
+     *
+     * @return {@code True} for 'break', {@code false} for 'continue'
+     */
+    public boolean mustTerminateLoop() {
+      return terminateLoop;
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ForStatement.java b/src/main/java/com/google/devtools/build/lib/syntax/ForStatement.java
index 054b21f..1660ce1 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/ForStatement.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ForStatement.java
@@ -15,6 +15,7 @@
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.syntax.FlowStatement.FlowException;
 
 import java.util.List;
 
@@ -63,11 +64,20 @@
     int i = 0;
     for (Object it : ImmutableList.copyOf(col)) {
       variable.assign(env, getLocation(), it);
-      for (Statement stmt : block) {
-        stmt.exec(env);
+
+      try {
+        for (Statement stmt : block) {
+          stmt.exec(env);
+        }
+      } catch (FlowException ex) {
+        if (ex.mustTerminateLoop()) {
+          return;
+        }
       }
+
       i++;
     }
+    
     // TODO(bazel-team): This should not happen if every collection is immutable.
     if (i != EvalUtils.size(col)) {
       throw new EvalException(getLocation(),
@@ -83,14 +93,20 @@
   @Override
   void validate(ValidationEnvironment env) throws EvalException {
     if (env.isTopLevel()) {
-      throw new EvalException(getLocation(),
-          "'For' is not allowed as a top level statement");
+      throw new EvalException(getLocation(), "'For' is not allowed as a top level statement");
     }
-    // TODO(bazel-team): validate variable. Maybe make it temporarily readonly.
-    collection.validate(env);
-    variable.validate(env, getLocation());
-    for (Statement stmt : block) {
-      stmt.validate(env);
+    env.enterLoop();
+
+    try {
+      // TODO(bazel-team): validate variable. Maybe make it temporarily readonly.
+      collection.validate(env);
+      variable.validate(env, getLocation());
+
+      for (Statement stmt : block) {
+        stmt.validate(env);
+      }
+    } finally {
+      env.exitLoop(getLocation());
     }
   }
 }
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 c264259..71277a0 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
@@ -336,7 +336,7 @@
 
   // Keywords that exist in Python and that we don't parse.
   private static final EnumSet<TokenKind> FORBIDDEN_KEYWORDS =
-      EnumSet.of(TokenKind.AS, TokenKind.ASSERT, TokenKind.BREAK, TokenKind.CONTINUE,
+      EnumSet.of(TokenKind.AS, TokenKind.ASSERT, 
           TokenKind.DEL, TokenKind.EXCEPT, TokenKind.FINALLY, TokenKind.FROM, TokenKind.GLOBAL,
           TokenKind.IMPORT, TokenKind.IS, TokenKind.LAMBDA, TokenKind.NONLOCAL, TokenKind.RAISE,
           TokenKind.TRY, TokenKind.WITH, TokenKind.WHILE, TokenKind.YIELD);
@@ -1154,6 +1154,7 @@
   //     small_stmt ::= assign_stmt
   //                  | expr
   //                  | RETURN expr
+  //                  | flow_stmt
   //     assign_stmt ::= expr ('=' | augassign) expr
   //     augassign ::= ('+=' )
   // Note that these are in Python, but not implemented here (at least for now):
@@ -1171,6 +1172,8 @@
     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);
     }
     Expression expression = parseExpression();
     if (token.kind == TokenKind.EQUALS) {
@@ -1408,6 +1411,12 @@
     }
   }
 
+  // flow_stmt ::= break_stmt | continue_stmt
+  private FlowStatement parseFlowStatement(TokenKind kind) {
+    expect(kind);
+    return (kind == TokenKind.BREAK) ? FlowStatement.BREAK : FlowStatement.CONTINUE;
+  }
+  
   // return_stmt ::= RETURN expr
   private ReturnStatement parseReturnStatement() {
     int start = token.left;
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 a615d67..7aedca2 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
@@ -47,6 +47,12 @@
 
   // Whether this validation environment is not modified therefore clonable or not.
   private boolean clonable;
+  
+  /**
+   * Tracks the number of nested for loops that contain the statement that is currently being
+   * validated
+   */
+  private int loopCount = 0;
 
   public ValidationEnvironment(Set<String> builtinVariables) {
     parent = null;
@@ -174,4 +180,33 @@
       statement.validate(this);
     }
   }
+
+  /**
+   * Returns whether the current statement is inside a for loop (either in this environment or one
+   * of its parents)
+   *
+   * @return True if the current statement is inside a for loop
+   */
+  public boolean isInsideLoop() {
+    return (loopCount > 0);
+  }
+  
+  /**
+   * Signals that the block of a for loop was entered
+   */
+  public void enterLoop()   {
+    ++loopCount;
+  }
+  
+  /**
+   * Signals that the block of a for loop was left
+   *
+   * @param location The current location
+   * @throws EvalException If there was no corresponding call to
+   *         {@code ValidationEnvironment#enterLoop}
+   */
+  public void exitLoop(Location location) throws EvalException {
+    Preconditions.checkState(loopCount > 0);
+    --loopCount;
+  }
 }