bazel syntax: remove Location parameters

Remove Location parameter from SkylarkIndexable.getIndex
(and thread parameter from SkylarkQueryable).
Also, make SkylarkIndexable and SkylarkQueryable
consistently take a StarlarkSemantics.

Eval: don't pass Locations down into evaluation;
obtain them only after an error, on the way up.
(This is a prerequisite for compilation, and for
finer-grained syntax locations. This CL addresses only
the easy cases.)

Also:
- Dict.get2: avoid duplicate hash lookup in success case.
- EvalUtils.index: pass mutability and semantics, not thread
- remove unnecessary parameters to binary operators.
- improvements to error messages
PiperOrigin-RevId: 289523719
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/OutputGroupInfo.java b/src/main/java/com/google/devtools/build/lib/analysis/OutputGroupInfo.java
index c226c3a..a8c31f7 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/OutputGroupInfo.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/OutputGroupInfo.java
@@ -35,9 +35,10 @@
 import com.google.devtools.build.lib.syntax.Depset;
 import com.google.devtools.build.lib.syntax.Dict;
 import com.google.devtools.build.lib.syntax.EvalException;
-import com.google.devtools.build.lib.syntax.EvalUtils;
 import com.google.devtools.build.lib.syntax.SkylarkIndexable;
+import com.google.devtools.build.lib.syntax.Starlark;
 import com.google.devtools.build.lib.syntax.StarlarkIterable;
+import com.google.devtools.build.lib.syntax.StarlarkSemantics;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -240,25 +241,22 @@
   }
 
   @Override
