// Copyright 2014 Google Inc. 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.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* The BUILD environment.
*/
public class Environment {
@SkylarkSignature(name = "True", returnType = Boolean.class,
doc = "Literal for the boolean true.")
private static final Boolean TRUE = true;
@SkylarkSignature(name = "False", returnType = Boolean.class,
doc = "Literal for the boolean false.")
private static final Boolean FALSE = false;
@SkylarkSignature(name = "PACKAGE_NAME", returnType = String.class,
doc = "The name of the package the rule or build extension is called from. "
+ "For example, in the BUILD file some/package/BUILD
, its value "
+ "will be some/package
. "
+ "This variable is special, because its value comes from outside of the extension "
+ "module (it comes from the BUILD file), so it can only be accessed in functions "
+ "(transitively) called from BUILD files. For example:
"
+ "
def extension():\n"
+ " return PACKAGE_NAME
"
+ "In this case calling extension()
works from the BUILD file (if the "
+ "function is loaded), but not as a top level function call in the extension module.")
public static final String PKG_NAME = "PACKAGE_NAME";
/**
* There should be only one instance of this type to allow "== None" tests.
*/
@Immutable
public static final class NoneType {
@Override
public String toString() { return "None"; }
private NoneType() {}
}
@SkylarkSignature(name = "None", returnType = NoneType.class, doc = "Literal for the None value.")
public static final NoneType NONE = new NoneType();
protected final Map env = new HashMap<>();
// BaseFunctions with namespaces. Works only in the global environment.
protected final Map, Map> functions = new HashMap<>();
/**
* The parent environment. For Skylark it's the global environment,
* used for global read only variable lookup.
*/
protected final Environment parent;
/**
* Map from a Skylark extension to an environment, which contains all symbols defined in the
* extension.
*/
protected Map importedExtensions;
/**
* A set of variables propagating through function calling. It's only used to call
* native rules from Skylark build extensions.
*/
protected Set propagatingVariables = new HashSet<>();
// Only used in the global environment.
// TODO(bazel-team): make this a final field part of constructor.
private boolean isLoadingPhase = false;
/**
* Is this Environment being evaluated during the loading phase?
* This is fixed during environment setup, and enables various functions
* that are not available during the analysis phase.
* @return true if this environment corresponds to code during the loading phase.
*/
boolean isLoadingPhase() {
return isLoadingPhase;
}
/**
* Enable loading phase only functions in this Environment.
* This should only be done during setup before code is evaluated.
*/
public void setLoadingPhase() {
isLoadingPhase = true;
}
/**
* Checks that the current Evaluation context is in loading phase.
* @param symbol name of the function being only authorized thus.
*/
public void checkLoadingPhase(String symbol, Location loc) throws EvalException {
if (!isLoadingPhase()) {
throw new EvalException(loc, symbol + "() can only be called during the loading phase");
}
}
/**
* Is this a global environment?
* @return true if this is a global (top-level) environment
* as opposed to inside the body of a function
*/
public boolean isGlobal() {
return true;
}
/**
* An EventHandler for errors and warnings. This is not used in the BUILD language,
* however it might be used in Skylark code called from the BUILD language.
*/
@Nullable protected EventHandler eventHandler;
private ImmutableList stackTrace;
/**
* Constructs an empty root non-Skylark environment.
* The root environment is also the global environment.
*/
public Environment(ImmutableList stackTrace) {
this.parent = null;
this.importedExtensions = new HashMap<>();
this.stackTrace = stackTrace;
setupGlobal();
}
public Environment() {
this(ImmutableList.of());
}
/**
* Constructs an empty child environment.
*/
public Environment(Environment parent, ImmutableList stackTrace) {
Preconditions.checkNotNull(parent);
this.parent = parent;
this.importedExtensions = new HashMap<>();
this.stackTrace = stackTrace;
}
public Environment(Environment parent) {
this(parent, ImmutableList.of());
}
/**
* Constructs an empty child environment with an EventHandler.
*/
public Environment(Environment parent, EventHandler eventHandler) {
this(parent);
this.eventHandler = Preconditions.checkNotNull(eventHandler);
}
public EventHandler getEventHandler() {
return eventHandler;
}
// Sets up the global environment
private void setupGlobal() {
// In Python 2.x, True and False are global values and can be redefined by the user.
// In Python 3.x, they are keywords. We implement them as values, for the sake of
// simplicity. We define them as Boolean objects.
update("False", FALSE);
update("True", TRUE);
update("None", NONE);
}
public boolean isSkylark() {
return false;
}
protected boolean hasVariable(String varname) {
return env.containsKey(varname);
}
/**
* @return the value from the environment whose name is "varname".
* @throws NoSuchVariableException if the variable is not defined in the Environment.
*
*/
public Object lookup(String varname) throws NoSuchVariableException {
Object value = env.get(varname);
if (value == null) {
if (parent != null) {
return parent.lookup(varname);
}
throw new NoSuchVariableException(varname);
}
return value;
}
/**
* Like lookup(String)
, but instead of throwing an exception in
* the case where "varname" is not defined, "defaultValue" is returned instead.
*
*/
public Object lookup(String varname, Object defaultValue) {
Object value = env.get(varname);
if (value == null) {
if (parent != null) {
return parent.lookup(varname, defaultValue);
}
return defaultValue;
}
return value;
}
/**
* Updates the value of variable "varname" in the environment, corresponding
* to an {@link AssignmentStatement}.
*/
public Environment update(String varname, Object value) {
Preconditions.checkNotNull(value, "update(value == null)");
env.put(varname, value);
return this;
}
/**
* Same as {@link #update}, but also marks the variable propagating, meaning it will
* be present in the execution environment of a UserDefinedFunction called from this
* Environment. Using this method is discouraged.
*/
public void updateAndPropagate(String varname, Object value) {
update(varname, value);
propagatingVariables.add(varname);
}
/**
* Remove the variable from the environment, returning
* any previous mapping (null if there was none).
*/
public Object remove(String varname) {
return env.remove(varname);
}
/**
* Returns the (immutable) set of names of all variables directly defined in this environment.
*/
public Set getDirectVariableNames() {
return env.keySet();
}
/**
* Returns the (immutable) set of names of all variables defined in this
* environment. Exposed for testing; not very efficient!
*/
@VisibleForTesting
public Set getVariableNames() {
if (parent == null) {
return env.keySet();
} else {
Set vars = new HashSet<>();
vars.addAll(env.keySet());
vars.addAll(parent.getVariableNames());
return vars;
}
}
@Override
public int hashCode() {
throw new UnsupportedOperationException(); // avoid nondeterminism
}
@Override
public boolean equals(Object that) {
throw new UnsupportedOperationException();
}
@Override
public String toString() {
StringBuilder out = new StringBuilder();
out.append("Environment{");
List keys = new ArrayList<>(env.keySet());
Collections.sort(keys);
for (String key : keys) {
out.append(key).append(" -> ").append(env.get(key)).append(", ");
}
out.append("}");
if (parent != null) {
out.append("=>");
out.append(parent);
}
return out.toString();
}
/**
* An exception thrown when an attempt is made to lookup a non-existent
* variable in the environment.
*/
public static class NoSuchVariableException extends Exception {
NoSuchVariableException(String variable) {
super("no such variable: " + variable);
}
}
/**
* An exception thrown when an attempt is made to import a symbol from a file
* that was not properly loaded.
*/
public static class LoadFailedException extends Exception {
LoadFailedException(String file) {
super("file '" + file + "' was not correctly loaded. Make sure the 'load' statement appears "
+ "in the global scope, in the BUILD file");
}
}
public void setImportedExtensions(Map importedExtensions) {
this.importedExtensions = importedExtensions;
}
public void importSymbol(PathFragment extension, Identifier symbol, String nameInLoadedFile)
throws NoSuchVariableException, LoadFailedException {
if (!importedExtensions.containsKey(extension)) {
throw new LoadFailedException(extension.toString());
}
Object value = importedExtensions.get(extension).lookup(nameInLoadedFile);
if (!isSkylark()) {
value = SkylarkType.convertFromSkylark(value);
}
update(symbol.getName(), value);
}
/**
* Registers a function with namespace to this global environment.
*/
public void registerFunction(Class> nameSpace, String name, BaseFunction function) {
nameSpace = getCanonicalRepresentation(nameSpace);
Preconditions.checkArgument(parent == null);
if (!functions.containsKey(nameSpace)) {
functions.put(nameSpace, new HashMap());
}
functions.get(nameSpace).put(name, function);
}
private Map getNamespaceFunctions(Class> nameSpace) {
nameSpace = getCanonicalRepresentation(nameSpace);
Environment topLevel = this;
while (topLevel.parent != null) {
topLevel = topLevel.parent;
}
return topLevel.functions.get(nameSpace);
}
/**
* Returns the canonical representation of the given class, i.e. the super class for which any
* functions were registered.
*
* Currently, this is only necessary for mapping the different subclasses of {@link
* java.util.Map} to the interface.
*/
private Class> getCanonicalRepresentation(Class> clazz) {
return Map.class.isAssignableFrom(clazz) ? Map.class : clazz;
}
/**
* Returns the function of the namespace of the given name or null of it does not exists.
*/
public BaseFunction getFunction(Class> nameSpace, String name) {
Map nameSpaceFunctions = getNamespaceFunctions(nameSpace);
return nameSpaceFunctions != null ? nameSpaceFunctions.get(name) : null;
}
/**
* Returns the function names registered with the namespace.
*/
public Set getFunctionNames(Class> nameSpace) {
Map nameSpaceFunctions = getNamespaceFunctions(nameSpace);
return nameSpaceFunctions != null ? nameSpaceFunctions.keySet() : ImmutableSet.of();
}
public ImmutableList getStackTrace() {
return stackTrace;
}
/**
* Adds the given element to the stack trace (iff the stack is empty) and returns whether it was
* successful.
*/
public boolean tryAddingStackTraceRoot(StackTraceElement element) {
if (stackTrace.isEmpty()) {
stackTrace = ImmutableList.of(element);
return true;
}
return false;
}
/**
* Removes the only remaining element from the stack trace.
*
* This particular element describes the outer-most calling function (usually a rule).
*
*
This method is required since {@link FuncallExpression} does not create a new {@link
* Environment}, hence it has to add and remove its {@link StackTraceElement} from an existing
* one.
*/
public void removeStackTraceRoot() {
Preconditions.checkArgument(stackTrace.size() == 1);
stackTrace = ImmutableList.of();
}
/**
* Returns whether the given {@link BaseFunction} is part of this {@link Environment}'s stack
* trace.
*/
public boolean stackTraceContains(BaseFunction function) {
for (StackTraceElement element : stackTrace) {
if (element.equals(function)) {
return true;
}
}
return false;
}
/**
* Returns a copy of this {@link Environment}'s stack trace, including the specified element.
*/
protected ImmutableList getCopyOfUpdatedStackTrace(StackTraceElement toAdd) {
return new ImmutableList.Builder().addAll(stackTrace).add(toAdd).build();
}
}