starlark: improve "unhashable type" error messages
StarlarkValue.isImmutable is now checkHashable.
Instead of returning a boolean,
it reports an informative error (or not).
Also, rename {EvalUtils->Starlark}.checkHashable and publish.
PiperOrigin-RevId: 336942245
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/starlark/Args.java b/src/main/java/com/google/devtools/build/lib/analysis/starlark/Args.java
index cd8e208..ba1fbbd 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/starlark/Args.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/starlark/Args.java
@@ -56,8 +56,9 @@
}
@Override
- public boolean isHashable() {
- return false; // even a frozen Args is not hashable
+ public void checkHashable() throws EvalException {
+ // Even a frozen Args is not hashable.
+ throw Starlark.errorf("unhashable type: '%s'", Starlark.type(this));
}
@Override
diff --git a/src/main/java/com/google/devtools/build/lib/packages/InternalModule.java b/src/main/java/com/google/devtools/build/lib/packages/InternalModule.java
index 3069e1a..0fe9715 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/InternalModule.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/InternalModule.java
@@ -46,9 +46,4 @@
public boolean isImmutable() {
return true;
}
-
- @Override
- public boolean isHashable() {
- return true;
- }
}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/StarlarkInfo.java b/src/main/java/com/google/devtools/build/lib/packages/StarlarkInfo.java
index d829fe9..0bfeb51 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/StarlarkInfo.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/StarlarkInfo.java
@@ -249,13 +249,9 @@
@Override
public boolean isImmutable() {
// If the provider is not yet exported, the hash code of the object is subject to change.
- // TODO(adonovan): implement isHashable?
if (!getProvider().isExported()) {
return false;
}
- // TODO(bazel-team): If we export at the end of a full module's evaluation, instead of at the
- // end of every top-level statement, then we can assume that exported implies frozen, and just
- // return true here without a traversal.
for (int i = table.length / 2; i < table.length; i++) {
if (!Starlark.isImmutable(table[i])) {
return false;
diff --git a/src/main/java/net/starlark/java/eval/Dict.java b/src/main/java/net/starlark/java/eval/Dict.java
index af75076..4d5c9c2 100644
--- a/src/main/java/net/starlark/java/eval/Dict.java
+++ b/src/main/java/net/starlark/java/eval/Dict.java
@@ -142,18 +142,19 @@
}
@Override
- public boolean isHashable() {
- return false; // even a frozen dict is unhashable
+ public void checkHashable() throws EvalException {
+ // Even a frozen dict is unhashable.
+ throw Starlark.errorf("unhashable type: 'dict'");
}
@Override
public int hashCode() {
- return contents.hashCode(); // not called by Dict.put (because !isHashable)
+ return contents.hashCode();
}
@Override
public boolean equals(Object o) {
- return contents.equals(o); // not called by Dict.put (because !isHashable)
+ return contents.equals(o);
}
@Override
@@ -419,7 +420,7 @@
*/
public void put(K key, V value, Location unused) throws EvalException {
Starlark.checkMutable(this);
- EvalUtils.checkHashable(key);
+ Starlark.checkHashable(key);
contents.put(key, value);
}
@@ -435,7 +436,7 @@
Starlark.checkMutable(this);
for (Map.Entry<KK, VV> e : map.entrySet()) {
KK k = e.getKey();
- EvalUtils.checkHashable(k);
+ Starlark.checkHashable(k);
contents.put(k, e.getValue());
}
}
@@ -530,7 +531,7 @@
@Override
public boolean containsKey(StarlarkSemantics semantics, Object key) throws EvalException {
- EvalUtils.checkHashable(key);
+ Starlark.checkHashable(key);
return this.containsKey(key);
}
diff --git a/src/main/java/net/starlark/java/eval/Eval.java b/src/main/java/net/starlark/java/eval/Eval.java
index 2b5779e..df67575 100644
--- a/src/main/java/net/starlark/java/eval/Eval.java
+++ b/src/main/java/net/starlark/java/eval/Eval.java
@@ -795,7 +795,7 @@
DictExpression.Entry body = (DictExpression.Entry) comp.getBody();
Object k = eval(fr, body.getKey());
try {
- EvalUtils.checkHashable(k);
+ Starlark.checkHashable(k);
Object v = eval(fr, body.getValue());
dict.put(k, v, (Location) null);
} catch (EvalException ex) {
diff --git a/src/main/java/net/starlark/java/eval/EvalUtils.java b/src/main/java/net/starlark/java/eval/EvalUtils.java
index 062548d..0dc2f10 100644
--- a/src/main/java/net/starlark/java/eval/EvalUtils.java
+++ b/src/main/java/net/starlark/java/eval/EvalUtils.java
@@ -95,43 +95,6 @@
}
};
- /** Throws EvalException if x is not hashable. */
- static void checkHashable(Object x) throws EvalException {
- if (!isHashable(x)) {
- // This results in confusing errors such as "unhashable type: tuple".
- // TODO(adonovan): ideally the error message would explain which
- // element of, say, a tuple is unhashable. The only practical way
- // to implement this is by implementing isHashable as a call to
- // 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'", Starlark.type(x));
- }
- }
-
- /**
- * Reports whether a legal Starlark value is considered hashable to Starlark, and thus suitable as
- * a key in a dict.
- */
- static boolean isHashable(Object o) {
- // Bazel makes widespread assumptions that all Starlark values can be hashed
- // by Java code, so we cannot implement isHashable by having
- // StarlarkValue.hashCode throw an unchecked exception, which would be more
- // efficient. Instead, before inserting a value in a dict, we must first ask
- // it whether it isHashable, and then call its hashCode method only if so.
- // For structs and tuples, this unfortunately visits the object graph twice.
- //
- // One subtlety: the struct.isHashable recursively asks whether its
- // elements are immutable, not hashable. Consequently, even though a list
- // may not be used as a dict key (even if frozen), a struct containing
- // a list is hashable. TODO(adonovan): fix this inconsistency.
- // Requires an incompatible change flag.
- if (o instanceof StarlarkValue) {
- return ((StarlarkValue) o).isHashable();
- }
- return Starlark.isImmutable(o);
- }
-
static void addIterator(Object x) {
if (x instanceof Mutability.Freezable) {
((Mutability.Freezable) x).updateIteratorCount(+1);
diff --git a/src/main/java/net/starlark/java/eval/Starlark.java b/src/main/java/net/starlark/java/eval/Starlark.java
index 1690f77..cc25353 100644
--- a/src/main/java/net/starlark/java/eval/Starlark.java
+++ b/src/main/java/net/starlark/java/eval/Starlark.java
@@ -141,6 +141,20 @@
}
/**
+ * Returns normally if the Starlark value is hashable and thus suitable as a dict key.
+ *
+ * @throws EvalException otherwise.
+ */
+ public static void checkHashable(Object x) throws EvalException {
+ if (x instanceof StarlarkValue) {
+ ((StarlarkValue) x).checkHashable();
+ } else {
+ Starlark.checkValid(x);
+ // String and Boolean are hashable.
+ }
+ }
+
+ /**
* Converts a Java value {@code x} to a Starlark one, if x is not already a valid Starlark value.
* An Integer, Long, or BigInteger is converted to a Starlark int, a Java List or Map is converted
* to a Starlark list or dict, respectively, and null becomes {@link #NONE}. Any other
diff --git a/src/main/java/net/starlark/java/eval/StarlarkList.java b/src/main/java/net/starlark/java/eval/StarlarkList.java
index 4fa3b88..c62ce38 100644
--- a/src/main/java/net/starlark/java/eval/StarlarkList.java
+++ b/src/main/java/net/starlark/java/eval/StarlarkList.java
@@ -79,8 +79,9 @@
}
@Override
- public boolean isHashable() {
- return false; // even a frozen list is unhashable in Starlark
+ public void checkHashable() throws EvalException {
+ // Even a frozen list is unhashable.
+ throw Starlark.errorf("unhashable type: 'list'");
}
@Override
diff --git a/src/main/java/net/starlark/java/eval/StarlarkValue.java b/src/main/java/net/starlark/java/eval/StarlarkValue.java
index 8241244..062ad10 100644
--- a/src/main/java/net/starlark/java/eval/StarlarkValue.java
+++ b/src/main/java/net/starlark/java/eval/StarlarkValue.java
@@ -72,8 +72,31 @@
return false;
}
- /** Reports whether the Starlark value is hashable and thus suitable as a dict key. */
- default boolean isHashable() {
- return this.isImmutable();
+ /**
+ * Returns normally if the Starlark value is hashable and thus suitable as a dict key.
+ *
+ * <p>(A StarlarkValue implementation may define hashCode and equals and thus be a valid
+ * java.util.Map key without being hashable by Starlark code.)
+ *
+ * @throws EvalException otherwise.
+ */
+ default void checkHashable() throws EvalException {
+ // Bazel makes widespread assumptions that all Starlark values can be hashed
+ // by Java code, so we cannot implement checkHashable by having
+ // StarlarkValue.hashCode throw an unchecked exception, which would be more
+ // efficient. Instead, before inserting a value in a dict, we must first check
+ // whether it is hashable by calling this function, and then call its hashCode
+ // method only if so.
+ // For structs and tuples, this unfortunately visits the object graph twice.
+ //
+ // One subtlety: Bazel's lib.packages.StarlarkInfo.checkHashable, by using this
+ // default implementation of checkHashable, which is based on isImmutable,
+ // recursively asks whether its elements are immutable, not hashable.
+ // Consequently, even though a list may not be used as a dict key (even if frozen),
+ // a struct containing a list is hashable.
+ // TODO(adonovan): fix this inconsistency. Requires a Bazel incompatible change.
+ if (!this.isImmutable()) {
+ throw Starlark.errorf("unhashable type: '%s'", Starlark.type(this));
+ }
}
}
diff --git a/src/main/java/net/starlark/java/eval/Tuple.java b/src/main/java/net/starlark/java/eval/Tuple.java
index 83b7f3b..ea3ea29 100644
--- a/src/main/java/net/starlark/java/eval/Tuple.java
+++ b/src/main/java/net/starlark/java/eval/Tuple.java
@@ -115,13 +115,10 @@
}
@Override
- public boolean isHashable() {
+ public void checkHashable() throws EvalException {
for (Object x : elems) {
- if (!EvalUtils.isHashable(x)) {
- return false;
- }
+ Starlark.checkHashable(x);
}
- return true;
}
@Override
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkIntegrationTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkIntegrationTest.java
index ac0aa23..7d1c58e 100644
--- a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkIntegrationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkIntegrationTest.java
@@ -3241,7 +3241,7 @@
reporter.removeHandler(failFastHandler);
getConfiguredTarget("//test:r");
- assertContainsEvent("unhashable type: 'tuple'");
+ assertContainsEvent("unhashable type: 'dict'");
}
@Test
diff --git a/src/test/java/net/starlark/java/eval/testdata/dict.sky b/src/test/java/net/starlark/java/eval/testdata/dict.sky
index 8195d97..c34bdef 100644
--- a/src/test/java/net/starlark/java/eval/testdata/dict.sky
+++ b/src/test/java/net/starlark/java/eval/testdata/dict.sky
@@ -108,3 +108,7 @@
dict().get({}) ### unhashable type: 'dict'
---
{1: 2}.get(len) ### unhashable type: 'function'
+---
+# For composite keys, the error message relates to the
+# unhashable subelement of the key, not the key itself.
+{(0, "", True, [0]): None} ### unhashable type: 'list'