-  public Object getIndex(Object key, Location loc) throws EvalException {
+  public Object getIndex(StarlarkSemantics semantics, Object key) throws EvalException {
     if (!(key instanceof String)) {
-      throw new EvalException(loc, String.format(
-          "Output grout names must be strings, got %s instead",
-          EvalUtils.getDataTypeName(key)));
+      throw Starlark.errorf(
+          "Output group names must be strings, got %s instead", Starlark.type(key));
     }
 
     NestedSet<Artifact> result = outputGroups.get(key);
     if (result != null) {
       return Depset.of(Artifact.TYPE, result);
     } else {
-      throw new EvalException(loc, String.format(
-          "Output group %s not present", key
-      ));
+      throw Starlark.errorf("Output group %s not present", key);
     }
   }
 
   @Override
-  public boolean containsKey(Object key, Location loc) throws EvalException {
+  public boolean containsKey(StarlarkSemantics semantics, Object key) throws EvalException {
     return outputGroups.containsKey(key);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ResolvedToolchainContext.java b/src/main/java/com/google/devtools/build/lib/analysis/ResolvedToolchainContext.java
index cf78c07..bf397b5 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/ResolvedToolchainContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ResolvedToolchainContext.java
@@ -27,15 +27,15 @@
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
-import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.rules.AliasConfiguredTarget;
 import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
 import com.google.devtools.build.lib.skyframe.ToolchainException;
 import com.google.devtools.build.lib.skyframe.UnloadedToolchainContext;
 import com.google.devtools.build.lib.skylarkbuildapi.platform.ToolchainContextApi;
 import com.google.devtools.build.lib.syntax.EvalException;
-import com.google.devtools.build.lib.syntax.EvalUtils;
 import com.google.devtools.build.lib.syntax.Printer;
+import com.google.devtools.build.lib.syntax.Starlark;
+import com.google.devtools.build.lib.syntax.StarlarkSemantics;
 import javax.annotation.Nullable;
 
 /**
@@ -157,8 +157,7 @@
   }
 
   private static Label transformKey(
-      Object key, Location loc, ImmutableMap<RepositoryName, RepositoryName> repoMapping)
-      throws EvalException {
+      Object key, ImmutableMap<RepositoryName, RepositoryName> repoMapping) throws EvalException {
     if (key instanceof Label) {
       return (Label) key;
     } else if (key instanceof ToolchainTypeInfo) {
@@ -167,43 +166,38 @@
       try {
         return Label.parseAbsolute((String) key, repoMapping);
       } catch (LabelSyntaxException e) {
-        throw new EvalException(
-            loc, String.format("Unable to parse toolchain %s: %s", key, e.getMessage()), e);
+        throw Starlark.errorf("Unable to parse toolchain %s: %s", key, e.getMessage());
       }
     } else {
-      throw new EvalException(
-          loc,
-          String.format(
-              "Toolchains only supports indexing by toolchain type, got %s instead",
-              EvalUtils.getDataTypeName(key)));
+      throw Starlark.errorf(
+          "Toolchains only supports indexing by toolchain type, got %s instead",
+          Starlark.type(key));
     }
   }
 
   @Override
-  public ToolchainInfo getIndex(Object key, Location loc) throws EvalException {
-    Label toolchainTypeLabel = transformKey(key, loc, repoMapping());
+  public ToolchainInfo getIndex(StarlarkSemantics semantics, Object key) throws EvalException {
+    Label toolchainTypeLabel = transformKey(key, repoMapping());
 
-    if (!containsKey(key, loc)) {
+    if (!containsKey(semantics, key)) {
       // TODO(bazel-configurability): The list of available toolchain types is confusing in the
       // presence of aliases, since it only contains the actual label, not the alias passed to the
       // rule definition.
-      throw new EvalException(
-          loc,
-          String.format(
-              "In %s, toolchain type %s was requested but only types [%s] are configured",
-              targetDescription(),
-              toolchainTypeLabel,
-              requiredToolchainTypes().stream()
-                  .map(ToolchainTypeInfo::typeLabel)
-                  .map(Label::toString)
-                  .collect(joining(", "))));
+      throw Starlark.errorf(
+          "In %s, toolchain type %s was requested but only types [%s] are configured",
+          targetDescription(),
+          toolchainTypeLabel,
+          requiredToolchainTypes().stream()
+              .map(ToolchainTypeInfo::typeLabel)
+              .map(Label::toString)
+              .collect(joining(", ")));
     }
     return forToolchainType(toolchainTypeLabel);
   }
 
   @Override
-  public boolean containsKey(Object key, Location loc) throws EvalException {
-    Label toolchainTypeLabel = transformKey(key, loc, repoMapping());
+  public boolean containsKey(StarlarkSemantics semantics, Object key) throws EvalException {
+    Label toolchainTypeLabel = transformKey(key, repoMapping());
     return requestedToolchainTypeLabels().containsKey(toolchainTypeLabel);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/configuredtargets/AbstractConfiguredTarget.java b/src/main/java/com/google/devtools/build/lib/analysis/configuredtargets/AbstractConfiguredTarget.java
index d92dd03..c1f9177 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/configuredtargets/AbstractConfiguredTarget.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/configuredtargets/AbstractConfiguredTarget.java
@@ -30,13 +30,11 @@
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.Order;
-import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.packages.Info;
 import com.google.devtools.build.lib.packages.PackageSpecification.PackageGroupContents;
 import com.google.devtools.build.lib.packages.Provider;
 import com.google.devtools.build.lib.skyframe.BuildConfigurationValue;
 import com.google.devtools.build.lib.syntax.EvalException;
-import com.google.devtools.build.lib.syntax.EvalUtils;
 import com.google.devtools.build.lib.syntax.Printer;
 import com.google.devtools.build.lib.syntax.SkylarkClassObject;
 import com.google.devtools.build.lib.syntax.Starlark;
@@ -147,32 +145,30 @@
   }
 
   @Override
-  public final Object getIndex(Object key, Location loc) throws EvalException {
+  public final Object getIndex(StarlarkSemantics semantics, Object key) throws EvalException {
     if (!(key instanceof Provider)) {
-      throw new EvalException(loc, String.format(
+      throw Starlark.errorf(
           "Type Target only supports indexing by object constructors, got %s instead",
-          EvalUtils.getDataTypeName(key)));
+          Starlark.type(key));
     }
     Provider constructor = (Provider) key;
     Object declaredProvider = get(constructor.getKey());
     if (declaredProvider != null) {
       return declaredProvider;
     }
-    throw new EvalException(
-        loc,
-        Starlark.format(
-            "%r%s doesn't contain declared provider '%s'",
-            this,
-            getRuleClassString().isEmpty() ? "" : " (rule '" + getRuleClassString() + "')",
-            constructor.getPrintableName()));
+    throw Starlark.errorf(
+        "%s%s doesn't contain declared provider '%s'",
+        Starlark.repr(this),
+        getRuleClassString().isEmpty() ? "" : " (rule '" + getRuleClassString() + "')",
+        constructor.getPrintableName());
   }
 
   @Override
-  public boolean containsKey(Object key, Location loc) throws EvalException {
+  public boolean containsKey(StarlarkSemantics semantics, Object key) throws EvalException {
     if (!(key instanceof Provider)) {
-      throw new EvalException(loc, String.format(
+      throw Starlark.errorf(
           "Type Target only supports querying by object constructors, got %s instead",
-          EvalUtils.getDataTypeName(key)));
+          Starlark.type(key));
     }
     return get(((Provider) key).getKey()) != null;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/platform/ConstraintCollection.java b/src/main/java/com/google/devtools/build/lib/analysis/platform/ConstraintCollection.java
index 4e40cd5..bcc1c75 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/platform/ConstraintCollection.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/platform/ConstraintCollection.java
@@ -29,7 +29,6 @@
 import com.google.common.collect.Streams;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
-import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization;
 import com.google.devtools.build.lib.skylarkbuildapi.platform.ConstraintCollectionApi;
@@ -39,6 +38,7 @@
 import com.google.devtools.build.lib.syntax.Sequence;
 import com.google.devtools.build.lib.syntax.Starlark;
 import com.google.devtools.build.lib.syntax.StarlarkList;
+import com.google.devtools.build.lib.syntax.StarlarkSemantics;
 import com.google.devtools.build.lib.util.Fingerprint;
 import java.util.Collection;
 import java.util.Map;
@@ -168,13 +168,11 @@
     return mismatchSettings.build();
   }
 
-  private ConstraintSettingInfo convertKey(Object key, Location loc) throws EvalException {
+  private static ConstraintSettingInfo convertKey(Object key) throws EvalException {
     if (!(key instanceof ConstraintSettingInfo)) {
-      throw new EvalException(
-          loc,
-          String.format(
-              "Constraint names must be platform_common.ConstraintSettingInfo, got %s instead",
-              EvalUtils.getDataTypeName(key)));
+      throw Starlark.errorf(
+          "Constraint names must be platform_common.ConstraintSettingInfo, got %s instead",
+          EvalUtils.getDataTypeName(key));
     }
 
     return (ConstraintSettingInfo) key;
@@ -242,15 +240,13 @@
   }
 
   @Override
-  public Object getIndex(Object key, Location loc) throws EvalException {
-    ConstraintSettingInfo constraint = convertKey(key, loc);
-    return get(constraint);
+  public Object getIndex(StarlarkSemantics semantics, Object key) throws EvalException {
+    return get(convertKey(key));
   }
 
   @Override
-  public boolean containsKey(Object key, Location loc) throws EvalException {
-    ConstraintSettingInfo constraint = convertKey(key, loc);
-    return has(constraint);
+  public boolean containsKey(StarlarkSemantics semantics, Object key) throws EvalException {
+    return has(convertKey(key));
   }
 
   // It's easier to use the Starlark repr as a string form, not what AutoValue produces.
diff --git a/src/main/java/com/google/devtools/build/lib/rules/AliasConfiguredTarget.java b/src/main/java/com/google/devtools/build/lib/rules/AliasConfiguredTarget.java
index 43996de..bfced9a 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/AliasConfiguredTarget.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/AliasConfiguredTarget.java
@@ -24,7 +24,6 @@
 import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
-import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.packages.Info;
 import com.google.devtools.build.lib.packages.Provider;
 import com.google.devtools.build.lib.skyframe.BuildConfigurationValue;
@@ -34,6 +33,7 @@
 import com.google.devtools.build.lib.syntax.Depset;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.Printer;
+import com.google.devtools.build.lib.syntax.StarlarkSemantics;
 import javax.annotation.Nullable;
 
 /**
@@ -109,13 +109,13 @@
   }
 
   @Override
-  public Object getIndex(Object key, Location loc) throws EvalException {
-    return actual.getIndex(key, loc);
+  public Object getIndex(StarlarkSemantics semantics, Object key) throws EvalException {
+    return actual.getIndex(semantics, key);
   }
 
   @Override
-  public boolean containsKey(Object key, Location loc) throws EvalException {
-    return actual.containsKey(key, loc);
+  public boolean containsKey(StarlarkSemantics semantics, Object key) throws EvalException {
+    return actual.containsKey(semantics, key);
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Dict.java b/src/main/java/com/google/devtools/build/lib/syntax/Dict.java
index b8db285..42e341e 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Dict.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Dict.java
@@ -152,9 +152,17 @@
   // SkylarkInterfaceUtils.getSkylarkCallable. The two 'get' methods cause it to get
   // confused as to which one has the annotation. Fix it and remove "2" suffix.
   public Object get2(Object key, Object defaultValue, StarlarkThread thread) throws EvalException {
-    if (containsKey(key, null, thread)) {
-      return this.get(key);
+    Object v = this.get(key);
+    if (v != null) {
+      return v;
     }
+
+    // This statement is executed for its effect, which is to throw "unhashable"
+    // if key is unhashable, instead of returning defaultValue.
+    // I think this is a bug: the correct behavior is simply 'return defaultValue'.
+    // See https://github.com/bazelbuild/starlark/issues/65.
+    containsKey(thread.getSemantics(), key);
+
     return defaultValue;
   }
 
@@ -508,21 +516,16 @@
   }
 
   @Override
-  public final Object getIndex(Object key, Location loc) throws EvalException {
-    if (!this.containsKey(key)) {
-      throw new EvalException(loc, Starlark.format("key %r not found in dictionary", key));
+  public Object getIndex(StarlarkSemantics semantics, Object key) throws EvalException {
+    Object v = get(key);
+    if (v == null) {
+      throw Starlark.errorf("key %s not found in dictionary", Starlark.repr(key));
     }
-    return this.get(key);
+    return v;
   }
 
   @Override
-  public final boolean containsKey(Object key, Location loc) throws EvalException {
-    return this.containsKey(key);
-  }
-
-  @Override
-  public final boolean containsKey(Object key, Location loc, StarlarkThread thread)
-      throws EvalException {
+  public boolean containsKey(StarlarkSemantics semantics, Object key) throws EvalException {
     EvalUtils.checkHashable(key);
     return this.containsKey(key);
   }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Eval.java b/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
index 8b8900d..fbe4ebb 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
@@ -98,8 +98,12 @@
       execAugmentedAssignment(node);
     } else {
       Object rvalue = eval(thread, node.getRHS());
-      // TODO(adonovan): use location of = operator.
-      assign(node.getLHS(), rvalue, thread, node.getStartLocation());
+      try {
+        assign(node.getLHS(), rvalue, thread);
+      } catch (EvalException ex) {
+        // TODO(adonovan): use location of = operator.
+        throw ex.ensureLocation(node.getStartLocation());
+      }
     }
   }
 
@@ -109,7 +113,7 @@
     EvalUtils.lock(o, node.getStartLocation());
     try {
       for (Object it : seq) {
-        assign(node.getLHS(), it, thread, node.getLHS().getStartLocation());
+        assign(node.getLHS(), it, thread);
 
         switch (execStatements(node.getBlock(), /*indented=*/ true)) {
           case PASS:
@@ -127,6 +131,8 @@
             throw new IllegalStateException("unreachable");
         }
       }
