Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reject hashing of frozen mutable types #8980

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions site/docs/skylark/backward-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Full, authorative list of incompatible changes is [GitHub issues with
General Starlark

* [Dictionary lookup of unhashable types](#dictionary-lookup-of-unhashable-types)
* [Frozen mutable types are no longer hashable](#frozen-mutable-types-are-no-longer-hashable)
* [Dictionary concatenation](#dictionary-concatenation)
* [String escapes](#string-escapes)
* [String.split empty separator](#string.split-empty-separator)
Expand Down Expand Up @@ -79,6 +80,15 @@ a dict will fail.
* Default: `false`
* Tracking issue: [#8730](https://github.com/bazelbuild/bazel/issues/8730)

### Frozen mutable types are no longer hashable

When this flag is enabled, mutable types (e.g. list, dict) do not become
hashable when frozen and thus cannot be used as dictionary keys.

* Flag: `--incompatible_disallow_hashing_frozen_mutables`
* Default: `false`
* Tracking issue: [#7800](https://github.com/bazelbuild/bazel/issues/7800)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi, I'm removing this page. Instead we just use GitHub issues to track the changes.

### Dictionary concatenation

We are removing the `+` operator on dictionaries. This includes the `+=` form
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,18 @@ public class StarlarkSemanticsOptions extends OptionsBase implements Serializabl
help = "If set to true, the default value of the `allow_empty` argument of glob() is False.")
public boolean incompatibleDisallowEmptyGlob;

@Option(
name = "incompatible_disallow_hashing_frozen_mutables",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.STARLARK_SEMANTICS,
effectTags = {OptionEffectTag.BUILD_FILE_SEMANTICS},
metadataTags = {
OptionMetadataTag.INCOMPATIBLE_CHANGE,
OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES
},
help = "If set to true, freezing a mutable object will not make it hashable.")
public boolean incompatibleDisallowHashingFrozenMutables;

@Option(
name = "incompatible_disallow_legacy_java_provider",
defaultValue = "false",
Expand Down Expand Up @@ -726,6 +738,7 @@ public StarlarkSemantics toSkylarkSemantics() {
.incompatibleDisablePartitionDefaultParameter(
incompatibleDisablePartitionDefaultParameter)
.incompatibleAllowTagsPropagation(incompatibleAllowTagsPropagation)
.incompatibleDisallowHashingFrozenMutables(incompatibleDisallowHashingFrozenMutables)
.build();
return INTERNER.intern(semantics);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,21 @@
public interface SkylarkValue extends SkylarkPrintable {

/**
* Returns if the value is immutable and thus suitable for being used as a dictionary key.
* Returns if the value is immutable.
*
* <p>Immutability is deep, i.e. in order for a value to be immutable, all values it is composed
* of must be immutable, too.
*/
default boolean isImmutable() {
return false;
}

/**
* Returns if the value is hashable and thus suitable for being used as a dictionary key.
*
* <p>Hashability implies immutability, but not vice versa.
*/
default boolean isHashable() {
return this.isImmutable();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public List<Clause> getClauses() {
@Override
public void evaluateAndCollect(Environment env) throws EvalException, InterruptedException {
Object key = keyExpression.eval(env);
EvalUtils.checkValidDictKey(key);
EvalUtils.checkValidDictKey(key, env);
result.put(key, valueExpression.eval(env), getLocation(), env);
}

Expand Down
20 changes: 18 additions & 2 deletions src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,31 @@ public int compare(Object o1, Object o2) {
* @param o an Object to validate
* @throws EvalException if o is not a valid key
*/
public static void checkValidDictKey(Object o) throws EvalException {
public static void checkValidDictKey(Object o, Environment env) throws EvalException {
// TODO(bazel-team): check that all recursive elements are both Immutable AND Comparable.
if (isImmutable(o)) {
if (env != null && env.getSemantics().incompatibleDisallowHashingFrozenMutables()) {
if (isHashable(o)) {
return;
}
} else if (isImmutable(o)) {
return;
}
// Same error message as Python (that makes it a TypeError).
throw new EvalException(null, Printer.format("unhashable type: '%r'", o.getClass()));
}

/**
* Is this object known or assumed to be recursively hashable by Skylark?
* @param o an Object
* @return true if the object is known to be a hashable value.
*/
public static boolean isHashable(Object o) {
if (o instanceof SkylarkValue) {
return ((SkylarkValue) o).isHashable();
}
return isImmutable(o.getClass());
}

/**
* Is this object known or assumed to be recursively immutable by Skylark?
* @param o an Object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ public Runtime.NoneType update(
SkylarkDict<K, V> dict =
(args instanceof SkylarkDict) ? (SkylarkDict<K, V>) args : getDictFromArgs(args, loc, env);
dict = SkylarkDict.plus(dict, (SkylarkDict<K, V>) kwargs, env);
putAll(dict, loc, env.mutability());
putAll(dict, loc, env.mutability(), env);
return Runtime.NONE;
}

Expand Down Expand Up @@ -355,7 +355,7 @@ protected Map<K, V> getContentsUnsafe() {
*/
public void put(K key, V value, Location loc, Mutability mutability) throws EvalException {
checkMutable(loc, mutability);
EvalUtils.checkValidDictKey(key);
EvalUtils.checkValidDictKey(key, null);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this function (put) is only used here: https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java#L178
However, I couldn't find a way to get environment here and passing null will just assume the flag is false.

contents.put(key, value);
}

Expand All @@ -365,7 +365,9 @@ public void put(K key, V value, Location loc, Mutability mutability) throws Eval
*/
// TODO(bazel-team): Decide whether to eliminate this overload.
public void put(K key, V value, Location loc, Environment env) throws EvalException {
put(key, value, loc, env.mutability());
checkMutable(loc, mutability);
EvalUtils.checkValidDictKey(key, env);
contents.put(key, value);
}

/**
Expand All @@ -377,11 +379,11 @@ public void put(K key, V value, Location loc, Environment env) throws EvalExcept
* @throws EvalException if some key is invalid or the dict is frozen
*/
public <KK extends K, VV extends V> void putAll(
Map<KK, VV> map, Location loc, Mutability mutability) throws EvalException {
Map<KK, VV> map, Location loc, Mutability mutability, Environment env) throws EvalException {
checkMutable(loc, mutability);
for (Map.Entry<KK, VV> e : map.entrySet()) {
KK k = e.getKey();
EvalUtils.checkValidDictKey(k);
EvalUtils.checkValidDictKey(k, env);
contents.put(k, e.getValue());
}
}
Expand Down Expand Up @@ -508,7 +510,7 @@ public final boolean containsKey(Object key, Location loc, StarlarkContext conte
@Override
public final boolean containsKey(Object key, Location loc, Environment env) throws EvalException {
if (env.getSemantics().incompatibleDisallowDictLookupUnhashableKeys()) {
EvalUtils.checkValidDictKey(key);
EvalUtils.checkValidDictKey(key, env);
}
return this.containsKey(key);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,16 @@ public boolean isImmutable() {
return true;
}

@Override
public boolean isHashable() {
for (Object item : this) {
if (!EvalUtils.isHashable(item)) {
return false;
}
}
return true;
}

@Override
public Mutability mutability() {
return Mutability.SHALLOW_IMMUTABLE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ public boolean isImmutable() {
return mutability().isFrozen();
}

@Override
public boolean isHashable() {
// Mutable structures are unhashable even if frozen
return false;
}

/**
* Add a new lock at {@code loc}. No effect if frozen.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ public boolean flagValue(FlagIdentifier flagIdentifier) {

public abstract boolean incompatibleAllowTagsPropagation();

public abstract boolean incompatibleDisallowHashingFrozenMutables();

@Memoized
@Override
public abstract int hashCode();
Expand Down Expand Up @@ -295,6 +297,7 @@ public static Builder builderWithDefaults() {
.incompatibleDisallowDictLookupUnhashableKeys(false)
.incompatibleDisablePartitionDefaultParameter(false)
.incompatibleAllowTagsPropagation(false)
.incompatibleDisallowHashingFrozenMutables(false)
.build();

/** Builder for {@link StarlarkSemantics}. All fields are mandatory. */
Expand Down Expand Up @@ -395,6 +398,8 @@ public abstract Builder incompatibleDisallowRuleExecutionPlatformConstraintsAllo

public abstract Builder incompatibleDisablePartitionDefaultParameter(boolean value);

public abstract Builder incompatibleDisallowHashingFrozenMutables(boolean value);

public abstract StarlarkSemantics build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ private static StarlarkSemanticsOptions buildRandomOptions(Random rand) throws E
"--incompatible_restrict_string_escapes=" + rand.nextBoolean(),
"--incompatible_disallow_dict_lookup_unhashable_keys=" + rand.nextBoolean(),
"--incompatible_disable_partition_default_parameter=" + rand.nextBoolean(),
"--incompatible_disallow_hashing_frozen_mutables=" + rand.nextBoolean(),
"--internal_skylark_flag_test_canary=" + rand.nextBoolean());
}

Expand Down Expand Up @@ -226,6 +227,7 @@ private static StarlarkSemantics buildRandomSemantics(Random rand) {
.incompatibleRestrictStringEscapes(rand.nextBoolean())
.incompatibleDisallowDictLookupUnhashableKeys(rand.nextBoolean())
.incompatibleDisablePartitionDefaultParameter(rand.nextBoolean())
.incompatibleDisallowHashingFrozenMutables(rand.nextBoolean())
.internalSkylarkFlagTestCanary(rand.nextBoolean())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3160,6 +3160,52 @@ public void testSplitEmptySeparator() throws Exception {
getConfiguredTarget("//test:r");
}

@Test
public void testHashFrozenList() throws Exception {
setSkylarkSemanticsOptions("--incompatible_disallow_hashing_frozen_mutables=false");
scratch.file("test/extension.bzl", "y = []");

scratch.file(
"test/BUILD",
"load('//test:extension.bzl', 'y')",
"{y: 1}",
"cc_library(name = 'r')");

getConfiguredTarget("//test:r");
}

@Test
public void testHashFrozenListForbidden() throws Exception {
setSkylarkSemanticsOptions("--incompatible_disallow_hashing_frozen_mutables=true");
scratch.file("test/extension.bzl", "y = []");

scratch.file(
"test/BUILD",
"load('//test:extension.bzl', 'y')",
"{y: 1}",
"cc_library(name = 'r')");

reporter.removeHandler(failFastHandler);
getConfiguredTarget("//test:r");
assertContainsEvent("unhashable type: 'list'");
}

@Test
public void testHashFrozenDeepMutableForbidden() throws Exception {
setSkylarkSemanticsOptions("--incompatible_disallow_hashing_frozen_mutables=true");
scratch.file("test/extension.bzl", "y = {}");

scratch.file(
"test/BUILD",
"load('//test:extension.bzl', 'y')",
"{('a', (y,), True): None}",
"cc_library(name = 'r')");

reporter.removeHandler(failFastHandler);
getConfiguredTarget("//test:r");
assertContainsEvent("unhashable type: 'tuple'");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error message is a bit confusing, implying that tuples are never hashable. Would it be feasible to change it to mention the actual unhashable type within this PR? The similar error in Python is "unhashable type: 'dict'".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to submit this PR first (and hopefully be included in Bazel 0.29) and iterate later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is really the object that is unhashable rather than the type, how about changing the message to
"unhashable object of type: dict"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw the same happens, regardless of this PR, if you try something like {([],): None} you will also get the same error unhashable type: 'tuple'.
I could still fix it in this PR by modifying the SkylarkValue interface to get the mutable/hashable object type within an immutable structure (like tuple), but I'm not sure if that's the best way to do so.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like @aiuto's suggestion.

}

@Test
public void testNoOutputsError() throws Exception {
scratch.file(
Expand Down