// 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.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. * *

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 Info 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. * *

{@code protected} because this is only used for {@link #isImmutable}. It saves us having to * get values one-by-one. */ protected abstract Iterable 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. * *

{@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 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. * *

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}. * *

{@code errorMessageFormatForUnknownField} is a string format, as for {@link * Provider#getErrorMessageFormatForUnknownField}. * *

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 Info 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 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. * *

The order of the values must correspond to the given layout. * *

{@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. * *

This is useful for code that depends on reflection. */ public static List> 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. * *

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 map; /** * Constructs a {@link Layout} from the given field names. * *

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 fields) { this(makeMap(fields)); } @AutoCodec.VisibleForSerialization @AutoCodec.Instantiator Layout(ImmutableMap map) { this.map = map; } private static ImmutableMap makeMap(Iterable fields) { ImmutableMap.Builder 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 getFields() { return map.keySet(); } /** Returns the entry set of the underlying map, in order. */ public ImmutableCollection> 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 ImmutableMap values; /** * Formattable string with one {@code '%s'} placeholder for the missing field name. * *

If null, uses the default format specified by the provider. */ @Nullable private final String errorMessageFormatForUnknownField; MapBackedSkylarkInfo( Provider provider, Map 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 getFieldNames() { return values.keySet(); } @Override protected Iterable 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 getFieldNames() { ImmutableSet.Builder result = ImmutableSet.builder(); for (Map.Entry entry : layout.entrySet()) { if (values[entry.getValue()] != null) { result.add(entry.getKey()); } } return result.build(); } @Override protected Iterable 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 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 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); } } }