starlark: improve error message for "for k,v in dict(...)"

Was: type 'string' is not iterable
Now: got 'string' in sequence assignment (want 2-element sequence)

Also:
- add test (testdata/loop.sky)
- migrate related tests from Java to loop.sky
- naming tweaks
PiperOrigin-RevId: 334874972
diff --git a/src/main/java/net/starlark/java/eval/Eval.java b/src/main/java/net/starlark/java/eval/Eval.java
index 14a8b87..2b5779e 100644
--- a/src/main/java/net/starlark/java/eval/Eval.java
+++ b/src/main/java/net/starlark/java/eval/Eval.java
@@ -355,11 +355,12 @@
     // assignments fail when the left side aliases the right,
     // which is a tricky case in Python assignment semantics.
     int nrhs = Starlark.len(x);
-    if (nrhs < 0) {
-      throw Starlark.errorf("got '%s' in sequence assignment", Starlark.type(x));
-    }
-    Iterable<?> rhs = Starlark.toIterable(x); // fails if x is a string
     int nlhs = lhs.size();
+    if (nrhs < 0 || x instanceof String) { // strings are not iterable
+      throw Starlark.errorf(
+          "got '%s' in sequence assignment (want %d-element sequence)", Starlark.type(x), nlhs);
+    }
+    Iterable<?> rhs = Starlark.toIterable(x);
     if (nlhs != nrhs) {
       throw Starlark.errorf(
           "too %s values to unpack (got %d, want %d)", nrhs < nlhs ? "few" : "many", nrhs, nlhs);
@@ -766,10 +767,10 @@
             Comprehension.For forClause = (Comprehension.For) clause;
 
             Object iterable = eval(fr, forClause.getIterable());
-            Iterable<?> listValue = Starlark.toIterable(iterable);
+            Iterable<?> seq = Starlark.toIterable(iterable);
             EvalUtils.addIterator(iterable);
             try {
-              for (Object elem : listValue) {
+              for (Object elem : seq) {
                 assign(fr, forClause.getVars(), elem);
                 execClauses(index + 1);
               }
diff --git a/src/main/java/net/starlark/java/syntax/ForStatement.java b/src/main/java/net/starlark/java/syntax/ForStatement.java
index 68cad7b..dffddf4 100644
--- a/src/main/java/net/starlark/java/syntax/ForStatement.java
+++ b/src/main/java/net/starlark/java/syntax/ForStatement.java
@@ -16,12 +16,12 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
-/** Syntax node for a for loop statement. */
+/** Syntax node for a for loop statement, {@code for vars in iterable: ...}. */
 public final class ForStatement extends Statement {
 
   private final int forOffset;
   private final Expression vars;
-  private final Expression collection;
+  private final Expression iterable;
   private final ImmutableList<Statement> body; // non-empty if well formed
 
   /** Constructs a for loop statement. */
@@ -29,26 +29,30 @@
       FileLocations locs,
       int forOffset,
       Expression vars,
-      Expression collection,
+      Expression iterable,
       ImmutableList<Statement> body) {
     super(locs);
     this.forOffset = forOffset;
     this.vars = Preconditions.checkNotNull(vars);
-    this.collection = Preconditions.checkNotNull(collection);
+    this.iterable = Preconditions.checkNotNull(iterable);
     this.body = body;
   }
 
+  /**
+   * Returns variables assigned by each iteration. May be a compound target such as {@code (a[b],
+   * c.d)}.
+   */
   public Expression getVars() {
     return vars;
   }
 
-  /**
-   * @return The collection we iterate on, e.g. `col` in `for x in col:`
-   */
+  /** Returns the iterable value. */
+  // TODO(adonovan): rename to getIterable.
   public Expression getCollection() {
-    return collection;
+    return iterable;
   }
 
+  /** Returns the statements of the loop body. Non-empty if parsing succeeded. */
   public ImmutableList<Statement> getBody() {
     return body;
   }
@@ -61,13 +65,13 @@
   @Override
   public int getEndOffset() {
     return body.isEmpty()
-        ? collection.getEndOffset() // wrong, but tree is ill formed
+        ? iterable.getEndOffset() // wrong, but tree is ill formed
         : body.get(body.size() - 1).getEndOffset();
   }
 
   @Override
   public String toString() {
-    return "for " + vars + " in " + collection + ": ...\n";
+    return "for " + vars + " in " + iterable + ": ...\n";
   }
 
   @Override
diff --git a/src/test/java/net/starlark/java/eval/EvaluationTest.java b/src/test/java/net/starlark/java/eval/EvaluationTest.java
index f1c47f8..e397239 100644
--- a/src/test/java/net/starlark/java/eval/EvaluationTest.java
+++ b/src/test/java/net/starlark/java/eval/EvaluationTest.java
@@ -401,168 +401,6 @@
   }
 
   @Test
-  public void testListComprehensions() throws Exception {
-    ev.new Scenario()
-        .testExactOrder("['foo/%s.java' % x for x in []]")
-        .testExactOrder(
-            "['foo/%s.java' % y for y in ['bar', 'wiz', 'quux']]",
-            "foo/bar.java", "foo/wiz.java", "foo/quux.java")
-        .testExactOrder(
-            "['%s/%s.java' % (z, t) for z in ['foo', 'bar'] " + "for t in ['baz', 'wiz', 'quux']]",
-            "foo/baz.java",
-            "foo/wiz.java",
-            "foo/quux.java",
-            "bar/baz.java",
-            "bar/wiz.java",
-            "bar/quux.java")
-        .testExactOrder(
-            "['%s/%s.java' % (b, b) for a in ['foo', 'bar'] " + "for b in ['baz', 'wiz', 'quux']]",
-            "baz/baz.java",
-            "wiz/wiz.java",
-            "quux/quux.java",
-            "baz/baz.java",
-            "wiz/wiz.java",
-            "quux/quux.java")
-        .testExactOrder(
-            "['%s/%s.%s' % (c, d, e) for c in ['foo', 'bar'] "
-                + "for d in ['baz', 'wiz', 'quux'] for e in ['java', 'cc']]",
-            "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")
-        .testExactOrder("[i for i in (1, 2)]", StarlarkInt.of(1), StarlarkInt.of(2))
-        .testExactOrder("[i for i in [2, 3] or [1, 2]]", StarlarkInt.of(2), StarlarkInt.of(3));
-  }
-
-  @Test
-  public void testNestedListComprehensions() throws Exception {
-    ev.new Scenario()
-        .setUp("li = [[1, 2], [3, 4]]")
-        .testExactOrder(
-            "[j for i in li for j in i]",
-            StarlarkInt.of(1),
-            StarlarkInt.of(2),
-            StarlarkInt.of(3),
-            StarlarkInt.of(4));
-    ev.new Scenario()
-        .setUp("input = [['abc'], ['def', 'ghi']]\n")
-        .testExactOrder(
-            "['%s %s' % (b, c) for a in input for b in a for c in b.elems()]",
-            "abc a", "abc b", "abc c", "def d", "def e", "def f", "ghi g", "ghi h", "ghi i");
-  }
-
-  @Test
-  public void testListComprehensionsMultipleVariables() throws Exception {
-    ev.new Scenario()
-        .testEval("[x + y for x, y in [(1, 2), (3, 4)]]", "[3, 7]")
-        .testEval("[z + t for (z, t) in [[1, 2], [3, 4]]]", "[3, 7]");
-  }
-
-  @Test
-  public void testSequenceAssignment() throws Exception {
-    // Assignment to empty list/tuple is permitted.
-    // See https://github.com/bazelbuild/starlark/issues/93 for discussion.
-    ev.exec("() = ()");
-    ev.exec("[] = ()");
-
-    // RHS not iterable
-    ev.checkEvalError(
-        "got 'int' in sequence assignment", //
-        "x, y = 1");
-    ev.checkEvalError(
-        "got 'int' in sequence assignment", //
-        "(x,) = 1");
-    ev.checkEvalError(
-        "got 'int' in sequence assignment", //
-        "[x] = 1");
-
-    // too few
-    ev.checkEvalError(
-        "too few values to unpack (got 0, want 2)", //
-        "x, y = ()");
-    ev.checkEvalError(
-        "too few values to unpack (got 0, want 2)", //
-        "[x, y] = ()");
-
-    // just right
-    ev.exec("x, y = 1, 2");
-    ev.exec("[x, y] = 1, 2");
-    ev.exec("(x,) = [1]");
-
-    // too many
-    ev.checkEvalError(
-        "got 'int' in sequence assignment", //
-        "() = 1");
-    ev.checkEvalError(
-        "too many values to unpack (got 1, want 0)", //
-        "() = (1,)");
-    ev.checkEvalError(
-        "too many values to unpack (got 3, want 2)", //
-        "x, y = 1, 2, 3");
-    ev.checkEvalError(
-        "too many values to unpack (got 3, want 2)", //
-        "[x, y] = 1, 2, 3");
-  }
-
-  @Test
-  public void testListComprehensionsMultipleVariablesFail() throws Exception {
-    ev.new Scenario()
-        .testIfErrorContains(
-            "too few values to unpack (got 2, want 3)", //
-            "[x + y for x, y, z in [(1, 2), (3, 4)]]")
-        .testIfExactError(
-            "got 'int' in sequence assignment", //
-            "[x + y for x, y in (1, 2)]");
-
-    ev.new Scenario()
-        .testIfErrorContains(
-            "too few values to unpack (got 2, want 3)", //
-            "def foo (): return [x + y for x, y, z in [(1, 2), (3, 4)]]",
-            "foo()");
-
-    ev.new Scenario()
-        .testIfErrorContains(
-            "got 'int' in sequence assignment", //
-            "def bar (): return [x + y for x, y in (1, 2)]",
-            "bar()");
-
-    ev.new Scenario()
-        .testIfErrorContains(
-            "too few values to unpack (got 2, want 3)", //
-            "[x + y for x, y, z in [(1, 2), (3, 4)]]");
-
-    ev.new Scenario()
-        .testIfErrorContains(
-            "got 'int' in sequence assignment", //
-            "[x2 + y2 for x2, y2 in (1, 2)]");
-
-    // Assignment to empty tuple is permitted.
-    // See https://github.com/bazelbuild/starlark/issues/93 for discussion.
-    ev.new Scenario().testEval("[1 for [] in [(), []]]", "[1, 1]");
-  }
-
-  @Test
-  public void testListComprehensionsWithFiltering() throws Exception {
-    ev.new Scenario()
-        .setUp("range3 = [0, 1, 2]")
-        .testEval("[a for a in (4, None, 2, None, 1) if a != None]", "[4, 2, 1]")
-        .testEval("[b+c for b in [0, 1, 2] for c in [0, 1, 2] if b + c > 2]", "[3, 3, 4]")
-        .testEval("[d+e for d in range3 if d % 2 == 1 for e in range3]", "[1, 2, 3]")
-        .testEval(
-            "[[f,g] for f in [0, 1, 2, 3, 4] if f for g in [5, 6, 7, 8] if f * g % 12 == 0 ]",
-            "[[2, 6], [3, 8], [4, 6]]")
-        .testEval("[h for h in [4, 2, 0, 1] if h]", "[4, 2, 1]");
-  }
-
-  @Test
   public void testListComprehensionDefinitionOrder() throws Exception {
     // This exercises the .bzl file behavior. This is a dynamic error.
     // (The error message for BUILD files is slightly different (no "local")
diff --git a/src/test/java/net/starlark/java/eval/testdata/loop.sky b/src/test/java/net/starlark/java/eval/testdata/loop.sky
new file mode 100644
index 0000000..1ef38c1
--- /dev/null
+++ b/src/test/java/net/starlark/java/eval/testdata/loop.sky
@@ -0,0 +1,150 @@
+# for statements and list comprehensions
+
+# --- for statements ---
+
+# sequence assignment
+
+# Assignment to empty list/tuple is permitted.
+# https://github.com/bazelbuild/starlark/issues/93 for discussion.
+() = ()
+[] = ()
+
+# RHS not iterable
+x, y = 1  ### got 'int' in sequence assignment
+---
+(x,) = 1 ### got 'int' in sequence assignment
+---
+[x] = 1 ### got 'int' in sequence assignment
+---
+
+# too few
+x, y = () ### too few values to unpack (got 0, want 2)
+---
+[x, y] = () ### too few values to unpack (got 0, want 2)
+---
+
+# just right
+x, y = 1, 2
+---
+[x, y] = 1, 2
+---
+(x,) = [1]
+---
+
+# too many
+() = 1 ### got 'int' in sequence assignment
+---
+() = (1,) ### too many values to unpack (got 1, want 0)
+---
+x, y = 1, 2, 3 ### too many values to unpack (got 3, want 2)
+---
+[x, y] = 1, 2, 3 ### too many values to unpack (got 3, want 2)
+---
+
+# Assignment to empty tuple is permitted.
+# See https://github.com/bazelbuild/starlark/issues/93 for discussion.
+assert_eq([1 for [] in [(), []]], [1, 1])
+
+# Iterating over dict without .items() gives informative error.
+assert_eq([v for v in dict(a = "b")], ["a"])
+[None for () in dict(a = "b")]  ### got 'string' in sequence assignment (want 0-element sequence)
+---
+[None for (v1,) in dict(a = "b")] ### got 'string' in sequence assignment (want 1-element sequence)
+---
+[None for v1, v2 in dict(a = "b")] ### got 'string' in sequence assignment (want 2-element sequence)
+---
+
+# --- list comprehensions ---
+
+assert_eq(["foo/%s.java" % x for x in []], [])
+assert_eq(
+    ["foo/%s.java" % y for y in ["bar", "wiz", "quux"]],
+    ["foo/bar.java", "foo/wiz.java", "foo/quux.java"])
+assert_eq(
+    ["%s/%s.java" % (z, t)
+     for z in ["foo", "bar"]
+     for t in ["baz", "wiz", "quux"]],
+    ["foo/baz.java",
+     "foo/wiz.java",
+     "foo/quux.java",
+     "bar/baz.java",
+     "bar/wiz.java",
+     "bar/quux.java"])
+assert_eq(
+    ["%s/%s.java" % (b, b)
+     for a in ["foo", "bar"]
+     for b in ["baz", "wiz", "quux"]],
+    ["baz/baz.java",
+     "wiz/wiz.java",
+     "quux/quux.java",
+     "baz/baz.java",
+     "wiz/wiz.java",
+     "quux/quux.java"])
+assert_eq(
+    ["%s/%s.%s" % (c, d, e)
+     for c in ["foo", "bar"]
+     for d in ["baz", "wiz", "quux"]
+     for e in ["java", "cc"]],
+    ["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"])
+assert_eq([i for i in (1, 2)], [1,2])
+assert_eq([i for i in [2, 3] or [1, 2]], [2, 3])
+
+# nested list comprehensions
+li = [[1, 2], [3, 4]]
+assert_eq([j for i in li for j in i], [1,2,3,4])
+input = [["abc"], ["def", "ghi"]]
+assert_eq(
+    ["%s %s" % (b, c)
+     for a in input
+     for b in a
+     for c in b.elems()],
+    ["abc a", "abc b", "abc c", "def d", "def e", "def f", "ghi g", "ghi h", "ghi i"])
+
+# filtering
+range3 = [0, 1, 2]
+assert_eq([a for a in (4, None, 2, None, 1)
+           if a != None],
+          [4, 2, 1])
+assert_eq([b+c for b in [0, 1, 2]
+           for c in [0, 1, 2]
+           if b + c > 2],
+          [3, 3, 4])
+assert_eq([d+e for d in range3
+           if d % 2 == 1
+           for e in range3],
+          [1, 2, 3])
+assert_eq([[f, g] for f in [0, 1, 2, 3, 4]
+           if f
+           for g in [5, 6, 7, 8]
+           if f * g % 12 == 0],
+          [[2, 6], [3, 8], [4, 6]])
+assert_eq([h for h in [4, 2, 0, 1] if h], [4, 2, 1])
+
+# multiple variables, ok
+assert_eq([x + y for x, y in [(1, 2), (3, 4)]], [3, 7])
+assert_eq([z + t for (z, t) in [[1, 2], [3, 4]]], [3, 7])
+
+# multiple variables, fail
+[x + y for x, y, z in [(1, 2), (3, 4)]] ### too few values to unpack (got 2, want 3)
+---
+[x + y for x, y in (1, 2)] ### got 'int' in sequence assignment (want 2-element sequence)
+---
+[x + y for x, y, z in [(1, 2), (3, 4)]] ### too few values to unpack (got 2, want 3)
+---
+[x + y for x, y in (1, 2)] ### got 'int' in sequence assignment (want 2-element sequence)
+---
+[x + y for x, y, z in [(1, 2), (3, 4)]] ### too few values to unpack (got 2, want 3)
+---
+[x2 + y2 for x2, y2 in (1, 2)] ### got 'int' in sequence assignment (want 2-element sequence)
+---