blob: 6b0a163c3a6747571f3ebbb09c17c4cc41d3b349 [file] [log] [blame]
// 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.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
/**
* An StarlarkThread represents a Starlark thread.
*
* <p>It holds the stack of active Starlark and built-in function calls. In addition, it may hold
* per-thread application state (see {@link #setThreadLocal}) that passes through Starlark functions
* but does not directly affect them, such as information about the BUILD file being loaded.
*
* <p>Every {@code StarlarkThread} has a {@link Mutability} field, and must be used within a
* function that creates and closes this {@link Mutability} with the try-with-resource pattern. This
* {@link Mutability} is also used when initializing mutable objects within that {@code
* StarlarkThread}. When the {@code Mutability} is closed at the end of the computation, it freezes
* the {@code StarlarkThread} along with all of those objects. This pattern enforces the discipline
* that there should be no dangling mutable {@code StarlarkThread}, or concurrency between
* interacting {@code StarlarkThread}s. It is a Starlark-level error to attempt to mutate a frozen
* {@code StarlarkThread} or its objects, but it is a Java-level error to attempt to mutate an
* unfrozen {@code StarlarkThread} or its objects from within a different {@code StarlarkThread}.
*/
public final class StarlarkThread {
/** The mutability of values created by this thread. */
private final Mutability mutability;
// profiler state
//
// The profiler field (and savedThread) are set when we first observe during a
// push (function call entry) that the profiler is active. They are unset
// not in the corresponding pop, but when the last frame is popped, because
// the profiler session might start in the middle of a call and/or run beyond
// the lifetime of this thread.
final AtomicInteger cpuTicks = new AtomicInteger();
@Nullable private CpuProfiler profiler;
StarlarkThread savedThread; // saved StarlarkThread, when profiling reentrant evaluation
private final Map<Class<?>, Object> threadLocals = new HashMap<>();
private boolean interruptible = true;
long steps; // count of logical computation steps executed so far
/**
* Returns the number of Starlark computation steps executed by this thread according to a
* small-step semantics. (Today, that means exec, eval, and assign operations executed by the
* tree-walking evaluator, but in future will mean byte code instructions; the two are not
* commensurable.)
*/
public long getExecutedSteps() {
return steps;
}
/**
* Disables polling of the {@link java.lang.Thread#interrupted} flag during Starlark evaluation.
*/
// TODO(adonovan): expose a public API for this if we can establish a stronger semantics. (There
// are other ways besides polling for evaluation to be interrupted, such as calling certain
// built-in functions.)
void ignoreThreadInterrupts() {
interruptible = false;
}
void checkInterrupt() throws InterruptedException {
if (interruptible && Thread.interrupted()) {
throw new InterruptedException();
}
}
/**
* setThreadLocal saves {@code value} as a thread-local variable of this Starlark thread, keyed by
* {@code key}, so that it can later be retrieved by {@code getThreadLocal(key)}.
*/
public <T> void setThreadLocal(Class<T> key, T value) {
threadLocals.put(key, value);
}
/**
* getThreadLocal returns the value {@code v} supplied to the most recent {@code
* setThreadLocal(key, v)} call, or null if there was no prior call.
*/
public <T> T getThreadLocal(Class<T> key) {
Object v = threadLocals.get(key);
return v == null ? null : key.cast(v);
}
/** A Frame records information about an active function call. */
static final class Frame implements Debug.Frame {
final StarlarkThread thread;
final StarlarkCallable fn; // the called function
@Nullable
final Debug.Debugger dbg = Debug.debugger.get(); // the debugger, if active for this frame
int compcount = 0; // number of enclosing comprehensions
Object result = Starlark.NONE; // the operand of a Starlark return statement
// Current PC location. Initially fn.getLocation(); for Starlark functions,
// it is updated at key points when it may be observed: calls, breakpoints, errors.
private Location loc;
// Indicates that setErrorLocation has been called already and the error
// location (loc) should not be overrwritten.
private boolean errorLocationSet;
// The locals of this frame, if fn is a StarlarkFunction, otherwise empty.
Map<String, Object> locals;
@Nullable private Object profileSpan; // current span of walltime call profiler
private Frame(StarlarkThread thread, StarlarkCallable fn) {
this.thread = thread;
this.fn = fn;
}
// Updates the PC location in this frame.
void setLocation(Location loc) {
this.loc = loc;
}
// Sets location only the first time it is called,
// to ensure that the location of the innermost expression
// is used for errors.
// (Once we switch to a bytecode interpreter, we can afford
// to update fr.pc before each fallible operation, but until then
// we must materialize Locations only after the fact of failure.)
// Sets errorLocationSet.
void setErrorLocation(Location loc) {
if (!errorLocationSet) {
errorLocationSet = true;
setLocation(loc);
}
}
@Override
public StarlarkCallable getFunction() {
return fn;
}
@Override
public Location getLocation() {
return loc;
}
@Override
public ImmutableMap<String, Object> getLocals() {
return ImmutableMap.copyOf(this.locals);
}
@Override
public String toString() {
return fn.getName() + "@" + loc;
}
}
/** The semantics options that affect how Starlark code is evaluated. */
private final StarlarkSemantics semantics;
/** PrintHandler for Starlark print statements. */
private PrintHandler printHandler = StarlarkThread::defaultPrintHandler;
/** Loader for Starlark load statements. Null if loading is disallowed. */
@Nullable private Loader loader = null;
/** Stack of active function calls. */
private final ArrayList<Frame> callstack = new ArrayList<>();
/** A hook for notifications of assignments at top level. */
PostAssignHook postAssignHook;
/** Pushes a function onto the call stack. */
void push(StarlarkCallable fn) {
Frame fr = new Frame(this, fn);
callstack.add(fr);
// Notify debug tools of the thread's first push.
if (callstack.size() == 1 && Debug.threadHook != null) {
Debug.threadHook.onPushFirst(this);
}
if (fn instanceof StarlarkFunction) {
StarlarkFunction sfn = (StarlarkFunction) fn;
fr.locals = Maps.newLinkedHashMapWithExpectedSize(sfn.getParameterNames().size());
} else {
// built-in function
fr.locals = ImmutableMap.of();
}
fr.loc = fn.getLocation();
// Start wall-time call profile span.
CallProfiler callProfiler = StarlarkThread.callProfiler;
if (callProfiler != null) {
fr.profileSpan = callProfiler.start(fn);
}
// Poll for newly installed CPU profiler.
if (profiler == null) {
this.profiler = CpuProfiler.get();
if (profiler != null) {
cpuTicks.set(0);
// Associated current Java thread with this StarlarkThread.
// (Save the previous association so we can restore it later.)
this.savedThread = CpuProfiler.setStarlarkThread(this);
}
}
}
/** Pops a function off the call stack. */
void pop() {
int last = callstack.size() - 1;
Frame fr = callstack.get(last);
if (profiler != null) {
int ticks = cpuTicks.getAndSet(0);
if (ticks > 0) {
profiler.addEvent(ticks, getDebugCallStack());
}
// If this is the final pop in this thread,
// unregister it from the profiler.
if (last == 0) {
// Restore the previous association (in case of reentrant evaluation).
CpuProfiler.setStarlarkThread(this.savedThread);
this.savedThread = null;
this.profiler = null;
}
}
callstack.remove(last); // pop
// End wall-time profile span.
CallProfiler callProfiler = StarlarkThread.callProfiler;
if (callProfiler != null && fr.profileSpan != null) {
callProfiler.end(fr.profileSpan);
}
// Notify debug tools of the thread's last pop.
if (last == 0 && Debug.threadHook != null) {
Debug.threadHook.onPopLast(this);
}
}
/** Returns the mutability for values created by this thread. */
public Mutability mutability() {
return mutability;
}
/**
* A PrintHandler determines how a Starlark thread deals with print statements. It is invoked by
* the built-in {@code print} function. Its default behavior is to write the message to standard
* error, preceded by the location of the print statement, {@code thread.getCallerLocation()}.
*/
@FunctionalInterface
public interface PrintHandler {
void print(StarlarkThread thread, String msg);
}
/** Returns the PrintHandler for Starlark print statements. */
PrintHandler getPrintHandler() {
return printHandler;
}
/** Sets the behavior of Starlark print statements executed by this thread. */
public void setPrintHandler(PrintHandler h) {
this.printHandler = Preconditions.checkNotNull(h);
}
private static void defaultPrintHandler(StarlarkThread thread, String msg) {
System.err.println(thread.getCallerLocation() + ": " + msg);
}
/**
* A Loader determines the behavior of load statements executed by this thread. It returns the
* named module, or null if not found.
*/
@FunctionalInterface
public interface Loader {
@Nullable
Module load(String module);
}
/** Returns the loader for Starlark load statements. */
Loader getLoader() {
return loader;
}
/** Sets the behavior of Starlark load statements executed by this thread. */
public void setLoader(Loader loader) {
this.loader = Preconditions.checkNotNull(loader);
}
/** Reports whether {@code fn} has been recursively reentered within this thread. */
boolean isRecursiveCall(StarlarkFunction fn) {
// Find fn buried within stack. (The top of the stack is assumed to be fn.)
for (int i = callstack.size() - 2; i >= 0; --i) {
Frame fr = callstack.get(i);
// TODO(adonovan): compare code, not closure values, otherwise
// one can defeat this check by writing the Y combinator.
if (fr.fn.equals(fn)) {
return true;
}
}
return false;
}
/**
* Returns the location of the program counter in the enclosing call frame. If called from within
* a built-in function, this is the location of the call expression that called the built-in. It
* returns BUILTIN if called with fewer than two frames (such as within a test).
*/
public Location getCallerLocation() {
return toplevel() ? Location.BUILTIN : frame(1).loc;
}
/**
* Reports whether the call stack has less than two frames. Zero frames means an idle thread. One
* frame means the function for the top-level statements of a file is active. More than that means
* a function call is in progress.
*
* <p>Every use of this function is a hack to work around the lack of proper local vs global
* identifier resolution at top level.
*/
boolean toplevel() {
return callstack.size() < 2;
}
// Returns the stack frame at the specified depth. 0 means top of stack, 1 is its caller, etc.
Frame frame(int depth) {
return callstack.get(callstack.size() - 1 - depth);
}
/**
* Constructs a StarlarkThread.
*
* @param mu the (non-frozen) mutability of values created by this thread.
* @param semantics the StarlarkSemantics for this thread.
*/
public StarlarkThread(Mutability mu, StarlarkSemantics semantics) {
Preconditions.checkArgument(!mu.isFrozen());
this.mutability = mu;
this.semantics = semantics;
}
/**
* Specifies a hook function to be run after each assignment at top level.
*
* <p>This is a short-term hack to allow us to consolidate all StarlarkFile execution in one place
* even while BzlLoadFunction implements the old "export" behavior, in which rules, aspects and
* providers are "exported" as soon as they are assigned, not at the end of file execution.
*/
public void setPostAssignHook(PostAssignHook postAssignHook) {
this.postAssignHook = postAssignHook;
}
/** A hook for notifications of assignments at top level. */
@FunctionalInterface
public interface PostAssignHook {
void assign(String name, Object value);
}
public StarlarkSemantics getSemantics() {
return semantics;
}
// Implementation of Debug.getCallStack.
// Intentionally obscured to steer most users to the simpler getCallStack.
ImmutableList<Debug.Frame> getDebugCallStack() {
return ImmutableList.<Debug.Frame>copyOf(callstack);
}
/** Returns the size of the callstack. This is needed for the debugger. */
int getCallStackSize() {
return callstack.size();
}
/**
* A CallStackEntry describes the name and PC location of an active function call. See {@link
* #getCallStack}.
*/
@Immutable
public static final class CallStackEntry {
public final String name;
public final Location location;
public CallStackEntry(String name, Location location) {
this.location = location;
this.name = name;
}
@Override
public String toString() {
return name + "@" + location;
}
}
/**
* Returns information about this thread's current stack of active function calls, outermost call
* first. For each function, it reports its name, and the location of its current program counter.
* The result is immutable and does not reference interpreter data structures, so it may retained
* indefinitely and safely shared with other threads.
*/
public ImmutableList<CallStackEntry> getCallStack() {
ImmutableList.Builder<CallStackEntry> stack = ImmutableList.builder();
for (Frame fr : callstack) {
stack.add(new CallStackEntry(fr.fn.getName(), fr.loc));
}
return stack.build();
}
@Override
public int hashCode() {
throw new UnsupportedOperationException(); // avoid nondeterminism
}
@Override
public boolean equals(Object that) {
throw new UnsupportedOperationException();
}
@Override
public String toString() {
return String.format("<StarlarkThread%s>", mutability);
}
/** CallProfiler records the start and end wall times of function calls. */
public interface CallProfiler {
Object start(StarlarkCallable fn);
void end(Object span);
}
/** Installs a global hook that will be notified of function calls. */
public static void setCallProfiler(@Nullable CallProfiler p) {
callProfiler = p;
}
@Nullable private static CallProfiler callProfiler = null;
}