+    } catch (EvalException ex) {
+      throw ex.ensureLocation(node.getLHS().getStartLocation());
     } finally {
       EvalUtils.unlock(o, node.getStartLocation());
     }
@@ -269,22 +275,22 @@
 
   /**
    * Updates the environment bindings, and possibly mutates objects, so as to assign the given value
-   * to the given expression. The expression must be valid for an {@code LValue}.
+   * to the given expression. May throw an EvalException without location.
    */
-  private void assign(Expression expr, Object value, StarlarkThread thread, Location loc)
+  private void assign(Expression expr, Object value, StarlarkThread thread)
       throws EvalException, InterruptedException {
     if (expr instanceof Identifier) {
       assignIdentifier((Identifier) expr, value, thread);
     } else if (expr instanceof IndexExpression) {
       Object object = eval(thread, ((IndexExpression) expr).getObject());
       Object key = eval(thread, ((IndexExpression) expr).getKey());
-      assignItem(object, key, value, loc);
+      assignItem(object, key, value);
     } else if (expr instanceof ListExpression) {
       ListExpression list = (ListExpression) expr;
-      assignList(list.getElements(), value, thread, loc);
+      assignList(list.getElements(), value, thread);
     } else {
       // Not possible for validated ASTs.
-      throw new EvalException(loc, "cannot assign to '" + expr + "'");
+      throw Starlark.errorf("cannot assign to '%s'", expr);
     }
   }
 
@@ -325,58 +331,52 @@
    *
    * @throws EvalException if the object is not a list or dict
    */
-  @SuppressWarnings("unchecked")
-  private void assignItem(Object object, Object key, Object value, Location loc)
-      throws EvalException {
+  private static void assignItem(Object object, Object key, Object value) throws EvalException {
     if (object instanceof Dict) {
+      @SuppressWarnings("unchecked")
       Dict<Object, Object> dict = (Dict<Object, Object>) object;
-      dict.put(key, value, loc);
+      dict.put(key, value, /*loc=*/ null);
     } else if (object instanceof StarlarkList) {
+      @SuppressWarnings("unchecked")
       StarlarkList<Object> list = (StarlarkList<Object>) object;
       int index = Starlark.toInt(key, "list index");
-      index = EvalUtils.getSequenceIndex(index, list.size(), loc);
-      list.set(index, value, loc);
+      index = EvalUtils.getSequenceIndex(index, list.size());
+      list.set(index, value, /*loc=*/ null);
     } else {
-      throw new EvalException(
-          loc,
-          "can only assign an element in a dictionary or a list, not in a '"
-              + EvalUtils.getDataTypeName(object)
-              + "'");
+      throw Starlark.errorf(
+          "can only assign an element in a dictionary or a list, not in a '%s'",
+          Starlark.type(object));
     }
   }
 
   /**
-   * Recursively assigns an iterable value to a sequence of assignable expressions.
-   *
-   * @throws EvalException if the list literal has length 0, or if the value is not an iterable of
-   *     matching length
+   * Recursively assigns an iterable value to a sequence of assignable expressions. May throw an
+   * EvalException without location.
    */
-  private void assignList(List<Expression> lhs, Object x, StarlarkThread thread, Location loc)
+  private void assignList(List<Expression> lhs, Object x, StarlarkThread thread)
       throws EvalException, InterruptedException {
     // TODO(adonovan): lock/unlock rhs during iteration so that
     // 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 new EvalException(loc, "type '" + EvalUtils.getDataTypeName(x) + "' is not iterable");
+      throw Starlark.errorf("type '%s' is not iterable", Starlark.type(x));
     }
     Iterable<?> rhs = Starlark.toIterable(x); // fails if x is a string
     int len = lhs.size();
     if (len == 0) {
-      throw new EvalException(
-          loc, "lists or tuples on the left-hand side of assignments must have at least one item");
+      throw Starlark.errorf(
+          "lists or tuples on the left-hand side of assignments must have at least one item");
     }
     if (len != nrhs) {
-      throw new EvalException(
-          loc,
-          String.format(
-              "assignment length mismatch: left-hand side has length %d, but right-hand side"
-                  + " evaluates to value of length %d",
-              len, nrhs));
+      throw Starlark.errorf(
+          "assignment length mismatch: left-hand side has length %d, but right-hand side evaluates"
+              + " to value of length %d",
+          len, nrhs);
     }
     int i = 0;
     for (Object item : rhs) {
-      assign(lhs.get(i), item, thread, loc);
+      assign(lhs.get(i), item, thread);
       i++;
     }
   }
@@ -386,6 +386,8 @@
     Expression lhs = stmt.getLHS();
     TokenKind op = stmt.getOperator();
     Expression rhs = stmt.getRHS();
