| // Copyright 2019 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.ImmutableMap; |
| import com.google.devtools.build.lib.syntax.Mutability.MutabilityException; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import javax.annotation.Nullable; |
| |
| /** |
| * A {@link Module} represents a Starlark module, a container of global variables populated by |
| * executing a Starlark file. Each top-level assignment updates a global variable in the module. |
| * |
| * <p>Each module references its "predeclared" environment, which is often shared among many |
| * modules. These are the names that are defined even at the start of execution. For example, in |
| * Bazel, the predeclared environment of the module for a BUILD or .bzl file defines name values |
| * such as cc_binary and glob. |
| * |
| * <p>The predeclared environment currently must include the "universal" names present in every |
| * Starlark thread in every dialect, such as None, len, and str. |
| * |
| * <p>Global bindings in a Module may shadow bindings inherited from the predeclared or universe |
| * block. |
| * |
| * <p>A module may carry an arbitrary piece of metadata called its "label". In Bazel, for example, |
| * the label is a build label such as "//dir:file.bzl", for use by the Label function. This is a |
| * hack. |
| * |
| * <p>A {@link Module} may be constructed in a two-phase process. To do this, call the nullary |
| * constructor to create an uninitialized {@link Module}, then call {@link #initialize}. It is |
| * illegal to use any other method in-between these two calls, or to call {@link #initialize} on an |
| * already initialized {@link Module}. |
| */ |
| // TODO(adonovan): |
| // - make fields private where possible. |
| // - remove references to this from StarlarkThread. |
| // - separate the universal predeclared environment and make it implicit. |
| // - eliminate initialize(). The only constructor we need is: |
| // (String name, Mutability mu, Map<String, Object> predeclared, Object label). |
| public final class Module implements ValidationEnvironment.Module { |
| |
| /** |
| * Final, except that it may be initialized after instantiation. Null mutability indicates that |
| * this Frame is uninitialized. |
| */ |
| @Nullable private Mutability mutability; |
| |
| /** Final, except that it may be initialized after instantiation. */ |
| @Nullable Module universe; |
| |
| // The label (an optional piece of metadata) associated with the file. |
| @Nullable Object label; |
| |
| /** Bindings are maintained in order of creation. */ |
| private final LinkedHashMap<String, Object> bindings; |
| |
| /** |
| * A list of bindings which *would* exist in this global frame under certain semantic flags, but |
| * do not exist using the semantic flags used in this frame's creation. This map should not be |
| * used for lookups; it should only be used to throw descriptive error messages when a lookup of a |
| * restricted object is attempted. |
| */ |
| final LinkedHashMap<String, FlagGuardedValue> restrictedBindings; |
| |
| /** Set of bindings that are exported (can be loaded from other modules). */ |
| final HashSet<String> exportedBindings; |
| |
| /** Constructs an uninitialized instance; caller must call {@link #initialize} before use. */ |
| public Module() { |
| this.mutability = null; |
| this.universe = null; |
| this.label = null; |
| this.bindings = new LinkedHashMap<>(); |
| this.restrictedBindings = new LinkedHashMap<>(); |
| this.exportedBindings = new HashSet<>(); |
| } |
| |
| /** |
| * Returns the module (file) of the innermost enclosing Starlark function on the call stack, or |
| * null if none of the active calls are functions defined in Starlark. |
| * |
| * <p>The name of this function is intentionally horrible to make you feel bad for using it. |
| */ |
| @Nullable |
| public static Module ofInnermostEnclosingStarlarkFunction(StarlarkThread thread) { |
| for (Debug.Frame fr : thread.getDebugCallStack().reverse()) { |
| if (fr.getFunction() instanceof StarlarkFunction) { |
| return ((StarlarkFunction) fr.getFunction()).getModule(); |
| } |
| } |
| return null; |
| } |
| |
| Module( |
| Mutability mutability, |
| @Nullable Module universe, |
| @Nullable Object label, |
| @Nullable Map<String, Object> bindings, |
| @Nullable Map<String, FlagGuardedValue> restrictedBindings) { |
| Preconditions.checkState(universe == null || universe.universe == null); |
| this.mutability = Preconditions.checkNotNull(mutability); |
| this.universe = universe; |
| if (label != null) { |
| this.label = label; |
| } else if (universe != null) { |
| this.label = universe.label; |
| } else { |
| this.label = null; |
| } |
| this.bindings = new LinkedHashMap<>(); |
| if (bindings != null) { |
| this.bindings.putAll(bindings); |
| } |
| this.restrictedBindings = new LinkedHashMap<>(); |
| if (restrictedBindings != null) { |
| this.restrictedBindings.putAll(restrictedBindings); |
| } |
| if (universe != null) { |
| this.restrictedBindings.putAll(universe.restrictedBindings); |
| } |
| this.exportedBindings = new HashSet<>(); |
| } |
| |
| public Module(Mutability mutability) { |
| this(mutability, null, null, null, null); |
| } |
| |
| public Module(Mutability mutability, @Nullable Module universe) { |
| this(mutability, universe, null, null, null); |
| } |
| |
| public Module(Mutability mutability, @Nullable Module universe, @Nullable Object label) { |
| this(mutability, universe, label, null, null); |
| } |
| |
| /** Constructs a global frame for the given builtin bindings. */ |
| public static Module createForBuiltins(Map<String, Object> bindings) { |
| Mutability mutability = Mutability.create("<builtins>").freeze(); |
| return new Module(mutability, null, null, bindings, null); |
| } |
| |
| /** |
| * Constructs a global frame based on the given parent frame, filtering out flag-restricted global |
| * objects. |
| */ |
| static Module filterOutRestrictedBindings( |
| Mutability mutability, Module parent, StarlarkSemantics semantics) { |
| if (parent == null) { |
| return new Module(mutability); |
| } |
| Map<String, Object> filteredBindings = new LinkedHashMap<>(); |
| Map<String, FlagGuardedValue> restrictedBindings = new LinkedHashMap<>(); |
| |
| for (Entry<String, Object> binding : parent.getTransitiveBindings().entrySet()) { |
| if (binding.getValue() instanceof FlagGuardedValue) { |
| FlagGuardedValue val = (FlagGuardedValue) binding.getValue(); |
| if (val.isObjectAccessibleUsingSemantics(semantics)) { |
| filteredBindings.put(binding.getKey(), val.getObject(semantics)); |
| } else { |
| restrictedBindings.put(binding.getKey(), val); |
| } |
| } else { |
| filteredBindings.put(binding.getKey(), binding.getValue()); |
| } |
| } |
| |
| restrictedBindings.putAll(parent.restrictedBindings); |
| |
| return new Module( |
| mutability, null /*parent */, parent.label, filteredBindings, restrictedBindings); |
| } |
| |
| private void checkInitialized() { |
| Preconditions.checkNotNull(mutability, "Attempted to use Frame before initializing it"); |
| } |
| |
| public void initialize( |
| Mutability mutability, |
| @Nullable Module universe, |
| @Nullable Object label, |
| Map<String, Object> bindings) { |
| Preconditions.checkState( |
| universe == null || universe.universe == null); // no more than 1 universe |
| Preconditions.checkState( |
| this.mutability == null, "Attempted to initialize an already initialized Frame"); |
| this.mutability = Preconditions.checkNotNull(mutability); |
| this.universe = universe; |
| if (label != null) { |
| this.label = label; |
| } else if (universe != null) { |
| this.label = universe.label; |
| } else { |
| this.label = null; |
| } |
| this.bindings.putAll(bindings); |
| } |
| |
| /** |
| * Returns a new {@link Module} with the same fields, except that {@link #label} is set to the |
| * given value. The label associated with each function (frame) on the stack is accessible using |
| * {@link #getLabel}, and is included in the result of {@code str(fn)} where {@code fn} is a |
| * StarlarkFunction. |
| */ |
| public Module withLabel(Object label) { |
| checkInitialized(); |
| return new Module(mutability, /*universe*/ null, label, bindings, /*restrictedBindings*/ null); |
| } |
| |
| /** Returns the {@link Mutability} of this {@link Module}. */ |
| public Mutability mutability() { |
| checkInitialized(); |
| return mutability; |
| } |
| |
| /** |
| * Returns the parent {@link Module}, if it exists. |
| * |
| * <p>TODO(laurentlb): Should be called getUniverse. |
| */ |
| @Nullable |
| public Module getParent() { |
| checkInitialized(); |
| return universe; |
| } |
| |
| /** |
| * Returns the label (an optional piece of metadata) associated with this {@code Module}. (For |
| * Bazel LOADING threads, this is the build label of its BUILD or .bzl file.) |
| */ |
| @Nullable |
| public Object getLabel() { |
| checkInitialized(); |
| return label; |
| } |
| |
| /** |
| * Returns a map of direct bindings of this {@link Module}, ignoring universe. |
| * |
| * <p>The bindings are returned in a deterministic order (for a given sequence of initial values |
| * and updates). |
| * |
| * <p>For efficiency an unmodifiable view is returned. Callers should assume that the view is |
| * invalidated by any subsequent modification to the {@link Module}'s bindings. |
| */ |
| public Map<String, Object> getBindings() { |
| checkInitialized(); |
| return Collections.unmodifiableMap(bindings); |
| } |
| |
| /** |
| * Returns a map of bindings that are exported (i.e. symbols declared using `=` and `def`, but not |
| * `load`). |
| */ |
| // TODO(adonovan): whether bindings are exported should be decided by the resolver; |
| // non-exported bindings should never be added to the module. |
| public Map<String, Object> getExportedBindings() { |
| checkInitialized(); |
| ImmutableMap.Builder<String, Object> result = new ImmutableMap.Builder<>(); |
| for (Map.Entry<String, Object> entry : bindings.entrySet()) { |
| if (exportedBindings.contains(entry.getKey())) { |
| result.put(entry); |
| } |
| } |
| return result.build(); |
| } |
| |
| @Override |
| public Set<String> getNames() { |
| return getTransitiveBindings().keySet(); |
| } |
| |
| @Override |
| public String getUndeclaredNameError(StarlarkSemantics semantics, String name) { |
| FlagGuardedValue v = restrictedBindings.get(name); |
| return v == null ? null : v.getErrorFromAttemptingAccess(semantics, name); |
| } |
| |
| /** Returns an environment containing both module and predeclared bindings. */ |
| // TODO(adonovan): eliminate in favor of explicit module vs. predeclared operations. |
| public Map<String, Object> getTransitiveBindings() { |
| checkInitialized(); |
| // Can't use ImmutableMap.Builder because it doesn't allow duplicates. |
| LinkedHashMap<String, Object> collectedBindings = new LinkedHashMap<>(); |
| if (universe != null) { |
| collectedBindings.putAll(universe.getTransitiveBindings()); |
| } |
| collectedBindings.putAll(getBindings()); |
| return collectedBindings; |
| } |
| |
| /** |
| * Returns the value of the specified module variable, or null if not bound. Does not look in the |
| * predeclared environment. |
| */ |
| public Object lookup(String varname) { |
| checkInitialized(); |
| return bindings.get(varname); |
| } |
| |
| /** |
| * Returns the value of the named variable in the module environment, or if not bound there, in |
| * the predeclared environment, or if not bound there, null. |
| */ |
| public Object get(String varname) { |
| // TODO(adonovan): delete this whole function, and getTransitiveBindings. |
| // With proper resolution, the interpreter will know whether |
| // to look in the module or the predeclared/universal environment. |
| checkInitialized(); |
| Object val = bindings.get(varname); |
| if (val != null) { |
| return val; |
| } |
| if (universe != null) { |
| return universe.get(varname); |
| } |
| return null; |
| } |
| |
| /** Updates a binding in the module environment. */ |
| public void put(String varname, Object value) throws MutabilityException { |
| Preconditions.checkNotNull(value, "Module.put(%s, null)", varname); |
| checkInitialized(); |
| if (mutability.isFrozen()) { |
| throw new MutabilityException("trying to mutate a frozen module"); |
| } |
| bindings.put(varname, value); |
| } |
| |
| @Override |
| public String toString() { |
| // TODO(adonovan): use the file name of the module (not visible to Starlark programs). |
| if (mutability == null) { |
| return "<Uninitialized Module>"; |
| } else { |
| return String.format("<Module%s>", mutability()); |
| } |
| } |
| } |