blob: 6d9eff5dcd9ec3677975e42d7657e8fde20fba0c [file] [log] [blame]
// Copyright 2016 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.syntax;
import com.google.common.annotations.VisibleForTesting;
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 com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
import com.google.devtools.build.lib.syntax.SkylarkMutable.MutableMap;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.annotation.Nullable;
/**
* A Skylark dictionary (dict).
*
* <p>Although this implements the {@link Map} interface, it is not mutable via that interface's
* methods. Instead, use the mutators that take in a {@link Mutability} object.
*/
@SkylarkModule(
name = "dict",
category = SkylarkModuleCategory.BUILTIN,
doc =
"A language built-in type representating a dictionary (associative mapping). "
+ "Dictionaries may be constructed with a special literal syntax:<br>"
+ "<pre class=\"language-python\">d = {\"a\": 2, \"b\": 5}</pre>"
+ "See also the <a href=\"globals.html#dict\">dict()</a> constructor function. "
+ "When using the literal syntax, it is an error to have duplicated keys. "
+ "Use square brackets to access elements:<br>"
+ "<pre class=\"language-python\">e = d[\"a\"] # e == 2</pre>"
+ "Like lists, they can also be constructed using a comprehension syntax:<br>"
+ "<pre class=\"language-python\">d = {i: 2*i for i in range(20)}\n"
+ "e = d[8] # e == 16</pre>"
+ "Dictionaries are mutable. You can add new elements or mutate existing ones:"
+ "<pre class=\"language-python\">d[\"key\"] = 5</pre>"
+ "<p>Iterating over a dict is equivalent to iterating over its keys. The "
+ "<code>in</code> operator tests for membership in the keyset of the dict.<br>"
+ "<pre class=\"language-python\">\"a\" in {\"a\" : 2, \"b\" : 5} "
+ "# evaluates as True</pre>"
+ "The iteration order for a dict is deterministic but not specified."
)
public final class SkylarkDict<K, V> extends MutableMap<K, V>
implements Map<K, V>, SkylarkIndexable {
private final LinkedHashMap<K, V> contents = new LinkedHashMap<>();
private final Mutability mutability;
private SkylarkDict(@Nullable Mutability mutability) {
this.mutability = mutability == null ? Mutability.IMMUTABLE : mutability;
}
private SkylarkDict(@Nullable Environment env) {
this.mutability = env == null ? Mutability.IMMUTABLE : env.mutability();
}
private static final SkylarkDict<?, ?> EMPTY = withMutability(Mutability.IMMUTABLE);
/** Returns an immutable empty dict. */
// Safe because the empty singleton is immutable.
@SuppressWarnings("unchecked")
public static <K, V> SkylarkDict<K, V> empty() {
return (SkylarkDict<K, V>) EMPTY;
}
/** Returns an empty dict with the given {@link Mutability}. */
public static <K, V> SkylarkDict<K, V> withMutability(@Nullable Mutability mutability) {
return new SkylarkDict<>(mutability);
}
/** @return a dict mutable in given environment only */
public static <K, V> SkylarkDict<K, V> of(@Nullable Environment env) {
return new SkylarkDict<>(env);
}
/** @return a dict mutable in given environment only, with given initial key and value */
public static <K, V> SkylarkDict<K, V> of(@Nullable Environment env, K k, V v) {
return SkylarkDict.<K, V>of(env).putUnsafe(k, v);
}
/** @return a dict mutable in given environment only, with two given initial key value pairs */
public static <K, V> SkylarkDict<K, V> of(
@Nullable Environment env, K k1, V v1, K k2, V v2) {
return SkylarkDict.<K, V>of(env).putUnsafe(k1, v1).putUnsafe(k2, v2);
}
// TODO(bazel-team): Make other methods that take in mutabilities instead of environments, make
// this method public.
@VisibleForTesting
static <K, V> SkylarkDict<K, V> copyOf(
@Nullable Mutability mutability, Map<? extends K, ? extends V> m) {
return SkylarkDict.<K, V>withMutability(mutability).putAllUnsafe(m);
}
/** @return a dict mutable in given environment only, with contents copied from given map */
public static <K, V> SkylarkDict<K, V> copyOf(
@Nullable Environment env, Map<? extends K, ? extends V> m) {
return SkylarkDict.<K, V>of(env).putAllUnsafe(m);
}
/** Puts the given entry into the dict, without calling {@link #checkMutable}. */
private SkylarkDict<K, V> putUnsafe(K k, V v) {
contents.put(k, v);
return this;
}
/** Puts all entries of the given map into the dict, without calling {@link #checkMutable}. */
private <KK extends K, VV extends V> SkylarkDict<K, V> putAllUnsafe(Map<KK, VV> m) {
for (Map.Entry<KK, VV> e : m.entrySet()) {
contents.put(e.getKey(), e.getValue());
}
return this;
}
@Override
public Mutability mutability() {
return mutability;
}
@Override
protected Map<K, V> getContentsUnsafe() {
return contents;
}
/**
* Puts an entry into a dict, after validating that mutation is allowed.
*
* @param key the key of the added entry
* @param value the value of the added entry
* @param loc the location to use for error reporting
* @param mutability the {@link Mutability} associated with the opreation
* @throws EvalException if the key is invalid or the dict is frozen
*/
public void put(K key, V value, Location loc, Mutability mutability) throws EvalException {
checkMutable(loc, mutability);
EvalUtils.checkValidDictKey(key);
contents.put(key, value);
}
/**
* Convenience version of {@link #put(K, V, Location, Mutability)} that uses the {@link
* Mutability} of an {@link Environment}.
*/
// 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());
}
/**
* Puts all the entries from a given map into the dict, after validating that mutation is allowed.
*
* @param map the map whose entries are added
* @param loc the location to use for error reporting
* @param mutability the {@link Mutability} associated with the operation
* @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 {
checkMutable(loc, mutability);
for (Map.Entry<KK, VV> e : map.entrySet()) {
KK k = e.getKey();
EvalUtils.checkValidDictKey(k);
contents.put(k, e.getValue());
}
}
/**
* Deletes the entry associated with the given key.
*
* @param key the key to delete
* @param loc the location to use for error reporting
* @param mutability the {@link Mutability} associated with the operation
* @return the value associated to the key, or {@code null} if not present
* @throws EvalException if the dict is frozen
*/
V remove(Object key, Location loc, Mutability mutability) throws EvalException {
checkMutable(loc, mutability);
return contents.remove(key);
}
/**
* Clears the dict.
*
* @param loc the location to use for error reporting
* @param mutability the {@link Mutability} associated with the operation
* @throws EvalException if the dict is frozen
*/
void clear(Location loc, Mutability mutability) throws EvalException {
checkMutable(loc, mutability);
contents.clear();
}
@Override
public void repr(SkylarkPrinter printer) {
printer.printList(entrySet(), "{", ", ", "}", null);
}
@Override
public String toString() {
return Printer.repr(this);
}
/**
* If {@code obj} is a {@code SkylarkDict}, casts it to an unmodifiable {@code Map<K, V>} after
* checking that each of its entries has key type {@code keyType} and value type {@code
* valueType}. If {@code obj} is {@code None} or null, treats it as an empty dict.
*
* <p>The returned map may or may not be a view that is affected by updates to the original dict.
*
* @param obj the object to cast. null and None are treated as an empty dict.
* @param keyType the expected type of all the dict's keys
* @param valueType the expected type of all the dict's values
* @param description a description of the argument being converted, or null, for debugging
*/
public static <K, V> Map<K, V> castSkylarkDictOrNoneToDict(
Object obj, Class<K> keyType, Class<V> valueType, @Nullable String description)
throws EvalException {
if (EvalUtils.isNullOrNone(obj)) {
return empty();
}
if (obj instanceof SkylarkDict) {
return ((SkylarkDict<?, ?>) obj).getContents(keyType, valueType, description);
}
throw new EvalException(
null,
String.format(
"%s is not of expected type dict or NoneType",
description == null ? Printer.repr(obj) : String.format("'%s'", description)));
}
/**
* Casts this dict to an unmodifiable {@code SkylarkDict<X, Y>}, after checking that all keys and
* values have types {@code keyType} and {@code valueType} respectively.
*
* <p>The returned map may or may not be a view that is affected by updates to the original dict.
*
* @param keyType the expected class of keys
* @param valueType the expected class of values
* @param description a description of the argument being converted, or null, for debugging
*/
@SuppressWarnings("unchecked")
public <X, Y> Map<X, Y> getContents(
Class<X> keyType, Class<Y> valueType, @Nullable String description)
throws EvalException {
Object keyDescription = description == null
? null : Printer.formattable("'%s' key", description);
Object valueDescription = description == null
? null : Printer.formattable("'%s' value", description);
for (Map.Entry<?, ?> e : this.entrySet()) {
SkylarkType.checkType(e.getKey(), keyType, keyDescription);
SkylarkType.checkType(e.getValue(), valueType, valueDescription);
}
return Collections.unmodifiableMap((SkylarkDict<X, Y>) this);
}
@Override
public final Object getIndex(Object key, Location loc) throws EvalException {
if (!this.containsKey(key)) {
throw new EvalException(loc, Printer.format("key %r not found in dictionary", key));
}
return this.get(key);
}
@Override
public final boolean containsKey(Object key, Location loc) throws EvalException {
return this.containsKey(key);
}
public static <K, V> SkylarkDict<K, V> plus(
SkylarkDict<? extends K, ? extends V> left,
SkylarkDict<? extends K, ? extends V> right,
@Nullable Environment env) {
SkylarkDict<K, V> result = SkylarkDict.of(env);
result.putAllUnsafe(left);
result.putAllUnsafe(right);
return result;
}
}