+    // TODO(adonovan): don't materialize Locations before an error has occurred.
+    // (Requires syntax tree to record offsets and defer Location conversion.)
     Location loc = stmt.getStartLocation(); // TODO(adonovan): use operator location
 
     if (lhs instanceof Identifier) {
@@ -399,11 +401,15 @@
       IndexExpression index = (IndexExpression) lhs;
       Object object = eval(thread, index.getObject());
       Object key = eval(thread, index.getKey());
-      Object x = EvalUtils.index(object, key, thread, loc);
+      Object x = EvalUtils.index(thread.mutability(), thread.getSemantics(), object, key);
       // Evaluate rhs after lhs.
       Object y = eval(thread, rhs);
       Object z = inplaceBinaryOp(op, x, y, thread, loc);
-      assignItem(object, key, z, loc);
+      try {
+        assignItem(object, key, z);
+      } catch (EvalException ex) {
+        throw ex.ensureLocation(loc);
+      }
     } else if (lhs instanceof ListExpression) {
       throw new EvalException(loc, "cannot perform augmented assignment on a list literal");
     } else {
@@ -414,7 +420,7 @@
 
   private static Object inplaceBinaryOp(
       TokenKind op, Object x, Object y, StarlarkThread thread, Location location)
-      throws EvalException, InterruptedException {
+      throws EvalException {
     // list += iterable  behaves like  list.extend(iterable)
     // TODO(b/141263526): following Python, allow list+=iterable (but not list+iterable).
     if (op == TokenKind.PLUS && x instanceof StarlarkList && y instanceof StarlarkList) {
@@ -507,11 +513,17 @@
             Object k = eval(thread, entry.getKey());
             Object v = eval(thread, entry.getValue());
             int before = dict.size();
-            Location loc = entry.getKey().getStartLocation(); // TODO(adonovan): use colon location
-            dict.put(k, v, loc);
+            try {
+              dict.put(k, v, /*loc=*/ null);
+            } catch (EvalException ex) {
+              // TODO(adonovan): use colon location
+              throw ex.ensureLocation(entry.getKey().getStartLocation());
+            }
             if (dict.size() == before) {
+              // TODO(adonovan): use colon location
               throw new EvalException(
-                  loc, "Duplicated key " + Starlark.repr(k) + " when creating dictionary");
+                  entry.getKey().getStartLocation(),
+                  "Duplicated key " + Starlark.repr(k) + " when creating dictionary");
             }
           }
           return dict;
@@ -582,7 +594,7 @@
             if (!(value instanceof StarlarkIterable)) {
               throw new EvalException(
                   star.getStartLocation(),
-                  "argument after * must be an iterable, not " + EvalUtils.getDataTypeName(value));
+                  "argument after * must be an iterable, not " + Starlark.type(value));
             }
             // TODO(adonovan): opt: if value.size is known, preallocate (and skip if empty).
             ArrayList<Object> list = new ArrayList<>();
@@ -597,7 +609,7 @@
             if (!(value instanceof Dict)) {
               throw new EvalException(
                   starstar.getStartLocation(),
-                  "argument after ** must be a dict, not " + EvalUtils.getDataTypeName(value));
+                  "argument after ** must be a dict, not " + Starlark.type(value));
             }
             Dict<?, ?> kwargs = (Dict<?, ?>) value;
             int j = named.length;
@@ -606,7 +618,7 @@
               if (!(e.getKey() instanceof String)) {
                 throw new EvalException(
                     starstar.getStartLocation(),
-                    "keywords must be strings, not " + EvalUtils.getDataTypeName(e.getKey()));
+                    "keywords must be strings, not " + Starlark.type(e.getKey()));
               }
               named[j++] = e.getKey();
               named[j++] = e.getValue();
@@ -673,8 +685,12 @@
           IndexExpression index = (IndexExpression) expr;
           Object object = eval(thread, index.getObject());
           Object key = eval(thread, index.getKey());
-          // TODO(adonovan): use location of lbracket token
-          return EvalUtils.index(object, key, thread, index.getStartLocation());
+          try {
+            return EvalUtils.index(thread.mutability(), thread.getSemantics(), object, key);
+          } catch (EvalException ex) {
+            // TODO(adonovan): use location of lbracket token
+            throw ex.ensureLocation(index.getStartLocation());
+          }
         }
 
       case INTEGER_LITERAL:
@@ -713,7 +729,11 @@
         {
           UnaryOperatorExpression unop = (UnaryOperatorExpression) expr;
           Object x = eval(thread, unop.getX());
-          return EvalUtils.unaryOp(unop.getOperator(), x, unop.getStartLocation());
+          try {
+            return EvalUtils.unaryOp(unop.getOperator(), x);
+          } catch (EvalException ex) {
+            throw ex.ensureLocation(unop.getStartLocation());
+          }
         }
     }
     throw new IllegalArgumentException("unexpected expression: " + expr.kind());
@@ -758,12 +778,15 @@
             Object iterable = eval(thread, forClause.getIterable());
             Location loc = comp.getStartLocation(); // TODO(adonovan): use location of 'for' token
             Iterable<?> listValue = Starlark.toIterable(iterable);
+            // TODO(adonovan): lock should not need loc.
             EvalUtils.lock(iterable, loc);
             try {
               for (Object elem : listValue) {
-                assign(forClause.getVars(), elem, thread, loc);
+                assign(forClause.getVars(), elem, thread);
                 execClauses(index + 1);
               }
+            } catch (EvalException ex) {
+              throw ex.ensureLocation(loc);
             } finally {
               EvalUtils.unlock(iterable, loc);
             }
@@ -783,7 +806,12 @@
           Object k = eval(thread, body.getKey());
           EvalUtils.checkHashable(k);
           Object v = eval(thread, body.getValue());
-          dict.put(k, v, comp.getStartLocation()); // TODO(adonovan): use colon location
+          try {
+            dict.put(k, v, /*loc=*/ null);
+          } catch (EvalException ex) {
+            // TODO(adonovan): use colon location
+            throw ex.ensureLocation(comp.getStartLocation());
+          }
         } else {
           list.add(eval(thread, ((Expression) comp.getBody())));
         }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
index 825110e..4137cb6 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
@@ -101,7 +101,7 @@
             return ((Comparable<Object>) o1).compareTo(o2);
           } catch (ClassCastException e) {
             throw new ComparisonException(
-                "Cannot compare " + getDataTypeName(o1) + " with " + getDataTypeName(o2));
+                "Cannot compare " + Starlark.type(o1) + " with " + Starlark.type(o2));
           }
         }
       };
@@ -116,7 +116,7 @@
       // Object.hashCode within a try/catch, and requiring all
       // unhashable Starlark values to throw a particular unchecked exception
       // with a helpful error message.
-      throw Starlark.errorf("unhashable type: '%s'", EvalUtils.getDataTypeName(x));
+      throw Starlark.errorf("unhashable type: '%s'", Starlark.type(x));
     }
   }
 
@@ -288,15 +288,14 @@
    * Resolves a positive or negative index to an index in the range [0, length), or throws
    * EvalException if it is out of range. If the index is negative, it counts backward from length.
    */
-  static int getSequenceIndex(int index, int length, Location loc) throws EvalException {
+  static int getSequenceIndex(int index, int length) throws EvalException {
     int actualIndex = index;
     if (actualIndex < 0) {
       actualIndex += length;
     }
     if (actualIndex < 0 || actualIndex >= length) {
-      throw new EvalException(
-          loc,
-          "index out of range (index is " + index + ", but sequence has " + length + " elements)");
+      throw Starlark.errorf(
+          "index out of range (index is %d, but sequence has %d elements)", index, length);
     }
     return actualIndex;
   }
@@ -380,49 +379,48 @@
       suffix = SpellChecker.didYouMean(name, CallUtils.getFieldNames(semantics, object));
     }
     return Starlark.errorf(
-        "'%s' value has no field or method '%s'%s", getDataTypeName(object), name, suffix);
+        "'%s' value has no field or method '%s'%s", Starlark.type(object), name, suffix);
   }
 
   /** Evaluates an eager binary operation, {@code x op y}. (Excludes AND and OR.) */
   static Object binaryOp(TokenKind op, Object x, Object y, StarlarkThread thread, Location location)
