// 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.Joiner; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Interner; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.concurrent.BlazeInterners; import com.google.devtools.build.lib.events.Location; import com.google.devtools.build.lib.skylarkinterface.SkylarkValue; import com.google.devtools.build.lib.syntax.SkylarkList.MutableList; import com.google.devtools.build.lib.syntax.SkylarkList.Tuple; import com.google.devtools.build.lib.util.Preconditions; import java.io.Serializable; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import javax.annotation.Nullable; /** * A class representing types available in Skylark. * * <p>A SkylarkType can be one of: * <ul> * <li>a Simple type that contains exactly the objects in a given class, * (including the special TOP and BOTTOM types that respectively contain * all the objects (Simple type for Object.class) and no object at all * (Simple type for EmptyType.class, isomorphic to Void.class). * <li>a Combination of a generic class (one of SET, selector) * and an argument type (that itself need not be Simple). * <li>a Union of a finite set of types * <li>a FunctionType associated with a name and a returnType * </ul> * * <p>In a style reminiscent of Java's null, Skylark's None is in all the types * as far as type inference goes, yet actually no type .contains(it). * * <p>The current implementation fails to distinguish between TOP and ANY, * between BOTTOM and EMPTY (VOID, ZERO, FALSE): * <ul> * <li>In type analysis, we often distinguish a notion of "the type of this object" * from the notion of "what I know about the type of this object". * Some languages have a Universal Base Class that contains all objects, and would be the ANY type. * The Skylark runtime, written in Java, has this ANY type, Java's Object.class. * But the Skylark validation engine doesn't really have a concept of an ANY class; * however, it does have a concept of a yet-undermined class, the TOP class * (called UNKOWN in previous code). In the future, we may have to distinguish between the two, * at which point type constructor classes would have to be generic in * "actual type" vs "partial knowledge of type". * <li>Similarly, and EMPTY type (also known as VOID, ZERO or FALSE, in other contexts) * is a type that has no instance, whereas the BOTTOM type is the type analysis that says * that there is no possible runtime type for the given object, which may imply that * the point in the program at which the object is evaluated cannot be reached, etc. * </ul> * So for now, we have puns between TOP and ANY, BOTTOM and EMPTY, between runtime (eval) and * validation-time (validate). Yet in the future, we may need to make a clear distinction, * especially if we are to have types such List(Any) vs List(Top), which contains the former, * but also plenty of other quite distinct types. And yet in a future future, the TOP type * would not be represented explicitly, instead a new type variable would be inserted everywhere * a type is unknown, to be unified with further type information as it becomes available. */ // TODO(bazel-team): move the FunctionType side-effect out of the type object // and into the validation environment. public abstract class SkylarkType implements Serializable { // The main primitives to override in subclasses /** Is the given value an element of this type? By default, no (empty type) */ public boolean contains(Object value) { return false; } /** * intersectWith() is the internal method from which function intersection(t1, t2) is computed. * OVERRIDE this method in your classes, but DO NOT TO CALL it: only call intersection(). * When computing intersection(t1, t2), whichever type defined before the other * knows nothing about the other and about their intersection, and returns BOTTOM; * the other knows about the former, and returns their intersection (which may be BOTTOM). * intersection() will call in one order then the other, and return whichever answer * isn't BOTTOM, if any. By default, types are disjoint and their intersection is BOTTOM. */ // TODO(bazel-team): should we define and use an Exception instead? protected SkylarkType intersectWith(SkylarkType other) { return BOTTOM; } /** @return true if any object of this SkylarkType can be cast to that Java class */ public boolean canBeCastTo(Class<?> type) { return SkylarkType.of(type).includes(this); } /** @return the smallest java Class known to contain all elements of this type */ // Note: most user-code should be using a variant that throws an Exception // if the result is Object.class but the type isn't TOP. public Class<?> getType() { return Object.class; } // The actual intersection function for users to use public static SkylarkType intersection(SkylarkType t1, SkylarkType t2) { if (t1.equals(t2)) { return t1; } SkylarkType t = t1.intersectWith(t2); if (t == BOTTOM) { return t2.intersectWith(t1); } else { return t; } } public boolean includes(SkylarkType other) { return intersection(this, other).equals(other); } public SkylarkType getArgType() { return TOP; } private static final class Empty {}; // Empty type, used as basis for Bottom // Notable types /** A singleton for the TOP type, that at analysis time means that any type is possible. */ public static final Simple TOP = new Top(); /** A singleton for the BOTTOM type, that contains no element */ public static final Simple BOTTOM = new Bottom(); /** NONE, the Unit type, isomorphic to Void, except its unique element prints as None */ // Note that we currently consider at validation time that None is in every type, // by declaring its type as TOP instead of NONE, even though at runtime, // we reject None from all types but NONE, and in particular from e.g. lists of Files. // TODO(bazel-team): resolve this inconsistency, one way or the other. public static final Simple NONE = Simple.of(Runtime.NoneType.class); /** The STRING type, for strings */ public static final Simple STRING = Simple.of(String.class); /** The INTEGER type, for 32-bit signed integers */ public static final Simple INT = Simple.of(Integer.class); /** The BOOLEAN type, that contains TRUE and FALSE */ public static final Simple BOOL = Simple.of(Boolean.class); /** The FUNCTION type, that contains all functions, otherwise dynamically typed at call-time */ public static final SkylarkFunctionType FUNCTION = new SkylarkFunctionType("unknown", TOP); /** The DICT type, that contains SkylarkDict */ public static final Simple DICT = Simple.of(SkylarkDict.class); /** The SEQUENCE type, that contains lists and tuples */ // TODO(bazel-team): this was added for backward compatibility with the BUILD language, // that doesn't make a difference between list and tuple, so that functions can be declared // that keep not making the difference. Going forward, though, we should investigate whether // we ever want to use this type, and if not, make sure no existing client code uses it. public static final Simple SEQUENCE = Simple.of(SkylarkList.class); /** The LIST type, that contains all MutableList-s */ public static final Simple LIST = Simple.of(MutableList.class); /** The TUPLE type, that contains all Tuple-s */ public static final Simple TUPLE = Simple.of(Tuple.class); /** The STRING_LIST type, a MutableList of strings */ public static final SkylarkType STRING_LIST = Combination.of(LIST, STRING); /** The INT_LIST type, a MutableList of integers */ public static final SkylarkType INT_LIST = Combination.of(LIST, INT); /** The SET type, that contains all SkylarkNestedSet-s, and the generic combinator for them */ public static final Simple SET = Simple.of(SkylarkNestedSet.class); // Common subclasses of SkylarkType /** the Top type contains all objects */ private static class Top extends Simple { private Top() { super(Object.class); } @Override public boolean contains(Object value) { return true; } @Override public SkylarkType intersectWith(SkylarkType other) { return other; } @Override public String toString() { return "Object"; } } /** the Bottom type contains no element */ private static class Bottom extends Simple { private Bottom() { super(Empty.class); } @Override public SkylarkType intersectWith(SkylarkType other) { return this; } @Override public String toString() { return "EmptyType"; } } /** a Simple type contains the instance of a Java class */ public static class Simple extends SkylarkType { private final Class<?> type; private Simple(Class<?> type) { this.type = type; } @Override public boolean contains(Object value) { return value != null && type.isInstance(value); } @Override public Class<?> getType() { return type; } @Override public boolean equals(Object other) { return this == other || (this.getClass() == other.getClass() && this.type.equals(((Simple) other).getType())); } @Override public int hashCode() { return 0x513973 + type.hashCode() * 503; // equal underlying types yield the same hashCode } @Override public String toString() { return EvalUtils.getDataTypeNameFromClass(type); } @Override public boolean canBeCastTo(Class<?> type) { return this.type == type || super.canBeCastTo(type); } private static final LoadingCache<Class<?>, Simple> simpleCache = CacheBuilder.newBuilder() .build( new CacheLoader<Class<?>, Simple>() { @Override public Simple load(Class<?> type) { return create(type); } }); private static Simple create(Class<?> type) { Simple simple; if (type == Object.class) { // Note that this is a bad encoding for "anything", not for "everything", i.e. // for skylark there isn't a type that contains everything, but there's a Top type // that corresponds to not knowing yet which more special type it will be. simple = TOP; } else if (type == Empty.class) { simple = BOTTOM; } else { // Consider all classes that have the same EvalUtils.getSkylarkType() as equivalent, // as a substitute to handling inheritance. Class<?> skylarkType = EvalUtils.getSkylarkType(type); if (skylarkType != type) { simple = Simple.of(skylarkType); } else { simple = new Simple(type); } } return simple; } /** * The public way to create a Simple type * @param type a Class * @return the Simple type that contains exactly the instances of that Class */ public static Simple of(Class<?> type) { return simpleCache.getUnchecked(type); } } /** Combination of a generic type and an argument type */ public static class Combination extends SkylarkType { // For the moment, we can only combine a Simple type with a Simple type, // and the first one has to be a Java generic class, // and in practice actually one of SkylarkList or SkylarkNestedSet private final SkylarkType genericType; // actually always a Simple, for now. private final SkylarkType argType; // not always Simple private Combination(SkylarkType genericType, SkylarkType argType) { this.genericType = genericType; this.argType = argType; } @Override public boolean contains(Object value) { // The empty collection is member of compatible types if (value == null || !genericType.contains(value)) { return false; } else { SkylarkType valueArgType = getGenericArgType(value); return valueArgType == TOP // empty objects are universal || argType.includes(valueArgType); } } @Override public SkylarkType intersectWith(SkylarkType other) { // For now, we only accept generics with a single covariant parameter if (genericType.equals(other)) { return this; } if (other instanceof Combination) { SkylarkType generic = genericType.intersectWith(((Combination) other).getGenericType()); if (generic == BOTTOM) { return BOTTOM; } SkylarkType arg = intersection(argType, ((Combination) other).getArgType()); if (arg == BOTTOM) { return BOTTOM; } return Combination.of(generic, arg); } if (other instanceof Simple) { SkylarkType generic = genericType.intersectWith(other); if (generic == BOTTOM) { return BOTTOM; } return SkylarkType.of(generic, getArgType()); } return BOTTOM; } @Override public boolean equals(Object other) { if (this == other) { return true; } else if (this.getClass() == other.getClass()) { Combination o = (Combination) other; return genericType.equals(o.getGenericType()) && argType.equals(o.getArgType()); } else { return false; } } @Override public int hashCode() { // equal underlying types yield the same hashCode return 0x20B14A71 + genericType.hashCode() * 1009 + argType.hashCode() * 1013; } @Override public Class<?> getType() { return genericType.getType(); } SkylarkType getGenericType() { return genericType; } @Override public SkylarkType getArgType() { return argType; } @Override public String toString() { return genericType + " of " + argType + "s"; } private static final Interner<Combination> combinationInterner = BlazeInterners.<Combination>newWeakInterner(); public static SkylarkType of(SkylarkType generic, SkylarkType argument) { // assume all combinations with TOP are the same as the simple type, and canonicalize. Preconditions.checkArgument(generic instanceof Simple); if (argument == TOP) { return generic; } else { return combinationInterner.intern(new Combination(generic, argument)); } } public static SkylarkType of(Class<?> generic, Class<?> argument) { return of(Simple.of(generic), Simple.of(argument)); } } /** Union types, used a lot in "dynamic" languages such as Python or Skylark */ public static class Union extends SkylarkType { private final ImmutableList<SkylarkType> types; private Union(ImmutableList<SkylarkType> types) { this.types = types; } @Override public boolean contains(Object value) { for (SkylarkType type : types) { if (type.contains(value)) { return true; } } return false; } @Override public boolean equals(Object other) { if (this.getClass() == other.getClass()) { Union o = (Union) other; if (types.containsAll(o.types) && o.types.containsAll(types)) { return true; } } return false; } @Override public int hashCode() { // equal underlying types yield the same hashCode int h = 0x4104; for (SkylarkType type : types) { // Important: addition is commutative, like Union h += type.hashCode(); } return h; } @Override public String toString() { return Joiner.on(" or ").join(types); } public static List<SkylarkType> addElements(List<SkylarkType> list, SkylarkType type) { if (type instanceof Union) { list.addAll(((Union) type).types); } else if (type != BOTTOM) { list.add(type); } return list; } @Override public SkylarkType intersectWith(SkylarkType other) { List<SkylarkType> otherTypes = addElements(new ArrayList<SkylarkType>(), other); List<SkylarkType> results = new ArrayList<>(); for (SkylarkType element : types) { for (SkylarkType otherElement : otherTypes) { addElements(results, intersection(element, otherElement)); } } return Union.of(results); } public static SkylarkType of(List<SkylarkType> types) { // When making the union of many types, // canonicalize them into elementary (non-Union) types, // and then eliminate trivially redundant types from the list. // list of all types in the input ArrayList<SkylarkType> elements = new ArrayList<>(); for (SkylarkType type : types) { addElements(elements, type); } // canonicalized list of types ArrayList<SkylarkType> canonical = new ArrayList<>(); for (SkylarkType newType : elements) { boolean done = false; // done with this element? int i = 0; for (SkylarkType existingType : canonical) { SkylarkType both = intersection(newType, existingType); if (newType.equals(both)) { // newType already included done = true; break; } else if (existingType.equals(both)) { // newType supertype of existingType canonical.set(i, newType); done = true; break; } } if (!done) { canonical.add(newType); } } if (canonical.isEmpty()) { return BOTTOM; } else if (canonical.size() == 1) { return canonical.get(0); } else { return new Union(ImmutableList.<SkylarkType>copyOf(canonical)); } } public static SkylarkType of(SkylarkType... types) { return of(Arrays.asList(types)); } public static SkylarkType of(SkylarkType t1, SkylarkType t2) { return of(ImmutableList.<SkylarkType>of(t1, t2)); } public static SkylarkType of(Class<?> t1, Class<?> t2) { return of(Simple.of(t1), Simple.of(t2)); } } public static SkylarkType of(Class<?> type) { if (SkylarkNestedSet.class.isAssignableFrom(type)) { return SET; } else if (BaseFunction.class.isAssignableFrom(type)) { return new SkylarkFunctionType("unknown", TOP); } else { return Simple.of(type); } } public static SkylarkType of(SkylarkType t1, SkylarkType t2) { return Combination.of(t1, t2); } public static SkylarkType of(Class<?> t1, Class<?> t2) { return Combination.of(t1, t2); } /** * A class representing the type of a Skylark function. */ public static final class SkylarkFunctionType extends SkylarkType { private final String name; @Nullable private final SkylarkType returnType; @Override public SkylarkType intersectWith(SkylarkType other) { // This gives the wrong result if both return types are incompatibly updated later! if (other instanceof SkylarkFunctionType) { SkylarkFunctionType fun = (SkylarkFunctionType) other; SkylarkType type1 = returnType == null ? TOP : returnType; SkylarkType type2 = fun.returnType == null ? TOP : fun.returnType; SkylarkType bothReturnType = intersection(returnType, fun.returnType); if (type1.equals(bothReturnType)) { return this; } else if (type2.equals(bothReturnType)) { return fun; } else { return new SkylarkFunctionType(name, bothReturnType); } } else { return BOTTOM; } } @Override public Class<?> getType() { return BaseFunction.class; } @Override public String toString() { return (returnType == TOP || returnType == null ? "" : returnType + "-returning ") + "function"; } @Override public boolean contains(Object value) { // This returns true a bit too much, not looking at the result type. return value instanceof BaseFunction; } public static SkylarkFunctionType of(String name, SkylarkType returnType) { return new SkylarkFunctionType(name, returnType); } private SkylarkFunctionType(String name, SkylarkType returnType) { this.name = name; this.returnType = returnType; } } // Utility functions regarding types public static SkylarkType typeOf(Object value) { if (value == null) { return BOTTOM; } else if (value instanceof SkylarkNestedSet) { return of(SET, ((SkylarkNestedSet) value).getContentType()); } else { return Simple.of(value.getClass()); } } public static SkylarkType getGenericArgType(Object value) { if (value instanceof SkylarkNestedSet) { return ((SkylarkNestedSet) value).getContentType(); } else { return TOP; } } private static boolean isTypeAllowedInSkylark(Object object) { if (object instanceof NestedSet<?>) { return false; } else if (object instanceof List<?> && !(object instanceof SkylarkList)) { return false; } return true; } /** * Throws EvalException if the type of the object is not allowed to be present in Skylark. */ static void checkTypeAllowedInSkylark(Object object, Location loc) throws EvalException { if (!isTypeAllowedInSkylark(object)) { throw new EvalException( loc, "internal error: type '" + object.getClass().getSimpleName() + "' is not allowed"); } } /** * General purpose type-casting facility. * * @param value - the actual value of the parameter * @param type - the expected Class for the value * @param loc - the location info used in the EvalException * @param format - a format String * @param args - arguments to format, in case there's an exception */ public static <T> T cast(Object value, Class<T> type, Location loc, String format, Object... args) throws EvalException { try { return type.cast(value); } catch (ClassCastException e) { throw new EvalException(loc, String.format(format, args)); } } /** * General purpose type-casting facility. * * @param value - the actual value of the parameter * @param genericType - a generic class of one argument for the value * @param argType - a covariant argument for the generic class * @param loc - the location info used in the EvalException * @param format - a format String * @param args - arguments to format, in case there's an exception */ @SuppressWarnings("unchecked") public static <T> T cast(Object value, Class<T> genericType, Class<?> argType, Location loc, String format, Object... args) throws EvalException { if (of(genericType, argType).contains(value)) { return (T) value; } else { throw new EvalException(loc, String.format(format, args)); } } /** * Cast a Map object into an Iterable of Map entries of the given key, value types. * @param obj the Map object, where null designates an empty map * @param keyType the class of map keys * @param valueType the class of map values * @param what a string indicating what this is about, to include in case of error */ @SuppressWarnings("unchecked") public static <KEY_TYPE, VALUE_TYPE> Map<KEY_TYPE, VALUE_TYPE> castMap(Object obj, Class<KEY_TYPE> keyType, Class<VALUE_TYPE> valueType, String what) throws EvalException { if (obj == null) { return ImmutableMap.of(); } if (!(obj instanceof Map<?, ?>)) { throw new EvalException( null, String.format( "expected a dictionary for '%s' but got '%s' instead", what, EvalUtils.getDataTypeName(obj))); } for (Map.Entry<?, ?> input : ((Map<?, ?>) obj).entrySet()) { if (!keyType.isAssignableFrom(input.getKey().getClass()) || !valueType.isAssignableFrom(input.getValue().getClass())) { throw new EvalException( null, String.format( "expected <%s, %s> type for '%s' but got <%s, %s> instead", keyType.getSimpleName(), valueType.getSimpleName(), what, EvalUtils.getDataTypeName(input.getKey()), EvalUtils.getDataTypeName(input.getValue()))); } } return (Map<KEY_TYPE, VALUE_TYPE>) obj; } private static Class<?> getGenericTypeFromMethod(Method method) { // This is where we can infer generic type information, so SkylarkNestedSets can be // created in a safe way. Eventually we should probably do something with Lists and Maps too. ParameterizedType t = (ParameterizedType) method.getGenericReturnType(); Type type = t.getActualTypeArguments()[0]; if (type instanceof Class) { return (Class<?>) type; } if (type instanceof WildcardType) { WildcardType wildcard = (WildcardType) type; Type upperBound = wildcard.getUpperBounds()[0]; if (upperBound instanceof Class) { // i.e. List<? extends SuperClass> return (Class<?>) upperBound; } } // It means someone annotated a method with @SkylarkCallable with no specific generic type info. // We shouldn't annotate methods which return List<?> or List<T>. throw new IllegalStateException("Cannot infer type from method signature " + method); } /** * Converts an object retrieved from a Java method to a Skylark-compatible type. */ static Object convertToSkylark(Object object, Method method, @Nullable Environment env) { if (object instanceof NestedSet<?>) { return new SkylarkNestedSet(getGenericTypeFromMethod(method), (NestedSet<?>) object); } return convertToSkylark(object, env); } /** * Converts an object to a Skylark-compatible type if possible. */ public static Object convertToSkylark(Object object, @Nullable Environment env) { if (object instanceof List && !(object instanceof SkylarkList)) { return new MutableList<>((List<?>) object, env); } if (object instanceof SkylarkValue) { return object; } if (object instanceof Map) { return SkylarkDict.<Object, Object>copyOf(env, (Map<?, ?>) object); } // TODO(bazel-team): ensure everything is a SkylarkValue at all times. // Preconditions.checkArgument(EvalUtils.isSkylarkAcceptable( // object.getClass()), // "invalid object %s of class %s not convertible to a Skylark value", // object, // object.getClass()); return object; } public static void checkType(Object object, Class<?> type, @Nullable Object description) throws EvalException { if (!type.isInstance(object)) { throw new EvalException( null, Printer.format( "expected type '%r' %sbut got type '%s' instead", type, description == null ? "" : String.format("for %s ", description), EvalUtils.getDataTypeName(object))); } } }