| // Copyright 2017 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.packages; |
| |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableCollection; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.ImmutableSortedMap; |
| import com.google.common.collect.Sets; |
| import com.google.common.collect.Sets.SetView; |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; |
| import com.google.devtools.build.lib.events.Location; |
| import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; |
| import com.google.devtools.build.lib.syntax.Concatable; |
| import com.google.devtools.build.lib.syntax.Environment; |
| import com.google.devtools.build.lib.syntax.EvalException; |
| import com.google.devtools.build.lib.syntax.EvalUtils; |
| import com.google.devtools.build.lib.syntax.SkylarkClassObject; |
| import com.google.devtools.build.lib.syntax.SkylarkType; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| import javax.annotation.Nullable; |
| |
| /** |
| * A standard implementation for provider instances. |
| * |
| * <p>Instances may be either schemaless or schemaful (corresponding to the two different concrete |
| * implementing classes). Schemaless instances are map-based, while schemaful instances have a fixed |
| * layout and array and are therefore more efficient. |
| */ |
| public abstract class SkylarkInfo extends StructImpl implements Concatable, SkylarkClassObject { |
| |
| // Private because this should not be subclassed outside this file. |
| private SkylarkInfo(Provider provider, @Nullable Location loc) { |
| super(provider, loc); |
| } |
| |
| @Override |
| public Concatter getConcatter() { |
| return SkylarkInfoConcatter.INSTANCE; |
| } |
| |
| @Override |
| public boolean isImmutable() { |
| // If the provider is not yet exported, the hash code of the object is subject to change. |
| 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 (Object item : getValues()) { |
| if (item != null && !EvalUtils.isImmutable(item)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Returns all the field values stored in the object, in the canonical order. |
| * |
| * <p>{@code protected} because this is only used for {@link #isImmutable}. It saves us having to |
| * get values one-by-one. |
| */ |
| protected abstract Iterable<Object> getValues(); |
| |
| /** |
| * Returns the custom (i.e. per-instance, as opposed to per-provider-type) error message string |
| * format used by this provider instance, or null if not set. |
| */ |
| @Nullable |
| public abstract String getCustomErrorMessageFormatForUnknownField(); |
| |
| /** Returns the layout for this provider if it is schemaful, null otherwise. */ |
| @Nullable |
| public abstract Layout getLayout(); |
| |
| /** Returns true if this provider is schemaful (array-based), false otherwise. */ |
| public boolean isCompact() { |
| return getLayout() != null; |
| } |
| |
| /** |
| * Creates a schemaless (map-based) provider instance with the given provider type and field |
| * values. |
| * |
| * <p>{@code loc} is the creation location for this instance. Built-in provider instances may use |
| * {@link Location#BUILTIN}, which is the default if null. |
| */ |
| public static SkylarkInfo createSchemaless( |
| Provider provider, Map<String, Object> values, @Nullable Location loc) { |
| return new MapBackedSkylarkInfo( |
| provider, values, loc, /*errorMessageFormatForUnknownField=*/ null); |
| } |
| |
| /** |
| * Creates a schemaless (map-based) provider instance with the given provider type, field values, |
| * and unknown-field error message. |
| * |
| * <p>This is used to create structs for special purposes, such as {@code ctx.attr} and the |
| * {@code native} module. The creation location will be {@link Location#BUILTIN}. |
| * |
| * <p>{@code errorMessageFormatForUnknownField} is a string format, as for {@link |
| * Provider#getErrorMessageFormatForUnknownField}. |
| * |
| * <p>It is preferred to not use this method. Instead, create a new subclass of {@link |
| * NativeProvider} with the desired error message format, and create a corresponding {@link |
| * NativeInfo} subclass. |
| */ |
| // TODO(bazel-team): Make the special structs that need a custom error message use a different |
| // provider (subclassing NativeProvider) and a different StructImpl implementation. Then remove |
| // this functionality, thereby saving a string pointer field for the majority of providers that |
| // don't need it. |
| public static SkylarkInfo createSchemalessWithCustomMessage( |
| Provider provider, Map<String, Object> values, String errorMessageFormatForUnknownField) { |
| Preconditions.checkNotNull(errorMessageFormatForUnknownField); |
| return new MapBackedSkylarkInfo( |
| provider, values, Location.BUILTIN, errorMessageFormatForUnknownField); |
| } |
| |
| /** |
| * Creates a schemaful (array-based) provider instance with the given provider type, layout, and |
| * values. |
| * |
| * <p>The order of the values must correspond to the given layout. |
| * |
| * <p>{@code loc} is the creation location for this instance. Built-in provider instances may use |
| * {@link Location#BUILTIN}, which is the default if null. |
| */ |
| public static SkylarkInfo createSchemaful( |
| Provider provider, |
| Layout layout, |
| Object[] values, |
| @Nullable Location loc) { |
| return new CompactSkylarkInfo(provider, layout, values, loc); |
| } |
| |
| /** |
| * Returns the concrete implementation classes of this abstract class. |
| * |
| * <p>This is useful for code that depends on reflection. |
| */ |
| public static List<Class<? extends SkylarkInfo>> getImplementationClasses() { |
| return ImmutableList.of(MapBackedSkylarkInfo.class, CompactSkylarkInfo.class); |
| } |
| |
| /** |
| * A specification of what fields a provider instance has, and how they are ordered in an |
| * array-backed implementation. |
| * |
| * <p>The provider instance may only have fields that appear in its layout. Not all fields in the |
| * layout need be present on the instance. |
| */ |
| @Immutable |
| @AutoCodec |
| public static final class Layout { |
| |
| /** |
| * A map from field names to a contiguous range of integers [0, n), ordered by integer value. |
| */ |
| private final ImmutableMap<String, Integer> map; |
| |
| /** |
| * Constructs a {@link Layout} from the given field names. |
| * |
| * <p>The order of the field names is preserved in the layout. |
| * |
| * @throws IllegalArgumentException if any field names are given more than once |
| */ |
| public Layout(Iterable<String> fields) { |
| this(makeMap(fields)); |
| } |
| |
| @AutoCodec.VisibleForSerialization |
| @AutoCodec.Instantiator |
| Layout(ImmutableMap<String, Integer> map) { |
| this.map = map; |
| } |
| |
| private static ImmutableMap<String, Integer> makeMap(Iterable<String> fields) { |
| ImmutableMap.Builder<String, Integer> layoutBuilder = ImmutableMap.builder(); |
| int i = 0; |
| for (String field : fields) { |
| layoutBuilder.put(field, i++); |
| } |
| return layoutBuilder.build(); |
| } |
| |
| @Override |
| public boolean equals(Object other) { |
| if (!(other instanceof Layout)) { |
| return false; |
| } |
| if (map == other) { |
| return true; |
| } |
| return map.equals(((Layout) other).map); |
| } |
| |
| @Override |
| public int hashCode() { |
| return map.hashCode(); |
| } |
| |
| /** Returns the number of fields in the layout. */ |
| public int size() { |
| return map.size(); |
| } |
| |
| /** Returns whether or not a field is mentioned in the layout. */ |
| public boolean hasField(String field) { |
| return map.containsKey(field); |
| } |
| |
| /** |
| * Returns the index position associated with the given field, or null if the field is not |
| * mentioned by the layout. |
| */ |
| public Integer getFieldIndex(String field) { |
| return map.get(field); |
| } |
| |
| /** Returns the field names specified by this layout, in order. */ |
| public ImmutableCollection<String> getFields() { |
| return map.keySet(); |
| } |
| |
| /** Returns the entry set of the underlying map, in order. */ |
| public ImmutableCollection<Map.Entry<String, Integer>> entrySet() { |
| return map.entrySet(); |
| } |
| } |
| |
| /** A {@link SkylarkInfo} implementation that stores its values in a map. */ |
| // TODO(b/72448383): Make private. |
| public static final class MapBackedSkylarkInfo extends SkylarkInfo { |
| private final ImmutableSortedMap<String, Object> values; |
| |
| /** |
| * Formattable string with one {@code '%s'} placeholder for the missing field name. |
| * |
| * <p>If null, uses the default format specified by the provider. |
| */ |
| @Nullable |
| private final String errorMessageFormatForUnknownField; |
| |
| MapBackedSkylarkInfo( |
| Provider provider, |
| Map<String, Object> values, |
| @Nullable Location loc, |
| @Nullable String errorMessageFormatForUnknownField) { |
| super(provider, loc); |
| // TODO(b/74396075): Phase out the unnecessary conversions done by this call to copyValues. |
| this.values = copyValues(values); |
| this.errorMessageFormatForUnknownField = errorMessageFormatForUnknownField; |
| } |
| |
| @Override |
| public boolean hasField(String name) { |
| return values.containsKey(name); |
| } |
| |
| @Override |
| public Object getValue(String name) { |
| return values.get(name); |
| } |
| |
| @Override |
| public ImmutableCollection<String> getFieldNames() { |
| return values.keySet(); |
| } |
| |
| @Override |
| protected Iterable<Object> getValues() { |
| return values.values(); |
| } |
| |
| @Override |
| protected String getErrorMessageFormatForUnknownField() { |
| return errorMessageFormatForUnknownField != null |
| ? errorMessageFormatForUnknownField : super.getErrorMessageFormatForUnknownField(); |
| } |
| |
| @Override |
| public String getCustomErrorMessageFormatForUnknownField() { |
| return errorMessageFormatForUnknownField; |
| } |
| |
| @Override |
| public Layout getLayout() { |
| return null; |
| } |
| } |
| |
| /** A {@link SkylarkInfo} implementation that stores its values in array to save space. */ |
| private static final class CompactSkylarkInfo extends SkylarkInfo implements Concatable { |
| |
| private final Layout layout; |
| /** Treated as immutable. */ |
| private final Object[] values; |
| |
| CompactSkylarkInfo( |
| Provider provider, |
| Layout layout, |
| Object[] values, |
| @Nullable Location loc) { |
| super(provider, loc); |
| this.layout = Preconditions.checkNotNull(layout); |
| Preconditions.checkArgument( |
| layout.size() == values.length, |
| "Layout has length %s, but number of given values was %s", layout.size(), values.length); |
| this.values = new Object[values.length]; |
| for (int i = 0; i < values.length; i++) { |
| // TODO(b/74396075): Phase out this unnecessary conversion. |
| this.values[i] = SkylarkType.convertToSkylark(values[i], (Environment) null); |
| } |
| } |
| |
| @Override |
| public Object getValue(String name) { |
| Integer index = layout.getFieldIndex(name); |
| if (index == null) { |
| return null; |
| } |
| return values[index]; |
| } |
| |
| @Override |
| public boolean hasField(String name) { |
| Integer index = layout.getFieldIndex(name); |
| return index != null && values[index] != null; |
| } |
| |
| @Override |
| public ImmutableCollection<String> getFieldNames() { |
| ImmutableSet.Builder<String> result = ImmutableSet.builder(); |
| for (Map.Entry<String, Integer> entry : layout.entrySet()) { |
| if (values[entry.getValue()] != null) { |
| result.add(entry.getKey()); |
| } |
| } |
| return result.build(); |
| } |
| |
| @Override |
| protected Iterable<Object> getValues() { |
| return Arrays.asList(values); |
| } |
| |
| @Override |
| public String getCustomErrorMessageFormatForUnknownField() { |
| return null; |
| } |
| |
| @Override |
| public Layout getLayout() { |
| return layout; |
| } |
| } |
| |
| /** Concatter for concrete {@link SkylarkInfo} subclasses. */ |
| private static final class SkylarkInfoConcatter implements Concatable.Concatter { |
| private static final SkylarkInfoConcatter INSTANCE = new SkylarkInfoConcatter(); |
| |
| private SkylarkInfoConcatter() {} |
| |
| @Override |
| public Concatable concat(Concatable left, Concatable right, Location loc) throws EvalException { |
| // Casts are safe because SkylarkInfoConcatter is only used by SkylarkInfo. |
| SkylarkInfo leftInfo = (SkylarkInfo) left; |
| SkylarkInfo rightInfo = (SkylarkInfo) right; |
| Provider provider = leftInfo.getProvider(); |
| if (!provider.equals(rightInfo.getProvider())) { |
| throw new EvalException( |
| loc, |
| String.format( |
| "Cannot use '+' operator on instances of different providers (%s and %s)", |
| provider.getPrintableName(), rightInfo.getProvider().getPrintableName())); |
| } |
| SetView<String> commonFields = |
| Sets.intersection( |
| ImmutableSet.copyOf(leftInfo.getFieldNames()), |
| ImmutableSet.copyOf(rightInfo.getFieldNames())); |
| if (!commonFields.isEmpty()) { |
| throw new EvalException( |
| loc, |
| "Cannot use '+' operator on provider instances with overlapping field(s): " |
| + Joiner.on(",").join(commonFields)); |
| } |
| // Keep homogeneous compact concatenations compact. |
| if (leftInfo instanceof CompactSkylarkInfo && rightInfo instanceof CompactSkylarkInfo) { |
| CompactSkylarkInfo compactLeft = (CompactSkylarkInfo) leftInfo; |
| CompactSkylarkInfo compactRight = (CompactSkylarkInfo) rightInfo; |
| Layout layout = compactLeft.layout; |
| if (layout.equals(compactRight.layout)) { |
| int nvals = layout.size(); |
| Object[] newValues = new Object[nvals]; |
| for (int i = 0; i < nvals; i++) { |
| newValues[i] = |
| (compactLeft.values[i] != null) ? compactLeft.values[i] : compactRight.values[i]; |
| } |
| return createSchemaful(provider, layout, newValues, loc); |
| } |
| } |
| // Fall back on making a map-based instance. |
| ImmutableMap.Builder<String, Object> newValues = ImmutableMap.builder(); |
| for (String field : leftInfo.getFieldNames()) { |
| newValues.put(field, leftInfo.getValue(field)); |
| } |
| for (String field : rightInfo.getFieldNames()) { |
| newValues.put(field, rightInfo.getValue(field)); |
| } |
| return createSchemaless(provider, newValues.build(), loc); |
| } |
| } |
| } |