Open source all the tests under lib/syntax/.

--
MOS_MIGRATED_REVID=87244284
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ASTNodeTest.java b/src/test/java/com/google/devtools/build/lib/syntax/ASTNodeTest.java
new file mode 100644
index 0000000..c607d90
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ASTNodeTest.java
@@ -0,0 +1,64 @@
+// Copyright 2015 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;
+
+import static org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests {@link ASTNode}.
+ */
+@RunWith(JUnit4.class)
+public class ASTNodeTest {
+
+  private ASTNode node;
+
+  @Before
+  public void setUp() throws Exception {
+    node = new ASTNode() {
+      @Override
+      public String toString() {
+        return null;
+      }
+      @Override
+      public void accept(SyntaxTreeVisitor visitor) {
+      }
+    };
+  }
+
+  @Test
+  public void testHashCodeNotSupported() {
+    try {
+      node.hashCode();
+      fail();
+    } catch (UnsupportedOperationException e) {
+      // yes!
+    }
+  }
+
+  @Test
+  public void testEqualsNotSupported() {
+    try {
+      node.equals(this);
+      fail();
+    } catch (UnsupportedOperationException e) {
+      // yes!
+    }
+  }
+
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/AbstractEvaluationTestCase.java b/src/test/java/com/google/devtools/build/lib/syntax/AbstractEvaluationTestCase.java
new file mode 100644
index 0000000..7937786
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/AbstractEvaluationTestCase.java
@@ -0,0 +1,52 @@
+// Copyright 2006 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;
+
+import java.util.List;
+
+/**
+ * Base class for test cases that use eval services.
+ */
+public abstract class AbstractEvaluationTestCase extends AbstractParserTestCase {
+
+  public Object eval(String input) throws Exception {
+    return eval(parseExpr(input));
+  }
+
+  public Object eval(String input, Environment env) throws Exception {
+    return eval(parseExpr(input), env);
+  }
+
+  public static Object eval(Expression e) throws Exception {
+    return eval(e, new Environment());
+  }
+
+  public static Object eval(Expression e, Environment env) throws Exception {
+    return e.eval(env);
+  }
+
+  public void exec(String input, Environment env) throws Exception {
+    exec(parseStmt(input), env);
+  }
+
+  public void exec(Statement s, Environment env) throws Exception {
+    s.exec(env);
+  }
+
+  public static void exec(List<Statement> li, Environment env) throws Exception {
+    for (Statement stmt : li) {
+      stmt.exec(env);
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/AbstractParserTestCase.java b/src/test/java/com/google/devtools/build/lib/syntax/AbstractParserTestCase.java
new file mode 100644
index 0000000..ba59864
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/AbstractParserTestCase.java
@@ -0,0 +1,100 @@
+// Copyright 2006 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;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
+import com.google.devtools.build.lib.packages.CachingPackageLocator;
+import com.google.devtools.build.lib.rules.SkylarkModules;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.util.FsApparatus;
+
+import junit.framework.TestCase;
+
+import java.util.List;
+
+/**
+ * Base class for test cases that use parsing services.
+ */
+public abstract class AbstractParserTestCase extends TestCase {
+  public static final class EmptyPackageLocator implements CachingPackageLocator {
+    @Override
+    public Path getBuildFileForPackage(String packageName) {
+      return null;
+    }
+  }
+
+  protected EventCollectionApparatus syntaxEvents = new EventCollectionApparatus();
+  private FsApparatus scratch = FsApparatus.newInMemory();
+  private CachingPackageLocator locator = new EmptyPackageLocator();
+
+  private static Lexer createLexer(String input,
+      EventCollectionApparatus syntaxEvents, FsApparatus scratch) {
+    Path someFile = scratch.path("/some/file.txt");
+    ParserInputSource inputSource = ParserInputSource.create(input, someFile);
+    return new Lexer(inputSource, syntaxEvents.reporter());
+  }
+
+  protected Lexer createLexer(String input) {
+    return createLexer(input, syntaxEvents, scratch);
+  }
+
+  protected List<Statement> parseFile(String input) {
+    return Parser.parseFile(createLexer(input), syntaxEvents.reporter(), locator, false)
+        .statements;
+  }
+
+  protected List<Statement> parseFile(String input, boolean parsePython) {
+    return Parser.parseFile(createLexer(input), syntaxEvents.reporter(), locator, parsePython)
+        .statements;
+  }
+
+  protected List<Statement> parseFileForSkylark(String input) {
+    return Parser.parseFileForSkylark(createLexer(input), syntaxEvents.reporter(), locator,
+        SkylarkModules.getValidationEnvironment()).statements;
+  }
+
+  protected List<Statement> parseFileForSkylark(
+      String input, ImmutableMap<String, SkylarkType> extraObject) {
+    return Parser.parseFileForSkylark(createLexer(input), syntaxEvents.reporter(), locator,
+        SkylarkModules.getValidationEnvironment(extraObject)).statements;
+  }
+
+  protected Parser.ParseResult parseFileWithComments(String input) {
+    return Parser.parseFile(createLexer(input), syntaxEvents.reporter(), locator, false);
+  }
+
+  protected Statement parseStmt(String input) {
+    return Parser.parseStatement(createLexer(input), syntaxEvents.reporter());
+  }
+
+  protected Expression parseExpr(String input) {
+    return Parser.parseExpression(createLexer(input), syntaxEvents.reporter());
+  }
+
+  public static List<Statement> parseFileForSkylark(
+      EventCollectionApparatus syntaxEvents, FsApparatus scratch, String input) {
+    return Parser.parseFileForSkylark(createLexer(input, syntaxEvents, scratch),
+        syntaxEvents.reporter(), null,
+        SkylarkModules.getValidationEnvironment()).statements;
+  }
+
+  public static List<Statement> parseFileForSkylark(
+      EventCollectionApparatus syntaxEvents, FsApparatus scratch, String input,
+      ImmutableMap<String, SkylarkType> extraObject) {
+    return Parser.parseFileForSkylark(createLexer(input, syntaxEvents, scratch),
+        syntaxEvents.reporter(), null,
+        SkylarkModules.getValidationEnvironment(extraObject)).statements;
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/BuildFileASTTest.java b/src/test/java/com/google/devtools/build/lib/syntax/BuildFileASTTest.java
new file mode 100644
index 0000000..eff5c92
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/BuildFileASTTest.java
@@ -0,0 +1,300 @@
+// Copyright 2006 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;
+
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventCollector;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
+import com.google.devtools.build.lib.packages.CachingPackageLocator;
+import com.google.devtools.build.lib.testutil.JunitTestUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.util.FsApparatus;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+public class BuildFileASTTest extends TestCase {
+
+  private FsApparatus scratch = FsApparatus.newInMemory();
+
+  private EventCollectionApparatus events = new EventCollectionApparatus(EventKind.ALL_EVENTS);
+
+  private class ScratchPathPackageLocator implements CachingPackageLocator {
+    @Override
+    public Path getBuildFileForPackage(String packageName) {
+      return scratch.path(packageName).getRelative("BUILD");
+    }
+  }
+
+  private CachingPackageLocator locator = new ScratchPathPackageLocator();
+
+  /**
+   * Parses the contents of the specified string (using DUMMY_PATH as the fake
+   * filename) and returns the AST. Resets the error handler beforehand.
+   */
+  private BuildFileAST parseBuildFile(String... lines) throws IOException {
+    Path file = scratch.file("/a/build/file/BUILD", lines);
+    return BuildFileAST.parseBuildFile(file, events.reporter(), locator, false);
+  }
+
+  public void testParseBuildFileOK() throws Exception {
+    Path buildFile = scratch.file("/BUILD",
+        "# a file in the build language",
+        "",
+        "x = [1,2,'foo',4] + [1,2, \"%s%d\" % ('foo', 1)]");
+
+    Environment env = new Environment();
+    Reporter reporter = new Reporter();
+    BuildFileAST buildfile = BuildFileAST.parseBuildFile(buildFile, reporter, null, false);
+
+    assertTrue(buildfile.exec(env, reporter));
+
+    // Test final environment is correctly modified:
+    //
+    // input1.BUILD contains:
+    // x = [1,2,'foo',4] + [1,2, "%s%d" % ('foo', 1)]
+    assertEquals(Arrays.<Object>asList(1, 2, "foo", 4, 1, 2, "foo1"),
+                 env.lookup("x"));
+  }
+
+  public void testEvalException() throws Exception {
+    Path buildFile = scratch.file("/input1.BUILD",
+        "x = 1",
+        "y = [2,3]",
+        "",
+        "z = x + y");
+
+    Environment env = new Environment();
+    Reporter reporter = new Reporter();
+    EventCollector collector = new EventCollector(EventKind.ALL_EVENTS);
+    reporter.addHandler(collector);
+    BuildFileAST buildfile = BuildFileAST.parseBuildFile(buildFile, reporter, null, false);
+
+    assertFalse(buildfile.exec(env, reporter));
+    Event e = JunitTestUtils.assertContainsEvent(collector,
+        "unsupported operand type(s) for +: 'int' and 'list'");
+    assertEquals(4, e.getLocation().getStartLineAndColumn().getLine());
+  }
+
+  public void testParsesFineWithNewlines() throws Exception {
+    BuildFileAST buildFileAST = parseBuildFile("foo()\n"
+                                               + "bar()\n"
+                                               + "something = baz()\n"
+                                               + "bar()");
+    assertEquals(4, buildFileAST.getStatements().size());
+  }
+
+  public void testFailsIfNewlinesAreMissing() throws Exception {
+    events.setFailFast(false);
+
+    BuildFileAST buildFileAST =
+      parseBuildFile("foo() bar() something = baz() bar()");
+
+    Event event = events.collector().iterator().next();
+    assertEquals("syntax error at \'bar\'", event.getMessage());
+    assertEquals("/a/build/file/BUILD",
+                 event.getLocation().getPath().toString());
+    assertEquals(1, event.getLocation().getStartLineAndColumn().getLine());
+    assertTrue(buildFileAST.containsErrors());
+  }
+
+  public void testImplicitStringConcatenationFails() throws Exception {
+    events.setFailFast(false);
+    BuildFileAST buildFileAST = parseBuildFile("a = 'foo' 'bar'");
+    Event event = events.collector().iterator().next();
+    assertEquals("Implicit string concatenation is forbidden, use the + operator",
+        event.getMessage());
+    assertEquals("/a/build/file/BUILD",
+                 event.getLocation().getPath().toString());
+    assertEquals(1, event.getLocation().getStartLineAndColumn().getLine());
+    assertEquals(10, event.getLocation().getStartLineAndColumn().getColumn());
+    assertTrue(buildFileAST.containsErrors());
+  }
+
+  public void testImplicitStringConcatenationAcrossLinesIsIllegal() throws Exception {
+    events.setFailFast(false);
+    BuildFileAST buildFileAST = parseBuildFile("a = 'foo'\n  'bar'");
+
+    Event event = events.collector().iterator().next();
+    assertEquals("indentation error", event.getMessage());
+    assertEquals("/a/build/file/BUILD",
+                 event.getLocation().getPath().toString());
+    assertEquals(2, event.getLocation().getStartLineAndColumn().getLine());
+    assertEquals(2, event.getLocation().getStartLineAndColumn().getColumn());
+    assertTrue(buildFileAST.containsErrors());
+  }
+
+  /**
+   * If the specified EventCollector does contain an event which has
+   * 'expectedEvent' as a substring, the matching event is
+   * returned. Otherwise this will return null.
+   */
+  public static Event findEvent(EventCollector eventCollector,
+                                String expectedEvent) {
+    for (Event event : eventCollector) {
+      if (event.getMessage().contains(expectedEvent)) {
+        return event;
+      }
+    }
+    return null;
+  }
+
+  public void testWithSyntaxErrorsDoesNotPrintDollarError() throws Exception {
+    events.setFailFast(false);
+    BuildFileAST buildFile = parseBuildFile(
+        "abi = cxx_abi + '-glibc-' + glibc_version + '-' + "
+        + "generic_cpu + '-' + sysname",
+        "libs = [abi + opt_level + '/lib/libcc.a']",
+        "shlibs = [abi + opt_level + '/lib/libcc.so']",
+        "+* shlibs", // syntax error at '+'
+        "cc_library(name = 'cc',",
+        "           srcs = libs,",
+        "           includes = [ abi + opt_level + '/include' ])");
+    assertTrue(buildFile.containsErrors());
+    Event event = events.collector().iterator().next();
+    assertEquals("syntax error at '+'", event.getMessage());
+    Environment env = new Environment();
+    assertFalse(buildFile.exec(env, events.reporter()));
+    assertNull(findEvent(events.collector(), "$error$"));
+    // This message should not be printed anymore.
+    Event event2 = findEvent(events.collector(), "contains syntax error(s)");
+    assertNull(event2);
+  }
+
+  public void testInclude() throws Exception {
+    scratch.file("/foo/bar/BUILD",
+        "c = 4\n"
+        + "d = 5\n");
+    Path buildFile = scratch.file("/BUILD",
+        "a = 2\n"
+        + "include(\"//foo/bar:BUILD\")\n"
+        + "b = 4\n");
+
+    BuildFileAST buildFileAST = BuildFileAST.parseBuildFile(buildFile, events.reporter(),
+                                                            locator, false);
+
+    assertFalse(buildFileAST.containsErrors());
+    assertEquals(5, buildFileAST.getStatements().size());
+  }
+
+  public void testInclude2() throws Exception {
+    scratch.file("/foo/bar/defs",
+        "a = 1\n");
+    Path buildFile = scratch.file("/BUILD",
+        "include(\"//foo/bar:defs\")\n"
+        + "b = a + 1\n");
+
+    BuildFileAST buildFileAST = BuildFileAST.parseBuildFile(buildFile, events.reporter(),
+                                                            locator, false);
+
+    assertFalse(buildFileAST.containsErrors());
+    assertEquals(3, buildFileAST.getStatements().size());
+
+    Environment env = new Environment();
+    Reporter reporter = new Reporter();
+    assertFalse(buildFileAST.exec(env, reporter));
+    assertEquals(2, env.lookup("b"));
+  }
+
+  public void testMultipleIncludes() throws Exception {
+    String fileA =
+        "include(\"//foo:fileB\")\n"
+        + "include(\"//foo:fileC\")\n";
+    scratch.file("/foo/fileB",
+        "b = 3\n"
+        + "include(\"//foo:fileD\")\n");
+    scratch.file("/foo/fileC",
+        "include(\"//foo:fileD\")\n"
+        + "c = b + 2\n");
+    scratch.file("/foo/fileD",
+        "b = b + 1\n"); // this code is included twice
+
+    BuildFileAST buildFileAST = parseBuildFile(fileA);
+    assertFalse(buildFileAST.containsErrors());
+    assertEquals(8, buildFileAST.getStatements().size());
+
+    Environment env = new Environment();
+    Reporter reporter = new Reporter();
+    assertFalse(buildFileAST.exec(env, reporter));
+    assertEquals(5, env.lookup("b"));
+    assertEquals(7, env.lookup("c"));
+  }
+
+  public void testFailInclude() throws Exception {
+    events.setFailFast(false);
+    BuildFileAST buildFileAST = parseBuildFile("include(\"//nonexistent\")");
+    assertEquals(1, buildFileAST.getStatements().size());
+    events.assertContainsEvent("Include of '//nonexistent' failed");
+  }
+
+
+  private class EmptyPackageLocator implements CachingPackageLocator {
+    @Override
+    public Path getBuildFileForPackage(String packageName) {
+      return null;
+    }
+  }
+
+  private CachingPackageLocator emptyLocator = new EmptyPackageLocator();
+
+  public void testFailInclude2() throws Exception {
+    events.setFailFast(false);
+    Path buildFile = scratch.file("/foo/bar/BUILD",
+        "include(\"//nonexistent:foo\")\n");
+    BuildFileAST buildFileAST = BuildFileAST.parseBuildFile(buildFile, events.reporter(),
+                                                            emptyLocator, false);
+    assertEquals(1, buildFileAST.getStatements().size());
+    events.assertContainsEvent("Package 'nonexistent' not found");
+  }
+
+  public void testInvalidInclude() throws Exception {
+    events.setFailFast(false);
+    BuildFileAST buildFileAST = parseBuildFile("include(2)");
+    assertEquals(0, buildFileAST.getStatements().size());
+    events.assertContainsEvent("syntax error at '2'");
+  }
+
+  public void testRecursiveInclude() throws Exception {
+    events.setFailFast(false);
+    Path buildFile = scratch.file("/foo/bar/BUILD",
+        "include(\"//foo/bar:BUILD\")\n");
+
+    BuildFileAST.parseBuildFile(buildFile, events.reporter(), locator, false);
+    events.assertContainsEvent("Recursive inclusion");
+  }
+
+  public void testParseErrorInclude() throws Exception {
+    events.setFailFast(false);
+
+    scratch.file("/foo/bar/file",
+        "a = 2 + % 3\n"); // parse error
+
+    parseBuildFile("include(\"//foo/bar:file\")");
+
+    // Check the location is properly reported
+    Event event = events.collector().iterator().next();
+    assertEquals("/foo/bar/file:1:9", event.getLocation().print());
+    assertEquals("syntax error at '%'", event.getMessage());
+  }
+
+  public void testNonExistentIncludeReported() throws Exception {
+    events.setFailFast(false);
+    BuildFileAST buildFileAST = parseBuildFile("include('//foo:bar')");
+    assertEquals(1, buildFileAST.getStatements().size());
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/EnvironmentTest.java b/src/test/java/com/google/devtools/build/lib/syntax/EnvironmentTest.java
new file mode 100644
index 0000000..0452913
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/EnvironmentTest.java
@@ -0,0 +1,133 @@
+// Copyright 2006 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;
+
+import com.google.common.collect.Sets;
+
+/**
+ * Tests of Environment.
+ */
+public class EnvironmentTest extends AbstractEvaluationTestCase {
+
+  // Test the API directly
+  public void testLookupAndUpdate() throws Exception {
+    Environment env = new Environment();
+
+    try {
+      env.lookup("foo");
+      fail();
+    } catch (Environment.NoSuchVariableException e) {
+       assertEquals("no such variable: foo", e.getMessage());
+    }
+
+    env.update("foo", "bar");
+
+    assertEquals("bar", env.lookup("foo"));
+  }
+
+  public void testLookupWithDefault() throws Exception {
+    Environment env = new Environment();
+    assertEquals(21, env.lookup("VERSION", 21));
+    env.update("VERSION", 42);
+    assertEquals(42, env.lookup("VERSION", 21));
+  }
+
+  public void testDoubleUpdateSucceeds() throws Exception {
+    Environment env = new Environment();
+    env.update("VERSION", 42);
+    assertEquals(42, env.lookup("VERSION"));
+    env.update("VERSION", 43);
+    assertEquals(43, env.lookup("VERSION"));
+  }
+
+  // Test assign through interpreter, lookup through API:
+  public void testAssign() throws Exception {
+    Environment env = new Environment();
+
+    try {
+      env.lookup("foo");
+      fail();
+    } catch (Environment.NoSuchVariableException e) {
+      assertEquals("no such variable: foo", e.getMessage());
+    }
+
+    exec(parseStmt("foo = 'bar'"), env);
+
+    assertEquals("bar", env.lookup("foo"));
+  }
+
+  // Test update through API, reference through interpreter:
+  public void testReference() throws Exception {
+    Environment env = new Environment();
+
+    try {
+      eval(parseExpr("foo"), env);
+      fail();
+    } catch (EvalException e) {
+      assertEquals("name 'foo' is not defined", e.getMessage());
+    }
+
+    env.update("foo", "bar");
+
+    assertEquals("bar", eval(parseExpr("foo"), env));
+  }
+
+  // Test assign and reference through interpreter:
+  public void testAssignAndReference() throws Exception {
+    Environment env = new Environment();
+
+    try {
+      eval(parseExpr("foo"), env);
+      fail();
+    } catch (EvalException e) {
+      assertEquals("name 'foo' is not defined", e.getMessage());
+    }
+
+    exec(parseStmt("foo = 'bar'"), env);
+
+    assertEquals("bar", eval(parseExpr("foo"), env));
+  }
+
+  public void testGetVariableNames() throws Exception {
+    Environment env = new Environment();
+    env.update("foo", "bar");
+    env.update("wiz", 3);
+
+    Environment nestedEnv = new Environment(env);
+    nestedEnv.update("foo", "bat");
+    nestedEnv.update("quux", 42);
+
+    assertEquals(Sets.newHashSet("True", "False", "None", "foo", "wiz"), env.getVariableNames());
+    assertEquals(Sets.newHashSet("True", "False", "None", "foo", "wiz", "quux"),
+        nestedEnv.getVariableNames());
+  }
+
+  public void testToString() throws Exception {
+    Environment env = new Environment();
+    env.update("subject", new StringLiteral("Hello, 'world'.", '\''));
+    env.update("from", new StringLiteral("Java", '"'));
+    assertEquals("Environment{False -> false, None -> None, True -> true, from -> \"Java\", "
+        + "subject -> 'Hello, \\'world\\'.', }", env.toString());
+  }
+
+  public void testBindToNullThrowsException() throws Exception {
+    try {
+      new Environment().update("some_name", null);
+      fail();
+    } catch (NullPointerException e) {
+      assertEquals("update(value == null)", e.getMessage());
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/EvalUtilsTest.java b/src/test/java/com/google/devtools/build/lib/syntax/EvalUtilsTest.java
new file mode 100644
index 0000000..595055b
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/EvalUtilsTest.java
@@ -0,0 +1,221 @@
+// Copyright 2006 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;
+
+import com.google.common.collect.Lists;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.IllegalFormatException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *  Test properties of the evaluator's datatypes and utility functions
+ *  without actually creating any parse trees.
+ */
+public class EvalUtilsTest extends TestCase {
+
+  private static List<?> makeList(Object ...args) {
+    return EvalUtils.makeSequence(Arrays.<Object>asList(args), false);
+  }
+  private static List<?> makeTuple(Object ...args) {
+    return EvalUtils.makeSequence(Arrays.<Object>asList(args), true);
+  }
+  private static Map<Object, Object> makeDict() {
+    return new LinkedHashMap<>();
+  }
+  private static FilesetEntry makeFilesetEntry() {
+    try {
+      return new FilesetEntry(Label.parseAbsolute("//foo:bar"),
+                              Lists.<Label>newArrayList(), Lists.newArrayList("xyz"), "",
+                              FilesetEntry.SymlinkBehavior.COPY, ".");
+    } catch (Label.SyntaxException e) {
+      throw new RuntimeException("Bad label: ", e);
+    }
+  }
+
+  public void testDataTypeNames() throws Exception {
+    assertEquals("string", EvalUtils.getDataTypeName("foo"));
+    assertEquals("int", EvalUtils.getDataTypeName(3));
+    assertEquals("tuple", EvalUtils.getDataTypeName(makeTuple(1, 2, 3)));
+    assertEquals("list",  EvalUtils.getDataTypeName(makeList(1, 2, 3)));
+    assertEquals("dict",  EvalUtils.getDataTypeName(makeDict()));
+    assertEquals("FilesetEntry",  EvalUtils.getDataTypeName(makeFilesetEntry()));
+    assertEquals("None", EvalUtils.getDataTypeName(Environment.NONE));
+  }
+
+  public void testDatatypeMutability() throws Exception {
+    assertTrue(EvalUtils.isImmutable("foo"));
+    assertTrue(EvalUtils.isImmutable(3));
+    assertTrue(EvalUtils.isImmutable(makeTuple(1, 2, 3)));
+    assertFalse(EvalUtils.isImmutable(makeList(1, 2, 3)));
+    assertFalse(EvalUtils.isImmutable(makeDict()));
+    assertFalse(EvalUtils.isImmutable(makeFilesetEntry()));
+  }
+
+  public void testPrintValue() throws Exception {
+    // Note that prettyPrintValue and printValue only differ on behaviour of
+    // labels and strings at toplevel.
+    assertEquals("foo\nbar", EvalUtils.printValue("foo\nbar"));
+    assertEquals("\"foo\\nbar\"", EvalUtils.prettyPrintValue("foo\nbar"));
+    assertEquals("'", EvalUtils.printValue("'"));
+    assertEquals("\"'\"", EvalUtils.prettyPrintValue("'"));
+    assertEquals("\"", EvalUtils.printValue("\""));
+    assertEquals("\"\\\"\"", EvalUtils.prettyPrintValue("\""));
+    assertEquals("3", EvalUtils.printValue(3));
+    assertEquals("3", EvalUtils.prettyPrintValue(3));
+    assertEquals("None", EvalUtils.prettyPrintValue(Environment.NONE));
+
+    assertEquals("//x:x", EvalUtils.printValue(Label.parseAbsolute("//x")));
+    assertEquals("\"//x:x\"", EvalUtils.prettyPrintValue(Label.parseAbsolute("//x")));
+
+    List<?> list = makeList("foo", "bar");
+    List<?> tuple = makeTuple("foo", "bar");
+
+    assertEquals("(1, [\"foo\", \"bar\"], 3)",
+                 EvalUtils.printValue(makeTuple(1, list, 3)));
+    assertEquals("(1, [\"foo\", \"bar\"], 3)",
+                 EvalUtils.prettyPrintValue(makeTuple(1, list, 3)));
+    assertEquals("[1, (\"foo\", \"bar\"), 3]",
+                 EvalUtils.printValue(makeList(1, tuple, 3)));
+    assertEquals("[1, (\"foo\", \"bar\"), 3]",
+                 EvalUtils.prettyPrintValue(makeList(1, tuple, 3)));
+
+    Map<Object, Object> dict = makeDict();
+    dict.put(1, tuple);
+    dict.put(2, list);
+    dict.put("foo", makeList());
+    assertEquals("{1: (\"foo\", \"bar\"), 2: [\"foo\", \"bar\"], \"foo\": []}",
+                EvalUtils.printValue(dict));
+    assertEquals("{1: (\"foo\", \"bar\"), 2: [\"foo\", \"bar\"], \"foo\": []}",
+                EvalUtils.prettyPrintValue(dict));
+    assertEquals("FilesetEntry(srcdir = \"//foo:bar\", files = [], "
+               + "excludes = [\"xyz\"], destdir = \"\", "
+               + "strip_prefix = \".\", symlinks = \"copy\")",
+                 EvalUtils.prettyPrintValue(makeFilesetEntry()));
+  }
+
+  private void checkFormatPositionalFails(String format, List<?> tuple,
+                                          String errorMessage) {
+    try {
+      EvalUtils.formatString(format, tuple);
+      fail();
+    } catch (IllegalFormatException e) {
+      assertEquals(errorMessage, e.getMessage());
+    }
+  }
+
+  public void testFormatPositional() throws Exception {
+    assertEquals("foo 3", EvalUtils.formatString("%s %d", makeTuple("foo", 3)));
+
+    // Note: formatString doesn't perform scalar x -> (x) conversion;
+    // The %-operator is responsible for that.
+    assertEquals("", EvalUtils.formatString("", makeTuple()));
+    assertEquals("foo", EvalUtils.formatString("%s", makeTuple("foo")));
+    assertEquals("3.14159", EvalUtils.formatString("%s", makeTuple(3.14159)));
+    checkFormatPositionalFails("%s", makeTuple(1, 2, 3),
+        "not all arguments converted during string formatting");
+    assertEquals("%foo", EvalUtils.formatString("%%%s", makeTuple("foo")));
+    checkFormatPositionalFails("%%s", makeTuple("foo"),
+        "not all arguments converted during string formatting");
+    checkFormatPositionalFails("% %s", makeTuple("foo"),
+        "invalid arguments for format string");
+    assertEquals("[1, 2, 3]", EvalUtils.formatString("%s", makeTuple(makeList(1, 2, 3))));
+    assertEquals("(1, 2, 3)", EvalUtils.formatString("%s", makeTuple(makeTuple(1, 2, 3))));
+    assertEquals("[]", EvalUtils.formatString("%s", makeTuple(makeList())));
+    assertEquals("()", EvalUtils.formatString("%s", makeTuple(makeTuple())));
+
+    checkFormatPositionalFails("%.3g", makeTuple(), "invalid arguments for format string");
+    checkFormatPositionalFails("%.3g", makeTuple(1, 2), "invalid arguments for format string");
+    checkFormatPositionalFails("%.s", makeTuple(), "invalid arguments for format string");
+  }
+
+  private String createExpectedFilesetEntryString(FilesetEntry.SymlinkBehavior symlinkBehavior) {
+    return "FilesetEntry(srcdir = \"//x:x\","
+           + " files = [\"//x:x\"],"
+           + " excludes = [],"
+           + " destdir = \"\","
+           + " strip_prefix = \".\","
+           + " symlinks = \"" + symlinkBehavior.toString().toLowerCase() + "\")";
+  }
+
+  private FilesetEntry createTestFilesetEntry(FilesetEntry.SymlinkBehavior symlinkBehavior)
+    throws Exception {
+    Label label = Label.parseAbsolute("//x");
+    return new FilesetEntry(label,
+                            Arrays.asList(label),
+                            Arrays.<String>asList(),
+                            "",
+                            symlinkBehavior,
+                            ".");
+  }
+
+  public void testFilesetEntrySymlinkAttr() throws Exception {
+    FilesetEntry entryDereference =
+      createTestFilesetEntry(FilesetEntry.SymlinkBehavior.DEREFERENCE);
+
+    assertEquals(createExpectedFilesetEntryString(FilesetEntry.SymlinkBehavior.DEREFERENCE),
+                 EvalUtils.prettyPrintValue(entryDereference));
+  }
+
+  private FilesetEntry createStripPrefixFilesetEntry(String stripPrefix)  throws Exception {
+    Label label = Label.parseAbsolute("//x");
+    return new FilesetEntry(
+        label,
+        Arrays.asList(label),
+        Arrays.<String>asList(),
+        "",
+        FilesetEntry.SymlinkBehavior.DEREFERENCE,
+        stripPrefix);
+  }
+
+  public void testFilesetEntryStripPrefixAttr() throws Exception {
+    FilesetEntry withoutStripPrefix = createStripPrefixFilesetEntry(".");
+    FilesetEntry withStripPrefix = createStripPrefixFilesetEntry("orange");
+
+    String prettyWithout = EvalUtils.prettyPrintValue(withoutStripPrefix);
+    String prettyWith = EvalUtils.prettyPrintValue(withStripPrefix);
+
+    assertTrue(prettyWithout.contains("strip_prefix = \".\""));
+    assertTrue(prettyWith.contains("strip_prefix = \"orange\""));
+  }
+
+  public void testRegressionCrashInPrettyPrintValue() throws Exception {
+    // Would cause crash in code such as this:
+    //  Fileset(name='x', entries=[], out=[FilesetEntry(files=['a'])])
+    // While formatting the "expected x, got y" message for the 'out'
+    // attribute, prettyPrintValue(FilesetEntry) would be recursively called
+    // with a List<Label> even though this isn't a valid datatype in the
+    // interpreter.
+    // Fileset isn't part of bazel, even though FilesetEntry is.
+    Label label = Label.parseAbsolute("//x");
+    assertEquals("FilesetEntry(srcdir = \"//x:x\","
+                 + " files = [\"//x:x\"],"
+                 + " excludes = [],"
+                 + " destdir = \"\","
+                 + " strip_prefix = \".\","
+                 + " symlinks = \"copy\")",
+                 EvalUtils.prettyPrintValue(
+                     new FilesetEntry(label,
+                                      Arrays.asList(label),
+                                      Arrays.<String>asList(),
+                                      "",
+                                      FilesetEntry.SymlinkBehavior.COPY,
+                                      ".")));
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
new file mode 100644
index 0000000..f7ed359
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
@@ -0,0 +1,492 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Test of evaluation behavior.  (Implicitly uses lexer + parser.)
+ */
+public class EvaluationTest extends AbstractEvaluationTestCase {
+
+  protected Environment env;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    PackageFactory factory = new PackageFactory(TestRuleClassProvider.getRuleClassProvider());
+    env = factory.getEnvironment();
+  }
+
+  public Environment singletonEnv(String id, Object value) {
+    Environment env = new Environment();
+    env.update(id, value);
+    return env;
+  }
+
+  @Override
+  public Object eval(String input) throws Exception {
+    return eval(parseExpr(input), env);
+  }
+
+  public void testExprs() throws Exception {
+    assertEquals("fooxbar",
+                 eval("'%sx' % 'foo' + 'bar'"));
+    assertEquals("fooxbar",
+                 eval("('%sx' % 'foo') + 'bar'"));
+    assertEquals("foobarx",
+                 eval("'%sx' % ('foo' + 'bar')"));
+    assertEquals(579,
+                 eval("123 + 456"));
+    assertEquals(333,
+                 eval("456 - 123"));
+    assertEquals(2,
+                 eval("8 % 3"));
+
+    checkEvalError("3 % 'foo'", "unsupported operand type(s) for %: 'int' and 'string'");
+  }
+
+  public void testListExprs() throws Exception {
+    assertEquals(Arrays.asList(1, 2, 3),
+        eval("[1, 2, 3]"));
+    assertEquals(Arrays.asList(1, 2, 3),
+        eval("(1, 2, 3)"));
+  }
+
+  public void testStringFormatMultipleArgs() throws Exception {
+    assertEquals("XYZ", eval("'%sY%s' % ('X', 'Z')"));
+  }
+
+  public void testAndOr() throws Exception {
+    assertEquals(8, eval("8 or 9"));
+    assertEquals(8, eval("8 or foo")); // check that 'foo' is not evaluated
+    assertEquals(9, eval("0 or 9"));
+    assertEquals(9, eval("8 and 9"));
+    assertEquals(0, eval("0 and 9"));
+    assertEquals(0, eval("0 and foo")); // check that 'foo' is not evaluated
+
+    assertEquals(2, eval("1 and 2 or 3"));
+    assertEquals(3, eval("0 and 2 or 3"));
+    assertEquals(3, eval("1 and 0 or 3"));
+
+    assertEquals(1, eval("1 or 2 and 3"));
+    assertEquals(3, eval("0 or 2 and 3"));
+    assertEquals(0, eval("0 or 0 and 3"));
+    assertEquals(1, eval("1 or 0 and 3"));
+    assertEquals(1, eval("1 or 0 and 3"));
+
+    assertEquals(9, eval("\"\" or 9"));
+    assertEquals("abc", eval("\"abc\" or 9"));
+    assertEquals(Environment.NONE, eval("None and 1"));
+  }
+
+  public void testNot() throws Exception {
+    assertEquals(false, eval("not 1"));
+    assertEquals(true, eval("not ''"));
+  }
+
+  public void testNotWithLogicOperators() throws Exception {
+    assertEquals(0, eval("0 and not 0"));
+    assertEquals(0, eval("not 0 and 0"));
+
+    assertEquals(true, eval("1 and not 0"));
+    assertEquals(true, eval("not 0 or 0"));
+
+    assertEquals(0, eval("not 1 or 0"));
+    assertEquals(1, eval("not 1 or 1"));
+
+    assertEquals(true, eval("not (0 and 0)"));
+    assertEquals(false, eval("not (1 or 0)"));
+  }
+
+  public void testNotWithArithmeticOperators() throws Exception {
+    assertEquals(true, eval("not 0 + 0"));
+    assertEquals(false, eval("not 2 - 1"));
+  }
+
+  public void testNotWithCollections() throws Exception {
+    assertEquals(true, eval("not []"));
+    assertEquals(false, eval("not {'a' : 1}"));
+  }
+
+  public void testEquality() throws Exception {
+    assertEquals(true, eval("1 == 1"));
+    assertEquals(false, eval("1 == 2"));
+    assertEquals(true, eval("'hello' == 'hel' + 'lo'"));
+    assertEquals(false, eval("'hello' == 'bye'"));
+    assertEquals(true, eval("[1, 2] == [1, 2]"));
+    assertEquals(false, eval("[1, 2] == [2, 1]"));
+    assertEquals(true, eval("None == None"));
+  }
+
+  public void testInequality() throws Exception {
+    assertEquals(false, eval("1 != 1"));
+    assertEquals(true, eval("1 != 2"));
+    assertEquals(false, eval("'hello' != 'hel' + 'lo'"));
+    assertEquals(true, eval("'hello' != 'bye'"));
+    assertEquals(false, eval("[1, 2] != [1, 2]"));
+    assertEquals(true, eval("[1, 2] != [2, 1]"));
+  }
+
+  public void testEqualityPrecedence() throws Exception {
+    assertEquals(true, eval("1 + 3 == 2 + 2"));
+    assertEquals(true, eval("not 1 == 2"));
+    assertEquals(false, eval("not 1 != 2"));
+    assertEquals(true, eval("2 and 3 == 3 or 1"));
+    assertEquals(2, eval("2 or 3 == 3 and 1"));
+  }
+
+  public void testLessThan() throws Exception {
+    assertEquals(true, eval("1 <= 1"));
+    assertEquals(false, eval("1 < 1"));
+    assertEquals(true, eval("'a' <= 'b'"));
+    assertEquals(false, eval("'c' < 'a'"));
+  }
+
+  public void testGreaterThan() throws Exception {
+    assertEquals(true, eval("1 >= 1"));
+    assertEquals(false, eval("1 > 1"));
+    assertEquals(false, eval("'a' >= 'b'"));
+    assertEquals(true, eval("'c' > 'a'"));
+  }
+
+  public void testCompareStringInt() throws Exception {
+    checkEvalError("'a' >= 1", "Cannot compare string with int");
+  }
+
+  public void testNotComparable() throws Exception {
+    checkEvalError("[1, 2] < [1, 3]", "[1, 2] is not comparable");
+  }
+
+  public void testSumFunction() throws Exception {
+    Function sum = new AbstractFunction("sum") {
+        @Override
+        public Object call(List<Object> args, Map<String, Object> kwargs,
+            FuncallExpression ast, Environment env) {
+          int sum = 0;
+          for (Object arg : args) {
+            sum += (Integer) arg;
+          }
+          return sum;
+        }
+      };
+
+    Environment env = singletonEnv(sum.getName(), sum);
+
+    String callExpr = "sum(1, 2, 3, 4, 5, 6)";
+    assertEquals(21, eval(callExpr, env));
+
+    assertEquals(sum, eval("sum", env));
+
+    assertEquals(0, eval("sum(a=1, b=2)", env));
+
+    // rebind 'sum' in a new environment:
+    env = new Environment();
+    exec(parseStmt("sum = 123456"), env);
+
+    assertEquals(123456, env.lookup("sum"));
+
+    // now we can't call it any more:
+    checkEvalError(callExpr, env, "'int' object is not callable");
+
+    assertEquals(123456, eval("sum", env));
+  }
+
+  public void testKeywordArgs() throws Exception {
+
+    // This function returns the list of keyword-argument keys or values,
+    // depending on whether its first (integer) parameter is zero.
+    Function keyval = new AbstractFunction("keyval") {
+        @Override
+        public Object call(List<Object> args,
+                           final Map<String, Object> kwargs,
+                           FuncallExpression ast,
+                           Environment env) {
+          ArrayList<String> keys = new ArrayList<>(kwargs.keySet());
+          Collections.sort(keys);
+          if ((Integer) args.get(0) == 0) {
+            return keys;
+          } else {
+            return Lists.transform(keys, new com.google.common.base.Function<String, Object> () {
+                  @Override public Object apply (String s) {
+                    return kwargs.get(s);
+                  }});
+          }
+        }
+      };
+
+    Environment env = singletonEnv(keyval.getName(), keyval);
+
+    assertEquals(eval("['bar', 'foo', 'wiz']"),
+                 eval("keyval(0, foo=1, bar='bar', wiz=[1,2,3])", env));
+
+    assertEquals(eval("['bar', 1, [1,2,3]]"),
+                 eval("keyval(1, foo=1, bar='bar', wiz=[1,2,3])", env));
+  }
+
+  public void testMult() throws Exception {
+    assertEquals(42, eval("6 * 7"));
+
+    assertEquals("ababab", eval("3 * 'ab'"));
+    assertEquals("", eval("0 * 'ab'"));
+    assertEquals("100000", eval("'1' + '0' * 5"));
+  }
+
+  public void testConcatStrings() throws Exception {
+    assertEquals("foobar", eval("'foo' + 'bar'"));
+  }
+
+  public void testConcatLists() throws Exception {
+    // list
+    Object x = eval("[1,2] + [3,4]");
+    assertEquals(Arrays.asList(1, 2, 3, 4), x);
+    assertFalse(EvalUtils.isImmutable(x));
+
+    // tuple
+    x = eval("(1,2) + (3,4)");
+    assertEquals(Arrays.asList(1, 2, 3, 4), x);
+    assertTrue(EvalUtils.isImmutable(x));
+
+    checkEvalError("(1,2) + [3,4]", // list + tuple
+        "can only concatenate list (not \"tuple\") to list");
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testListComprehensions() throws Exception {
+    Iterable<Object> eval = (Iterable<Object>) eval(
+        "['foo/%s.java' % x for x in []]");
+    assertThat(eval).isEmpty();
+
+    eval = (Iterable<Object>) eval(
+        "['foo/%s.java' % x for x in ['bar', 'wiz', 'quux']]");
+    assertThat(eval).containsExactly("foo/bar.java", "foo/wiz.java", "foo/quux.java").inOrder();
+
+    eval = (Iterable<Object>) eval(
+        "['%s/%s.java' % (x, y) "
+        + "for x in ['foo', 'bar'] "
+        + "for y in ['baz', 'wiz', 'quux']]");
+    assertThat(eval).containsExactly("foo/baz.java", "foo/wiz.java", "foo/quux.java",
+        "bar/baz.java", "bar/wiz.java", "bar/quux.java").inOrder();
+
+    eval = (Iterable<Object>) eval(
+        "['%s/%s.java' % (x, x) "
+        + "for x in ['foo', 'bar'] "
+        + "for x in ['baz', 'wiz', 'quux']]");
+    assertThat(eval).containsExactly("baz/baz.java", "wiz/wiz.java", "quux/quux.java",
+        "baz/baz.java", "wiz/wiz.java", "quux/quux.java").inOrder();
+
+    eval = (Iterable<Object>) eval(
+        "['%s/%s.%s' % (x, y, z) "
+        + "for x in ['foo', 'bar'] "
+        + "for y in ['baz', 'wiz', 'quux'] "
+        + "for z in ['java', 'cc']]");
+    assertThat(eval).containsExactly("foo/baz.java", "foo/baz.cc", "foo/wiz.java", "foo/wiz.cc",
+        "foo/quux.java", "foo/quux.cc", "bar/baz.java", "bar/baz.cc", "bar/wiz.java", "bar/wiz.cc",
+        "bar/quux.java", "bar/quux.cc").inOrder();
+  }
+
+  // TODO(bazel-team): should this test work in Skylark?
+  @SuppressWarnings("unchecked")
+  public void testListComprehensionModifiesGlobalEnv() throws Exception {
+    Environment env = singletonEnv("x", 42);
+    assertThat((Iterable<Object>) eval(parseExpr("[x + 1 for x in [1,2,3]]"), env))
+        .containsExactly(2, 3, 4).inOrder();
+    assertEquals(3, env.lookup("x")); // (x is global)
+  }
+
+  public void testDictComprehensions() throws Exception {
+    assertEquals(Collections.emptyMap(), eval("{x : x for x in []}"));
+    assertEquals(ImmutableMap.of(1, 1, 2, 2), eval("{x : x for x in [1, 2]}"));
+    assertEquals(ImmutableMap.of("a", "v_a", "b", "v_b"),
+        eval("{x : 'v_' + x for x in ['a', 'b']}"));
+    assertEquals(ImmutableMap.of("k_a", "a", "k_b", "b"),
+        eval("{'k_' + x : x for x in ['a', 'b']}"));
+    assertEquals(ImmutableMap.of("k_a", "v_a", "k_b", "v_b"),
+        eval("{'k_' + x : 'v_' + x for x in ['a', 'b']}"));
+  }
+
+  public void testDictComprehensions_MultipleKey() throws Exception {
+    assertEquals(ImmutableMap.of(1, 1, 2, 2), eval("{x : x for x in [1, 2, 1]}"));
+    assertEquals(ImmutableMap.of("ab", "ab", "c", "c"),
+        eval("{x : x for x in ['ab', 'c', 'a' + 'b']}"));
+  }
+
+  public void testDictComprehensions_ToString() throws Exception {
+    assertEquals("{x: x for x in [1, 2]}", parseExpr("{x : x for x in [1, 2]}").toString());
+    assertEquals("{x + 'a': x for x in [1, 2]}",
+        parseExpr("{x + 'a' : x for x in [1, 2]}").toString());
+  }
+
+  public void testListConcatenation() throws Exception {
+    assertEquals(Arrays.asList(1, 2, 3, 4), eval("[1, 2] + [3, 4]", env));
+    assertEquals(ImmutableList.of(1, 2, 3, 4), eval("(1, 2) + (3, 4)", env));
+    checkEvalError("[1, 2] + (3, 4)", "can only concatenate tuple (not \"list\") to tuple");
+    checkEvalError("(1, 2) + [3, 4]", "can only concatenate list (not \"tuple\") to list");
+  }
+
+  public void testListComprehensionFailsOnNonSequence() throws Exception {
+    checkEvalError("[x + 1 for x in 123]", "type 'int' is not an iterable");
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testListComprehensionOnString() throws Exception {
+    assertThat((Iterable<Object>) eval("[x for x in 'abc']")).containsExactly("a", "b", "c")
+        .inOrder();
+  }
+
+  public void testInvalidAssignment() throws Exception {
+    Environment env = singletonEnv("x", 1);
+    checkEvalError(parseStmt("x + 1 = 2"), env, "can only assign to variables, not to 'x + 1'");
+  }
+
+  public void testListComprehensionOnDictionary() throws Exception {
+    List<Statement> input = parseFile("val = ['var_' + n for n in {'a':1,'b':2}]");
+    exec(input, env);
+    Iterable<?> result = (Iterable<?>) env.lookup("val");
+    assertThat(result).hasSize(2);
+    assertEquals("var_a", Iterables.get(result, 0));
+    assertEquals("var_b", Iterables.get(result, 1));
+  }
+
+  public void testListComprehensionOnDictionaryCompositeExpression() throws Exception {
+    exec(parseFile("d = {1:'a',2:'b'}\n"
+                  + "l = [d[x] for x in d]"), env);
+    assertEquals("[a, b]", env.lookup("l").toString());
+  }
+
+  public void testInOnListContains() throws Exception {
+    assertEquals(Boolean.TRUE, eval("'b' in ['a', 'b']"));
+  }
+
+  public void testInOnListDoesNotContain() throws Exception {
+    assertEquals(Boolean.FALSE, eval("'c' in ['a', 'b']"));
+  }
+
+  public void testInOnTupleContains() throws Exception {
+    assertEquals(Boolean.TRUE, eval("'b' in ('a', 'b')"));
+  }
+
+  public void testInOnTupleDoesNotContain() throws Exception {
+    assertEquals(Boolean.FALSE, eval("'c' in ('a', 'b')"));
+  }
+
+  public void testInOnDictContains() throws Exception {
+    assertEquals(Boolean.TRUE, eval("'b' in {'a' : 1, 'b' : 2}"));
+  }
+
+  public void testInOnDictDoesNotContainKey() throws Exception {
+    assertEquals(Boolean.FALSE, eval("'c' in {'a' : 1, 'b' : 2}"));
+  }
+
+  public void testInOnDictDoesNotContainVal() throws Exception {
+    assertEquals(Boolean.FALSE, eval("1 in {'a' : 1, 'b' : 2}"));
+  }
+
+  public void testInOnStringContains() throws Exception {
+    assertEquals(Boolean.TRUE, eval("'b' in 'abc'"));
+  }
+
+  public void testInOnStringDoesNotContain() throws Exception {
+    assertEquals(Boolean.FALSE, eval("'d' in 'abc'"));
+  }
+
+  public void testInOnStringLeftNotString() throws Exception {
+    checkEvalError("1 in '123'",
+        "in operator only works on strings if the left operand is also a string");
+  }
+
+  public void testInFailsOnNonIterable() throws Exception {
+    checkEvalError("'a' in 1",
+        "in operator only works on lists, tuples, dictionaries and strings");
+  }
+
+  public void testInCompositeForPrecedence() throws Exception {
+    assertEquals(0, eval("not 'a' in ['a'] or 0"));
+  }
+
+  private Object createObjWithStr() {
+    return new Object() {
+      @Override
+      public String toString() {
+        return "str marker";
+      }
+    };
+  }
+
+  public void testPercOnObject() throws Exception {
+    env.update("obj", createObjWithStr());
+    assertEquals("str marker", eval("'%s' % obj", env));
+  }
+
+  public void testPercOnObjectList() throws Exception {
+    env.update("obj", createObjWithStr());
+    assertEquals("str marker str marker", eval("'%s %s' % (obj, obj)", env));
+  }
+
+  public void testPercOnObjectInvalidFormat() throws Exception {
+    env.update("obj", createObjWithStr());
+    checkEvalError("'%d' % obj", env, "invalid arguments for format string");
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testDictKeys() throws Exception {
+    exec("v = {'a': 1}.keys() + ['b', 'c']", env);
+    assertThat((Iterable<Object>) env.lookup("v")).containsExactly("a", "b", "c").inOrder();
+  }
+
+  public void testDictKeysTooManyArgs() throws Exception {
+    checkEvalError("{'a': 1}.keys('abc')", env, "Invalid number of arguments (expected 0)");
+    checkEvalError("{'a': 1}.keys(arg='abc')", env, "Invalid number of arguments (expected 0)");
+  }
+
+  protected void checkEvalError(String input, String msg) throws Exception {
+    checkEvalError(input, env, msg);
+  }
+
+  protected void checkEvalError(String input, Environment env, String msg) throws Exception {
+    try {
+      eval(input, env);
+      fail();
+    } catch (EvalException e) {
+      assertEquals(msg, e.getMessage());
+    }
+  }
+
+  protected void checkEvalError(Statement input, Environment env, String msg) throws Exception {
+    checkEvalError(ImmutableList.of(input), env, msg);
+  }
+
+  protected void checkEvalError(List<Statement> input, Environment env, String msg)
+      throws Exception {
+    try {
+      exec(input, env);
+      fail();
+    } catch (EvalException e) {
+      assertEquals(msg, e.getMessage());
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/FunctionTest.java b/src/test/java/com/google/devtools/build/lib/syntax/FunctionTest.java
new file mode 100644
index 0000000..fc14922
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/FunctionTest.java
@@ -0,0 +1,424 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.packages.MethodLibrary;
+import com.google.devtools.build.lib.syntax.SkylarkType.SkylarkFunctionType;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A test class for functions and scoping.
+ */
+public class FunctionTest extends AbstractEvaluationTestCase {
+
+  private Environment env;
+
+  private static final ImmutableMap<String, SkylarkType> OUTER_FUNC_TYPES =
+      ImmutableMap.<String, SkylarkType>of(
+          "outer_func", SkylarkFunctionType.of("outer_func", SkylarkType.NONE));
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    env = new SkylarkEnvironment(syntaxEvents.collector());
+  }
+
+  public void testFunctionDef() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def func(a,b,c):\n"
+        + "  a = 1\n"
+        + "  b = a\n");
+
+    exec(input, env);
+    UserDefinedFunction stmt = (UserDefinedFunction) env.lookup("func");
+    assertNotNull(stmt);
+    assertEquals("func", stmt.getName());
+    assertEquals(3, stmt.getFunctionSignature().getSignature().getShape().getMandatoryPositionals());
+    assertThat(stmt.getStatements()).hasSize(2);
+  }
+
+  public void testFunctionDefDuplicateArguments() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFileForSkylark(
+        "def func(a,b,a):\n"
+        + "  a = 1\n");
+    syntaxEvents.assertContainsEvent("duplicate parameter name in function definition");
+  }
+
+  public void testFunctionDefCallOuterFunc() throws Exception {
+    final List<Object> params = new ArrayList<>();
+    List<Statement> input = parseFileForSkylark(
+        "def func(a):\n"
+        + "  outer_func(a)\n"
+        + "func(1)\n"
+        + "func(2)",
+        OUTER_FUNC_TYPES);
+    createOuterFunction(env, params);
+    exec(input, env);
+    assertThat(params).containsExactly(1, 2).inOrder();
+  }
+
+  private void createOuterFunction(Environment env, final List<Object> params) {
+    Function outerFunc = new AbstractFunction("outer_func") {
+
+      @Override
+      public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast,
+          Environment env) throws EvalException, InterruptedException {
+        params.addAll(args);
+        return Environment.NONE;
+      }
+    };
+    env.update("outer_func", outerFunc);
+  }
+
+  public void testFunctionDefNoEffectOutsideScope() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def func():\n"
+        + "  a = 2\n"
+        + "func()\n");
+    env.update("a", 1);
+    exec(input, env);
+    assertEquals(1, env.lookup("a"));
+  }
+
+  public void testFunctionDefGlobalVaribleReadInFunction() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "a = 1\n"
+        + "def func():\n"
+        + "  b = a\n"
+        + "  return b\n"
+        + "c = func()\n");
+    exec(input, env);
+    assertEquals(1, env.lookup("c"));
+  }
+
+  public void testFunctionDefLocalGlobalScope() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "a = 1\n"
+        + "def func():\n"
+        + "  a = 2\n"
+        + "  b = a\n"
+        + "  return b\n"
+        + "c = func()\n");
+    exec(input, env);
+    assertEquals(2, env.lookup("c"));
+  }
+
+  public void testFunctionDefLocalVariableReferencedBeforeAssignment() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "a = 1\n"
+        + "def func():\n"
+        + "  b = a\n"
+        + "  a = 2\n"
+        + "  return b\n"
+        + "c = func()\n");
+    try {
+      exec(input, env);
+      fail();
+    } catch (EvalException e) {
+      assertThat(e.getMessage()).contains("Variable 'a' is referenced before assignment.");
+    }
+  }
+
+  public void testFunctionDefLocalVariableReferencedAfterAssignment() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "a = 1\n"
+        + "def func():\n"
+        + "  a = 2\n"
+        + "  b = a\n"
+        + "  a = 3\n"
+        + "  return b\n"
+        + "c = func()\n");
+    exec(input, env);
+    assertEquals(2, env.lookup("c"));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testSkylarkGlobalComprehensionIsAllowed() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "a = [i for i in [1, 2, 3]]\n");
+    exec(input, env);
+    assertThat((Iterable<Object>) env.lookup("a")).containsExactly(1, 2, 3).inOrder();
+  }
+
+  public void testFunctionReturn() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def func():\n"
+        + "  return 2\n"
+        + "b = func()\n");
+    exec(input, env);
+    assertEquals(2, env.lookup("b"));
+  }
+
+  public void testFunctionReturnFromALoop() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def func():\n"
+        + "  for i in [1, 2, 3, 4, 5]:\n"
+        + "    return i\n"
+        + "b = func()\n");
+    exec(input, env);
+    assertEquals(1, env.lookup("b"));
+  }
+
+  public void testFunctionExecutesProperly() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def func(a):\n"
+        + "  b = 1\n"
+        + "  if a:\n"
+        + "    b = 2\n"
+        + "  return b\n"
+        + "c = func(0)\n"
+        + "d = func(1)\n");
+    exec(input, env);
+    assertEquals(1, env.lookup("c"));
+    assertEquals(2, env.lookup("d"));
+  }
+
+  public void testFunctionCallFromFunction() throws Exception {
+    final List<Object> params = new ArrayList<>();
+    List<Statement> input = parseFileForSkylark(
+        "def func2(a):\n"
+        + "  outer_func(a)\n"
+        + "def func1(b):\n"
+        + "  func2(b)\n"
+        + "func1(1)\n"
+        + "func1(2)\n",
+        OUTER_FUNC_TYPES);
+    createOuterFunction(env, params);
+    exec(input, env);
+    assertThat(params).containsExactly(1, 2).inOrder();
+  }
+
+  public void testFunctionCallFromFunctionReadGlobalVar() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "a = 1\n"
+        + "def func2():\n"
+        + "  return a\n"
+        + "def func1():\n"
+        + "  return func2()\n"
+        + "b = func1()\n");
+    exec(input, env);
+    assertEquals(1, env.lookup("b"));
+  }
+
+  public void testSingleLineFunction() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def func(): return 'a'\n"
+        + "s = func()\n");
+    exec(input, env);
+    assertEquals("a", env.lookup("s"));
+  }
+
+  public void testFunctionReturnsDictionary() throws Exception {
+    MethodLibrary.setupMethodEnvironment(env);
+    List<Statement> input = parseFileForSkylark(
+        "def func(): return {'a' : 1}\n"
+        + "d = func()\n"
+        + "a = d['a']\n");
+    exec(input, env);
+    assertEquals(1, env.lookup("a"));
+  }
+
+  public void testFunctionReturnsList() throws Exception {
+    MethodLibrary.setupMethodEnvironment(env);
+    List<Statement> input = parseFileForSkylark(
+        "def func(): return [1, 2, 3]\n"
+        + "d = func()\n"
+        + "a = d[1]\n");
+    exec(input, env);
+    assertEquals(2, env.lookup("a"));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testFunctionListArgumentsAreImmutable() throws Exception {
+    MethodLibrary.setupMethodEnvironment(env);
+    List<Statement> input = parseFileForSkylark(
+          "l = [1]\n"
+        + "def func(l):\n"
+        + "  l += [2]\n"
+        + "func(l)");
+    exec(input, env);
+    assertThat((Iterable<Object>) env.lookup("l")).containsExactly(1);
+  }
+
+  public void testFunctionDictArgumentsAreImmutable() throws Exception {
+    MethodLibrary.setupMethodEnvironment(env);
+    List<Statement> input = parseFileForSkylark(
+          "d = {'a' : 1}\n"
+        + "def func(d):\n"
+        + "  d += {'a' : 2}\n"
+        + "func(d)");
+    exec(input, env);
+    assertEquals(ImmutableMap.of("a", 1), env.lookup("d"));
+  }
+
+  public void testFunctionNameAliasing() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+          "def func(a):\n"
+        + "  return a + 1\n"
+        + "alias = func\n"
+        + "r = alias(1)");
+    exec(input, env);
+    assertEquals(2, env.lookup("r"));
+  }
+
+  public void testCallingFunctionsWithMixedModeArgs() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+          "def func(a, b, c):\n"
+        + "  return a + b + c\n"
+        + "v = func(1, c = 2, b = 3)");
+    exec(input, env);
+    assertEquals(6, env.lookup("v"));
+  }
+
+  private String functionWithOptionalArgs() {
+    return "def func(a, b = None, c = None):\n"
+         + "  r = a + 'a'\n"
+         + "  if b:\n"
+         + "    r += 'b'\n"
+         + "  if c:\n"
+         + "    r += 'c'\n"
+         + "  return r\n";
+  }
+
+  public void testWhichOptionalArgsAreDefinedForFunctions() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        functionWithOptionalArgs()
+        + "v1 = func('1', 1, 1)\n"
+        + "v2 = func(b = 2, a = '2', c = 2)\n"
+        + "v3 = func('3')\n"
+        + "v4 = func('4', c = 1)\n");
+    exec(input, env);
+    assertEquals("1abc", env.lookup("v1"));
+    assertEquals("2abc", env.lookup("v2"));
+    assertEquals("3a", env.lookup("v3"));
+    assertEquals("4ac", env.lookup("v4"));
+  }
+
+  public void testDefaultArguments() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+          "def func(a, b = 'b', c = 'c'):\n"
+        + "  return a + b + c\n"
+        + "v1 = func('a', 'x', 'y')\n"
+        + "v2 = func(b = 'x', a = 'a', c = 'y')\n"
+        + "v3 = func('a')\n"
+        + "v4 = func('a', c = 'y')\n");
+    exec(input, env);
+    assertEquals("axy", env.lookup("v1"));
+    assertEquals("axy", env.lookup("v2"));
+    assertEquals("abc", env.lookup("v3"));
+    assertEquals("aby", env.lookup("v4"));
+  }
+
+  public void testDefaultArgumentsInsufficientArgNum() throws Exception {
+    checkError("func(a, b = null, c = null) received insufficient arguments",
+        "def func(a, b = 'b', c = 'c'):",
+        "  return a + b + c",
+        "func()");
+  }
+
+  public void testKwargs() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def foo(a, b = 'b', c = 'c'):\n"
+      + "  return a + b + c\n"
+      + "args = {'a': 'x', 'c': 'z'}\n"
+      + "v1 = foo(**args)\n"
+      + "v2 = foo('x', **{'b': 'y'})\n"
+      + "v3 = foo(c = 'z', a = 'x', **{'b': 'y'})");
+    exec(input, env);
+    assertEquals("xbz", env.lookup("v1"));
+    assertEquals("xyc", env.lookup("v2"));
+    assertEquals("xyz", env.lookup("v3"));
+  }
+
+  public void testKwargsBadKey() throws Exception {
+    checkError("Keywords must be strings, not int",
+        "def func(a, b):",
+        "  return a + b",
+        "func('a', **{3: 1})");
+  }
+
+  public void testKwargsIsNotDict() throws Exception {
+    checkError("Argument after ** must be a dictionary, not int",
+        "def func(a, b):",
+        "  return a + b",
+        "func('a', **42)");
+  }
+
+  public void testKwargsCollision() throws Exception {
+    checkError("func(a, b) got multiple values for keyword argument 'b'",
+        "def func(a, b):",
+        "  return a + b",
+        "func('a', 'b', **{'b': 'foo'})");
+  }
+
+  public void testKwargsCollisionWithNamed() throws Exception {
+    checkError("duplicate keyword 'b' in call to func",
+        "def func(a, b):",
+        "  return a + b",
+        "func('a', b = 'b', **{'b': 'foo'})");
+  }
+
+  public void testDefaultArguments2() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "a = 2\n"
+        + "def foo(x=a): return x\n"
+        + "def bar():\n"
+        + "  a = 3\n"
+        + "  return foo()\n"
+        + "v = bar()\n");
+    exec(input, env);
+    assertEquals(2, env.lookup("v"));
+  }
+
+  public void testMixingPositionalOptional() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+                "def f(name, value = '', optional = ''): return value\n"
+                + "v = f('name', 'value')\n");
+    exec(input, env);
+    assertEquals("value", env.lookup("v"));
+  }
+
+  public void testStarArg() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+                "def f(name, value = '1', optional = '2'): return name + value + optional\n"
+                        + "v1 = f(*['name', 'value'])\n"
+                        + "v2 = f('0', *['name', 'value'])\n"
+                        + "v3 = f('0', *['b'], optional = '3')\n"
+                + "v4 = f(*[],name='a')\n");
+    exec(input, env);
+    assertEquals("namevalue2", env.lookup("v1"));
+    assertEquals("0namevalue", env.lookup("v2"));
+    assertEquals("0b3", env.lookup("v3"));
+    assertEquals("a12", env.lookup("v4"));
+  }
+
+  private void checkError(String msg, String... lines)
+      throws Exception {
+    try {
+      List<Statement> input = parseFileForSkylark(Joiner.on("\n").join(lines));
+      exec(input, env);
+      fail();
+    } catch (EvalException e) {
+      assertEquals(msg, e.getMessage());
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/GlobCriteriaTest.java b/src/test/java/com/google/devtools/build/lib/syntax/GlobCriteriaTest.java
new file mode 100644
index 0000000..75fed7d
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/GlobCriteriaTest.java
@@ -0,0 +1,169 @@
+// Copyright 2009 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;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.testutil.Suite;
+import com.google.devtools.build.lib.testutil.TestSpec;
+
+import junit.framework.TestCase;
+
+/**
+ * Links for {@link GlobCriteria}
+ */
+@TestSpec(size = Suite.SMALL_TESTS)
+public class GlobCriteriaTest extends TestCase {
+
+  public void testParse_EmptyList() throws Exception {
+    GlobCriteria gc = GlobCriteria.parse("[]");
+    assertFalse(gc.isGlob());
+    assertTrue(gc.getIncludePatterns().isEmpty());
+    assertTrue(gc.getExcludePatterns().isEmpty());
+  }
+
+  public void testParse_SingleList() throws Exception {
+    GlobCriteria gc = GlobCriteria.parse("['abc']");
+    assertFalse(gc.isGlob());
+    assertEquals(ImmutableList.of("abc"), gc.getIncludePatterns());
+    assertTrue(gc.getExcludePatterns().isEmpty());
+  }
+
+  public void testParse_MultipleList() throws Exception {
+    GlobCriteria gc = GlobCriteria.parse("['abc', 'def', 'ghi']");
+    assertFalse(gc.isGlob());
+    assertEquals(ImmutableList.of("abc", "def", "ghi"), gc.getIncludePatterns());
+    assertTrue(gc.getExcludePatterns().isEmpty());
+  }
+
+  public void testParse_EmptyGlob() throws Exception {
+    GlobCriteria gc = GlobCriteria.parse("glob([])");
+    assertTrue(gc.isGlob());
+    assertTrue(gc.getIncludePatterns().isEmpty());
+    assertTrue(gc.getExcludePatterns().isEmpty());
+  }
+
+  public void testParse_SingleGlob() throws Exception {
+    GlobCriteria gc = GlobCriteria.parse("glob(['abc'])");
+    assertTrue(gc.isGlob());
+    assertEquals(ImmutableList.of("abc"), gc.getIncludePatterns());
+    assertTrue(gc.getExcludePatterns().isEmpty());
+  }
+
+  public void testParse_MultipleGlob() throws Exception {
+    GlobCriteria gc = GlobCriteria.parse("glob(['abc', 'def', 'ghi'])");
+    assertTrue(gc.isGlob());
+    assertEquals(ImmutableList.of("abc", "def", "ghi"), gc.getIncludePatterns());
+    assertTrue(gc.getExcludePatterns().isEmpty());
+  }
+
+  public void testParse_EmptyGlobWithExclude() throws Exception {
+    GlobCriteria gc = GlobCriteria.parse("glob([], exclude=['xyz'])");
+    assertTrue(gc.isGlob());
+    assertTrue(gc.getIncludePatterns().isEmpty());
+    assertEquals(ImmutableList.of("xyz"), gc.getExcludePatterns());
+  }
+
+  public void testParse_SingleGlobWithExclude() throws Exception {
+    GlobCriteria gc = GlobCriteria.parse("glob(['abc'], exclude=['xyz'])");
+    assertTrue(gc.isGlob());
+    assertEquals(ImmutableList.of("abc"), gc.getIncludePatterns());
+    assertEquals(ImmutableList.of("xyz"), gc.getExcludePatterns());
+  }
+
+  public void testParse_MultipleGlobWithExclude() throws Exception {
+    GlobCriteria gc = GlobCriteria.parse("glob(['abc', 'def', 'ghi'], exclude=['xyz'])");
+    assertTrue(gc.isGlob());
+    assertEquals(ImmutableList.of("abc", "def", "ghi"), gc.getIncludePatterns());
+    assertEquals(ImmutableList.of("xyz"), gc.getExcludePatterns());
+  }
+
+  public void testParse_MultipleGlobWithMultipleExclude() throws Exception {
+    GlobCriteria gc = GlobCriteria.parse(
+        "glob(['abc', 'def', 'ghi'], exclude=['rst', 'uvw', 'xyz'])");
+    assertTrue(gc.isGlob());
+    assertEquals(ImmutableList.of("abc", "def", "ghi"), gc.getIncludePatterns());
+    assertEquals(ImmutableList.of("rst", "uvw", "xyz"), gc.getExcludePatterns());
+  }
+
+  public void testParse_GlobWithSlashesAndWildcards() throws Exception {
+    GlobCriteria gc = GlobCriteria.parse("glob(['java/src/net/jsunit/*.java'])");
+    assertTrue(gc.isGlob());
+    assertEquals(ImmutableList.of("java/src/net/jsunit/*.java"), gc.getIncludePatterns());
+    assertTrue(gc.getExcludePatterns().isEmpty());
+  }
+
+  public void testParse_ExcludeWithInvalidLabel() throws Exception {
+    GlobCriteria gc = GlobCriteria.parse("glob(['abc', 'def', 'ghi'], exclude=['xyz~'])");
+    assertTrue(gc.isGlob());
+    assertEquals(ImmutableList.of("abc", "def", "ghi"), gc.getIncludePatterns());
+    assertEquals(ImmutableList.of("xyz~"), gc.getExcludePatterns());
+  }
+
+  public void testParse_InvalidFormat_TooManySpacesList() throws Exception {
+    try {
+      GlobCriteria.parse("glob(['abc,  'def', 'ghi'], exclude=['xyz~'])");
+      fail();
+    } catch (IllegalArgumentException e) {
+      // expected
+    }
+  }
+
+  public void testParse_InvalidFormat_MissingQuote() throws Exception {
+    try {
+      GlobCriteria.parse("glob(['abc, 'def', 'ghi'], exclude=['xyz~'])");
+      fail();
+    } catch (IllegalArgumentException e) {
+      // expected
+    }
+  }
+
+  public void testParse_InvalidFormat_TooManySpacesExclude() throws Exception {
+    try {
+      GlobCriteria.parse("glob(['abc', 'def', 'ghi'],  exclude=['xyz~'])");
+      fail();
+    } catch (IllegalArgumentException e) {
+      // expected
+    }
+  }
+
+  public void testParse_InvalidFormat_MissingQuoteExclude() throws Exception {
+    try {
+      GlobCriteria.parse("glob(['abc, 'def', 'ghi'], exclude=['xyz~])");
+      fail();
+    } catch (IllegalArgumentException e) {
+      // expected
+    }
+  }
+
+  public void testParse_InvalidFormat_ExcludeWithList() throws Exception {
+    try {
+      GlobCriteria.parse("['abc, 'def', 'ghi'], exclude=['xyz~']");
+      fail();
+    } catch (IllegalArgumentException e) {
+      // expected
+    }
+  }
+
+  public void testParse_veryLongString() throws Exception {
+    StringBuilder builder = new StringBuilder();
+    builder.append("['File0.java'");
+    for (int i = 1; i < 5000; ++i) {
+      builder.append(", 'File").append(i).append(".java'");
+    }
+    builder.append("]");
+    String s = builder.toString();
+    GlobCriteria gc = GlobCriteria.parse(s);
+    assertEquals(s, gc.toString());
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/GlobListTest.java b/src/test/java/com/google/devtools/build/lib/syntax/GlobListTest.java
new file mode 100644
index 0000000..94e0667
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/GlobListTest.java
@@ -0,0 +1,103 @@
+// Copyright 2009 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;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.testutil.Suite;
+import com.google.devtools.build.lib.testutil.TestSpec;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+
+/**
+ * Tests for {@link GlobList}
+ */
+@TestSpec(size = Suite.SMALL_TESTS)
+@RunWith(JUnit4.class)
+public class GlobListTest {
+
+  @Test
+  public void testParse_glob() throws Exception {
+    String expression = "glob(['abc'])";
+    assertEquals(expression, GlobList.parse(expression).toExpression());
+  }
+
+  @Test
+  public void testParse_multipleGlobs() throws Exception {
+    String expression = "glob(['abc']) + glob(['def']) + glob(['ghi'])";
+    assertEquals(expression, GlobList.parse(expression).toExpression());
+  }
+
+  @Test
+  public void testParse_multipleLists() throws Exception {
+    String expression = "['abc'] + ['def'] + ['ghi']";
+    assertEquals(expression, GlobList.parse(expression).toExpression());
+  }
+
+  @Test
+  public void testParse_complexExpression() throws Exception {
+    String expression = "glob(['abc', 'def', 'ghi'], "
+      + "exclude=['rst', 'uvw', 'xyz']) "
+      + "+ glob(['abc', 'def', 'ghi'], exclude=['rst', 'uvw', 'xyz'])";
+    assertEquals(expression, GlobList.parse(expression).toExpression());
+  }
+
+  @Test
+  public void testConcat_GlobToGlob() throws Exception {
+    GlobList<String> glob1 = GlobList.parse(
+        "glob(['abc'], exclude=['def']) + glob(['xyz'])");
+    GlobList<String> glob2 = GlobList.parse(
+        "glob(['xyzzy']) + glob(['foo'], exclude=['bar'])");
+    GlobList<String> cat = GlobList.concat(glob1, glob2);
+    assertEquals(glob1.toExpression() + " + " + glob2.toExpression(), cat.toExpression());
+  }
+
+  @Test
+  public void testConcat_GlobToList() throws Exception {
+    GlobList<String> glob = GlobList.parse(
+        "glob(['abc'], exclude=['def']) + glob(['xyz'])");
+    List<String> list = ImmutableList.of("xyzzy", "foo", "bar");
+    GlobList<String> cat = GlobList.concat(list, glob);
+    assertEquals("['xyzzy', 'foo', 'bar'] + glob(['abc'], exclude=['def']) + glob(['xyz'])",
+        cat.toExpression());
+  }
+
+  @Test
+  public void testConcat_ListToGlob() throws Exception {
+    GlobList<String> glob = GlobList.parse(
+        "glob(['abc'], exclude=['def']) + glob(['xyz'])");
+    List<String> list = ImmutableList.of("xyzzy", "foo", "bar");
+    GlobList<String> cat = GlobList.concat(glob, list);
+    assertEquals("glob(['abc'], exclude=['def']) + glob(['xyz']) + ['xyzzy', 'foo', 'bar']",
+        cat.toExpression());
+  }
+
+  @Test
+  public void testGetCriteria() throws Exception {
+    List<String> include = ImmutableList.of("abc", "def", "ghi");
+    List<String> exclude = ImmutableList.of("rst", "uvw", "xyz");
+    List<String> matches = ImmutableList.of("xyzzy", "foo", "bar");
+    GlobList<String> glob = GlobList.captureResults(include, exclude, matches);
+    assertEquals(matches, glob);
+    ImmutableList<GlobCriteria> criteria = glob.getCriteria();
+    assertEquals(1, criteria.size());
+    assertEquals(include, criteria.get(0).getIncludePatterns());
+    assertEquals(exclude, criteria.get(0).getExcludePatterns());
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/LabelTest.java b/src/test/java/com/google/devtools/build/lib/syntax/LabelTest.java
new file mode 100644
index 0000000..a0daf20
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/LabelTest.java
@@ -0,0 +1,364 @@
+// Copyright 2005 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;
+
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertContainsRegex;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.regex.Pattern;
+
+/**
+ * Tests for {@link Label}.
+ */
+@RunWith(JUnit4.class)
+public class LabelTest {
+
+  private static final String BAD_PACKAGE_CHARS =
+      "package names may contain only A-Z, a-z, 0-9, '/', '-' and '_'";
+
+  private static final String TARGET_UPLEVEL =
+      "target names may not contain up-level references '..'";
+
+  @Test
+  public void testAbsolute() throws Exception {
+    {
+      Label l = Label.parseAbsolute("//foo/bar:baz");
+      assertEquals("foo/bar", l.getPackageName());
+      assertEquals("baz", l.getName());
+    }
+    {
+      Label l = Label.parseAbsolute("//foo/bar");
+      assertEquals("foo/bar", l.getPackageName());
+      assertEquals("bar", l.getName());
+    }
+  }
+
+  private static String parseCommandLine(String label, String prefix) throws SyntaxException {
+    return Label.parseCommandLineLabel(label, new PathFragment(prefix)).toString();
+  }
+
+  @Test
+  public void testLabelResolution() throws Exception {
+    assertEquals("//absolute:label", parseCommandLine("//absolute:label", ""));
+    assertEquals("//absolute:label", parseCommandLine("//absolute:label", "absolute"));
+    assertEquals("//absolute:label", parseCommandLine(":label", "absolute"));
+    assertEquals("//absolute:label", parseCommandLine("label", "absolute"));
+    assertEquals("//absolute:label", parseCommandLine("absolute:label", ""));
+    assertEquals("//absolute/path:label", parseCommandLine("path:label", "absolute"));
+    assertEquals("//absolute/path:label/path", parseCommandLine("path:label/path", "absolute"));
+    assertEquals("//absolute:label/path", parseCommandLine("label/path", "absolute"));
+  }
+
+  @Test
+  public void testLabelResolutionAbsolutePath() throws Exception {
+    try {
+      parseCommandLine("//absolute:label", "/absolute");
+      fail();
+    } catch (IllegalArgumentException e) {
+      // Expected exception
+    }
+  }
+
+  @Test
+  public void testLabelResolutionBadSyntax() throws Exception {
+    try {
+      parseCommandLine("//absolute:A+bad%syntax", "");
+      fail();
+    } catch (SyntaxException e) {
+      // Expected exception
+    }
+  }
+
+  @Test
+  public void testGetRelative() throws Exception {
+    Label base = Label.parseAbsolute("//foo/bar:baz");
+    {
+      Label l = base.getRelative("//p1/p2:target");
+      assertEquals("p1/p2", l.getPackageName());
+      assertEquals("target", l.getName());
+    }
+    {
+      Label l = base.getRelative(":quux");
+      assertEquals("foo/bar", l.getPackageName());
+      assertEquals("quux", l.getName());
+    }
+    try {
+      base.getRelative("/p1/p2:target");
+      fail();
+    } catch (Label.SyntaxException e) {
+      /* ok */
+    }
+    try {
+      base.getRelative("quux:");
+      fail();
+    } catch (Label.SyntaxException e) {
+      /* ok */
+    }
+    try {
+      base.getRelative(":");
+      fail();
+    } catch (Label.SyntaxException e) {
+      /* ok */
+    }
+    try {
+      base.getRelative("::");
+      fail();
+    } catch (Label.SyntaxException e) {
+      /* ok */
+    }
+  }
+
+  @Test
+  public void testFactory() throws Exception {
+    Label l = Label.create("foo/bar", "quux");
+    assertEquals("foo/bar", l.getPackageName());
+    assertEquals("quux", l.getName());
+  }
+
+  @Test
+  public void testIdentities() throws Exception {
+
+    Label l1 = Label.parseAbsolute("//foo/bar:baz");
+    Label l2 = Label.parseAbsolute("//foo/bar:baz");
+    Label l3 = Label.parseAbsolute("//foo/bar:quux");
+
+    assertTrue(l1.equals(l1));
+    assertTrue(l2.equals(l1));
+    assertTrue(l1.equals(l2));
+    assertTrue(l2.equals(l1));
+
+    assertFalse(l3.equals(l1));
+    assertFalse(l1.equals(l3));
+
+    assertEquals(l1.hashCode(), l2.hashCode());
+  }
+
+  @Test
+  public void testToString() throws Exception {
+    {
+      String s = "//foo/bar:baz";
+      Label l = Label.parseAbsolute(s);
+      assertEquals(s, l.toString());
+    }
+    {
+      Label l = Label.parseAbsolute("//foo/bar");
+      assertEquals("//foo/bar:bar", l.toString());
+    }
+  }
+
+  @Test
+  public void testDotDot() throws Exception {
+    Label.parseAbsolute("//foo/bar:baz..gif");
+  }
+
+  /**
+   * Asserts that creating a label throws a SyntaxException.
+   * @param label the label to create.
+   */
+  private static void assertSyntaxError(String expectedError, String label) {
+    try {
+      Label.parseAbsolute(label);
+      fail("Label '" + label + "' did not contain a syntax error");
+    } catch (SyntaxException e) {
+      assertContainsRegex(Pattern.quote(expectedError), e.getMessage());
+    }
+  }
+
+  @Test
+  public void testBadCharacters() throws Exception {
+    assertSyntaxError("package names may contain only",
+                      "//foo/bar baz");
+    assertSyntaxError("target names may not contain ':'",
+                      "//foo:bar:baz");
+    assertSyntaxError("target names may not contain ':'",
+                      "//foo:bar:");
+    assertSyntaxError("target names may not contain ':'",
+                      "//foo/bar::");
+    assertSyntaxError("target names may not contain '&'",
+                      "//foo:bar&");
+    assertSyntaxError("target names may not contain '$'",
+                      "//foo/bar:baz$a");
+    assertSyntaxError("target names may not contain '('",
+                      "//foo/bar:baz(foo)");
+    assertSyntaxError("target names may not contain ')'",
+                      "//foo/bar:bazfoo)");
+  }
+
+  @Test
+  public void testUplevelReferences() throws Exception {
+    assertSyntaxError(BAD_PACKAGE_CHARS,
+                      "//foo/bar/..:baz");
+    assertSyntaxError(BAD_PACKAGE_CHARS,
+                      "//foo/../baz:baz");
+    assertSyntaxError(BAD_PACKAGE_CHARS,
+                      "//../bar/baz:baz");
+    assertSyntaxError(BAD_PACKAGE_CHARS,
+                      "//..:foo");
+    assertSyntaxError(TARGET_UPLEVEL,
+                      "//foo:bar/../baz");
+    assertSyntaxError(TARGET_UPLEVEL,
+                      "//foo:../bar/baz");
+    assertSyntaxError(TARGET_UPLEVEL,
+                      "//foo:bar/baz/..");
+    assertSyntaxError(TARGET_UPLEVEL,
+                      "//foo:..");
+  }
+
+  @Test
+  public void testDotAsAPathSegment() throws Exception {
+    assertSyntaxError("package names may contain only A-Z, a-z, 0-9, '/', '-' and '_'",
+                      "//foo/bar/.:baz");
+    assertSyntaxError(BAD_PACKAGE_CHARS,
+                      "//foo/./baz:baz");
+    assertSyntaxError(BAD_PACKAGE_CHARS,
+                      "//./bar/baz:baz");
+    assertSyntaxError(BAD_PACKAGE_CHARS,
+                      "//.:foo");
+    assertSyntaxError("target names may not contain '.' as a path segment",
+                      "//foo:bar/./baz");
+    assertSyntaxError("target names may not contain '.' as a path segment",
+                      "//foo:./bar/baz");
+    // TODO(bazel-team): enable when we have removed the "Workaround" in Label
+    // that rewrites broken Labels by removing the trailing '.'
+    //assertSyntaxError(TARGET_UPLEVEL,
+    //                  "//foo:bar/baz/.");
+    //assertSyntaxError(TARGET_UPLEVEL,
+    //                  "//foo:.");
+  }
+
+  @Test
+  public void testTrailingDotSegment() throws Exception {
+    assertEquals(Label.parseAbsolute("//foo:dir/."), Label.parseAbsolute("//foo:dir"));
+  }
+
+  @Test
+  public void testSomeOtherBadLabels() throws Exception {
+    assertSyntaxError("package names may not end with '/'",
+                      "//foo/:bar");
+    assertSyntaxError("empty package name", "//:foo");
+    assertSyntaxError("package names may not start with '/'", "///p:foo");
+    assertSyntaxError("package names may not contain '//' path separators",
+                      "//a//b:foo");
+  }
+
+  @Test
+  public void testSomeGoodLabels() throws Exception {
+    Label.parseAbsolute("//foo:..bar");
+    Label.parseAbsolute("//Foo:..bar");
+    Label.parseAbsolute("//-Foo:..bar");
+    Label.parseAbsolute("//00:..bar");
+    Label.parseAbsolute("//package:foo+bar");
+    Label.parseAbsolute("//package:foo_bar");
+    Label.parseAbsolute("//package:foo=bar");
+    Label.parseAbsolute("//package:foo-bar");
+    Label.parseAbsolute("//package:foo.bar");
+    Label.parseAbsolute("//package:foo@bar");
+    Label.parseAbsolute("//package:foo~bar");
+  }
+
+  /**
+   * Regression test: we previously expanded the set of characters which are considered label chars
+   * to include "@" (see test above). An unexpected side-effect is that "@D" in genrule(cmd) was
+   * considered to be a valid relative label! The fix is to forbid "@x" in package names.
+   */
+  @Test
+  public void testAtVersionIsIllegal() throws Exception {
+    assertSyntaxError(BAD_PACKAGE_CHARS, "//foo/bar@123:baz");
+  }
+
+  @Test
+  public void testDoubleSlashPathSeparator() throws Exception {
+    assertSyntaxError("package names may not contain '//' path separators",
+                      "//foo//bar:baz");
+    assertSyntaxError("target names may not contain '//' path separator",
+                      "//foo:bar//baz");
+  }
+
+  @Test
+  public void testNonPrintableCharacters() throws Exception {
+    assertSyntaxError(
+      "target names may not contain non-printable characters: '\\x02'",
+      "//foo:..\002bar");
+  }
+
+  /** Make sure that control characters - such as CR - are escaped on output. */
+  @Test
+  public void testInvalidLineEndings() throws Exception {
+    assertSyntaxError("invalid target name '..bar\\r': "
+        + "target names may not end with carriage returns", "//foo:..bar\r");
+  }
+
+  @Test
+  public void testEmptyName() throws Exception {
+    assertSyntaxError("invalid target name '': empty target name", "//foo/bar:");
+  }
+
+  @Test
+  public void testSerializationSimple() throws Exception {
+    checkSerialization("//a", 91);
+  }
+
+  @Test
+  public void testSerializationNested() throws Exception {
+    checkSerialization("//foo/bar:baz", 99);
+  }
+
+  @Test
+  public void testSerializationWithoutTargetName() throws Exception {
+    checkSerialization("//foo/bar", 99);
+  }
+
+  private void checkSerialization(String labelString, int expectedSize) throws Exception {
+    Label a = Label.parseAbsolute(labelString);
+    byte[] sa = TestUtils.serializeObject(a);
+    assertEquals(expectedSize, sa.length);
+
+    Label a2 = (Label) TestUtils.deserializeObject(sa);
+    assertEquals(a, a2);
+  }
+
+  @Test
+  public void testRepoLabel() throws Exception {
+    Label label = Label.parseRepositoryLabel("@foo//bar/baz:bat/boo");
+    assertEquals("@foo//bar/baz:bat/boo", label.toString());
+  }
+
+  @Test
+  public void testNoRepo() throws Exception {
+    Label label = Label.parseRepositoryLabel("//bar/baz:bat/boo");
+    assertEquals("//bar/baz:bat/boo", label.toString());
+  }
+
+  @Test
+  public void testInvalidRepo() throws Exception {
+    try {
+      Label.parseRepositoryLabel("foo//bar/baz:bat/boo");
+      fail();
+    } catch (SyntaxException e) {
+      assertEquals("invalid repository name 'foo': workspace name must start with '@'",
+          e.getMessage());
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/LexerTest.java b/src/test/java/com/google/devtools/build/lib/syntax/LexerTest.java
new file mode 100644
index 0000000..fd5385c
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/LexerTest.java
@@ -0,0 +1,399 @@
+// Copyright 2006 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;
+
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.util.FsApparatus;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests of tokenization behavior of the {@link Lexer}.
+ */
+public class LexerTest extends TestCase implements EventHandler {
+
+  private FsApparatus scratch = FsApparatus.newInMemory();
+
+  /**
+   * Create a lexer which takes input from the specified string. Resets the
+   * error handler beforehand.
+   */
+  private Lexer createLexer(String input) {
+    Path somePath = scratch.path("/some/path.txt");
+    ParserInputSource inputSource = ParserInputSource.create(input, somePath);
+    Reporter reporter = new Reporter();
+    reporter.addHandler(this);
+    return new Lexer(inputSource, reporter);
+  }
+
+  public Token[] tokens(String input) {
+    return createLexer(input).getTokens().toArray(new Token[0]);
+  }
+
+  /**
+   * Lexes the specified input string, and returns a string containing just the
+   * linenumbers of each token.
+   */
+  private String linenums(String input) {
+    Lexer lexer = createLexer(input);
+    StringBuilder buf = new StringBuilder();
+    for (Token tok : lexer.getTokens()) {
+      if (buf.length() > 0) {
+        buf.append(' ');
+      }
+      int line =
+        lexer.createLocation(tok.left, tok.left).getStartLineAndColumn().getLine();
+      buf.append(line);
+    }
+    return buf.toString();
+  }
+
+  private String lastError;
+
+  private Location lastErrorLocation;
+
+  @Override
+  public void handle(Event event) {
+    if (EventKind.ERRORS.contains(event.getKind())) {
+      lastErrorLocation = event.getLocation();
+      lastError = lastErrorLocation.getPath() + ":"
+          + event.getLocation().getStartLineAndColumn().getLine() + ": "
+          + event.getMessage();
+    }
+  }
+
+  /**
+   * Returns a string containing the names of the tokens and their associated
+   * values. (String-literals are printed without escaping.)
+   */
+  private static String values(Token[] tokens) {
+    StringBuilder buffer = new StringBuilder();
+    for (Token token : tokens) {
+      if (buffer.length() > 0) {
+        buffer.append(' ');
+      }
+      buffer.append(token.kind.name());
+      if (token.value != null) {
+        buffer.append('(').append(token.value).append(')');
+      }
+    }
+    return buffer.toString();
+  }
+
+  /**
+   * Returns a string containing just the names of the tokens.
+   */
+  private static String names(Token[] tokens) {
+    StringBuilder buf = new StringBuilder();
+    for (Token tok : tokens) {
+      if (buf.length() > 0) {
+        buf.append(' ');
+      }
+      buf.append(tok.kind.name());
+    }
+    return buf.toString();
+  }
+
+  /**
+   * Returns a string containing just the half-open position intervals of each
+   * token. e.g. "[3,4) [4,9)".
+   */
+  private static String positions(Token[] tokens) {
+    StringBuilder buf = new StringBuilder();
+    for (Token tok : tokens) {
+      if (buf.length() > 0) {
+        buf.append(' ');
+      }
+      buf.append('[')
+         .append(tok.left)
+         .append(',')
+         .append(tok.right)
+         .append(')');
+    }
+    return buf.toString();
+  }
+
+  public void testBasics1() throws Exception {
+    assertEquals("IDENTIFIER RPAREN NEWLINE EOF", names(tokens("wiz) ")));
+    assertEquals("IDENTIFIER RPAREN NEWLINE EOF", names(tokens("wiz )")));
+    assertEquals("IDENTIFIER RPAREN NEWLINE EOF", names(tokens(" wiz)")));
+    assertEquals("IDENTIFIER RPAREN NEWLINE EOF", names(tokens(" wiz ) ")));
+    assertEquals("IDENTIFIER RPAREN NEWLINE EOF", names(tokens("wiz\t)")));
+  }
+
+  public void testBasics2() throws Exception {
+    assertEquals("RPAREN NEWLINE EOF", names(tokens(")")));
+    assertEquals("RPAREN NEWLINE EOF", names(tokens(" )")));
+    assertEquals("RPAREN NEWLINE EOF", names(tokens(" ) ")));
+    assertEquals("RPAREN NEWLINE EOF", names(tokens(") ")));
+  }
+
+  public void testBasics3() throws Exception {
+    assertEquals("INT COMMENT NEWLINE INT NEWLINE EOF", names(tokens("123#456\n789")));
+    assertEquals("INT COMMENT NEWLINE INT NEWLINE EOF", names(tokens("123 #456\n789")));
+    assertEquals("INT COMMENT NEWLINE INT NEWLINE EOF", names(tokens("123#456 \n789")));
+    assertEquals("INT COMMENT NEWLINE INDENT INT NEWLINE OUTDENT NEWLINE EOF",
+                 names(tokens("123#456\n 789")));
+    assertEquals("INT COMMENT NEWLINE INT NEWLINE EOF", names(tokens("123#456\n789 ")));
+  }
+
+  public void testBasics4() throws Exception {
+    assertEquals("NEWLINE EOF", names(tokens("")));
+    assertEquals("COMMENT NEWLINE EOF", names(tokens("# foo")));
+    assertEquals("INT INT INT INT NEWLINE EOF", names(tokens("1 2 3 4")));
+    assertEquals("INT DOT INT NEWLINE EOF", names(tokens("1.234")));
+    assertEquals("IDENTIFIER LPAREN IDENTIFIER COMMA IDENTIFIER RPAREN "
+                 + "NEWLINE EOF", names(tokens("foo(bar, wiz)")));
+  }
+
+  public void testIntegers() throws Exception {
+    // Detection of MINUS immediately following integer constant proves we
+    // don't consume too many chars.
+
+    // decimal
+    assertEquals("INT(12345) MINUS NEWLINE EOF", values(tokens("12345-")));
+
+    // octal
+    assertEquals("INT(5349) MINUS NEWLINE EOF", values(tokens("012345-")));
+
+    // octal (bad)
+    assertEquals("INT(0) MINUS NEWLINE EOF", values(tokens("012349-")));
+    assertEquals("/some/path.txt:1: invalid base-8 integer constant: 012349",
+                 lastError.toString());
+
+    // hexadecimal (uppercase)
+    assertEquals("INT(1193055) MINUS NEWLINE EOF", values(tokens("0X12345F-")));
+
+    // hexadecimal (lowercase)
+    assertEquals("INT(1193055) MINUS NEWLINE EOF", values(tokens("0x12345f-")));
+
+    // hexadecimal (lowercase) [note: "g" cause termination of token]
+    assertEquals("INT(74565) IDENTIFIER(g) MINUS NEWLINE EOF",
+                 values(tokens("0x12345g-")));
+  }
+
+  public void testIntegersAndDot() throws Exception {
+    assertEquals("INT(1) DOT INT(2345) NEWLINE EOF", values(tokens("1.2345")));
+
+    assertEquals("INT(1) DOT INT(2) DOT INT(345) NEWLINE EOF",
+                 values(tokens("1.2.345")));
+
+    assertEquals("INT(1) DOT INT(0) NEWLINE EOF", values(tokens("1.23E10")));
+    assertEquals("/some/path.txt:1: invalid base-10 integer constant: 23E10",
+                 lastError.toString());
+
+    assertEquals("INT(1) DOT INT(0) MINUS INT(10) NEWLINE EOF",
+                 values(tokens("1.23E-10")));
+    assertEquals("/some/path.txt:1: invalid base-10 integer constant: 23E",
+                 lastError.toString());
+
+    assertEquals("DOT INT(123) NEWLINE EOF", values(tokens(". 123")));
+    assertEquals("DOT INT(123) NEWLINE EOF", values(tokens(".123")));
+    assertEquals("DOT IDENTIFIER(abc) NEWLINE EOF", values(tokens(".abc")));
+
+    assertEquals("IDENTIFIER(foo) DOT INT(123) NEWLINE EOF",
+                 values(tokens("foo.123")));
+    assertEquals("IDENTIFIER(foo) DOT IDENTIFIER(bcd) NEWLINE EOF",
+                 values(tokens("foo.bcd"))); // 'b' are hex chars
+    assertEquals("IDENTIFIER(foo) DOT IDENTIFIER(xyz) NEWLINE EOF",
+                 values(tokens("foo.xyz")));
+  }
+
+  public void testStringDelimiters() throws Exception {
+    assertEquals("STRING(foo) NEWLINE EOF", values(tokens("\"foo\"")));
+    assertEquals("STRING(foo) NEWLINE EOF", values(tokens("'foo'")));
+  }
+
+  public void testQuotesInStrings() throws Exception {
+    assertEquals("STRING(foo'bar) NEWLINE EOF", values(tokens("'foo\\'bar'")));
+    assertEquals("STRING(foo'bar) NEWLINE EOF", values(tokens("\"foo'bar\"")));
+    assertEquals("STRING(foo\"bar) NEWLINE EOF", values(tokens("'foo\"bar'")));
+    assertEquals("STRING(foo\"bar) NEWLINE EOF",
+                 values(tokens("\"foo\\\"bar\"")));
+  }
+
+  public void testStringEscapes() throws Exception {
+    assertEquals("STRING(a\tb\nc\rd) NEWLINE EOF",
+                 values(tokens("'a\\tb\\nc\\rd'"))); // \t \r \n
+    assertEquals("STRING(x\\hx) NEWLINE EOF",
+                 values(tokens("'x\\hx'"))); // \h is unknown => "\h"
+    assertEquals("STRING(\\$$) NEWLINE EOF", values(tokens("'\\$$'")));
+    assertEquals("STRING(ab) NEWLINE EOF",
+                 values(tokens("'a\\\nb'"))); // escape end of line
+
+    assertEquals("STRING(abcd) NEWLINE EOF",
+                 values(tokens("r'abcd'")));
+    assertEquals("STRING(abcd) NEWLINE EOF",
+                 values(tokens("r\"abcd\"")));
+    assertEquals("STRING(a\\tb\\nc\\rd) NEWLINE EOF",
+                 values(tokens("r'a\\tb\\nc\\rd'"))); // r'a\tb\nc\rd'
+    assertEquals("STRING(a\\\") NEWLINE EOF",
+                 values(tokens("r\"a\\\"\""))); // r"a\""
+    assertEquals("STRING(a\\\\b) NEWLINE EOF",
+                 values(tokens("r'a\\\\b'"))); // r'a\\b'
+    assertEquals("STRING(ab) IDENTIFIER(r) NEWLINE EOF",
+                 values(tokens("r'ab'r")));
+
+    assertEquals("STRING(abcd) NEWLINE EOF",
+                 values(tokens("\"ab\\ucd\"")));
+    assertEquals("/some/path.txt:1: escape sequence not implemented: \\u",
+                 lastError.toString());
+  }
+
+  public void testOctalEscapes() throws Exception {
+    // Regression test for a bug.
+    assertEquals("STRING(\0 \1 \t \u003f I I1 \u00ff \u00ff \u00fe) NEWLINE EOF",
+                 values(tokens("'\\0 \\1 \\11 \\77 \\111 \\1111 \\377 \\777 \\776'")));
+    // Test boundaries (non-octal char, EOF).
+    assertEquals("STRING(\1b \1) NEWLINE EOF", values(tokens("'\\1b \\1'")));
+  }
+
+  public void testTripleQuotedStrings() throws Exception {
+    assertEquals("STRING(a\"b'c \n d\"\"e) NEWLINE EOF",
+                 values(tokens("\"\"\"a\"b'c \n d\"\"e\"\"\"")));
+    assertEquals("STRING(a\"b'c \n d\"\"e) NEWLINE EOF",
+                 values(tokens("'''a\"b'c \n d\"\"e'''")));
+  }
+
+  public void testBadChar() throws Exception {
+    assertEquals("IDENTIFIER(a) IDENTIFIER(b) NEWLINE EOF",
+                 values(tokens("a$b")));
+    assertEquals("/some/path.txt:1: invalid character: '$'",
+                 lastError.toString());
+  }
+
+  public void testIndentation() throws Exception {
+    assertEquals("INT(1) NEWLINE INT(2) NEWLINE INT(3) NEWLINE EOF",
+                 values(tokens("1\n2\n3")));
+    assertEquals("INT(1) NEWLINE INDENT INT(2) NEWLINE INT(3) NEWLINE OUTDENT "
+                 + "INT(4) NEWLINE EOF", values(tokens("1\n  2\n  3\n4 ")));
+    assertEquals("INT(1) NEWLINE INDENT INT(2) NEWLINE INT(3) NEWLINE OUTDENT "
+                 + "NEWLINE EOF", values(tokens("1\n  2\n  3")));
+    assertEquals("INT(1) NEWLINE INDENT INT(2) NEWLINE INDENT INT(3) NEWLINE "
+                 + "OUTDENT OUTDENT NEWLINE EOF",
+                 values(tokens("1\n  2\n    3")));
+    assertEquals("INT(1) NEWLINE INDENT INT(2) NEWLINE INDENT INT(3) NEWLINE "
+                 + "OUTDENT INT(4) NEWLINE OUTDENT INT(5) NEWLINE EOF",
+                 values(tokens("1\n  2\n    3\n  4\n5")));
+
+    assertEquals("INT(1) NEWLINE INDENT INT(2) NEWLINE INDENT INT(3) NEWLINE "
+                 + "OUTDENT INT(4) NEWLINE OUTDENT INT(5) NEWLINE EOF",
+                 values(tokens("1\n  2\n    3\n   4\n5")));
+    assertEquals("/some/path.txt:4: indentation error", lastError.toString());
+  }
+
+  public void testIndentationInsideParens() throws Exception {
+    // Indentation is ignored inside parens:
+    assertEquals("INT(1) LPAREN INT(2) INT(3) INT(4) INT(5) NEWLINE EOF",
+                 values(tokens("1 (\n  2\n    3\n  4\n5")));
+    assertEquals("INT(1) LBRACE INT(2) INT(3) INT(4) INT(5) NEWLINE EOF",
+                 values(tokens("1 {\n  2\n    3\n  4\n5")));
+    assertEquals("INT(1) LBRACKET INT(2) INT(3) INT(4) INT(5) NEWLINE EOF",
+                 values(tokens("1 [\n  2\n    3\n  4\n5")));
+    assertEquals("INT(1) LBRACKET INT(2) RBRACKET NEWLINE INDENT INT(3) "
+                 + "NEWLINE INT(4) NEWLINE OUTDENT INT(5) NEWLINE EOF",
+                 values(tokens("1 [\n  2]\n    3\n    4\n5")));
+  }
+
+  public void testIndentationAtEOF() throws Exception {
+    // Matching OUTDENTS are created at EOF:
+    assertEquals("INDENT INT(1) NEWLINE OUTDENT NEWLINE EOF",
+                 values(tokens("\n  1")));
+  }
+
+  public void testBlankLineIndentation() throws Exception {
+    // Blank lines and comment lines should not generate any newlines indents
+    // (but note that every input ends with NEWLINE EOF).
+    assertEquals("COMMENT NEWLINE EOF", names(tokens("\n      #\n")));
+    assertEquals("COMMENT NEWLINE EOF", names(tokens("      #")));
+    assertEquals("COMMENT NEWLINE EOF", names(tokens("      #\n")));
+    assertEquals("COMMENT NEWLINE EOF", names(tokens("      #comment\n")));
+    assertEquals("DEF IDENTIFIER LPAREN IDENTIFIER RPAREN COLON NEWLINE "
+                 + "COMMENT INDENT RETURN IDENTIFIER NEWLINE "
+                 + "OUTDENT NEWLINE EOF",
+                 names(tokens("def f(x):\n"
+                              + "  # comment\n"
+                              + "\n"
+                              + "  \n"
+                              + "  return x\n")));
+  }
+
+  public void testMultipleCommentLines() throws Exception {
+    assertEquals("COMMENT NEWLINE COMMENT COMMENT COMMENT "
+                 + "DEF IDENTIFIER LPAREN IDENTIFIER RPAREN COLON NEWLINE "
+                 + "INDENT RETURN IDENTIFIER NEWLINE OUTDENT NEWLINE EOF",
+                 names(tokens("# Copyright\n"
+                              + "#\n"
+                              + "# A comment line\n"
+                              + "# An adjoining line\n"
+                              + "def f(x):\n"
+                              + "  return x\n")));
+  }
+
+  public void testBackslash() throws Exception {
+    assertEquals("IDENTIFIER IDENTIFIER NEWLINE EOF",
+                 names(tokens("a\\\nb")));
+    assertEquals("IDENTIFIER ILLEGAL IDENTIFIER NEWLINE EOF",
+                 names(tokens("a\\ b")));
+    assertEquals("IDENTIFIER LPAREN INT RPAREN NEWLINE EOF",
+                 names(tokens("a(\\\n2)")));
+  }
+
+  public void testTokenPositions() throws Exception {
+    //            foo   (     bar   ,     {      1       :
+    assertEquals("[0,3) [3,4) [4,7) [7,8) [9,10) [10,11) [11,12)"
+             //      'quux'  }       )       NEWLINE EOF
+                 + " [13,19) [19,20) [20,21) [20,21) [21,21)",
+                 positions(tokens("foo(bar, {1: 'quux'})")));
+  }
+
+  public void testLineNumbers() throws Exception {
+    assertEquals("1 1 1 1 2 2 2 2 4 4 4 4 4",
+                 linenums("foo = 1\nbar = 2\n\nwiz = 3"));
+
+    assertEquals("IDENTIFIER(foo) EQUALS INT(1) NEWLINE "
+                 + "IDENTIFIER(bar) EQUALS INT(2) NEWLINE "
+                 + "IDENTIFIER(wiz) EQUALS NEWLINE "
+                 + "IDENTIFIER(bar) EQUALS INT(2) NEWLINE EOF",
+                 values(tokens("foo = 1\nbar = 2\n\nwiz = $\nbar = 2")));
+    assertEquals("/some/path.txt:4: invalid character: '$'",
+                 lastError.toString());
+
+    // '\\n' in string should not increment linenum:
+    String s = "1\n'foo\\nbar'\3";
+    assertEquals("INT(1) NEWLINE STRING(foo\nbar) NEWLINE EOF",
+                 values(tokens(s)));
+    assertEquals("1 1 2 2 2", linenums(s));
+  }
+
+  public void testContainsErrors() throws Exception {
+    Lexer lexerSuccess = createLexer("foo");
+    assertFalse(lexerSuccess.containsErrors());
+
+    Lexer lexerFail = createLexer("f$o");
+    assertTrue(lexerFail.containsErrors());
+
+    String s = "'unterminated";
+    lexerFail = createLexer(s);
+    assertTrue(lexerFail.containsErrors());
+    assertEquals(0, lastErrorLocation.getStartOffset());
+    assertEquals(s.length(), lastErrorLocation.getEndOffset());
+    assertEquals("STRING(unterminated) NEWLINE EOF", values(tokens(s)));
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/LineNumberTableTest.java b/src/test/java/com/google/devtools/build/lib/syntax/LineNumberTableTest.java
new file mode 100644
index 0000000..f1fdd77
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/LineNumberTableTest.java
@@ -0,0 +1,113 @@
+// Copyright 2006 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;
+
+import com.google.devtools.build.lib.events.Location.LineAndColumn;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.util.FsApparatus;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for {@link LineNumberTable}.
+ */
+public class LineNumberTableTest extends TestCase {
+  private FsApparatus scratch = FsApparatus.newInMemory();
+
+  private LineNumberTable create(String buffer) {
+    return LineNumberTable.create(buffer.toCharArray(),
+        scratch.path("/fake/file"));
+  }
+
+  public void testEmpty() {
+    LineNumberTable table = create("");
+    assertEquals(new LineAndColumn(1, 1), table.getLineAndColumn(0));
+  }
+
+  public void testNewline() {
+    LineNumberTable table = create("\n");
+    assertEquals(new LineAndColumn(1, 1), table.getLineAndColumn(0));
+    assertEquals(new LineAndColumn(2, 1), table.getLineAndColumn(1));
+  }
+
+  public void testOneLiner() {
+    LineNumberTable table = create("foo");
+    assertEquals(new LineAndColumn(1, 1), table.getLineAndColumn(0));
+    assertEquals(new LineAndColumn(1, 2), table.getLineAndColumn(1));
+    assertEquals(new LineAndColumn(1, 3), table.getLineAndColumn(2));
+    assertEquals(Pair.of(0, 3), table.getOffsetsForLine(1));
+  }
+
+  public void testMultiLiner() {
+    LineNumberTable table = create("\ntwo\nthree\n\nfive\n");
+
+    // \n
+    assertEquals(new LineAndColumn(1, 1), table.getLineAndColumn(0));
+    assertEquals(Pair.of(0, 1), table.getOffsetsForLine(1));
+
+    // two\n
+    assertEquals(new LineAndColumn(2, 1), table.getLineAndColumn(1));
+    assertEquals(new LineAndColumn(2, 2), table.getLineAndColumn(2));
+    assertEquals(new LineAndColumn(2, 3), table.getLineAndColumn(3));
+    assertEquals(new LineAndColumn(2, 4), table.getLineAndColumn(4));
+    assertEquals(Pair.of(1, 5), table.getOffsetsForLine(2));
+
+    // three\n
+    assertEquals(new LineAndColumn(3, 1), table.getLineAndColumn(5));
+    assertEquals(new LineAndColumn(3, 6), table.getLineAndColumn(10));
+    assertEquals(Pair.of(5, 11), table.getOffsetsForLine(3));
+
+    // \n
+    assertEquals(new LineAndColumn(4, 1), table.getLineAndColumn(11));
+    assertEquals(Pair.of(11, 12), table.getOffsetsForLine(4));
+
+    // five\n
+    assertEquals(new LineAndColumn(5, 1), table.getLineAndColumn(12));
+    assertEquals(new LineAndColumn(5, 5), table.getLineAndColumn(16));
+    assertEquals(Pair.of(12, 17), table.getOffsetsForLine(5));
+  }
+
+  public void testHashLine() {
+    String data = "#\n"
+        + "#line 67 \"/foo\"\n"
+        + "cc_binary(name='a',\n"
+        + "          srcs=[])\n"
+        + "#line 23 \"/ba.r\"\n"
+        + "vardef(x,y)\n";
+
+    LineNumberTable table = create(data);
+
+    // Note: no attempt is made to return accurate column information.
+    assertEquals(new LineAndColumn(67, 1), table.getLineAndColumn(data.indexOf("cc_binary")));
+    assertEquals(new LineAndColumn(67, 1), table.getLineAndColumn(data.indexOf("name='a'")));
+    assertEquals("/fake/file", table.getPath(0).toString());
+    // Note: newlines ignored; "srcs" is still (intentionally) considered to be
+    // on L67.  Consider the alternative, and assume that rule 'a' is 50 lines
+    // when pretty-printed: the last line of 'a' would be reported as line 67 +
+    // 50, which may be in a part of the original BUILD file that has nothing
+    // to do with this rule.  In other words, the size of rules before and
+    // after pretty printing are essentially unrelated.
+    assertEquals(new LineAndColumn(67, 1), table.getLineAndColumn(data.indexOf("srcs")));
+    assertEquals("/foo", table.getPath(data.indexOf("srcs")).toString());
+    assertEquals(Pair.of(2, 57), table.getOffsetsForLine(67));
+
+    assertEquals(new LineAndColumn(23, 1), table.getLineAndColumn(data.indexOf("vardef")));
+    assertEquals(new LineAndColumn(23, 1), table.getLineAndColumn(data.indexOf("x,y")));
+    assertEquals("/ba.r", table.getPath(data.indexOf("x,y")).toString());
+    assertEquals(Pair.of(57, 86), table.getOffsetsForLine(23));
+
+    assertEquals(Pair.of(0, 0), table.getOffsetsForLine(42));
+  }
+
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/MixedModeFunctionTest.java b/src/test/java/com/google/devtools/build/lib/syntax/MixedModeFunctionTest.java
new file mode 100644
index 0000000..c7c2c40
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/MixedModeFunctionTest.java
@@ -0,0 +1,130 @@
+// Copyright 2006 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;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Arrays;
+
+/**
+ * Tests for {@link MixedModeFunction}.
+ */
+public class MixedModeFunctionTest extends AbstractEvaluationTestCase {
+
+  private Environment singletonEnv(String id, Object value) {
+    Environment env = new Environment();
+    env.update(id, value);
+    return env;
+  }
+
+  /**
+   * Handy implementation of {@link MixedModeFunction} that just tuples up its args and returns
+   * them.
+   */
+  private static class TestingMixedModeFunction extends MixedModeFunction {
+    TestingMixedModeFunction(Iterable<String> parameters,
+                             int numMandatoryParameters,
+                             boolean onlyNamedArguments) {
+      super("mixed", parameters, numMandatoryParameters, onlyNamedArguments);
+    }
+    @Override
+    public Object call(Object[] namedParameters, FuncallExpression ast) {
+      return Arrays.asList(namedParameters);
+    }
+  }
+
+  private void checkMixedMode(Function func,
+                              String callExpression,
+                              String expectedOutput) throws Exception {
+    Environment env = singletonEnv(func.getName(), func);
+
+    if (expectedOutput.charAt(0) == '[') { // a tuple => expected to pass
+      assertEquals(expectedOutput,
+                   eval(callExpression, env).toString());
+    } else { // expected to fail with an exception
+      try {
+        eval(callExpression, env);
+        fail();
+      } catch (EvalException e) {
+        assertEquals(expectedOutput, e.getMessage());
+      }
+    }
+  }
+
+  private static final String[] mixedModeExpressions = {
+    "mixed()",
+    "mixed(1)",
+    "mixed(1, 2)",
+    "mixed(1, 2, 3)",
+    "mixed(1, 2, wiz=3, quux=4)",
+    "mixed(foo=1)",
+    "mixed(foo=1, bar=2)",
+    "mixed(bar=2, foo=1)",
+    "mixed(2, foo=1)",
+    "mixed(bar=2, foo=1, wiz=3)",
+  };
+
+  public void checkMixedModeFunctions(boolean onlyNamedArguments,
+                                      String expectedSignature,
+                                      String[] expectedResults)
+      throws Exception {
+    MixedModeFunction func =
+        new TestingMixedModeFunction(ImmutableList.of("foo", "bar"), 1, onlyNamedArguments);
+
+    assertEquals(expectedSignature, func.getSignature());
+
+    for (int ii = 0; ii < mixedModeExpressions.length; ++ii) {
+      String expr = mixedModeExpressions[ii];
+      String expected = expectedResults[ii];
+      checkMixedMode(func, expr, expected);
+    }
+  }
+
+  public void testNoSurplusArguments() throws Exception {
+    checkMixedModeFunctions(false,
+                            "mixed(foo, bar = null)",
+                            new String[]
+      {
+        "mixed(foo, bar = null) received insufficient arguments",
+        "[1, null]",
+        "[1, 2]",
+        "too many positional arguments in call to mixed(foo, bar = null)",
+        "unexpected keywords 'quux', 'wiz' in call to mixed(foo, bar = null)",
+        "[1, null]",
+        "[1, 2]",
+        "[1, 2]",
+        "mixed(foo, bar = null) got multiple values for keyword"
+        + " argument 'foo'",
+        "unexpected keyword 'wiz' in call to mixed(foo, bar = null)",
+      });
+  }
+
+  public void testOnlyNamedArguments() throws Exception {
+    checkMixedModeFunctions(true,
+                            "mixed(foo, bar = null)",
+                            new String[]
+      {
+        "mixed(foo, bar = null) received insufficient arguments",
+        "mixed(foo, bar = null) does not accept positional arguments",
+        "mixed(foo, bar = null) does not accept positional arguments",
+        "mixed(foo, bar = null) does not accept positional arguments",
+        "mixed(foo, bar = null) does not accept positional arguments",
+        "[1, null]",
+        "[1, 2]",
+        "[1, 2]",
+        "mixed(foo, bar = null) does not accept positional arguments",
+        "unexpected keyword 'wiz' in call to mixed(foo, bar = null)",
+      });
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ParserInputSourceTest.java b/src/test/java/com/google/devtools/build/lib/syntax/ParserInputSourceTest.java
new file mode 100644
index 0000000..1d0e50c
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ParserInputSourceTest.java
@@ -0,0 +1,116 @@
+// Copyright 2006 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;
+
+import static com.google.devtools.build.lib.util.StringUtilities.joinLines;
+
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.util.FsApparatus;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A test case for {@link ParserInputSource}.
+ */
+public class ParserInputSourceTest extends TestCase {
+
+  private FsApparatus scratch = FsApparatus.newInMemory();
+
+  public void testCreateFromFile() throws IOException {
+    String content = joinLines("Line 1", "Line 2", "Line 3", "");
+    Path file = scratch.file("/tmp/my/file.txt", content);
+    ParserInputSource input = ParserInputSource.create(file);
+    assertEquals(content, new String(input.getContent()));
+    assertEquals("/tmp/my/file.txt", input.getPath().toString());
+  }
+
+  public void testCreateFromString() {
+    String content = "Content provided as a string.";
+    String pathName = "/the/name/of/the/content.txt";
+    Path path = scratch.path(pathName);
+    ParserInputSource input = ParserInputSource.create(content, path);
+    assertEquals(content, new String(input.getContent()));
+    assertEquals(pathName, input.getPath().toString());
+  }
+
+  public void testCreateFromCharArray() {
+    String content = "Content provided as a string.";
+    String pathName = "/the/name/of/the/content.txt";
+    Path path = scratch.path(pathName);
+    char[] contentChars = content.toCharArray();
+    ParserInputSource input = ParserInputSource.create(contentChars, path);
+    assertEquals(content, new String(input.getContent()));
+    assertEquals(pathName, input.getPath().toString());
+  }
+
+  public void testCreateFromInputStream() throws IOException {
+    String content = "Content provided as a string.";
+    byte[] bytes = content.getBytes("ISO-8859-1");
+    ByteArrayInputStream in = new ByteArrayInputStream(bytes);
+    String pathName = "/the/name/of/the/content.txt";
+    Path path = scratch.path(pathName);
+    ParserInputSource input = ParserInputSource.create(in, path);
+    assertEquals(content, new String(input.getContent()));
+    assertEquals(pathName, input.getPath().toString());
+  }
+
+  public void testIOExceptionIfInputFileDoesNotExistForSingleArgConstructor() {
+    try {
+      Path path = scratch.path("/does/not/exist");
+      ParserInputSource.create(path);
+      fail();
+    } catch (IOException e) {
+      String expected = "/does/not/exist (No such file or directory)";
+      assertEquals(expected, e.getMessage());
+    }
+  }
+
+  public void testWillNotTryToReadInputFileIfContentProvidedAsString() {
+    Path path = scratch.path("/will/not/try/to/read");
+    ParserInputSource.create("Content provided as string.", path);
+  }
+
+  public void testWillNotTryToReadInputFileIfContentProvidedAsChars() {
+    Path path = scratch.path("/will/not/try/to/read");
+    char[] content = "Content provided as char array.".toCharArray();
+    ParserInputSource.create(content, path);
+  }
+
+  public void testWillCloseStreamWhenReadingFromInputStream() {
+    final StringBuilder log = new StringBuilder();
+    InputStream in = new InputStream() {
+      @Override
+      public int read() throws IOException {
+        throw new IOException("Fault injected.");
+      }
+      @Override
+      public void close() {
+        log.append("Stream closed.");
+      }
+    };
+    try {
+      Path path = scratch.path("/will/not/try/to/read");
+      ParserInputSource.create(in, path);
+      fail();
+    } catch (IOException e) {
+      assertEquals("Fault injected.", e.getMessage());
+    }
+    assertEquals("Stream closed.", log.toString());
+  }
+
+}
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
new file mode 100644
index 0000000..e698728
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
@@ -0,0 +1,876 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.DictionaryLiteral.DictionaryEntryLiteral;
+
+import java.util.List;
+
+/**
+ *  Tests of parser behaviour.
+ *
+ */
+public class ParserTest extends AbstractParserTestCase {
+
+  private static String getText(String text, ASTNode node) {
+    return text.substring(node.getLocation().getStartOffset(),
+                          node.getLocation().getEndOffset());
+  }
+
+  // helper func for testListLiterals:
+  private static int getIntElem(DictionaryEntryLiteral entry, boolean key) {
+    return ((IntegerLiteral) (key ? entry.getKey() : entry.getValue())).getValue();
+  }
+
+  // helper func for testListLiterals:
+  private static DictionaryEntryLiteral getElem(DictionaryLiteral list, int index) {
+    return list.getEntries().get(index);
+  }
+
+  // helper func for testListLiterals:
+  private static int getIntElem(ListLiteral list, int index) {
+    return ((IntegerLiteral) list.getElements().get(index)).getValue();
+  }
+
+  // helper func for testListLiterals:
+  private static Expression getElem(ListLiteral list, int index) {
+    return list.getElements().get(index);
+  }
+
+  // helper func for testing arguments:
+  private static Expression getArg(FuncallExpression f, int index) {
+    return f.getArguments().get(index).getValue();
+  }
+
+  public void testPrecedence1() throws Exception {
+    BinaryOperatorExpression e =
+      (BinaryOperatorExpression) parseExpr("'%sx' % 'foo' + 'bar'");
+
+    assertEquals(Operator.PLUS, e.getOperator());
+  }
+
+  public void testPrecedence2() throws Exception {
+    BinaryOperatorExpression e =
+      (BinaryOperatorExpression) parseExpr("('%sx' % 'foo') + 'bar'");
+    assertEquals(Operator.PLUS, e.getOperator());
+  }
+
+  public void testPrecedence3() throws Exception {
+    BinaryOperatorExpression e =
+      (BinaryOperatorExpression) parseExpr("'%sx' % ('foo' + 'bar')");
+    assertEquals(Operator.PERCENT, e.getOperator());
+  }
+
+  public void testPrecedence4() throws Exception {
+    BinaryOperatorExpression e =
+        (BinaryOperatorExpression) parseExpr("1 + - (2 - 3)");
+    assertEquals(Operator.PLUS, e.getOperator());
+  }
+
+  public void testUnaryMinusExpr() throws Exception {
+    FuncallExpression e = (FuncallExpression) parseExpr("-5");
+    FuncallExpression e2 = (FuncallExpression) parseExpr("- 5");
+
+    assertEquals("-", e.getFunction().getName());
+    assertEquals("-", e2.getFunction().getName());
+
+    assertThat(e.getArguments()).hasSize(1);
+    assertEquals(1, e.getNumPositionalArguments());
+
+    IntegerLiteral arg0 = (IntegerLiteral) e.getArguments().get(0).getValue();
+    assertEquals(5, (int) arg0.getValue());
+  }
+
+  public void testFuncallExpr() throws Exception {
+    FuncallExpression e = (FuncallExpression) parseExpr("foo(1, 2, bar=wiz)");
+
+    Ident ident = e.getFunction();
+    assertEquals("foo", ident.getName());
+
+    assertThat(e.getArguments()).hasSize(3);
+    assertEquals(2, e.getNumPositionalArguments());
+
+    IntegerLiteral arg0 = (IntegerLiteral) e.getArguments().get(0).getValue();
+    assertEquals(1, (int) arg0.getValue());
+
+    IntegerLiteral arg1 = (IntegerLiteral) e.getArguments().get(1).getValue();
+    assertEquals(2, (int) arg1.getValue());
+
+    Argument.Passed arg2 = e.getArguments().get(2);
+    assertEquals("bar", arg2.getName());
+    Ident arg2val = (Ident) arg2.getValue();
+    assertEquals("wiz", arg2val.getName());
+  }
+
+  public void testMethCallExpr() throws Exception {
+    FuncallExpression e =
+      (FuncallExpression) parseExpr("foo.foo(1, 2, bar=wiz)");
+
+    Ident ident = e.getFunction();
+    assertEquals("foo", ident.getName());
+
+    assertThat(e.getArguments()).hasSize(3);
+    assertEquals(2, e.getNumPositionalArguments());
+
+    IntegerLiteral arg0 = (IntegerLiteral) e.getArguments().get(0).getValue();
+    assertEquals(1, (int) arg0.getValue());
+
+    IntegerLiteral arg1 = (IntegerLiteral) e.getArguments().get(1).getValue();
+    assertEquals(2, (int) arg1.getValue());
+
+    Argument.Passed arg2 = e.getArguments().get(2);
+    assertEquals("bar", arg2.getName());
+    Ident arg2val = (Ident) arg2.getValue();
+    assertEquals("wiz", arg2val.getName());
+  }
+
+  public void testChainedMethCallExpr() throws Exception {
+    FuncallExpression e =
+      (FuncallExpression) parseExpr("foo.replace().split(1)");
+
+    Ident ident = e.getFunction();
+    assertEquals("split", ident.getName());
+
+    assertThat(e.getArguments()).hasSize(1);
+    assertEquals(1, e.getNumPositionalArguments());
+
+    IntegerLiteral arg0 = (IntegerLiteral) e.getArguments().get(0).getValue();
+    assertEquals(1, (int) arg0.getValue());
+  }
+
+  public void testPropRefExpr() throws Exception {
+    DotExpression e = (DotExpression) parseExpr("foo.foo");
+
+    Ident ident = e.getField();
+    assertEquals("foo", ident.getName());
+  }
+
+  public void testStringMethExpr() throws Exception {
+    FuncallExpression e = (FuncallExpression) parseExpr("'foo'.foo()");
+
+    Ident ident = e.getFunction();
+    assertEquals("foo", ident.getName());
+
+    assertThat(e.getArguments()).isEmpty();
+  }
+
+  public void testStringLiteralOptimizationValue() throws Exception {
+    StringLiteral l = (StringLiteral) parseExpr("'abc' + 'def'");
+    assertEquals("abcdef", l.value);
+  }
+
+  public void testStringLiteralOptimizationToString() throws Exception {
+    StringLiteral l = (StringLiteral) parseExpr("'abc' + 'def'");
+    assertEquals("'abcdef'", l.toString());
+  }
+
+  public void testStringLiteralOptimizationLocation() throws Exception {
+    StringLiteral l = (StringLiteral) parseExpr("'abc' + 'def'");
+    assertEquals(0, l.getLocation().getStartOffset());
+    assertEquals(13, l.getLocation().getEndOffset());
+  }
+
+  public void testStringLiteralOptimizationDifferentQuote() throws Exception {
+    assertThat(parseExpr("'abc' + \"def\"")).isInstanceOf(BinaryOperatorExpression.class);
+  }
+
+  public void testSubstring() throws Exception {
+    FuncallExpression e = (FuncallExpression) parseExpr("'FOO.CC'[:].lower()[1:]");
+    assertEquals("$substring", e.getFunction().getName());
+    assertThat(e.getArguments()).hasSize(2);
+
+    e = (FuncallExpression) parseExpr("'FOO.CC'.lower()[1:].startswith('oo')");
+    assertEquals("startswith", e.getFunction().getName());
+    assertThat(e.getArguments()).hasSize(1);
+
+    e = (FuncallExpression) parseExpr("'FOO.CC'[1:][:2]");
+    assertEquals("$substring", e.getFunction().getName());
+    assertThat(e.getArguments()).hasSize(2);
+  }
+
+  private void assertLocation(int start, int end, Location location)
+      throws Exception {
+    int actualStart = location.getStartOffset();
+    int actualEnd = location.getEndOffset();
+
+    if (actualStart != start || actualEnd != end) {
+      fail("Expected location = [" + start + ", " + end + "), found ["
+          + actualStart + ", " + actualEnd + ")");
+    }
+  }
+
+  public void testErrorRecovery() throws Exception {
+    syntaxEvents.setFailFast(false);
+
+    String expr = "f(1, [x for foo foo foo], 3)";
+    FuncallExpression e = (FuncallExpression) parseExpr(expr);
+
+    syntaxEvents.assertContainsEvent("syntax error at 'foo'");
+
+    // Test that the actual parameters are: (1, $error$, 3):
+
+    Ident ident = e.getFunction();
+    assertEquals("f", ident.getName());
+
+    assertThat(e.getArguments()).hasSize(3);
+    assertEquals(3, e.getNumPositionalArguments());
+
+    IntegerLiteral arg0 = (IntegerLiteral) e.getArguments().get(0).getValue();
+    assertEquals(1, (int) arg0.getValue());
+
+    Argument.Passed arg1 = e.getArguments().get(1);
+    Ident arg1val = ((Ident) arg1.getValue());
+    assertEquals("$error$", arg1val.getName());
+
+    assertLocation(5, 24, arg1val.getLocation());
+    assertEquals("[x for foo foo foo]", expr.substring(5, 24));
+    assertEquals(25, arg1val.getLocation().getEndLineAndColumn().getColumn());
+
+    IntegerLiteral arg2 = (IntegerLiteral) e.getArguments().get(2).getValue();
+    assertEquals(3, (int) arg2.getValue());
+  }
+
+  public void testDoesntGetStuck() throws Exception {
+    syntaxEvents.setFailFast(false);
+
+    // Make sure the parser does not get stuck when trying
+    // to parse an expression containing a syntax error.
+    // This usually results in OutOfMemoryError because the
+    // parser keeps filling up the error log.
+    // We need to make sure that we will always advance
+    // in the token stream.
+    parseExpr("f(1, ], 3)");
+    parseExpr("f(1, ), 3)");
+    parseExpr("[ ) for v in 3)");
+
+    syntaxEvents.assertContainsEvent(""); // "" matches any;
+                                          // i.e. there were some events
+  }
+
+  public void testSecondaryLocation() {
+    String expr = "f(1 % 2)";
+    FuncallExpression call = (FuncallExpression) parseExpr(expr);
+    Argument.Passed arg = call.getArguments().get(0);
+    assertTrue(arg.getLocation().getEndOffset() < call.getLocation().getEndOffset());
+  }
+
+  public void testPrimaryLocation() {
+    String expr = "f(1 + 2)";
+    FuncallExpression call = (FuncallExpression) parseExpr(expr);
+    Argument.Passed arg = call.getArguments().get(0);
+    assertTrue(arg.getLocation().getEndOffset() < call.getLocation().getEndOffset());
+  }
+
+  public void testAssignLocation() {
+    String expr = "a = b;c = d\n";
+    List<Statement> statements = parseFile(expr);
+    Statement statement = statements.get(0);
+    assertEquals(5, statement.getLocation().getEndOffset());
+  }
+
+  public void testAssign() {
+    String expr = "list[0] = 5; dict['key'] = value\n";
+    List<Statement> statements = parseFile(expr);
+    assertThat(statements).hasSize(2);
+  }
+
+  public void testInvalidAssign() {
+    syntaxEvents.setFailFast(false);
+    parseExpr("1 + (b = c)");
+    syntaxEvents.assertContainsEvent("syntax error");
+    syntaxEvents.collector().clear();
+  }
+
+  public void testAugmentedAssign() throws Exception {
+    assertEquals("[x = x + 1\n]", parseFile("x += 1").toString());
+  }
+
+  public void testPrettyPrintFunctions() throws Exception {
+    assertEquals("[x[1:3]\n]", parseFile("x[1:3]").toString());
+    assertEquals("[str[42]\n]", parseFile("str[42]").toString());
+    assertEquals("[ctx.new_file(['hello'])\n]", parseFile("ctx.new_file('hello')").toString());
+    assertEquals("[new_file(['hello'])\n]", parseFile("new_file('hello')").toString());
+  }
+
+  public void testFuncallLocation() {
+    String expr = "a(b);c = d\n";
+    List<Statement> statements = parseFile(expr);
+    Statement statement = statements.get(0);
+    assertEquals(4, statement.getLocation().getEndOffset());
+  }
+
+  public void testSpecialFuncallLocation() throws Exception {
+    List<Statement> statements = parseFile("-x\n");
+    assertLocation(0, 3, statements.get(0).getLocation());
+
+    statements = parseFile("arr[15]\n");
+    assertLocation(0, 8, statements.get(0).getLocation());
+
+    statements = parseFile("str[1:12]\n");
+    assertLocation(0, 10, statements.get(0).getLocation());
+  }
+
+  public void testListPositions() throws Exception {
+    String expr = "[0,f(1),2]";
+    ListLiteral list = (ListLiteral) parseExpr(expr);
+    assertEquals("[0,f(1),2]", getText(expr, list));
+    assertEquals("0",    getText(expr, getElem(list, 0)));
+    assertEquals("f(1)", getText(expr, getElem(list, 1)));
+    assertEquals("2",    getText(expr, getElem(list, 2)));
+  }
+
+  public void testDictPositions() throws Exception {
+    String expr = "{1:2,2:f(1),3:4}";
+    DictionaryLiteral list = (DictionaryLiteral) parseExpr(expr);
+    assertEquals("{1:2,2:f(1),3:4}", getText(expr, list));
+    assertEquals("1:2",    getText(expr, getElem(list, 0)));
+    assertEquals("2:f(1)", getText(expr, getElem(list, 1)));
+    assertEquals("3:4",    getText(expr, getElem(list, 2)));
+  }
+
+  public void testArgumentPositions() throws Exception {
+    String stmt = "f(0,g(1,2),2)";
+    FuncallExpression f = (FuncallExpression) parseExpr(stmt);
+    assertEquals(stmt, getText(stmt, f));
+    assertEquals("0",    getText(stmt, getArg(f, 0)));
+    assertEquals("g(1,2)", getText(stmt, getArg(f, 1)));
+    assertEquals("2",    getText(stmt, getArg(f, 2)));
+  }
+
+  public void testListLiterals1() throws Exception {
+    ListLiteral list = (ListLiteral) parseExpr("[0,1,2]");
+    assertFalse(list.isTuple());
+    assertThat(list.getElements()).hasSize(3);
+    assertFalse(list.isTuple());
+    for (int i = 0; i < 3; ++i) {
+      assertEquals(i, getIntElem(list, i));
+    }
+  }
+
+  public void testTupleLiterals2() throws Exception {
+    ListLiteral tuple = (ListLiteral) parseExpr("(0,1,2)");
+    assertTrue(tuple.isTuple());
+    assertThat(tuple.getElements()).hasSize(3);
+    assertTrue(tuple.isTuple());
+    for (int i = 0; i < 3; ++i) {
+      assertEquals(i, getIntElem(tuple, i));
+    }
+  }
+
+  public void testTupleLiterals3() throws Exception {
+    ListLiteral emptyTuple = (ListLiteral) parseExpr("()");
+    assertTrue(emptyTuple.isTuple());
+    assertThat(emptyTuple.getElements()).isEmpty();
+  }
+
+  public void testTupleLiterals4() throws Exception {
+    ListLiteral singletonTuple = (ListLiteral) parseExpr("(42,)");
+    assertTrue(singletonTuple.isTuple());
+    assertThat(singletonTuple.getElements()).hasSize(1);
+    assertEquals(42, getIntElem(singletonTuple, 0));
+  }
+
+  public void testTupleLiterals5() throws Exception {
+    IntegerLiteral intLit = (IntegerLiteral) parseExpr("(42)"); // not a tuple!
+    assertEquals(42, (int) intLit.getValue());
+  }
+
+  public void testListLiterals6() throws Exception {
+    ListLiteral emptyList = (ListLiteral) parseExpr("[]");
+    assertFalse(emptyList.isTuple());
+    assertThat(emptyList.getElements()).isEmpty();
+  }
+
+  public void testListLiterals7() throws Exception {
+    ListLiteral singletonList = (ListLiteral) parseExpr("[42,]");
+    assertFalse(singletonList.isTuple());
+    assertThat(singletonList.getElements()).hasSize(1);
+    assertEquals(42, getIntElem(singletonList, 0));
+  }
+
+  public void testListLiterals8() throws Exception {
+    ListLiteral singletonList = (ListLiteral) parseExpr("[42]"); // a singleton
+    assertFalse(singletonList.isTuple());
+    assertThat(singletonList.getElements()).hasSize(1);
+    assertEquals(42, getIntElem(singletonList, 0));
+  }
+
+  public void testDictionaryLiterals() throws Exception {
+    DictionaryLiteral dictionaryList =
+      (DictionaryLiteral) parseExpr("{1:42}"); // a singleton dictionary
+    assertThat(dictionaryList.getEntries()).hasSize(1);
+    DictionaryEntryLiteral tuple = getElem(dictionaryList, 0);
+    assertEquals(1, getIntElem(tuple, true));
+    assertEquals(42, getIntElem(tuple, false));
+  }
+
+  public void testDictionaryLiterals1() throws Exception {
+    DictionaryLiteral dictionaryList =
+      (DictionaryLiteral) parseExpr("{}"); // an empty dictionary
+    assertThat(dictionaryList.getEntries()).isEmpty();
+  }
+
+  public void testDictionaryLiterals2() throws Exception {
+    DictionaryLiteral dictionaryList =
+      (DictionaryLiteral) parseExpr("{1:42,}"); // a singleton dictionary
+    assertThat(dictionaryList.getEntries()).hasSize(1);
+    DictionaryEntryLiteral tuple = getElem(dictionaryList, 0);
+    assertEquals(1, getIntElem(tuple, true));
+    assertEquals(42, getIntElem(tuple, false));
+  }
+
+  public void testDictionaryLiterals3() throws Exception {
+    DictionaryLiteral dictionaryList = (DictionaryLiteral) parseExpr("{1:42,2:43,3:44}");
+    assertThat(dictionaryList.getEntries()).hasSize(3);
+    for (int i = 0; i < 3; i++) {
+      DictionaryEntryLiteral tuple = getElem(dictionaryList, i);
+      assertEquals(i + 1, getIntElem(tuple, true));
+      assertEquals(i + 42, getIntElem(tuple, false));
+    }
+  }
+
+  public void testListLiterals9() throws Exception {
+    ListLiteral singletonList =
+      (ListLiteral) parseExpr("[ abi + opt_level + \'/include\' ]");
+    assertFalse(singletonList.isTuple());
+    assertThat(singletonList.getElements()).hasSize(1);
+  }
+
+  public void testListComprehensionSyntax() throws Exception {
+    syntaxEvents.setFailFast(false);
+
+    parseExpr("[x for");
+    syntaxEvents.assertContainsEvent("syntax error at 'newline'");
+    syntaxEvents.collector().clear();
+
+    parseExpr("[x for x");
+    syntaxEvents.assertContainsEvent("syntax error at 'newline'");
+    syntaxEvents.collector().clear();
+
+    parseExpr("[x for x in");
+    syntaxEvents.assertContainsEvent("syntax error at 'newline'");
+    syntaxEvents.collector().clear();
+
+    parseExpr("[x for x in []");
+    syntaxEvents.assertContainsEvent("syntax error at 'newline'");
+    syntaxEvents.collector().clear();
+
+    parseExpr("[x for x for y in ['a']]");
+    syntaxEvents.assertContainsEvent("syntax error at 'for'");
+    syntaxEvents.collector().clear();
+  }
+
+  public void testListComprehension() throws Exception {
+    ListComprehension list =
+      (ListComprehension) parseExpr(
+          "['foo/%s.java' % x "
+          + "for x in []]");
+    assertThat(list.getLists()).hasSize(1);
+
+    list = (ListComprehension) parseExpr("['foo/%s.java' % x "
+        + "for x in ['bar', 'wiz', 'quux']]");
+    assertThat(list.getLists()).hasSize(1);
+
+    list = (ListComprehension) parseExpr("['%s/%s.java' % (x, y) "
+        + "for x in ['foo', 'bar'] for y in ['baz', 'wiz', 'quux']]");
+    assertThat(list.getLists()).hasSize(2);
+  }
+
+  public void testParserContainsErrorsIfSyntaxException() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseExpr("'foo' %%");
+    syntaxEvents.assertContainsEvent("syntax error at '%'");
+  }
+
+  public void testParserDoesNotContainErrorsIfSuccess() throws Exception {
+    parseExpr("'foo'");
+  }
+
+  public void testParserContainsErrors() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseStmt("+");
+    syntaxEvents.assertContainsEvent("syntax error at '+'");
+  }
+
+  public void testSemicolonAndNewline() throws Exception {
+    List<Statement> stmts = parseFile(
+      "foo='bar'; foo(bar)" + '\n'
+      + "" + '\n'
+      + "foo='bar'; foo(bar)"
+    );
+    assertThat(stmts).hasSize(4);
+  }
+
+  public void testSemicolonAndNewline2() throws Exception {
+    syntaxEvents.setFailFast(false);
+    List<Statement> stmts = parseFile(
+      "foo='foo' error(bar)" + '\n'
+      + "" + '\n'
+    );
+    syntaxEvents.assertContainsEvent("syntax error at 'error'");
+    assertThat(stmts).hasSize(2);
+  }
+
+  public void testExprAsStatement() throws Exception {
+    List<Statement> stmts = parseFile(
+      "li = []\n"
+      + "li.append('a.c')\n"
+      + "\"\"\" string comment \"\"\"\n"
+      + "foo(bar)"
+    );
+    assertThat(stmts).hasSize(4);
+  }
+
+  public void testParseBuildFileWithSingeRule() throws Exception {
+    List<Statement> stmts = parseFile(
+      "genrule(name = 'foo'," + '\n'
+      + "   srcs = ['input.csv']," + '\n'
+      + "   outs = [ 'result.txt'," + '\n'
+      + "           'result.log']," + '\n'
+      + "   cmd = 'touch result.txt result.log')" + '\n'
+      );
+    assertThat(stmts).hasSize(1);
+  }
+
+  public void testParseBuildFileWithMultipleRules() throws Exception {
+    List<Statement> stmts = parseFile(
+      "genrule(name = 'foo'," + '\n'
+      + "   srcs = ['input.csv']," + '\n'
+      + "   outs = [ 'result.txt'," + '\n'
+      + "           'result.log']," + '\n'
+      + "   cmd = 'touch result.txt result.log')" + '\n'
+      + "" + '\n'
+      + "genrule(name = 'bar'," + '\n'
+      + "   srcs = ['input.csv']," + '\n'
+      + "   outs = [ 'graph.svg']," + '\n'
+      + "   cmd = 'touch graph.svg')" + '\n'
+      );
+    assertThat(stmts).hasSize(2);
+  }
+
+  public void testParseBuildFileWithComments() throws Exception {
+    Parser.ParseResult result = parseFileWithComments(
+      "# Test BUILD file" + '\n'
+      + "# with multi-line comment" + '\n'
+      + "" + '\n'
+      + "genrule(name = 'foo'," + '\n'
+      + "   srcs = ['input.csv']," + '\n'
+      + "   outs = [ 'result.txt'," + '\n'
+      + "           'result.log']," + '\n'
+      + "   cmd = 'touch result.txt result.log')" + '\n'
+      );
+    assertThat(result.statements).hasSize(1);
+    assertThat(result.comments).hasSize(2);
+  }
+
+  public void testParseBuildFileWithManyComments() throws Exception {
+    Parser.ParseResult result = parseFileWithComments(
+      "# 1" + '\n'
+      + "# 2" + '\n'
+      + "" + '\n'
+      + "# 4 " + '\n'
+      + "# 5" + '\n'
+      + "#" + '\n' // 6 - find empty comment for syntax highlighting
+      + "# 7 " + '\n'
+      + "# 8" + '\n'
+      + "genrule(name = 'foo'," + '\n'
+      + "   srcs = ['input.csv']," + '\n'
+      + "   # 11" + '\n'
+      + "   outs = [ 'result.txt'," + '\n'
+      + "           'result.log'], # 13" + '\n'
+      + "   cmd = 'touch result.txt result.log')" + '\n'
+      + "# 15" + '\n'
+      );
+    assertThat(result.statements).hasSize(1); // Single genrule
+    StringBuilder commentLines = new StringBuilder();
+    for (Comment comment : result.comments) {
+      // Comments start and end on the same line
+      assertEquals(comment.getLocation().getStartLineAndColumn().getLine() + " ends on "
+          + comment.getLocation().getEndLineAndColumn().getLine(),
+          comment.getLocation().getStartLineAndColumn().getLine(),
+          comment.getLocation().getEndLineAndColumn().getLine());
+      commentLines.append('(');
+      commentLines.append(comment.getLocation().getStartLineAndColumn().getLine());
+      commentLines.append(',');
+      commentLines.append(comment.getLocation().getStartLineAndColumn().getColumn());
+      commentLines.append(") ");
+    }
+    assertWithMessage("Found: " + commentLines)
+        .that(result.comments.size()).isEqualTo(10); // One per '#'
+  }
+
+  public void testMissingComma() throws Exception {
+    syntaxEvents.setFailFast(false);
+    // Regression test.
+    // Note: missing comma after name='foo'
+    parseFile("genrule(name = 'foo'\n"
+              + "      srcs = ['in'])");
+    syntaxEvents.assertContainsEvent("syntax error at 'srcs'");
+  }
+
+  public void testDoubleSemicolon() throws Exception {
+    syntaxEvents.setFailFast(false);
+    // Regression test.
+    parseFile("x = 1; ; x = 2;");
+    syntaxEvents.assertContainsEvent("syntax error at ';'");
+  }
+
+  public void testFunctionDefinitionErrorRecovery() throws Exception {
+    // Parser skips over entire function definitions, and reports a meaningful
+    // error.
+    syntaxEvents.setFailFast(false);
+    List<Statement> stmts = parseFile(
+        "x = 1;\n"
+        + "def foo(x, y, **z):\n"
+        + "  # a comment\n"
+        + "  x = 2\n"
+        + "  foo(bar)\n"
+        + "  return z\n"
+        + "x = 3");
+    assertThat(stmts).hasSize(2);
+  }
+
+  public void testFunctionDefinitionIgnored() throws Exception {
+    // Parser skips over entire function definitions without reporting error,
+    // when parsePython is set to true.
+    List<Statement> stmts = parseFile(
+        "x = 1;\n"
+        + "def foo(x, y, **z):\n"
+        + "  # a comment\n"
+        + "  if true:"
+        + "    x = 2\n"
+        + "  foo(bar)\n"
+        + "  return z\n"
+        + "x = 3", true /* parsePython */);
+    assertThat(stmts).hasSize(2);
+
+    stmts = parseFile(
+        "x = 1;\n"
+        + "def foo(x, y, **z): return x\n"
+        + "x = 3", true /* parsePython */);
+    assertThat(stmts).hasSize(2);
+  }
+
+  public void testMissingBlock() throws Exception {
+    syntaxEvents.setFailFast(false);
+    List<Statement> stmts = parseFile(
+        "x = 1;\n"
+        + "def foo(x):\n"
+        + "x = 2;\n",
+        true /* parsePython */);
+    assertThat(stmts).hasSize(2);
+    syntaxEvents.assertContainsEvent("expected an indented block");
+  }
+
+  public void testInvalidDef() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFile(
+        "x = 1;\n"
+        + "def foo(x)\n"
+        + "x = 2;\n",
+        true /* parsePython */);
+    syntaxEvents.assertContainsEvent("syntax error at 'EOF'");
+  }
+
+  public void testSkipIfBlock() throws Exception {
+    // Skip over 'if' blocks, when parsePython is set
+    List<Statement> stmts = parseFile(
+        "x = 1;\n"
+        + "if x == 1:\n"
+        + "  foo(x)\n"
+        + "else:\n"
+        + "  bar(x)\n"
+        + "x = 3;\n",
+        true /* parsePython */);
+    assertThat(stmts).hasSize(2);
+  }
+
+  public void testSkipIfBlockFail() throws Exception {
+    // Do not parse 'if' blocks, when parsePython is not set
+    syntaxEvents.setFailFast(false);
+    List<Statement> stmts = parseFile(
+        "x = 1;\n"
+        + "if x == 1:\n"
+        + "  x = 2\n"
+        + "x = 3;\n",
+        false /* no parsePython */);
+    assertThat(stmts).hasSize(2);
+    syntaxEvents.assertContainsEvent("This Python-style construct is not supported");
+  }
+
+  public void testForLoopMultipleVariablesFail() throws Exception {
+    // For loops with multiple variables are not allowed, when parsePython is not set
+    syntaxEvents.setFailFast(false);
+    List<Statement> stmts = parseFile(
+        "[ i for i, j, k in [(1, 2, 3)] ]\n",
+        false /* no parsePython */);
+    assertThat(stmts).hasSize(1);
+    syntaxEvents.assertContainsEvent("For loops with multiple variables are not yet supported.");
+  }
+
+  public void testForLoopMultipleVariables() throws Exception {
+    // For loops with multiple variables is ok, when parsePython is set
+    List<Statement> stmts1 = parseFile(
+        "[ i for i, j, k in [(1, 2, 3)] ]\n",
+        true /* parsePython */);
+    assertThat(stmts1).hasSize(1);
+
+    List<Statement> stmts2 = parseFile(
+        "[ i for i, j in [(1, 2, 3)] ]\n",
+        true /* parsePython */);
+    assertThat(stmts2).hasSize(1);
+
+    List<Statement> stmts3 = parseFile(
+        "[ i for (i, j, k) in [(1, 2, 3)] ]\n",
+        true /* parsePython */);
+    assertThat(stmts3).hasSize(1);
+  }
+
+  public void testForLoopBadSyntax() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFile(
+        "[1 for (a, b, c in var]\n",
+        false /* no parsePython */);
+    syntaxEvents.assertContainsEvent("syntax error");
+  }
+
+  public void testForLoopBadSyntax2() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFile(
+        "[1 for () in var]\n",
+        false /* no parsePython */);
+    syntaxEvents.assertContainsEvent("syntax error");
+  }
+
+  public void testFunCallBadSyntax() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFile("f(1,\n");
+    syntaxEvents.assertContainsEvent("syntax error");
+  }
+
+  public void testFunCallBadSyntax2() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFile("f(1, 5, ,)\n");
+    syntaxEvents.assertContainsEvent("syntax error");
+  }
+
+  public void testLoadOneSymbol() throws Exception {
+    List<Statement> statements = parseFileForSkylark(
+        "load('/foo/bar/file', 'fun_test')\n");
+    LoadStatement stmt = (LoadStatement) statements.get(0);
+    assertEquals("/foo/bar/file.bzl", stmt.getImportPath().toString());
+    assertThat(stmt.getSymbols()).hasSize(1);
+  }
+
+  public void testLoadMultipleSymbols() throws Exception {
+    List<Statement> statements = parseFileForSkylark(
+        "load('file', 'foo', 'bar')\n");
+    LoadStatement stmt = (LoadStatement) statements.get(0);
+    assertEquals("file.bzl", stmt.getImportPath().toString());
+    assertThat(stmt.getSymbols()).hasSize(2);
+  }
+
+  public void testLoadSyntaxError() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFileForSkylark("load(non_quoted, 'a')\n");
+    syntaxEvents.assertContainsEvent("syntax error");
+  }
+
+  public void testLoadSyntaxError2() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFileForSkylark("load('non_quoted', a)\n");
+    syntaxEvents.assertContainsEvent("syntax error");
+  }
+
+  public void testLoadNotAtTopLevel() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFileForSkylark("if 1: load(8)\n");
+    syntaxEvents.assertContainsEvent("function 'load' does not exist");
+  }
+
+  public void testParseErrorNotComparison() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFile("2 < not 3");
+    syntaxEvents.assertContainsEvent("syntax error at 'not'");
+  }
+
+  public void testNotWithArithmeticOperatorsBadSyntax() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFile("0 + not 0");
+    syntaxEvents.assertContainsEvent("syntax error at 'not'");
+  }
+
+  public void testOptionalArgBeforeMandatoryArgInFuncDef() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFileForSkylark("def func(a, b = 'a', c):\n  return 0\n");
+    syntaxEvents.assertContainsEvent(
+        "a mandatory positional parameter must not follow an optional parameter");
+  }
+
+  public void testKwargBeforePositionalArg() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFileForSkylark(
+        "def func(a, b): return a + b\n"
+        + "func(**{'b': 1}, 'a')");
+    syntaxEvents.assertContainsEvent("unexpected tokens after kwarg");
+  }
+
+  public void testDuplicateKwarg() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFileForSkylark(
+        "def func(a, b): return a + b\n"
+        + "func(**{'b': 1}, **{'a': 2})");
+    syntaxEvents.assertContainsEvent("unexpected tokens after kwarg");
+  }
+
+  public void testUnnamedStar() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFileForSkylark(
+        "def func(a, b1=2, b2=3, *, c1, c2, d=4): return a + b1 + b2 + c1 + c2 + d\n");
+    syntaxEvents.assertContainsEvent("no star, star-star or named-only parameters (for now)");
+  }
+
+  public void testTopLevelForFails() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFileForSkylark("for i in []: 0\n");
+    syntaxEvents.assertContainsEvent(
+        "for loops are not allowed on top-level. Put it into a function");
+  }
+
+  public void testNestedFunctionFails() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFileForSkylark(
+          "def func(a):\n"
+        + "  def bar(): return 0\n"
+        + "  return bar()\n");
+    syntaxEvents.assertContainsEvent(
+        "nested functions are not allowed. Move the function to top-level");
+  }
+
+  public void testIncludeFailureSkylark() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFileForSkylark("include('//foo:bar')");
+    syntaxEvents.assertContainsEvent("function 'include' does not exist");
+  }
+
+  public void testIncludeFailure() throws Exception {
+    syntaxEvents.setFailFast(false);
+    parseFile("include('nonexistent')\n");
+    syntaxEvents.assertContainsEvent("Invalid label 'nonexistent'");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
new file mode 100644
index 0000000..5ddb2af2
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
@@ -0,0 +1,799 @@
+// Copyright 2015 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
+import com.google.devtools.build.lib.analysis.FileConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
+import com.google.devtools.build.lib.packages.MethodLibrary;
+import com.google.devtools.build.lib.rules.SkylarkModules;
+import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject;
+
+import java.util.List;
+
+/**
+ * Evaluation tests with Skylark Environment.
+ */
+public class SkylarkEvaluationTest extends EvaluationTest {
+
+  @SkylarkModule(name = "Mock", doc = "")
+  static class Mock {
+    @SkylarkCallable(doc = "")
+    public static Integer valueOf(String str) {
+      return Integer.valueOf(str);
+    }
+    @SkylarkCallable(doc = "")
+    public Boolean isEmpty(String str) {
+      return str.isEmpty();
+    }
+    public void value() {}
+    @SkylarkCallable(doc = "")
+    public Mock returnMutable() {
+      return new Mock();
+    }
+    @SkylarkCallable(name = "struct_field", doc = "", structField = true)
+    public String structField() {
+      return "a";
+    }
+    @SkylarkCallable(name = "function", doc = "", structField = false)
+    public String function() {
+      return "a";
+    }
+    @SuppressWarnings("unused")
+    @SkylarkCallable(name = "nullfunc_failing", doc = "", allowReturnNones = false)
+    public Object nullfuncFailing(String p1, Integer p2) {
+      return null;
+    }
+    @SkylarkCallable(name = "nullfunc_working", doc = "", allowReturnNones = true)
+    public Object nullfuncWorking() {
+      return null;
+    }
+    @SkylarkCallable(name = "voidfunc", doc = "")
+    public void voidfunc() {}
+    @SkylarkCallable(name = "string_list", doc = "")
+    public ImmutableList<String> stringList() {
+      return ImmutableList.<String>of("a", "b");
+    }
+    @SkylarkCallable(name = "string", doc = "")
+    public String string() {
+      return "a";
+    }
+  }
+
+  @SkylarkModule(name = "MockInterface", doc = "")
+  static interface MockInterface {
+    @SkylarkCallable(doc = "")
+    public Boolean isEmptyInterface(String str);
+  }
+
+  static final class MockSubClass extends Mock implements MockInterface {
+    @Override
+    public Boolean isEmpty(String str) {
+      return str.isEmpty();
+    }
+    @Override
+    public Boolean isEmptyInterface(String str) {
+      return str.isEmpty();
+    }
+    @SkylarkCallable(doc = "")
+    public Boolean isEmptyClassNotAnnotated(String str) {
+      return str.isEmpty();
+    }
+  }
+
+  static final class MockClassObject implements ClassObject {
+    @Override
+    public Object getValue(String name) {
+      switch (name) {
+        case "field": return "a";
+        case "nset": return NestedSetBuilder.stableOrder().build();
+      }
+      throw new IllegalStateException();
+    }
+
+    @Override
+    public ImmutableCollection<String> getKeys() {
+      return ImmutableList.of("field", "nset");
+    }
+
+    @Override
+    public String errorMessage(String name) {
+      return null;
+    }
+  }
+
+  @SkylarkModule(name = "MockMultipleMethodClass", doc = "")
+  static final class MockMultipleMethodClass {
+    @SuppressWarnings("unused")
+    @SkylarkCallable(doc = "")
+    public void method(Object o) {}
+    @SuppressWarnings("unused")
+    @SkylarkCallable(doc = "")
+    public void method(String i) {}
+  }
+
+  private static final ImmutableMap<String, SkylarkType> MOCK_TYPES = ImmutableMap
+      .<String, SkylarkType>of("mock", SkylarkType.UNKNOWN, "Mock", SkylarkType.UNKNOWN);
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    syntaxEvents = new EventCollectionApparatus(EventKind.ALL_EVENTS);
+    env = new SkylarkEnvironment(syntaxEvents.collector());
+    MethodLibrary.setupMethodEnvironment(env);
+  }
+
+  @Override
+  public Environment singletonEnv(String id, Object value) {
+    SkylarkEnvironment env = new SkylarkEnvironment(syntaxEvents.collector());
+    env.update(id, value);
+    return env;
+  }
+
+  public void testSimpleIf() throws Exception {
+    exec(parseFileForSkylark(
+        "def foo():\n"
+        + "  a = 0\n"
+        + "  x = 0\n"
+        + "  if x: a = 5\n"
+        + "  return a\n"
+        + "a = foo()"), env);
+    assertEquals(0, env.lookup("a"));
+  }
+
+  public void testNestedIf() throws Exception {
+    executeNestedIf(0, 0, env);
+    assertEquals(0, env.lookup("x"));
+
+    executeNestedIf(1, 0, env);
+    assertEquals(3, env.lookup("x"));
+
+    executeNestedIf(1, 1, env);
+    assertEquals(5, env.lookup("x"));
+  }
+
+  private void executeNestedIf(int x, int y, Environment env) throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def foo():\n"
+        + "  x = " + x + "\n"
+        + "  y = " + y + "\n"
+        + "  a = 0\n"
+        + "  b = 0\n"
+        + "  if x:\n"
+        + "    if y:\n"
+        + "      a = 2\n"
+        + "    b = 3\n"
+        + "  return a + b\n"
+        + "x = foo()");
+    exec(input, env);
+  }
+
+  public void testIfElse() throws Exception {
+    executeIfElse("something", 2);
+    executeIfElse("", 3);
+  }
+
+  private void executeIfElse(String y, int expectedA) throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def foo():\n"
+        + "  y = '" + y + "'\n"
+        + "  x = 5\n"
+        + "  if x:\n"
+        + "    if y: a = 2\n"
+        + "    else: a = 3\n"
+        + "  return a\n"
+        + "a = foo()");
+
+    exec(input, env);
+    assertEquals(expectedA, env.lookup("a"));
+  }
+
+  public void testIfElifElse_IfExecutes() throws Exception {
+    execIfElifElse(1, 0, 1);
+  }
+
+  public void testIfElifElse_ElifExecutes() throws Exception {
+    execIfElifElse(0, 1, 2);
+  }
+
+  public void testIfElifElse_ElseExecutes() throws Exception {
+    execIfElifElse(0, 0, 3);
+  }
+
+  private void execIfElifElse(int x, int y, int v) throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def foo():\n"
+        + "  x = " + x + "\n"
+        + "  y = " + y + "\n"
+        + "  if x:\n"
+        + "    return 1\n"
+        + "  elif y:\n"
+        + "    return 2\n"
+        + "  else:\n"
+        + "    return 3\n"
+        + "v = foo()");
+    exec(input, env);
+    assertEquals(v, env.lookup("v"));
+  }
+
+  public void testForOnList() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def foo():\n"
+        + "  s = ''\n"
+        + "  for i in ['hello', ' ', 'world']:\n"
+        + "    s = s + i\n"
+        + "  return s\n"
+        + "s = foo()\n");
+
+    exec(input, env);
+    assertEquals("hello world", env.lookup("s"));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testForOnString() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def foo():\n"
+        + "  s = []\n"
+        + "  for i in 'abc':\n"
+        + "    s = s + [i]\n"
+        + "  return s\n"
+        + "s = foo()\n");
+
+    exec(input, env);
+    assertThat((Iterable<Object>) env.lookup("s")).containsExactly("a", "b", "c").inOrder();
+  }
+
+  public void testForAssignmentList() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def foo():\n"
+        + "  d = ['a', 'b', 'c']\n"
+        + "  s = ''\n"
+        + "  for i in d:\n"
+        + "    s = s + i\n"
+        + "    d = ['d', 'e', 'f']\n"  // check that we use the old list
+        + "  return s\n"
+        + "s = foo()\n");
+
+    exec(input, env);
+    assertEquals("abc", env.lookup("s"));
+  }
+
+  public void testForAssignmentDict() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+          "def func():\n"
+        + "  d = {'a' : 1, 'b' : 2, 'c' : 3}\n"
+        + "  s = ''\n"
+        + "  for i in d:\n"
+        + "    s = s + i\n"
+        + "    d = {'d' : 1, 'e' : 2, 'f' : 3}\n"
+        + "  return s\n"
+        + "s = func()");
+
+    exec(input, env);
+    assertEquals("abc", env.lookup("s"));
+  }
+
+  public void testForNotIterable() throws Exception {
+    env.update("mock", new Mock());
+    List<Statement> input = parseFileForSkylark(
+          "def func():\n"
+        + "  for i in mock.value_of('1'): a = i\n"
+        + "func()\n", MOCK_TYPES);
+    checkEvalError(input, env, "type 'int' is not an iterable");
+  }
+
+  public void testForOnDictionary() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def foo():\n"
+        + "  d = {1: 'a', 2: 'b', 3: 'c'}\n"
+        + "  s = ''\n"
+        + "  for i in d: s = s + d[i]\n"
+        + "  return s\n"
+        + "s = foo()");
+
+    exec(input, env);
+    assertEquals("abc", env.lookup("s"));
+  }
+
+  public void testForLoopReuseVariable() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def foo():\n"
+        + "  s = ''\n"
+        + "  for i in ['a', 'b']:\n"
+        + "    for i in ['c', 'd']: s = s + i\n"
+        + "  return s\n"
+        + "s = foo()");
+
+    exec(input, env);
+    assertEquals("cdcd", env.lookup("s"));
+  }
+
+  public void testNoneAssignment() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def foo(x=None):\n"
+        + "  x = 1\n"
+        + "  x = None\n"
+        + "  return 2\n"
+        + "s = foo()");
+
+    exec(input, env);
+    assertEquals(2, env.lookup("s"));
+  }
+
+  public void testJavaCalls() throws Exception {
+    env.update("mock", new Mock());
+    List<Statement> input = parseFileForSkylark(
+        "b = mock.is_empty('a')", MOCK_TYPES);
+    exec(input, env);
+    assertEquals(Boolean.FALSE, env.lookup("b"));
+  }
+
+  public void testJavaCallsOnSubClass() throws Exception {
+    env.update("mock", new MockSubClass());
+    List<Statement> input = parseFileForSkylark(
+        "b = mock.is_empty('a')", MOCK_TYPES);
+    exec(input, env);
+    assertEquals(Boolean.FALSE, env.lookup("b"));
+  }
+
+  public void testJavaCallsOnInterface() throws Exception {
+    env.update("mock", new MockSubClass());
+    List<Statement> input = parseFileForSkylark(
+        "b = mock.is_empty_interface('a')", MOCK_TYPES);
+    exec(input, env);
+    assertEquals(Boolean.FALSE, env.lookup("b"));
+  }
+
+  public void testJavaCallsNotSkylarkCallable() throws Exception {
+    env.update("mock", new Mock());
+    List<Statement> input = parseFileForSkylark("mock.value()", MOCK_TYPES);
+    checkEvalError(input, env, "No matching method found for value() in Mock");
+  }
+
+  public void testJavaCallsNoMethod() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "s = 3.bad()");
+    checkEvalError(input, env, "No matching method found for bad() in int");
+  }
+
+  public void testJavaCallsNoMethodErrorMsg() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "s = 3.bad('a', 'b', 'c')");
+    checkEvalError(input, env,
+        "No matching method found for bad(string, string, string) in int");
+  }
+
+  public void testJavaCallsMultipleMethod() throws Exception {
+    env.update("mock", new MockMultipleMethodClass());
+    List<Statement> input = parseFileForSkylark(
+        "s = mock.method('string')", MOCK_TYPES);
+    checkEvalError(input, env,
+        "Multiple matching methods for method(string) in MockMultipleMethodClass");
+  }
+
+  public void testJavaCallWithKwargs() throws Exception {
+    List<Statement> input = parseFileForSkylark("comp = 3.compare_to(x = 4)");
+    checkEvalError(input, env, "Keyword arguments are not allowed when calling a java method"
+                   + "\nwhile calling method 'compare_to' on object 3 of type int");
+  }
+
+  public void testNoJavaCallsWithoutSkylark() throws Exception {
+    List<Statement> input = parseFileForSkylark("s = 3.to_string()\n");
+    checkEvalError(input, env, "No matching method found for to_string() in int");
+  }
+
+  public void testNoJavaCallsIfClassNotAnnotated() throws Exception {
+    env.update("mock", new MockSubClass());
+    List<Statement> input = parseFileForSkylark(
+        "b = mock.is_empty_class_not_annotated('a')", MOCK_TYPES);
+    checkEvalError(input, env,
+        "No matching method found for is_empty_class_not_annotated(string) in MockSubClass");
+  }
+
+  public void testStructAccess() throws Exception {
+    env.update("mock", new Mock());
+    List<Statement> input = parseFileForSkylark(
+        "v = mock.struct_field", MOCK_TYPES);
+    exec(input, env);
+    assertEquals("a", env.lookup("v"));
+  }
+
+  public void testStructAccessAsFuncall() throws Exception {
+    env.update("mock", new Mock());
+    checkEvalError(parseFileForSkylark("v = mock.struct_field()", MOCK_TYPES), env,
+        "No matching method found for struct_field() in Mock");
+  }
+
+  public void testStructAccessOfMethod() throws Exception {
+    env.update("mock", new Mock());
+    checkEvalError(parseFileForSkylark(
+        "v = mock.function", MOCK_TYPES), env, "Object of type 'Mock' has no field 'function'");
+  }
+
+  public void testJavaFunctionReturnsMutableObject() throws Exception {
+    env.update("mock", new Mock());
+    List<Statement> input = parseFileForSkylark("mock.return_mutable()", MOCK_TYPES);
+    checkEvalError(input, env, "Method 'return_mutable' returns a mutable object (type of Mock)");
+  }
+
+  public void testJavaFunctionReturnsNullFails() throws Exception {
+    env.update("mock", new Mock());
+    List<Statement> input = parseFileForSkylark("mock.nullfunc_failing('abc', 1)", MOCK_TYPES);
+    checkEvalError(input, env, "Method invocation returned None,"
+        + " please contact Skylark developers: nullfunc_failing(\"abc\", 1)");
+  }
+
+  public void testClassObjectAccess() throws Exception {
+    env.update("mock", new MockClassObject());
+    exec(parseFileForSkylark("v = mock.field", MOCK_TYPES), env);
+    assertEquals("a", env.lookup("v"));
+  }
+
+  public void testClassObjectCannotAccessNestedSet() throws Exception {
+    env.update("mock", new MockClassObject());
+    checkEvalError(parseFileForSkylark("v = mock.nset", MOCK_TYPES), env,
+        "Type is not allowed in Skylark: EmptyNestedSet");
+  }
+
+  public void testJavaFunctionReturnsNone() throws Exception {
+    env.update("mock", new Mock());
+    exec(parseFileForSkylark("v = mock.nullfunc_working()", MOCK_TYPES), env);
+    assertSame(Environment.NONE, env.lookup("v"));
+  }
+
+  public void testVoidJavaFunctionReturnsNone() throws Exception {
+    env.update("mock", new Mock());
+    exec(parseFileForSkylark("v = mock.voidfunc()", MOCK_TYPES), env);
+    assertSame(Environment.NONE, env.lookup("v"));
+  }
+
+  public void testAugmentedAssignment() throws Exception {
+    exec(parseFileForSkylark(
+        "def f1(x):\n"
+        + "  x += 1\n"
+        + "  return x\n"
+        + "\n"
+        + "foo = f1(41)\n"), env);
+    assertEquals(42, env.lookup("foo"));
+  }
+
+  public void testStaticDirectJavaCall() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "val = Mock.value_of('8')", MOCK_TYPES);
+
+    env.update("Mock", Mock.class);
+    exec(input, env);
+    assertEquals(8, env.lookup("val"));
+  }
+
+  public void testStaticDirectJavaCallMethodIsNonStatic() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "val = Mock.is_empty('a')", MOCK_TYPES);
+
+    env.update("Mock", Mock.class);
+    checkEvalError(input, env, "Method 'is_empty' is not static");
+  }
+
+  public void testDictComprehensions_IterationOrder() throws Exception {
+    List<Statement> input = parseFileForSkylark(
+        "def foo():\n"
+        + "  d = {x : x for x in ['c', 'a', 'b']}\n"
+        + "  s = ''\n"
+        + "  for a in d:\n"
+        + "    s += a\n"
+        + "  return s\n"
+        + "s = foo()");
+    exec(input, env);
+    assertEquals("cab", env.lookup("s"));
+  }
+
+  public void testStructCreation() throws Exception {
+    exec(parseFileForSkylark("x = struct(a = 1, b = 2)"), env);
+    assertThat(env.lookup("x")).isInstanceOf(ClassObject.class);
+  }
+
+  public void testStructFields() throws Exception {
+    exec(parseFileForSkylark("x = struct(a = 1, b = 2)"), env);
+    ClassObject x = (ClassObject) env.lookup("x");
+    assertEquals(1, x.getValue("a"));
+    assertEquals(2, x.getValue("b"));
+  }
+
+  public void testStructAccessingFieldsFromSkylark() throws Exception {
+    exec(parseFileForSkylark(
+          "x = struct(a = 1, b = 2)\n"
+        + "x1 = x.a\n"
+        + "x2 = x.b\n"), env);
+    assertEquals(1, env.lookup("x1"));
+    assertEquals(2, env.lookup("x2"));
+  }
+
+  public void testStructAccessingUnknownField() throws Exception {
+    checkEvalError(parseFileForSkylark(
+          "x = struct(a = 1, b = 2)\n"
+        + "y = x.c\n"), env, "Object of type 'struct' has no field 'c'");
+  }
+
+  public void testStructAccessingFieldsWithArgs() throws Exception {
+    checkEvalError(parseFileForSkylark(
+          "x = struct(a = 1, b = 2)\n"
+        + "x1 = x.a(1)\n"),
+        env, "No matching method found for a(int) in struct");
+  }
+
+  public void testStructPosArgs() throws Exception {
+    checkEvalError(parseFileForSkylark(
+          "x = struct(1, b = 2)\n"),
+        env, "struct only supports keyword arguments");
+  }
+
+  public void testStructConcatenationFieldNames() throws Exception {
+    exec(parseFileForSkylark(
+          "x = struct(a = 1, b = 2)\n"
+        + "y = struct(c = 1, d = 2)\n"
+        + "z = x + y\n"), env);
+    SkylarkClassObject z = (SkylarkClassObject) env.lookup("z");
+    assertEquals(ImmutableSet.of("a", "b", "c", "d"), z.getKeys());
+  }
+
+  public void testStructConcatenationFieldValues() throws Exception {
+    exec(parseFileForSkylark(
+          "x = struct(a = 1, b = 2)\n"
+        + "y = struct(c = 1, d = 2)\n"
+        + "z = x + y\n"), env);
+    SkylarkClassObject z = (SkylarkClassObject) env.lookup("z");
+    assertEquals(1, z.getValue("a"));
+    assertEquals(2, z.getValue("b"));
+    assertEquals(1, z.getValue("c"));
+    assertEquals(2, z.getValue("d"));
+  }
+
+  public void testStructConcatenationCommonFields() throws Exception {
+    checkEvalError(parseFileForSkylark(
+          "x = struct(a = 1, b = 2)\n"
+        + "y = struct(c = 1, a = 2)\n"
+        + "z = x + y\n"), env, "Cannot concat structs with common field(s): a");
+  }
+
+  public void testDotExpressionOnNonStructObject() throws Exception {
+    checkEvalError(parseFileForSkylark(
+          "x = 'a'.field"), env, "Object of type 'string' has no field 'field'");
+  }
+
+  public void testPlusEqualsOnDict() throws Exception {
+    MethodLibrary.setupMethodEnvironment(env);
+    exec(parseFileForSkylark(
+          "def func():\n"
+        + "  d = {'a' : 1}\n"
+        + "  d += {'b' : 2}\n"
+        + "  return d\n"
+        + "d = func()"), env);
+    assertEquals(ImmutableMap.of("a", 1, "b", 2), env.lookup("d"));
+  }
+
+  public void testDictAssignmentAsLValue() throws Exception {
+    exec(parseFileForSkylark(
+          "def func():\n"
+        + "  d = {'a' : 1}\n"
+        + "  d['b'] = 2\n"
+        + "  return d\n"
+        + "d = func()"), env);
+    assertEquals(ImmutableMap.of("a", 1, "b", 2), env.lookup("d"));
+  }
+
+  public void testDictAssignmentAsLValueNoSideEffects() throws Exception {
+    MethodLibrary.setupMethodEnvironment(env);
+    exec(parseFileForSkylark(
+          "def func(d):\n"
+        + "  d['b'] = 2\n"
+        + "d = {'a' : 1}\n"
+        + "func(d)"), env);
+    assertEquals(ImmutableMap.of("a", 1), env.lookup("d"));
+  }
+
+  public void testListIndexAsLValueAsLValue() throws Exception {
+    checkEvalError(parseFileForSkylark(
+          "def id(l):\n"
+        + "  return l\n"
+        + "def func():\n"
+        + "  l = id([1])\n"
+        + "  l[0] = 2\n"
+        + "  return l\n"
+        + "l = func()"), env, "unsupported operand type(s) for +: 'list' and 'dict'");
+  }
+
+  public void testTopLevelDict() throws Exception {
+    exec(parseFileForSkylark(
+        "if 1:\n"
+      + "  v = 'a'\n"
+      + "else:\n"
+      + "  v = 'b'"), env);
+    assertEquals("a", env.lookup("v"));
+  }
+
+  public void testUserFunctionKeywordArgs() throws Exception {
+    exec(parseFileForSkylark(
+        "def foo(a, b, c):\n"
+      + "  return a + b + c\n"
+      + "s = foo(1, c=2, b=3)"), env);
+    assertEquals(6, env.lookup("s"));
+  }
+
+  public void testNoneTrueFalseInSkylark() throws Exception {
+    exec(parseFileForSkylark(
+        "a = None\n"
+      + "b = True\n"
+      + "c = False"), env);
+    assertSame(Environment.NONE, env.lookup("a"));
+    assertTrue((Boolean) env.lookup("b"));
+    assertFalse((Boolean) env.lookup("c"));
+  }
+
+  public void testHasattr() throws Exception {
+    exec(parseFileForSkylark(
+        "s = struct(a=1)\n"
+      + "x = hasattr(s, 'a')\n"
+      + "y = hasattr(s, 'b')\n"), env);
+    assertTrue((Boolean) env.lookup("x"));
+    assertFalse((Boolean) env.lookup("y"));
+  }
+
+  public void testHasattrMethods() throws Exception {
+    env.update("mock", new Mock());
+    ValidationEnvironment validEnv = SkylarkModules.getValidationEnvironment();
+    validEnv.update("mock", SkylarkType.of(Mock.class), null);
+    exec(Parser.parseFileForSkylark(createLexer(
+          "a = hasattr(mock, 'struct_field')\n"
+        + "b = hasattr(mock, 'function')\n"
+        + "c = hasattr(mock, 'is_empty')\n"
+        + "d = hasattr('str', 'replace')\n"
+        + "e = hasattr(mock, 'other')\n"),
+            syntaxEvents.reporter(), null, validEnv).statements, env);
+    assertTrue((Boolean) env.lookup("a"));
+    assertTrue((Boolean) env.lookup("b"));
+    assertTrue((Boolean) env.lookup("c"));
+    assertTrue((Boolean) env.lookup("d"));
+    assertFalse((Boolean) env.lookup("e"));
+  }
+
+  public void testGetattr() throws Exception {
+    exec(parseFileForSkylark(
+        "s = struct(a='val')\n"
+      + "x = getattr(s, 'a')\n"
+      + "y = getattr(s, 'b', 'def')\n"
+      + "z = getattr(s, 'b', default = 'def')\n"
+      + "w = getattr(s, 'a', default='ignored')"), env);
+    assertEquals("val", env.lookup("x"));
+    assertEquals("def", env.lookup("y"));
+    assertEquals("def", env.lookup("z"));
+    assertEquals("val", env.lookup("w"));
+  }
+
+  public void testGetattrNoAttr() throws Exception {
+    checkEvalError(parseFileForSkylark(
+          "s = struct(a='val')\n"
+        + "getattr(s, 'b')"),
+        env, "Object of type 'struct' has no field 'b'");
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testListAnTupleConcatenationDoesNotWorkInSkylark() throws Exception {
+    checkEvalError(parseFileForSkylark("[1, 2] + (3, 4)"), env,
+        "cannot concatenate lists and tuples");
+  }
+
+  public void testCannotCreateMixedListInSkylark() throws Exception {
+    env.update("mock", new Mock());
+    checkEvalError(parseFileForSkylark("[mock.string(), 1, 2]", MOCK_TYPES), env,
+        "Incompatible types in list: found a int but the first element is a string");
+  }
+
+  public void testCannotConcatListInSkylarkWithDifferentGenericTypes() throws Exception {
+    env.update("mock", new Mock());
+    checkEvalError(parseFileForSkylark("mock.string_list() + [1, 2]", MOCK_TYPES), env,
+        "cannot concatenate list of string with list of int");
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testConcatEmptyListWithNonEmptyWorks() throws Exception {
+    exec(parseFileForSkylark("l = [] + ['a', 'b']", MOCK_TYPES), env);
+    assertThat((Iterable<Object>) env.lookup("l")).containsExactly("a", "b").inOrder();
+  }
+
+  public void testFormatStringWithTuple() throws Exception {
+    exec(parseFileForSkylark("v = '%s%s' % ('a', 1)"), env);
+    assertEquals("a1", env.lookup("v"));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testDirFindsClassObjectFields() throws Exception {
+    env.update("mock", new MockClassObject());
+    exec(parseFileForSkylark("v = dir(mock)", MOCK_TYPES), env);
+    assertThat((Iterable<String>) env.lookup("v")).containsExactly("field", "nset").inOrder();
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testDirFindsJavaObjectStructFieldsAndMethods() throws Exception {
+    env.update("mock", new Mock());
+    exec(parseFileForSkylark("v = dir(mock)", MOCK_TYPES), env);
+    assertThat((Iterable<String>) env.lookup("v")).containsExactly("function", "is_empty",
+        "nullfunc_failing", "nullfunc_working", "return_mutable", "string", "string_list",
+        "struct_field", "value_of", "voidfunc").inOrder();
+  }
+
+  public void testPrint() throws Exception {
+    exec(parseFileForSkylark("print('hello')"), env);
+    syntaxEvents.assertContainsEvent("hello");
+    exec(parseFileForSkylark("print('a', 'b')"), env);
+    syntaxEvents.assertContainsEvent("a b");
+    exec(parseFileForSkylark("print('a', 'b', sep='x')"), env);
+    syntaxEvents.assertContainsEvent("axb");
+  }
+
+  public void testPrintBadKwargs() throws Exception {
+    checkEvalError("print(end='x', other='y')", "unexpected keywords: '[end, other]'");
+  }
+
+  public void testSkylarkTypes() {
+    assertEquals(TransitiveInfoCollection.class,
+        EvalUtils.getSkylarkType(FileConfiguredTarget.class));
+    assertEquals(TransitiveInfoCollection.class,
+        EvalUtils.getSkylarkType(RuleConfiguredTarget.class));
+    assertEquals(Artifact.class, EvalUtils.getSkylarkType(SpecialArtifact.class));
+  }
+
+  // Override tests in EvaluationTest incompatible with Skylark
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void testConcatLists() throws Exception {
+    // list
+    Object x = eval("[1,2] + [3,4]");
+    assertThat((Iterable<Object>) x).containsExactly(1, 2, 3, 4).inOrder();
+    assertFalse(((SkylarkList) x).isTuple());
+
+    // tuple
+    x = eval("(1,2)");
+    assertThat((Iterable<Object>) x).containsExactly(1, 2).inOrder();
+    assertTrue(((SkylarkList) x).isTuple());
+
+    x = eval("(1,2) + (3,4)");
+    assertThat((Iterable<Object>) x).containsExactly(1, 2, 3, 4).inOrder();
+    assertTrue(((SkylarkList) x).isTuple());
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void testListExprs() throws Exception {
+    assertThat((Iterable<Object>) eval("[1, 2, 3]")).containsExactly(1, 2, 3).inOrder();
+    assertThat((Iterable<Object>) eval("(1, 2, 3)")).containsExactly(1, 2, 3).inOrder();
+  }
+
+  @Override
+  public void testListConcatenation() throws Exception {}
+
+  @Override
+  public void testKeywordArgs() {}
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkListTest.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkListTest.java
new file mode 100644
index 0000000..2347198
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkListTest.java
@@ -0,0 +1,139 @@
+// 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;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.packages.MethodLibrary;
+
+import java.util.Iterator;
+
+/**
+ * Tests for SkylarkList.
+ */
+public class SkylarkListTest extends AbstractEvaluationTestCase {
+
+  @Immutable
+  private static final class CustomIterable implements Iterable<Object> {
+
+    @Override
+    public Iterator<Object> iterator() {
+      // Throw an exception whenever we request the iterator, to test that lazy lists
+      // are truly lazy.
+      throw new IllegalArgumentException("Iterator requested");
+    }
+  }
+
+  private static final SkylarkList list =
+      SkylarkList.lazyList(new CustomIterable(), Integer.class);
+  private static final ImmutableMap<String, SkylarkType> extraObjects =
+      ImmutableMap.of("lazy", SkylarkType.of(SkylarkList.class, Integer.class));
+
+  private Environment env;
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    env = new SkylarkEnvironment(syntaxEvents.collector());
+    env.update("lazy", list);
+    MethodLibrary.setupMethodEnvironment(env);
+  }
+
+  public void testLazyListIndex() throws Exception {
+    checkError("Iterator requested", "a = lazy[0]");
+  }
+
+  public void testLazyListSize() throws Exception {
+    checkError("Iterator requested", "a = len(lazy)");
+  }
+
+  public void testLazyListEmpty() throws Exception {
+    checkError("Iterator requested", "if lazy:\n  a = 1");
+  }
+
+  public void testLazyListConcat() throws Exception {
+    exec("v = [1, 2] + lazy");
+    assertTrue(env.lookup("v") instanceof SkylarkList);
+  }
+
+  public void testConcatListIndex() throws Exception {
+    exec("l = [1, 2] + [3, 4]",
+         "e0 = l[0]",
+         "e1 = l[1]",
+         "e2 = l[2]",
+         "e3 = l[3]");
+    assertEquals(1, env.lookup("e0"));
+    assertEquals(2, env.lookup("e1"));
+    assertEquals(3, env.lookup("e2"));
+    assertEquals(4, env.lookup("e3"));
+  }
+
+  public void testConcatListHierarchicalIndex() throws Exception {
+    exec("l = [1] + (([2] + [3, 4]) + [5])",
+         "e0 = l[0]",
+         "e1 = l[1]",
+         "e2 = l[2]",
+         "e3 = l[3]",
+         "e4 = l[4]");
+    assertEquals(1, env.lookup("e0"));
+    assertEquals(2, env.lookup("e1"));
+    assertEquals(3, env.lookup("e2"));
+    assertEquals(4, env.lookup("e3"));
+    assertEquals(5, env.lookup("e4"));
+  }
+
+  public void testConcatListSize() throws Exception {
+    exec("l = [1, 2] + [3, 4]",
+         "s = len(l)");
+    assertEquals(4, env.lookup("s"));
+  }
+
+  public void testConcatListToString() throws Exception {
+    exec("l = [1, 2] + [3, 4]",
+         "s = str(l)");
+    assertEquals("[1, 2, 3, 4]", env.lookup("s"));
+  }
+
+  public void testConcatListNotEmpty() throws Exception {
+    exec("l = [1, 2] + [3, 4]",
+        "if l:",
+        "  v = 1",
+        "else:",
+        "  v = 0");
+    assertEquals(1, env.lookup("v"));
+  }
+
+  public void testConcatListEmpty() throws Exception {
+    exec("l = [] + []",
+        "if l:",
+        "  v = 1",
+        "else:",
+        "  v = 0");
+    assertEquals(0, env.lookup("v"));
+  }
+
+  private void exec(String... input) throws Exception {
+    exec(parseFileForSkylark(Joiner.on("\n").join(input), extraObjects), env);
+  }
+
+  private void checkError(String msg, String... input) throws Exception {
+    try {
+      exec(input);
+      fail();
+    } catch (Exception e) {
+      assertEquals(msg, e.getMessage());
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkNestedSetTest.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkNestedSetTest.java
new file mode 100644
index 0000000..4a9a446
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkNestedSetTest.java
@@ -0,0 +1,170 @@
+// Copyright 2015 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;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.MethodLibrary;
+import com.google.devtools.build.lib.syntax.Environment.NoSuchVariableException;
+
+/**
+ * Tests for SkylarkNestedSet.
+ */
+public class SkylarkNestedSetTest extends AbstractEvaluationTestCase {
+
+  private Environment env;
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    env = new SkylarkEnvironment(syntaxEvents.collector());
+    MethodLibrary.setupMethodEnvironment(env);
+  }
+
+  public void testNsetBuilder() throws Exception {
+    exec("n = set(order='stable')");
+    assertTrue(env.lookup("n") instanceof SkylarkNestedSet);
+  }
+
+  public void testNsetOrder() throws Exception {
+    exec("n = set(['a', 'b'], order='compile')");
+    assertEquals(Order.COMPILE_ORDER, get("n").getSet(String.class).getOrder());
+  }
+
+  public void testEmptyNsetGenericType() throws Exception {
+    exec("n = set()");
+    assertEquals(Object.class, get("n").getGenericType());
+  }
+
+  public void testFunctionReturnsNset() throws Exception {
+    exec("def func():",
+         "  n = set()",
+         "  n += ['a']",
+         "  return n",
+         "s = func()");
+    assertEquals(ImmutableList.of("a"), get("s").toCollection());
+  }
+
+  public void testNsetTwoReferences() throws Exception {
+    exec("def func():",
+         "  n1 = set()",
+         "  n1 += ['a']",
+         "  n2 = n1",
+         "  n2 += ['b']",
+         "  return n1",
+         "n = func()");
+    assertEquals(ImmutableList.of("a"), get("n").toCollection());
+  }
+
+  public void testNsetNestedItem() throws Exception {
+    exec("def func():",
+        "  n1 = set()",
+        "  n2 = set()",
+        "  n1 += ['a']",
+        "  n2 += ['b']",
+        "  n1 += n2",
+        "  return n1",
+        "n = func()");
+    assertEquals(ImmutableList.of("b", "a"), get("n").toCollection());
+  }
+
+  public void testNsetNestedItemBadOrder() throws Exception {
+    checkError("LINK_ORDER != COMPILE_ORDER",
+        "set(['a', 'b'], order='compile') + set(['c', 'd'], order='link')");
+  }
+
+  public void testNsetItemList() throws Exception {
+    exec("def func():",
+        "  n = set()",
+        "  n += ['a', 'b']",
+        "  return n",
+        "n = func()");
+    assertEquals(ImmutableList.of("a", "b"), get("n").toCollection());
+  }
+
+  public void testNsetFuncParamNoSideEffects() throws Exception {
+    exec("def func1(n):",
+        "  n += ['b']",
+        "def func2():",
+        "  n = set()",
+        "  n += ['a']",
+        "  func1(n)",
+        "  return n",
+        "n = func2()");
+    assertEquals(ImmutableList.of("a"), get("n").toCollection());
+  }
+
+  public void testNsetTransitiveOrdering() throws Exception {
+    exec("def func():",
+        "  na = set(['a'], order='compile')",
+        "  nb = set(['b'], order='compile')",
+        "  nc = set(['c'], order='compile') + na",
+        "  return set() + nb + nc",
+        "n = func()");
+    // The iterator lists the Transitive sets first
+    assertEquals(ImmutableList.of("b", "a", "c"), get("n").toCollection());
+  }
+
+  public void testNsetOrdering() throws Exception {
+    exec("def func():",
+        "  na = set()",
+        "  na += [4]",
+        "  na += [2, 4]",
+        "  na += [3, 4, 5]",
+        "  return na",
+        "n = func()");
+    // The iterator lists the Transitive sets first
+    assertEquals(ImmutableList.of(4, 2, 3, 5), get("n").toCollection());
+  }
+
+  public void testNsetBadOrder() throws Exception {
+    checkError("Invalid order: non_existing",
+        "set(order='non_existing')");
+  }
+
+  public void testNsetBadRightOperand() throws Exception {
+    checkError("cannot add 'string'-s to nested sets",
+        "l = ['a']\n",
+        "set() + l[0]");
+  }
+
+  public void testNsetBadCompositeItem() throws Exception {
+    checkError("nested set item is composite (type of struct)",
+        "set([struct(a='a')])");
+  }
+
+  public void testNsetToString() throws Exception {
+    exec("s = set() + [2, 4, 6] + [3, 4, 5]",
+        "x = str(s)");
+    assertEquals("set([2, 4, 6, 3, 5])", env.lookup("x"));
+  }
+
+  private void exec(String... input) throws Exception {
+    exec(parseFileForSkylark(Joiner.on("\n").join(input)), env);
+  }
+
+  private SkylarkNestedSet get(String varname) throws NoSuchVariableException {
+    return (SkylarkNestedSet) env.lookup(varname);
+  }
+
+  private void checkError(String msg, String... input) throws Exception {
+    try {
+      exec(input);
+      fail();
+    } catch (Exception e) {
+      assertEquals(msg, e.getMessage());
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkShell.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkShell.java
new file mode 100644
index 0000000..6c394f9
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkShell.java
@@ -0,0 +1,94 @@
+// Copyright 2015 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;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
+import com.google.devtools.build.lib.packages.CachingPackageLocator;
+import com.google.devtools.build.lib.rules.SkylarkModules;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.util.FsApparatus;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * SkylarkShell is a standalone shell executing Skylark. This is intended for
+ * testing purposes and not for end-users. This is very limited (environment is
+ * almost empty), but it can be used to play with the language and reproduce
+ * bugs. Imports and includes are not supported.
+ */
+class SkylarkShell {
+  static final EventCollectionApparatus syntaxEvents = new EventCollectionApparatus();
+  static final FsApparatus scratch = FsApparatus.newInMemory();
+  static final CachingPackageLocator locator = new AbstractParserTestCase.EmptyPackageLocator();
+  static final Path path = scratch.path("stdin");
+
+  private static void exec(String inputSource, Environment env) {
+    try {
+      ParserInputSource input = ParserInputSource.create(inputSource, path);
+      Lexer lexer = new Lexer(input, syntaxEvents.reporter());
+      Parser.ParseResult result =
+          Parser.parseFileForSkylark(lexer, syntaxEvents.reporter(), locator,
+              SkylarkModules.getValidationEnvironment(
+                  ImmutableMap.<String, SkylarkType>of()));
+
+      Object last = null;
+      for (Statement st : result.statements) {
+        if (st instanceof ExpressionStatement) {
+          last = ((ExpressionStatement) st).getExpression().eval(env);
+        } else {
+          st.exec(env);
+          last = null;
+        }
+      }
+      if (last != null) {
+        System.out.println(last);
+      }
+    } catch (Throwable e) { // Catch everything to avoid killing the shell.
+      e.printStackTrace();
+    }
+  }
+
+  public static void main(String[] args) {
+    Environment env = SkylarkModules.getNewEnvironment(new EventHandler() {
+      @Override
+      public void handle(Event event) {
+        System.out.println(event.getMessage());
+      }
+    });
+    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
+
+    String currentInput = "";
+    String line;
+    System.out.print(">> ");
+    try {
+      while ((line = br.readLine()) != null) {
+        if (line.isEmpty()) {
+          exec(currentInput, env);
+          currentInput = "";
+          System.out.print(">> ");
+        } else {
+          currentInput = currentInput + "\n" + line;
+          System.out.print(".. ");
+        }
+      }
+    } catch (IOException io) {
+      io.printStackTrace();
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ValidationTests.java b/src/test/java/com/google/devtools/build/lib/syntax/ValidationTests.java
new file mode 100644
index 0000000..723d72c
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ValidationTests.java
@@ -0,0 +1,576 @@
+// Copyright 2015 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;
+
+import com.google.common.base.Joiner;
+
+/**
+ * Tests for the validation process of Skylark files.
+ */
+public class ValidationTests extends AbstractParserTestCase {
+
+  public void testIncompatibleLiteralTypesStringInt() {
+    checkError("bad variable 'a': int is incompatible with string at /some/file.txt",
+        "def foo():\n",
+        "  a = '1'",
+        "  a = 1");
+  }
+
+  public void testIncompatibleLiteralTypesDictString() {
+    checkError("bad variable 'a': int is incompatible with dict at /some/file.txt",
+        "def foo():\n",
+        "  a = {1 : 'x'}",
+        "  a = 1");
+  }
+
+  public void testIncompatibleLiteralTypesInIf() {
+    checkError("bad variable 'a': int is incompatible with string at /some/file.txt",
+        "def foo():\n",
+        "  if 1:",
+        "    a = 'a'",
+        "  else:",
+        "    a = 1");
+  }
+
+  public void testAssignmentNotValidLValue() {
+    checkError("can only assign to variables, not to ''a''", "'a' = 1");
+  }
+
+  public void testForNotIterable() throws Exception {
+    checkError("type 'int' is not iterable",
+          "def func():\n"
+        + "  for i in 5: a = i\n");
+  }
+
+  public void testForIterableWithUknownArgument() throws Exception {
+    parse("def func(x=None):\n"
+        + "  for i in x: a = i\n");
+  }
+
+  public void testForNotIterableBinaryExpression() throws Exception {
+    checkError("type 'int' is not iterable",
+          "def func():\n"
+        + "  for i in 1 + 1: a = i\n");
+  }
+
+  public void testOptionalArgument() throws Exception {
+    checkError("type 'int' is not iterable",
+          "def func(x=5):\n"
+        + "  for i in x: a = i\n");
+  }
+
+  public void testOptionalArgumentHasError() throws Exception {
+    checkError("unsupported operand type(s) for +: 'int' and 'string'",
+          "def func(x=5+'a'):\n"
+        + "  return 0\n");
+  }
+
+  public void testTopLevelForStatement() throws Exception {
+    checkError("'For' is not allowed as a top level statement", "for i in [1,2,3]: a = i\n");
+  }
+
+  public void testReturnOutsideFunction() throws Exception {
+    checkError("Return statements must be inside a function", "return 2\n");
+  }
+
+  public void testTwoReturnTypes() throws Exception {
+    checkError("bad return type of foo: string is incompatible with int at /some/file.txt:3:5",
+        "def foo(x):",
+        "  if x:",
+        "    return 1",
+        "  else:",
+        "    return 'a'");
+  }
+
+  public void testTwoFunctionsWithTheSameName() throws Exception {
+    checkError("function foo already exists",
+        "def foo():",
+        "  return 1",
+        "def foo(x, y):",
+        "  return 1");
+  }
+
+  public void testDynamicTypeCheck() throws Exception {
+    checkError("bad variable 'a': string is incompatible with int at /some/file.txt:2:3",
+        "def foo():",
+        "  a = 1",
+        "  a = '1'");
+  }
+
+  public void testFunctionLocalVariable() throws Exception {
+    checkError("name 'a' is not defined",
+        "def func2(b):",
+        "  c = b",
+        "  c = a",
+        "def func1():",
+        "  a = 1",
+        "  func2(2)");
+  }
+
+  public void testFunctionLocalVariableDoesNotEffectGlobalValidationEnv() throws Exception {
+    checkError("name 'a' is not defined",
+        "def func1():",
+        "  a = 1",
+        "def func2(b):",
+        "  b = a");
+  }
+
+  public void testFunctionParameterDoesNotEffectGlobalValidationEnv() throws Exception {
+    checkError("name 'a' is not defined",
+        "def func1(a):",
+        "  return a",
+        "def func2():",
+        "  b = a");
+  }
+
+  public void testLocalValidationEnvironmentsAreSeparated() throws Exception {
+    parse(
+          "def func1():\n"
+        + "  a = 1\n"
+        + "def func2():\n"
+        + "  a = 'abc'\n");
+  }
+
+  public void testListComprehensionNotIterable() throws Exception {
+    checkError("type 'int' is not iterable",
+        "[i for i in 1 for j in [2]]");
+  }
+
+  public void testListComprehensionNotIterable2() throws Exception {
+    checkError("type 'int' is not iterable",
+        "[i for i in [1] for j in 123]");
+  }
+
+  public void testListIsNotComparable() {
+    checkError("list is not comparable", "['a'] > 1");
+  }
+
+  public void testStringCompareToInt() {
+    checkError("bad comparison: int is incompatible with string", "'a' > 1");
+  }
+
+  public void testInOnInt() {
+    checkError("operand 'in' only works on strings, dictionaries, "
+        + "lists, sets or tuples, not on a(n) int", "1 in 2");
+  }
+
+  public void testUnsupportedOperator() {
+    checkError("unsupported operand type(s) for -: 'string' and 'int'", "'a' - 1");
+  }
+
+  public void testBuiltinSymbolsAreReadOnly() throws Exception {
+    checkError("Variable rule is read only", "rule = 1");
+  }
+
+  public void testSkylarkGlobalVariablesAreReadonly() throws Exception {
+    checkError("Variable a is read only",
+        "a = 1\n"
+        + "a = 2");
+  }
+
+  public void testFunctionDefRecursion() throws Exception {
+    checkError("function 'func' does not exist",
+        "def func():\n"
+      + "  func()\n");
+  }
+
+  public void testMutualRecursion() throws Exception {
+    checkError("function 'bar' does not exist",
+        "def foo(i):\n"
+      + "  bar(i)\n"
+      + "def bar(i):\n"
+      + "  foo(i)\n"
+      + "foo(4)");
+  }
+
+  public void testFunctionReturnValue() {
+    checkError("unsupported operand type(s) for +: 'int' and 'string'",
+          "def foo(): return 1\n"
+        + "a = foo() + 'a'\n");
+  }
+
+  public void testFunctionReturnValueInFunctionDef() {
+    checkError("unsupported operand type(s) for +: 'int' and 'string'",
+          "def foo(): return 1\n"
+        + "def bar(): a = foo() + 'a'\n");
+  }
+
+  public void testFunctionDoesNotExistInFunctionDef() {
+    checkError("function 'foo' does not exist",
+          "def bar(): a = foo() + 'a'\n"
+        + "def foo(): return 1\n");
+  }
+
+  public void testStructMembersAreImmutable() {
+    checkError("can only assign to variables, not to 's.x'",
+        "s = struct(x = 'a')\n"
+      + "s.x = 'b'\n");
+  }
+
+  public void testStructDictMembersAreImmutable() {
+    checkError("can only assign to variables, not to 's.x['b']'",
+        "s = struct(x = {'a' : 1})\n"
+      + "s.x['b'] = 2\n");
+  }
+
+  public void testTupleAssign() throws Exception {
+    checkError("unsupported operand type(s) for +: 'list' and 'dict'",
+        "d = (1, 2)\n"
+      + "d[0] = 2\n");
+  }
+
+  public void testAssignOnNonCollection() throws Exception {
+    checkError("unsupported operand type(s) for +: 'string' and 'dict'",
+        "d = 'abc'\n"
+      + "d[0] = 2");
+  }
+
+  public void testNsetBadRightOperand() throws Exception {
+    checkError("can only concatenate nested sets with other nested sets or list of items, "
+        + "not 'string'", "set() + 'a'");
+  }
+
+  public void testNsetBadItemType() throws Exception {
+    checkError("bad nested set: incompatible generic variable types int with string",
+        "(set() + ['a']) + [1]");
+  }
+
+  public void testNsetBadNestedItemType() throws Exception {
+    checkError("bad nested set: incompatible generic variable types int with string",
+        "(set() + ['b']) + (set() + [1])");
+  }
+
+  public void testTypeInferenceForMethodLibraryFunction() throws Exception {
+    checkError("bad variable 'l': string is incompatible with int at /some/file.txt:2:3",
+          "def foo():\n"
+        + "  l = len('abc')\n"
+        + "  l = 'a'");
+  }
+
+  public void testListLiteralBadTypes() throws Exception {
+    checkError("bad list literal: int is incompatible with string at /some/file.txt:1:1",
+        "['a', 1]");
+  }
+
+  public void testTupleLiteralWorksForDifferentTypes() throws Exception {
+    parse("('a', 1)");
+  }
+
+  public void testDictLiteralBadKeyTypes() throws Exception {
+    checkError("bad dict literal: int is incompatible with string at /some/file.txt:1:1",
+        "{'a': 1, 1: 2}");
+  }
+
+  public void testDictLiteralDifferentValueTypeWorks() throws Exception {
+    parse("{'a': 1, 'b': 'c'}");
+  }
+
+  public void testListConcatBadTypes() throws Exception {
+    checkError("bad list concatenation: incompatible generic variable types int with string",
+        "['a'] + [1]");
+  }
+
+  public void testDictConcatBadKeyTypes() throws Exception {
+    checkError("bad dict concatenation: incompatible generic variable types int with string",
+        "{'a': 1} + {1: 2}");
+  }
+
+  public void testDictLiteralBadKeyType() throws Exception {
+    checkError("Dict cannot contain composite type 'list' as key", "{['a']: 1}");
+  }
+
+  public void testAndTypeInfer() throws Exception {
+    checkError("unsupported operand type(s) for +: 'string' and 'int'", "('a' and 'b') + 1");
+  }
+
+  public void testOrTypeInfer() throws Exception {
+    checkError("unsupported operand type(s) for +: 'string' and 'int'", "('' or 'b') + 1");
+  }
+
+  public void testAndDifferentTypes() throws Exception {
+    checkError("bad and operator: int is incompatible with string at /some/file.txt:1:1",
+        "'ab' and 3");
+  }
+
+  public void testOrDifferentTypes() throws Exception {
+    checkError("bad or operator: int is incompatible with string at /some/file.txt:1:1",
+        "'ab' or 3");
+  }
+
+  public void testOrNone() throws Exception {
+    parse("a = None or 3");
+  }
+
+  public void testNoneAssignment() throws Exception {
+    parse("def func():\n"
+        + "  a = None\n"
+        + "  a = 2\n"
+        + "  a = None\n");
+  }
+
+  public void testNoneAssignmentError() throws Exception {
+    checkError("bad variable 'a': string is incompatible with int at /some/file.txt",
+          "def func():\n"
+        + "  a = None\n"
+        + "  a = 2\n"
+        + "  a = None\n"
+        + "  a = 'b'\n");
+  }
+
+  public void testDictComprehensionNotOnList() throws Exception {
+    checkError("Dict comprehension elements must be a list", "{k : k for k in 'abc'}");
+  }
+
+  public void testTypeInferenceForUserDefinedFunction() throws Exception {
+    checkError("bad variable 'a': string is incompatible with int at /some/file.txt",
+          "def func():\n"
+        + "  return 'a'\n"
+        + "def foo():\n"
+        + "  a = 1\n"
+        + "  a = func()\n");
+  }
+
+  public void testCallingNonFunction() {
+    checkError("a is not a function",
+        "a = '1':\n"
+      + "a()\n");
+  }
+
+  public void testFuncallArgument() {
+    checkError("unsupported operand type(s) for +: 'int' and 'string'",
+        "def foo(x): return x\n"
+      + "a = foo(1 + 'a')");
+  }
+
+  // Skylark built-in functions specific tests
+
+  public void testTypeInferenceForSkylarkBuiltinGlobalFunction() throws Exception {
+    checkError("bad variable 'a': string is incompatible with function at /some/file.txt:3:3",
+          "def impl(ctx): return None\n"
+        + "def foo():\n"
+        + "  a = rule(impl)\n"
+        + "  a = 'a'\n");
+  }
+
+  public void testTypeInferenceForSkylarkBuiltinObjectFunction() throws Exception {
+    checkError("bad variable 'a': string is incompatible with Attribute at /some/file.txt",
+        "def foo():\n"
+        + "  a = attr.int()\n"
+        + "  a = 'a'\n");
+  }
+
+  public void testFuncReturningDictAssignmentAsLValue() throws Exception {
+    checkError("can only assign to variables, not to 'dict([])['b']'",
+          "def dict():\n"
+        + "  return {'a': 1}\n"
+        + "def func():\n"
+        + "  dict()['b'] = 2\n"
+        + "  return d\n");
+  }
+
+  public void testListIndexAsLValue() {
+    checkError("unsupported operand type(s) for +: 'list' and 'dict'",
+        "def func():\n"
+      + "  l = [1]\n"
+      + "  l[0] = 2\n"
+      + "  return l\n");
+  }
+
+  public void testStringIndexAsLValue() {
+    checkError("unsupported operand type(s) for +: 'string' and 'dict'",
+        "def func():\n"
+      + "  s = 'abc'\n"
+      + "  s[0] = 'd'\n"
+      + "  return s\n");
+  }
+
+  public void testEmptyLiteralGenericIsSetInLaterConcatWorks() {
+    parse("def func():\n"
+        + "  s = {}\n"
+        + "  s['a'] = 'b'\n");
+  }
+
+  public void testTypeIsInferredForStructs() {
+    checkError("unsupported operand type(s) for +: 'struct' and 'string'",
+        "(struct(a = 1) + struct(b = 1)) + 'x'");
+  }
+
+  public void testReadOnlyWorksForSimpleBranching() {
+    parse("if 1:\n"
+        + "  v = 'a'\n"
+        + "else:\n"
+        + "  v = 'b'");
+  }
+
+  public void testReadOnlyWorksForNestedBranching() {
+    parse("if 1:\n"
+        + "  if 0:\n"
+        + "    v = 'a'\n"
+        + "  else:\n"
+        + "    v = 'b'\n"
+        + "else:\n"
+        + "  if 0:\n"
+        + "    v = 'c'\n"
+        + "  else:\n"
+        + "    v = 'd'\n");
+  }
+
+  public void testTypeCheckWorksForSimpleBranching() {
+    checkError("bad variable 'v': int is incompatible with string at /some/file.txt:2:3",
+          "if 1:\n"
+        + "  v = 'a'\n"
+        + "else:\n"
+        + "  v = 1");
+  }
+
+  public void testTypeCheckWorksForNestedBranching() {
+    checkError("bad variable 'v': int is incompatible with string at /some/file.txt:5:5",
+        "if 1:\n"
+      + "  v = 'a'\n"
+      + "else:\n"
+      + "  if 0:\n"
+      + "    v = 'b'\n"
+      + "  else:\n"
+      + "    v = 1\n");
+  }
+
+  public void testTypeCheckWorksForDifferentLevelBranches() {
+    checkError("bad variable 'v': int is incompatible with string at /some/file.txt:2:3",
+        "if 1:\n"
+      + "  v = 'a'\n"
+      + "else:\n"
+      + "  if 0:\n"
+      + "    v = 1\n");
+  }
+
+  public void testReadOnlyWorksForDifferentLevelBranches() {
+    checkError("Variable v is read only",
+        "if 1:\n"
+      + "  if 1:\n"
+      + "    v = 'a'\n"
+      + "  v = 'b'\n");
+  }
+
+  public void testReadOnlyWorksWithinSimpleBranch() {
+    checkError("Variable v is read only",
+        "if 1:\n"
+      + "  v = 'a'\n"
+      + "else:\n"
+      + "  v = 'b'\n"
+      + "  v = 'c'\n");
+  }
+
+  public void testReadOnlyWorksWithinNestedBranch() {
+    checkError("Variable v is read only",
+        "if 1:\n"
+      + "  v = 'a'\n"
+      + "else:\n"
+      + "  if 1:\n"
+      + "    v = 'b'\n"
+      + "  else:\n"
+      + "    v = 'c'\n"
+      + "    v = 'd'\n");
+  }
+
+  public void testReadOnlyWorksAfterSimpleBranch() {
+    checkError("Variable v is read only",
+        "if 1:\n"
+      + "  v = 'a'\n"
+      + "else:\n"
+      + "  w = 'a'\n"
+      + "v = 'b'");
+  }
+
+  public void testReadOnlyWorksAfterNestedBranch() {
+    checkError("Variable v is read only",
+        "if 1:\n"
+      + "  if 1:\n"
+      + "    v = 'a'\n"
+      + "v = 'b'");
+  }
+
+  public void testReadOnlyWorksAfterNestedBranch2() {
+    checkError("Variable v is read only",
+        "if 1:\n"
+      + "  v = 'a'\n"
+      + "else:\n"
+      + "  if 0:\n"
+      + "    w = 1\n"
+      + "v = 'b'\n");
+  }
+
+  public void testModulesReadOnlyInFuncDefBody() {
+    checkError("Variable cmd_helper is read only",
+        "def func():",
+        "  cmd_helper = set()");
+  }
+
+  public void testBuiltinGlobalFunctionsReadOnlyInFuncDefBody() {
+    checkError("Variable rule is read only",
+        "def func():",
+        "  rule = 'abc'");
+  }
+
+  public void testBuiltinGlobalFunctionsReadOnlyAsFuncDefArg() {
+    checkError("Variable rule is read only",
+        "def func(rule):",
+        "  return rule");
+  }
+
+  public void testFilesModulePlusStringErrorMessage() throws Exception {
+    checkError("unsupported operand type(s) for +: 'cmd_helper (a language module)' and 'string'",
+        "cmd_helper += 'a'");
+  }
+
+  public void testFunctionReturnsFunction() {
+    parse(
+        "def impl(ctx):",
+        "  return None",
+        "",
+        "skylark_rule = rule(implementation = impl)",
+        "",
+        "def macro(name):",
+        "  skylark_rule(name = name)");
+  }
+
+  public void testTypeForBooleanLiterals() {
+    parse("len([1, 2]) == 0 and True");
+    parse("len([1, 2]) == 0 and False");
+  }
+
+  public void testLoadRelativePathOneSegment() throws Exception {
+    parse("load('extension', 'a')\n");
+  }
+
+  public void testLoadAbsolutePathMultipleSegments() throws Exception {
+    parse("load('/pkg/extension', 'a')\n");
+  }
+
+  public void testLoadRelativePathMultipleSegments() throws Exception {
+    checkError("Path 'pkg/extension.bzl' is not valid. It should either start with "
+        + "a slash or refer to a file in the current directory.",
+        "load('pkg/extension', 'a')\n");
+  }
+
+  private void parse(String... lines) {
+    parseFileForSkylark(Joiner.on("\n").join(lines));
+    syntaxEvents.assertNoEvents();
+  }
+
+  private void checkError(String errorMsg, String... lines) {
+    syntaxEvents.setFailFast(false);
+    parseFileForSkylark(Joiner.on("\n").join(lines));
+    syntaxEvents.assertContainsEvent(errorMsg);
+  }
+}