| // Copyright 2014 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.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Ordering; |
| import com.google.devtools.build.lib.collect.nestedset.NestedSet; |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; |
| import com.google.devtools.build.lib.events.Location; |
| import com.google.devtools.build.lib.skylarkinterface.SkylarkInterfaceUtils; |
| import com.google.devtools.build.lib.skylarkinterface.SkylarkModule; |
| import com.google.devtools.build.lib.skylarkinterface.SkylarkValue; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| import javax.annotation.Nullable; |
| |
| /** |
| * Utilities used by the evaluator. |
| */ |
| public final class EvalUtils { |
| |
| private EvalUtils() {} |
| |
| /** |
| * The exception that SKYLARK_COMPARATOR might throw. This is an unchecked exception |
| * because Comparator doesn't let us declare exceptions. It should normally be caught |
| * and wrapped in an EvalException. |
| */ |
| public static class ComparisonException extends RuntimeException { |
| public ComparisonException(String msg) { |
| super(msg); |
| } |
| } |
| |
| /** |
| * Compare two Skylark objects. |
| * |
| * <p>It may throw an unchecked exception ComparisonException that should be wrapped in an |
| * EvalException. |
| */ |
| public static final Ordering<Object> SKYLARK_COMPARATOR = |
| new Ordering<Object>() { |
| private int compareLists(SkylarkList o1, SkylarkList o2) { |
| if (o1 instanceof RangeList || o2 instanceof RangeList) { |
| throw new ComparisonException("Cannot compare range objects"); |
| } |
| |
| for (int i = 0; i < Math.min(o1.size(), o2.size()); i++) { |
| int cmp = compare(o1.get(i), o2.get(i)); |
| if (cmp != 0) { |
| return cmp; |
| } |
| } |
| return Integer.compare(o1.size(), o2.size()); |
| } |
| |
| @Override |
| @SuppressWarnings("unchecked") |
| public int compare(Object o1, Object o2) { |
| o1 = SkylarkType.convertToSkylark(o1, (Environment) null); |
| o2 = SkylarkType.convertToSkylark(o2, (Environment) null); |
| |
| if (o1 instanceof SkylarkList |
| && o2 instanceof SkylarkList |
| && ((SkylarkList) o1).isTuple() == ((SkylarkList) o2).isTuple()) { |
| return compareLists((SkylarkList) o1, (SkylarkList) o2); |
| } |
| |
| if (!(o1.getClass().isAssignableFrom(o2.getClass()) |
| || o2.getClass().isAssignableFrom(o1.getClass()))) { |
| throw new ComparisonException( |
| "Cannot compare " + getDataTypeName(o1) + " with " + getDataTypeName(o2)); |
| } |
| |
| if (o1 instanceof ClassObject) { |
| throw new ComparisonException("Cannot compare structs"); |
| } |
| if (o1 instanceof SkylarkNestedSet) { |
| throw new ComparisonException("Cannot compare depsets"); |
| } |
| try { |
| return ((Comparable<Object>) o1).compareTo(o2); |
| } catch (ClassCastException e) { |
| throw new ComparisonException( |
| "Cannot compare " + getDataTypeName(o1) + " with " + getDataTypeName(o2)); |
| } |
| } |
| }; |
| |
| /** |
| * Checks that an Object is a valid key for a Skylark dict. |
| * @param o an Object to validate |
| * @throws EvalException if o is not a valid key |
| */ |
| public static void checkValidDictKey(Object o) throws EvalException { |
| // TODO(bazel-team): check that all recursive elements are both Immutable AND Comparable. |
| 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 immutable by Skylark? |
| * @param o an Object |
| * @return true if the object is known to be an immutable value. |
| */ |
| // NB: This is used as the basis for accepting objects in SkylarkNestedSet-s, |
| // as well as for accepting objects as keys for Skylark dict-s. |
| public static boolean isImmutable(Object o) { |
| if (o instanceof SkylarkValue) { |
| return ((SkylarkValue) o).isImmutable(); |
| } |
| return isImmutable(o.getClass()); |
| } |
| |
| /** |
| * Is this class known to be *recursively* immutable by Skylark? |
| * For instance, class Tuple is not it, because it can contain mutable values. |
| * @param c a Class |
| * @return true if the class is known to represent only recursively immutable values. |
| */ |
| // NB: This is used as the basis for accepting objects in SkylarkNestedSet-s, |
| // as well as for accepting objects as keys for Skylark dict-s. |
| static boolean isImmutable(Class<?> c) { |
| return c.isAnnotationPresent(Immutable.class) // TODO(bazel-team): beware of containers! |
| || c.equals(String.class) |
| || c.equals(Integer.class) |
| || c.equals(Boolean.class); |
| } |
| |
| /** |
| * Returns true if the type is acceptable to be returned to the Skylark language. |
| */ |
| public static boolean isSkylarkAcceptable(Class<?> c) { |
| return SkylarkValue.class.isAssignableFrom(c) // implements SkylarkValue |
| || c.equals(String.class) // basic values |
| || c.equals(Integer.class) |
| || c.equals(Boolean.class) |
| // there is a registered Skylark ancestor class (useful e.g. when using AutoValue) |
| || SkylarkInterfaceUtils.getSkylarkModule(c) != null |
| || ImmutableMap.class.isAssignableFrom(c) // will be converted to SkylarkDict |
| || NestedSet.class.isAssignableFrom(c) // will be converted to SkylarkNestedSet |
| || PathFragment.class.isAssignableFrom(c); // other known class |
| } |
| |
| // TODO(bazel-team): move the following few type-related functions to SkylarkType |
| /** |
| * Return the Skylark-type of {@code c} |
| * |
| * <p>The result will be a type that Skylark understands and is either equal to {@code c} |
| * or is a supertype of it. For example, all instances of (all subclasses of) SkylarkList |
| * are considered to be SkylarkLists. |
| * |
| * <p>Skylark's type validation isn't equipped to deal with inheritance so we must tell it which |
| * of the superclasses or interfaces of {@code c} is the one that matters for type compatibility. |
| * |
| * @param c a class |
| * @return a super-class of c to be used in validation-time type inference. |
| */ |
| public static Class<?> getSkylarkType(Class<?> c) { |
| // TODO(bazel-team): Iterable and Class likely do not belong here. |
| if (String.class.equals(c) |
| || Boolean.class.equals(c) |
| || Integer.class.equals(c) |
| || Iterable.class.equals(c) |
| || Class.class.equals(c)) { |
| return c; |
| } |
| // TODO(bazel-team): We should require all Skylark-addressable values that aren't builtin types |
| // (String/Boolean/Integer) to implement SkylarkValue. We should also require them to have a |
| // (possibly inherited) @SkylarkModule annotation. |
| Class<?> parent = SkylarkInterfaceUtils.getParentWithSkylarkModule(c); |
| if (parent != null) { |
| return parent; |
| } |
| Preconditions.checkArgument( |
| SkylarkValue.class.isAssignableFrom(c), |
| "%s is not allowed as a Starlark value (getSkylarkType() failed)", |
| c); |
| return c; |
| } |
| |
| /** |
| * Returns a pretty name for the datatype of object 'o' in the Build language. |
| */ |
| public static String getDataTypeName(Object o) { |
| return getDataTypeName(o, false); |
| } |
| |
| /** |
| * Returns a pretty name for the datatype of object {@code object} in Skylark |
| * or the BUILD language, with full details if the {@code full} boolean is true. |
| */ |
| public static String getDataTypeName(Object object, boolean fullDetails) { |
| Preconditions.checkNotNull(object); |
| if (fullDetails) { |
| if (object instanceof SkylarkNestedSet) { |
| SkylarkNestedSet set = (SkylarkNestedSet) object; |
| return "depset of " + set.getContentType() + "s"; |
| } |
| if (object instanceof SelectorList) { |
| SelectorList list = (SelectorList) object; |
| return "select of " + getDataTypeNameFromClass(list.getType()); |
| } |
| } |
| return getDataTypeNameFromClass(object.getClass()); |
| } |
| |
| /** |
| * Returns a pretty name for the datatype equivalent of class 'c' in the Build language. |
| */ |
| public static String getDataTypeNameFromClass(Class<?> c) { |
| return getDataTypeNameFromClass(c, true); |
| } |
| |
| /** |
| * Returns a pretty name for the datatype equivalent of class 'c' in the Build language. |
| * @param highlightNameSpaces Determines whether the result should also contain a special comment |
| * when the given class identifies a Skylark name space. |
| */ |
| public static String getDataTypeNameFromClass(Class<?> c, boolean highlightNameSpaces) { |
| SkylarkModule module = SkylarkInterfaceUtils.getSkylarkModule(c); |
| if (module != null) { |
| return module.name() |
| + ((module.namespace() && highlightNameSpaces) ? " (a language module)" : ""); |
| } else if (c.equals(Object.class)) { |
| return "unknown"; |
| } else if (c.equals(String.class)) { |
| return "string"; |
| } else if (c.equals(Integer.class)) { |
| return "int"; |
| } else if (c.equals(Boolean.class)) { |
| return "bool"; |
| } else if (List.class.isAssignableFrom(c)) { // This is a Java List that isn't a SkylarkList |
| return "List"; // This case shouldn't happen in normal code, but we keep it for debugging. |
| } else if (Map.class.isAssignableFrom(c)) { // This is a Java Map that isn't a SkylarkDict |
| return "Map"; // This case shouldn't happen in normal code, but we keep it for debugging. |
| } else if (StarlarkFunction.class.isAssignableFrom(c)) { |
| return "function"; |
| } else if (c.equals(SelectorValue.class)) { |
| return "select"; |
| } else if (NestedSet.class.isAssignableFrom(c)) { |
| // TODO(bazel-team): no one should be seeing naked NestedSet at all. |
| return "depset"; |
| } else { |
| if (c.getSimpleName().isEmpty()) { |
| return c.getName(); |
| } else { |
| return c.getSimpleName(); |
| } |
| } |
| } |
| |
| public static Object checkNotNull(Expression expr, Object obj) throws EvalException { |
| if (obj == null) { |
| throw new EvalException( |
| expr.getLocation(), |
| "unexpected null value, please send a bug report. " |
| + "This was generated by expression '" |
| + expr |
| + "'"); |
| } |
| return obj; |
| } |
| |
| /** |
| * Returns the truth value of an object, according to Python rules. |
| * http://docs.python.org/2/library/stdtypes.html#truth-value-testing |
| */ |
| public static boolean toBoolean(Object o) { |
| if (o == null || o == Runtime.NONE) { |
| return false; |
| } else if (o instanceof Boolean) { |
| return (Boolean) o; |
| } else if (o instanceof String) { |
| return !((String) o).isEmpty(); |
| } else if (o instanceof Integer) { |
| return (Integer) o != 0; |
| } else if (o instanceof Collection<?>) { |
| return !((Collection<?>) o).isEmpty(); |
| } else if (o instanceof Map<?, ?>) { |
| return !((Map<?, ?>) o).isEmpty(); |
| } else if (o instanceof NestedSet<?>) { |
| return !((NestedSet<?>) o).isEmpty(); |
| } else if (o instanceof SkylarkNestedSet) { |
| return !((SkylarkNestedSet) o).isEmpty(); |
| } else if (o instanceof Iterable<?>) { |
| return !Iterables.isEmpty((Iterable<?>) o); |
| } else { |
| return true; |
| } |
| } |
| |
| public static Collection<?> toCollection(Object o, Location loc, @Nullable Environment env) |
| throws EvalException { |
| if (o instanceof Collection) { |
| return (Collection<?>) o; |
| } else if (o instanceof SkylarkList) { |
| return ((SkylarkList) o).getImmutableList(); |
| } else if (o instanceof Map) { |
| // For dictionaries we iterate through the keys only |
| if (o instanceof SkylarkDict) { |
| // SkylarkDicts handle ordering themselves |
| SkylarkDict<?, ?> dict = (SkylarkDict) o; |
| List<Object> list = Lists.newArrayListWithCapacity(dict.size()); |
| for (Map.Entry<?, ?> entries : dict.entrySet()) { |
| list.add(entries.getKey()); |
| } |
| return ImmutableList.copyOf(list); |
| } |
| // For determinism, we sort the keys. |
| try { |
| return SKYLARK_COMPARATOR.sortedCopy(((Map<?, ?>) o).keySet()); |
| } catch (ComparisonException e) { |
| throw new EvalException(loc, e); |
| } |
| } else if (o instanceof SkylarkNestedSet) { |
| return nestedSetToCollection((SkylarkNestedSet) o, loc, env); |
| } else { |
| throw new EvalException(loc, |
| "type '" + getDataTypeName(o) + "' is not a collection"); |
| } |
| } |
| |
| private static Collection<?> nestedSetToCollection( |
| SkylarkNestedSet set, Location loc, @Nullable Environment env) throws EvalException { |
| if (env != null && env.getSemantics().incompatibleDepsetIsNotIterable()) { |
| throw new EvalException( |
| loc, |
| "type 'depset' is not iterable. Use the `to_list()` method to get a list. Use " |
| + "--incompatible_depset_is_not_iterable=false to temporarily disable this check."); |
| } |
| return set.toCollection(); |
| } |
| |
| public static Iterable<?> toIterable(Object o, Location loc, @Nullable Environment env) |
| throws EvalException { |
| if (o instanceof SkylarkNestedSet) { |
| return nestedSetToCollection((SkylarkNestedSet) o, loc, env); |
| } else if (o instanceof Iterable) { |
| return (Iterable<?>) o; |
| } else if (o instanceof Map) { |
| return toCollection(o, loc, env); |
| } else { |
| throw new EvalException(loc, |
| "type '" + getDataTypeName(o) + "' is not iterable"); |
| } |
| } |
| |
| /** |
| * Given an {@link Iterable}, returns it as-is. Given a {@link SkylarkNestedSet}, returns its |
| * contents as an iterable. Throws {@link EvalException} for any other value. |
| * |
| * <p>This is a kludge for the change that made {@code SkylarkNestedSet} not implement {@code |
| * Iterable}. It is different from {@link #toIterable} in its behavior for strings and other types |
| * that are not strictly Java-iterable. |
| * |
| * @throws EvalException if {@code o} is not an iterable or set |
| * @deprecated avoid writing APIs that implicitly treat depsets as iterables. It encourages |
| * unnecessary flattening of depsets. |
| * <p>TODO(bazel-team): Remove this if/when implicit iteration over {@code SkylarkNestedSet} |
| * is no longer supported. |
| */ |
| @Deprecated |
| public static Iterable<?> toIterableStrict(Object o, Location loc, @Nullable Environment env) |
| throws EvalException { |
| if (o instanceof Iterable) { |
| return (Iterable<?>) o; |
| } else if (o instanceof SkylarkNestedSet) { |
| return nestedSetToCollection((SkylarkNestedSet) o, loc, env); |
| } else { |
| throw new EvalException(loc, |
| "expected Iterable or depset, but got '" + getDataTypeName(o) + "' (strings and maps " |
| + "are not allowed here)"); |
| } |
| } |
| |
| public static void lock(Object object, Location loc) { |
| if (object instanceof SkylarkMutable) { |
| ((SkylarkMutable) object).lock(loc); |
| } |
| } |
| |
| public static void unlock(Object object, Location loc) { |
| if (object instanceof SkylarkMutable) { |
| ((SkylarkMutable) object).unlock(loc); |
| } |
| } |
| |
| // The following functions for indexing and slicing match the behavior of Python. |
| |
| /** |
| * 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. |
| */ |
| public static int getSequenceIndex(int index, int length, Location loc) |
| 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)"); |
| } |
| return actualIndex; |
| } |
| |
| /** |
| * Performs index resolution after verifying that the given object has index type. |
| */ |
| public static int getSequenceIndex(Object index, int length, Location loc) |
| throws EvalException { |
| if (!(index instanceof Integer)) { |
| throw new EvalException( |
| loc, "indices must be integers, not " + EvalUtils.getDataTypeName(index)); |
| } |
| return getSequenceIndex(((Integer) index).intValue(), length, loc); |
| } |
| |
| /** |
| * Resolves a positive or negative index to an integer that can denote the left or right boundary |
| * of a slice. If reverse is false, the slice has positive stride (i.e., its elements are in their |
| * normal order) and the result is guaranteed to be in range [0, length + 1). If reverse is true, |
| * the slice has negative stride and the result is in range [-1, length). In either case, if the |
| * index is negative, it counts backward from length. Note that an input index of -1 represents |
| * the last element's position, while an output integer of -1 represents the imaginary position |
| * to the left of the first element. |
| */ |
| public static int clampRangeEndpoint(int index, int length, boolean reverse) { |
| if (index < 0) { |
| index += length; |
| } |
| if (!reverse) { |
| return Math.max(Math.min(index, length), 0); |
| } else { |
| return Math.max(Math.min(index, length - 1), -1); |
| } |
| } |
| |
| /** |
| * Resolves a positive or negative index to an integer that can denote the boundary for a |
| * slice with positive stride. |
| */ |
| public static int clampRangeEndpoint(int index, int length) { |
| return clampRangeEndpoint(index, length, false); |
| } |
| |
| /** |
| * Calculates the indices of the elements that should be included in the slice [start:end:step] |
| * of a sequence with the given length. Each of start, end, and step must be supplied, and step |
| * may not be 0. |
| */ |
| public static List<Integer> getSliceIndices(int start, int end, int step, int length) { |
| if (step == 0) { |
| throw new IllegalArgumentException("Slice step cannot be zero"); |
| } |
| start = clampRangeEndpoint(start, length, step < 0); |
| end = clampRangeEndpoint(end, length, step < 0); |
| // precise computation is slightly more involved, but since it can overshoot only by a single |
| // element it's fine |
| final int expectedMaxSize = Math.abs(start - end) / Math.abs(step) + 1; |
| ImmutableList.Builder<Integer> indices = ImmutableList.builderWithExpectedSize(expectedMaxSize); |
| for (int current = start; step > 0 ? current < end : current > end; current += step) { |
| indices.add(current); |
| } |
| return indices.build(); |
| } |
| |
| /** |
| * Calculates the indices of the elements in a slice, after validating the arguments and replacing |
| * Runtime.NONE with default values. Throws an EvalException if a bad argument is given. |
| */ |
| public static List<Integer> getSliceIndices( |
| Object startObj, Object endObj, Object stepObj, int length, Location loc) |
| throws EvalException { |
| int start; |
| int end; |
| int step; |
| |
| if (stepObj == Runtime.NONE) { |
| step = 1; |
| } else if (stepObj instanceof Integer) { |
| step = ((Integer) stepObj).intValue(); |
| } else { |
| throw new EvalException( |
| loc, String.format("slice step must be an integer, not '%s'", stepObj)); |
| } |
| if (step == 0) { |
| throw new EvalException(loc, "slice step cannot be zero"); |
| } |
| |
| if (startObj == Runtime.NONE) { |
| start = (step > 0) ? 0 : length - 1; |
| } else if (startObj instanceof Integer) { |
| start = ((Integer) startObj).intValue(); |
| } else { |
| throw new EvalException( |
| loc, String.format("slice start must be an integer, not '%s'", startObj)); |
| } |
| if (endObj == Runtime.NONE) { |
| // If step is negative, can't use -1 for end since that would be converted |
| // to the rightmost element's position. |
| end = (step > 0) ? length : -length - 1; |
| } else if (endObj instanceof Integer) { |
| end = ((Integer) endObj).intValue(); |
| } else { |
| throw new EvalException(loc, String.format("slice end must be an integer, not '%s'", endObj)); |
| } |
| |
| return getSliceIndices(start, end, step, length); |
| } |
| |
| /** @return true if x is Java null or Skylark None */ |
| public static boolean isNullOrNone(Object x) { |
| return x == null || x == Runtime.NONE; |
| } |
| |
| /** |
| * Build a SkylarkDict of kwarg arguments from a list, removing null-s or None-s. |
| * |
| * @param env the Environment in which this map can be mutated. |
| * @param init a series of key, value pairs (as consecutive arguments) |
| * as in {@code optionMap(k1, v1, k2, v2, k3, v3)} |
| * where each key is a String, each value is an arbitrary Objet. |
| * @return a {@code Map<String, Object>} that has all the specified entries, |
| * where key, value pairs appearing earlier have precedence, |
| * i.e. {@code k1, v1} may override {@code k3, v3}. |
| * |
| * Ignore any entry where the value is null or None. |
| * Keys cannot be null. |
| */ |
| @SuppressWarnings("unchecked") |
| public static <K, V> SkylarkDict<K, V> optionMap(Environment env, Object... init) { |
| ImmutableMap.Builder<K, V> b = new ImmutableMap.Builder<>(); |
| Preconditions.checkState(init.length % 2 == 0); |
| for (int i = init.length - 2; i >= 0; i -= 2) { |
| K key = (K) Preconditions.checkNotNull(init[i]); |
| V value = (V) init[i + 1]; |
| if (!isNullOrNone(value)) { |
| b.put(key, value); |
| } |
| } |
| return SkylarkDict.copyOf(env, b.build()); |
| } |
| } |