-      throws EvalException, InterruptedException {
+      throws EvalException {
     try {
       switch (op) {
         case PLUS:
           return plus(x, y, thread, location);
 
         case PIPE:
-          return pipe(x, y, thread, location);
+          return pipe(thread.getSemantics(), x, y);
 
         case AMPERSAND:
-          return and(x, y, location);
+          return and(x, y);
 
         case CARET:
-          return xor(x, y, location);
+          return xor(x, y);
 
         case GREATER_GREATER:
-          return rightShift(x, y, location);
+          return rightShift(x, y);
 
         case LESS_LESS:
-          return leftShift(x, y, location);
+          return leftShift(x, y);
 
         case MINUS:
-          return minus(x, y, location);
+          return minus(x, y);
 
         case STAR:
-          return mult(x, y, thread, location);
+          return star(thread.mutability(), x, y);
 
         case SLASH:
-          throw new EvalException(
-              location,
-              "The `/` operator is not allowed. Please use the `//` operator for integer "
-                  + "division.");
+          throw Starlark.errorf(
+              "The `/` operator is not allowed. Please use the `//` operator for integer"
+                  + " division.");
 
         case SLASH_SLASH:
-          return divide(x, y, location);
+          return slash(x, y);
 
         case PERCENT:
-          return percent(x, y, location);
+          return percent(x, y);
 
         case EQUALS_EQUALS:
           return x.equals(y);
@@ -431,41 +429,43 @@
           return !x.equals(y);
 
         case LESS:
-          return compare(x, y, location) < 0;
+          return compare(x, y) < 0;
 
         case LESS_EQUALS:
-          return compare(x, y, location) <= 0;
+          return compare(x, y) <= 0;
 
         case GREATER:
-          return compare(x, y, location) > 0;
+          return compare(x, y) > 0;
 
         case GREATER_EQUALS:
-          return compare(x, y, location) >= 0;
+          return compare(x, y) >= 0;
 
         case IN:
-          return in(x, y, thread, location);
+          return in(thread.getSemantics(), x, y);
 
         case NOT_IN:
-          return !in(x, y, thread, location);
+          return !in(thread.getSemantics(), x, y);
 
         default:
           throw new AssertionError("Unsupported binary operator: " + op);
       }
-    } catch (ArithmeticException e) {
-      throw new EvalException(location, e.getMessage());
+    } catch (ArithmeticException ex) {
+      throw new EvalException(null, ex.getMessage());
     }
   }
 
   /** Implements comparison operators. */
-  private static int compare(Object x, Object y, Location location) throws EvalException {
+  private static int compare(Object x, Object y) throws EvalException {
     try {
       return SKYLARK_COMPARATOR.compare(x, y);
     } catch (ComparisonException e) {
-      throw new EvalException(location, e);
+      throw new EvalException(null, e.getMessage());
     }
   }
 
   /** Implements 'x + y'. */
+  // StarlarkThread is needed for Mutability and Semantics.
+  // Location is exposed to Selector{List,Value} and Concatter.
   static Object plus(Object x, Object y, StarlarkThread thread, Location location)
       throws EvalException {
     // int + int
@@ -500,7 +500,7 @@
       if (concatter != null && concatter.equals(robj.getConcatter())) {
         return concatter.concat(lobj, robj, location);
       } else {
-        throw unknownBinaryOperator(x, y, TokenKind.PLUS, location);
+        throw unknownBinaryOperator(x, y, TokenKind.PLUS);
       }
     }
 
@@ -516,18 +516,16 @@
       }
       return Depset.unionOf((Depset) x, y);
     }
-    throw unknownBinaryOperator(x, y, TokenKind.PLUS, location);
+    throw unknownBinaryOperator(x, y, TokenKind.PLUS);
   }
 
   /** Implements 'x | y'. */
-  private static Object pipe(Object x, Object y, StarlarkThread thread, Location location)
-      throws EvalException {
+  private static Object pipe(StarlarkSemantics semantics, Object x, Object y) throws EvalException {
     if (x instanceof Integer && y instanceof Integer) {
       return ((Integer) x) | ((Integer) y);
     } else if (x instanceof Depset) {
-      if (thread.getSemantics().incompatibleDepsetUnion()) {
-        throw new EvalException(
-            location,
+      if (semantics.incompatibleDepsetUnion()) {
+        throw Starlark.errorf(
             "`|` operator on a depset is forbidden. See "
                 + "https://docs.bazel.build/versions/master/skylark/depsets.html for "
                 + "recommendations. Use --incompatible_depset_union=false "
@@ -535,20 +533,19 @@
       }
       return Depset.unionOf((Depset) x, y);
     }
-    throw unknownBinaryOperator(x, y, TokenKind.PIPE, location);
+    throw unknownBinaryOperator(x, y, TokenKind.PIPE);
   }
 
   /** Implements 'x - y'. */
-  private static Object minus(Object x, Object y, Location location) throws EvalException {
+  private static Object minus(Object x, Object y) throws EvalException {
     if (x instanceof Integer && y instanceof Integer) {
       return Math.subtractExact((Integer) x, (Integer) y);
     }
-    throw unknownBinaryOperator(x, y, TokenKind.MINUS, location);
+    throw unknownBinaryOperator(x, y, TokenKind.MINUS);
   }
 
   /** Implements 'x * y'. */
-  private static Object mult(Object x, Object y, StarlarkThread thread, Location location)
-      throws EvalException {
+  private static Object star(Mutability mu, Object x, Object y) throws EvalException {
     Integer number = null;
     Object otherFactor = null;
 
@@ -566,20 +563,20 @@
       } else if (otherFactor instanceof String) {
         return Strings.repeat((String) otherFactor, Math.max(0, number));
       } else if (otherFactor instanceof Tuple) {
-        return ((Tuple<?>) otherFactor).repeat(number, thread.mutability());
+        return ((Tuple<?>) otherFactor).repeat(number, mu);
       } else if (otherFactor instanceof StarlarkList) {
-        return ((StarlarkList<?>) otherFactor).repeat(number, thread.mutability());
+        return ((StarlarkList<?>) otherFactor).repeat(number, mu);
       }
     }
-    throw unknownBinaryOperator(x, y, TokenKind.STAR, location);
+    throw unknownBinaryOperator(x, y, TokenKind.STAR);
   }
 
   /** Implements 'x // y'. */
-  private static Object divide(Object x, Object y, Location location) throws EvalException {
+  private static Object slash(Object x, Object y) throws EvalException {
     // int / int
     if (x instanceof Integer && y instanceof Integer) {
       if (y.equals(0)) {
-        throw new EvalException(location, "integer division by zero");
+        throw Starlark.errorf("integer division by zero");
       }
       // Integer division doesn't give the same result in Java and in Python 2 with
       // negative numbers.
@@ -588,15 +585,15 @@
       // We want to follow Python semantics, so we use float division and round down.
       return (int) Math.floor(Double.valueOf((Integer) x) / (Integer) y);
     }
-    throw unknownBinaryOperator(x, y, TokenKind.SLASH_SLASH, location);
+    throw unknownBinaryOperator(x, y, TokenKind.SLASH_SLASH);
   }
 
   /** Implements 'x % y'. */
-  private static Object percent(Object x, Object y, Location location) throws EvalException {
+  private static Object percent(Object x, Object y) throws EvalException {
     // int % int
     if (x instanceof Integer && y instanceof Integer) {
       if (y.equals(0)) {
-        throw new EvalException(location, "integer modulo by zero");
+        throw Starlark.errorf("integer modulo by zero");
       }
       // Python and Java implement division differently, wrt negative numbers.
       // In Python, sign of the result is the sign of the divisor.
@@ -618,96 +615,84 @@
           return Starlark.formatWithList(pattern, (Tuple) y);
         }
         return Starlark.format(pattern, y);
-      } catch (IllegalFormatException e) {
-        throw new EvalException(location, e.getMessage());
+      } catch (IllegalFormatException ex) {
+        throw new EvalException(null, ex.getMessage());
       }
     }
-    throw unknownBinaryOperator(x, y, TokenKind.PERCENT, location);
+    throw unknownBinaryOperator(x, y, TokenKind.PERCENT);
   }
 
   /** Implements 'x & y'. */
-  private static Object and(Object x, Object y, Location location) throws EvalException {
+  private static Object and(Object x, Object y) throws EvalException {
     if (x instanceof Integer && y instanceof Integer) {
       return (Integer) x & (Integer) y;
     }
-    throw unknownBinaryOperator(x, y, TokenKind.AMPERSAND, location);
+    throw unknownBinaryOperator(x, y, TokenKind.AMPERSAND);
   }
 
   /** Implements 'x ^ y'. */
-  private static Object xor(Object x, Object y, Location location) throws EvalException {
+  private static Object xor(Object x, Object y) throws EvalException {
     if (x instanceof Integer && y instanceof Integer) {
       return (Integer) x ^ (Integer) y;
     }
-    throw unknownBinaryOperator(x, y, TokenKind.CARET, location);
+    throw unknownBinaryOperator(x, y, TokenKind.CARET);
   }
 
   /** Implements 'x >> y'. */
-  private static Object rightShift(Object x, Object y, Location location) throws EvalException {
+  private static Object rightShift(Object x, Object y) throws EvalException {
     if (x instanceof Integer && y instanceof Integer) {
       if ((Integer) y < 0) {
-        throw new EvalException(location, "negative shift count: " + y);
+        throw Starlark.errorf("negative shift count: %s", y);
       } else if ((Integer) y >= Integer.SIZE) {
         return ((Integer) x < 0) ? -1 : 0;
       }
       return (Integer) x >> (Integer) y;
     }
-    throw unknownBinaryOperator(x, y, TokenKind.GREATER_GREATER, location);
+    throw unknownBinaryOperator(x, y, TokenKind.GREATER_GREATER);
   }
 
   /** Implements 'x << y'. */
-  private static Object leftShift(Object x, Object y, Location location) throws EvalException {
+  private static Object leftShift(Object x, Object y) throws EvalException {
     if (x instanceof Integer && y instanceof Integer) {
       if ((Integer) y < 0) {
-        throw new EvalException(location, "negative shift count: " + y);
+        throw Starlark.errorf("negative shift count: %s", y);
       }
       Integer result = (Integer) x << (Integer) y;
-      if (!rightShift(result, y, location).equals(x)) {
+      if (!rightShift(result, y).equals(x)) {
         throw new ArithmeticException("integer overflow");
       }
       return result;
     }
-    throw unknownBinaryOperator(x, y, TokenKind.LESS_LESS, location);
+    throw unknownBinaryOperator(x, y, TokenKind.LESS_LESS);
   }
 
   /** Implements 'x in y'. */
-  private static boolean in(Object x, Object y, StarlarkThread thread, Location location)
-      throws EvalException {
+  private static boolean in(StarlarkSemantics semantics, Object x, Object y) throws EvalException {
     if (y instanceof SkylarkQueryable) {
-      return ((SkylarkQueryable) y).containsKey(x, location, thread);
+      return ((SkylarkQueryable) y).containsKey(semantics, x);
     } else if (y instanceof String) {
       if (x instanceof String) {
         return ((String) y).contains((String) x);
       } else {
-        throw new EvalException(
-            location,
-            "'in <string>' requires string as left operand, not '" + getDataTypeName(x) + "'");
+        throw Starlark.errorf(
+            "'in <string>' requires string as left operand, not '%s'", Starlark.type(x));
       }
     } else {
-      throw new EvalException(
-          location,
-          "argument of type '"
-              + getDataTypeName(y)
-              + "' is not iterable. "
-              + "in operator only works on lists, tuples, dicts and strings.");
+      throw Starlark.errorf(
+          "unsupported binary operation: %s in %s (right operand must be string, sequence, or"
+              + " dict)",
+          Starlark.type(x), Starlark.type(y));
     }
   }
 
   /** Returns an exception signifying incorrect types for the given operator. */
-  private static EvalException unknownBinaryOperator(
-      Object x, Object y, TokenKind op, Location location) {
-    // NB: this message format is identical to that used by CPython 2.7.6 or 3.4.0,
-    // though python raises a TypeError.
-    // TODO(adonovan): make error more concise: "unsupported binary op: list + int".
-    return new EvalException(
-        location,
-        String.format(
-            "unsupported operand type(s) for %s: '%s' and '%s'",
-            op, getDataTypeName(x), getDataTypeName(y)));
+  private static EvalException unknownBinaryOperator(Object x, Object y, TokenKind op) {
+    return Starlark.errorf(
+        "unsupported binary operation: %s %s %s", Starlark.type(x), op, Starlark.type(y));
   }
 
   /** Evaluates a unary operation. */
-  static Object unaryOp(TokenKind op, Object x, Location loc)
-      throws EvalException, InterruptedException {
+  static Object unaryOp(TokenKind op, Object x) throws EvalException {
     switch (op) {
       case NOT:
         return !Starlark.truth(x);
@@ -718,7 +703,7 @@
             return Math.negateExact((Integer) x);
           } catch (ArithmeticException e) {
             // Fails for -MIN_INT.
-            throw new EvalException(loc, e.getMessage());
+            throw new EvalException(null, e.getMessage());
           }
         }
         break;
@@ -738,8 +723,7 @@
       default:
         /* fall through */
     }
-    throw new EvalException(
-        loc, String.format("unsupported unary operation: %s%s", op, getDataTypeName(x)));
+    throw Starlark.errorf("unsupported unary operation: %s%s", op, Starlark.type(x));
   }
 
   /**
@@ -747,24 +731,22 @@
    *
    * @throws EvalException if {@code object} is not a sequence or mapping.
    */
-  static Object index(Object object, Object key, StarlarkThread thread, Location loc)
-      throws EvalException, InterruptedException {
+  static Object index(Mutability mu, StarlarkSemantics semantics, Object object, Object key)
+      throws EvalException {
     if (object instanceof SkylarkIndexable) {
-      Object result = ((SkylarkIndexable) object).getIndex(key, loc);
+      Object result = ((SkylarkIndexable) object).getIndex(semantics, key);
       // TODO(bazel-team): We shouldn't have this fromJava call here. If it's needed at all,
       // it should go in the implementations of SkylarkIndexable#getIndex that produce non-Skylark
       // values.
-      return result == null ? null : Starlark.fromJava(result, thread.mutability());
+      return result == null ? null : Starlark.fromJava(result, mu);
     } else if (object instanceof String) {
       String string = (String) object;
       int index = Starlark.toInt(key, "string index");
-      index = getSequenceIndex(index, string.length(), loc);
+      index = getSequenceIndex(index, string.length());
       return string.substring(index, index + 1);
     } else {
-      throw new EvalException(
-          loc,
-          String.format(
-              "type '%s' has no operator [](%s)", getDataTypeName(object), getDataTypeName(key)));
+      throw Starlark.errorf(
+          "type '%s' has no operator [](%s)", Starlark.type(object), Starlark.type(key));
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Sequence.java b/src/main/java/com/google/devtools/build/lib/syntax/Sequence.java
index 1b08d7a..d743ca0 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Sequence.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Sequence.java
@@ -15,7 +15,6 @@
 package com.google.devtools.build.lib.syntax;
 
 import com.google.common.collect.ImmutableList;
-import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory;
 import java.util.Collections;
@@ -53,21 +52,15 @@
     return ImmutableList.copyOf(this);
   }
 
-  /**
-   * Retrieve an entry from a Sequence.
-   *
-   * @param key the index
-   * @param loc a {@link Location} in case of error
-   * @throws EvalException if the key is invalid
-   */
+  /** Retrieves an entry from a Sequence. */
   @Override
-  default E getIndex(Object key, Location loc) throws EvalException {
+  default E getIndex(StarlarkSemantics semantics, Object key) throws EvalException {
     int index = Starlark.toInt(key, "sequence index");
-    return get(EvalUtils.getSequenceIndex(index, size(), loc));
+    return get(EvalUtils.getSequenceIndex(index, size()));
   }
 
   @Override
-  default boolean containsKey(Object key, Location loc) throws EvalException {
+  default boolean containsKey(StarlarkSemantics semantics, Object key) throws EvalException {
     return contains(key);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkIndexable.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkIndexable.java
index 8f67534..c27ad7e 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkIndexable.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkIndexable.java
@@ -14,13 +14,9 @@
 
 package com.google.devtools.build.lib.syntax;
 
-import com.google.devtools.build.lib.events.Location;
-
-/**
- * Skylark values that support index access, i.e. `object[key]`
- */
+/** A Starlark value that support indexed access, {@code object[key]}. */
 public interface SkylarkIndexable extends SkylarkQueryable {
 
   /** Returns the value associated with the given key. */
-  Object getIndex(Object key, Location loc) throws EvalException;
+  Object getIndex(StarlarkSemantics semantics, Object key) throws EvalException;
 }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkQueryable.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkQueryable.java
index 66d0bba..638a3f0 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkQueryable.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkQueryable.java
@@ -14,25 +14,12 @@
 
 package com.google.devtools.build.lib.syntax;
 
-import com.google.devtools.build.lib.events.Location;
-
-/**
- * Skylark values that support querying by other objects, i.e. `foo in object`. Semantics of the
- * operation may differ, i.e. dicts check for keys and lists for values.
- */
+/** A Starlark value that supports membership tests, {@code key in object}. */
 // TODO(adonovan): merge with SkylarkIndexable: no type supports 'x in y' without y[x],
 // and 'x in y' can be defined in terms of y[x], at least as a default implementation.
 // (Implementations of 'x in y' may choose to interpret failure of y[x] as false or a failure.)
 public interface SkylarkQueryable extends StarlarkValue {
 
   /** Returns whether the key is in the object. */
-  boolean containsKey(Object key, Location loc) throws EvalException;
-
-  // Variant used when called directly from a Starlark thread.
-  // This is a temporary workaround to enable --incompatible_disallow_dict_lookup_unhashable_keys.
-  // TODO(adonovan): remove when that flag is removed.
-  default boolean containsKey(Object key, Location loc, StarlarkThread thread)
-      throws EvalException {
-    return this.containsKey(key, loc);
-  }
+  boolean containsKey(StarlarkSemantics semantics, Object key) throws EvalException;
 }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/StarlarkList.java b/src/main/java/com/google/devtools/build/lib/syntax/StarlarkList.java
index 73d3366..83428d5 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/StarlarkList.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/StarlarkList.java
@@ -444,7 +444,7 @@
       })
   public Object pop(Object i) throws EvalException {
     int arg = i == Starlark.NONE ? -1 : (Integer) i;
-    int index = EvalUtils.getSequenceIndex(arg, size, /*loc=*/ null);
+    int index = EvalUtils.getSequenceIndex(arg, size);
     Object result = elems[index];
     remove(index, /*loc=*/ null);
     return result;
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/SingleToolchainResolutionFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/SingleToolchainResolutionFunctionTest.java
index 933956a..ee39c29 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/SingleToolchainResolutionFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/SingleToolchainResolutionFunctionTest.java
@@ -30,7 +30,6 @@
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.Order;
-import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.packages.Info;
 import com.google.devtools.build.lib.packages.NativeProvider;
 import com.google.devtools.build.lib.packages.Provider;
@@ -38,6 +37,7 @@
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.skyframe.util.SkyframeExecutorTestUtils;
 import com.google.devtools.build.lib.syntax.Printer;
+import com.google.devtools.build.lib.syntax.StarlarkSemantics;
 import com.google.devtools.build.skyframe.EvaluationResult;
 import com.google.devtools.build.skyframe.SkyKey;
 import javax.annotation.Nullable;
@@ -263,12 +263,12 @@
     public void repr(Printer printer) {}
 
     @Override
-    public Object getIndex(Object key, Location loc) {
+    public Object getIndex(StarlarkSemantics semantics, Object key) {
       return null;
     }
 
     @Override
-    public boolean containsKey(Object key, Location loc) {
+    public boolean containsKey(StarlarkSemantics semantics, Object key) {
       return false;
     }
   }
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java
index 34d8df9..3033f7e 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java
@@ -969,7 +969,7 @@
   public void testStructPlusArtifactErrorMessage() throws Exception {
     setRuleContext(createRuleContext("//foo:foo"));
     checkEvalErrorContains(
-        "unsupported operand type(s) for +: 'File' and 'struct'",
+        "unsupported binary operation: File + struct",
         "ruleContext.files.tools[0] + struct(a = 1)");
   }
 
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
index a5233db..236aea0 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
@@ -183,7 +183,7 @@
         .testExpression("123 + 456", 579)
         .testExpression("456 - 123", 333)
         .testExpression("8 % 3", 2)
-        .testIfErrorContains("unsupported operand type(s) for %: 'int' and 'string'", "3 % 'foo'")
+        .testIfErrorContains("unsupported binary operation: int % string", "3 % 'foo'")
         .testExpression("-5", -5)
         .testIfErrorContains("unsupported unary operation: -string", "-'foo'");
   }
@@ -341,7 +341,7 @@
         .testExpression("-7 // 2", -4)
         .testExpression("-7 // -2", 3)
         .testExpression("2147483647 // 2", 1073741823)
-        .testIfErrorContains("unsupported operand type(s) for //: 'string' and 'int'", "'str' // 2")
+        .testIfErrorContains("unsupported binary operation: string // int", "'str' // 2")
         .testIfExactError("integer division by zero", "5 // 0");
   }
 
@@ -385,8 +385,7 @@
     assertThat(x).isEqualTo(Tuple.of(1, 2, 3, 4));
     assertThat(EvalUtils.isImmutable(x)).isTrue();
 
-    checkEvalError("unsupported operand type(s) for +: 'tuple' and 'list'",
-        "(1,2) + [3,4]"); // list + tuple
+    checkEvalError("unsupported binary operation: tuple + list", "(1,2) + [3,4]");
   }
 
   @Test
@@ -572,10 +571,8 @@
     newTest()
         .testExpression("[1, 2] + [3, 4]", StarlarkList.of(thread.mutability(), 1, 2, 3, 4))
         .testExpression("(1, 2) + (3, 4)", Tuple.of(1, 2, 3, 4))
-        .testIfExactError(
-            "unsupported operand type(s) for +: 'list' and 'tuple'", "[1, 2] + (3, 4)")
-        .testIfExactError(
-            "unsupported operand type(s) for +: 'tuple' and 'list'", "(1, 2) + [3, 4]");
+        .testIfExactError("unsupported binary operation: list + tuple", "[1, 2] + (3, 4)")
+        .testIfExactError("unsupported binary operation: tuple + list", "(1, 2) + [3, 4]");
   }
 
   @Test
@@ -744,7 +741,8 @@
     newTest()
         .testIfErrorContains(
             "'in <string>' requires string as left operand, not 'int'", "1 in '123'")
-        .testIfErrorContains("'int' is not iterable. in operator only works on ", "'a' in 1");
+        .testIfErrorContains(
+            "unsupported binary operation: string in int (right operand must be", "'a' in 1");
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java b/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
index 176f82e..39b226e 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
@@ -97,7 +97,7 @@
                 + LINE_SEPARATOR
                 + "\t\ts += \"2\""
                 + LINE_SEPARATOR
-                + "unsupported operand type(s) for +: 'int' and 'string'",
+                + "unsupported binary operation: int + string",
             "def foo():",
             "  s = 1",
             "  s += '2'",
@@ -229,7 +229,7 @@
   @Test
   public void testBooleanUnsupportedOperationFails() throws Exception {
     new BothModesTest()
-        .testIfErrorContains("unsupported operand type(s) for +: 'bool' and 'bool'", "True + True");
+        .testIfErrorContains("unsupported binary operation: bool + bool", "True + True");
   }
 
   @Test
@@ -474,7 +474,7 @@
         .testExpression("repr(a)", "range(0, 3)")
         .testExpression("repr(range(1,2,3))", "range(1, 2, 3)")
         .testExpression("type(a)", "range")
-        .testIfErrorContains("unsupported operand type(s) for +: 'range' and 'range'", "a + a")
+        .testIfErrorContains("unsupported binary operation: range + range", "a + a")
         .testIfErrorContains("'range' value has no field or method 'append'", "a.append(3)")
         .testExpression("str(list(range(5)))", "[0, 1, 2, 3, 4]")
         .testExpression("str(list(range(0)))", "[]")
@@ -504,7 +504,7 @@
         .testExpression("str(range(0, 10, 2)[::-2])", "range(8, -2, -4)")
         .testExpression("str(range(5)[1::-1])", "range(1, -1, -1)")
         .testIfErrorContains("step cannot be 0", "range(2, 3, 0)")
-        .testIfErrorContains("unsupported operand type(s) for *: 'range' and 'int'", "range(3) * 3")
+        .testIfErrorContains("unsupported binary operation: range * int", "range(3) * 3")
         .testIfErrorContains("Cannot compare range objects", "range(3) < range(5)")
         .testIfErrorContains("Cannot compare range objects", "range(4) > [1]")
         .testExpression("4 in range(1, 10)", true)
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
index 10f8408..6beb2fd 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
@@ -1468,7 +1468,7 @@
     new SkylarkTest("--incompatible_depset_union=false")
         .testExpression("str(depset([1, 3]) | depset([1, 2]))", "depset([1, 2, 3])")
         .testExpression("str(depset([1, 2]) | [1, 3])", "depset([1, 2, 3])")
-        .testIfExactError("unsupported operand type(s) for |: 'int' and 'bool'", "2 | False");
+        .testIfExactError("unsupported binary operation: int | bool", "2 | False");
   }
 
   @Test
@@ -1476,7 +1476,8 @@
     new SkylarkTest()
         .testIfErrorContains("not iterable", "list(depset(['a', 'b']))")
         .testIfErrorContains("not iterable", "max(depset([1, 2, 3]))")
-        .testIfErrorContains("not iterable", "1 in depset([1, 2, 3])")
+        .testIfErrorContains(
+            "unsupported binary operation: int in depset", "1 in depset([1, 2, 3])")
         .testIfErrorContains("not iterable", "sorted(depset(['a', 'b']))")
         .testIfErrorContains("not iterable", "tuple(depset(['a', 'b']))")
         .testIfErrorContains("not iterable", "[x for x in depset()]")
@@ -1648,11 +1649,10 @@
   @Test
   public void testPlusOnDictDeprecated() throws Exception {
     new SkylarkTest()
-        .testIfErrorContains(
-            "unsupported operand type(s) for +: 'dict' and 'dict'", "{1: 2} + {3: 4}");
+        .testIfErrorContains("unsupported binary operation: dict + dict", "{1: 2} + {3: 4}");
     new SkylarkTest()
         .testIfErrorContains(
-            "unsupported operand type(s) for +: 'dict' and 'dict'",
+            "unsupported binary operation: dict + dict",
             "def func():",
             "  d = {1: 2}",
             "  d += {3: 4}",
@@ -1857,8 +1857,8 @@
 
   @Test
   public void testListAnTupleConcatenationDoesNotWorkInSkylark() throws Exception {
-    new SkylarkTest().testIfExactError("unsupported operand type(s) for +: 'list' and 'tuple'",
-        "[1, 2] + (3, 4)");
+    new SkylarkTest()
+        .testIfExactError("unsupported binary operation: list + tuple", "[1, 2] + (3, 4)");
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/StarlarkFileTest.java b/src/test/java/com/google/devtools/build/lib/syntax/StarlarkFileTest.java
index db95ec2..4c2a991 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/StarlarkFileTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/StarlarkFileTest.java
@@ -69,7 +69,7 @@
       EvalUtils.exec(file, thread);
       throw new AssertionError("execution succeeded unexpectedly");
     } catch (EvalException ex) {
-      assertThat(ex.getMessage()).contains("unsupported operand type(s) for +: 'int' and 'list'");
+      assertThat(ex.getMessage()).contains("unsupported binary operation: int + list");
       assertThat(ex.getLocation().line()).isEqualTo(4);
     }
   }
diff --git a/src/test/starlark/testdata/dict.sky b/src/test/starlark/testdata/dict.sky
index f2f73f0..8195d97 100644
--- a/src/test/starlark/testdata/dict.sky
+++ b/src/test/starlark/testdata/dict.sky
@@ -91,6 +91,10 @@
 assert_eq("a" in dict(a=1), True)
 ---
 
+# What's going on here? Functions _are_ hashable.
+# 'len in {}' and '{}.get(len, False)' should both successfully evaluate to False.
+# TODO(adonovan): clarify spec and fix this test (https://github.com/bazelbuild/starlark/issues/65)
+
 # unhashable types
 
 {} in {} ### unhashable type: 'dict'
diff --git a/src/test/starlark/testdata/int.sky b/src/test/starlark/testdata/int.sky
index d4710cf..ca3cc67 100644
--- a/src/test/starlark/testdata/int.sky
+++ b/src/test/starlark/testdata/int.sky
@@ -125,9 +125,9 @@
 assert_eq(~8 >> 1 | 3 ^ 4 & -2 << 2 * 3 + 4 // -2, -5)
 
 ---
-1 & False ### unsupported operand type(s) for &: 'int' and 'bool'
+1 & False ### unsupported binary operation: int & bool
 ---
-"a" ^ 5 ### unsupported operand type(s) for ^: 'string' and 'int'
+"a" ^ 5 ### unsupported binary operation: string ^ int
 ---
 ~False ### unsupported unary operation: ~bool
 ---