bazel syntax: simplify StarlarkThread call stack
This change slightly simplifies the stack maintained by StarlarkThread,
and signposts the next steps.
StarlarkThread.Continuation
- rename it to CallFrame
- turn it from linked list into an ArrayList
- clarify but retain its current mostly off-by-one semantics for now
- replace FuncallExpression by just Location
- rename {enter,exit}Scope to push/pop and simplify
- simplify debugger operations in terms of call stack depth
Also:
- rename StarlarkFunction.getDefinitionGlobals -> getModule
- move dummy LexicalFrame for builtins to StarlarkThread.
- move calls to thread.push outside the try block, so we
so don't unintentionally suppress crashes.
Originally reviewed as CL 283349613 but split out.
PiperOrigin-RevId: 283617971
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/StarlarkThread.java b/src/main/java/com/google/devtools/build/lib/syntax/StarlarkThread.java
index 8913ade..7489bd8 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/StarlarkThread.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/StarlarkThread.java
@@ -18,6 +18,8 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
@@ -90,19 +92,13 @@
// 3) Debugging support (thread name, profiling counters, etc).
// And that is all. See go.starlark.net for the model.
//
-// The Frame interface should be hidden from clients and then eliminated.
-// The dynamic lookup mechanism should go away.
-// The Module class should be redesigned.
-// The concept struggling to get out of it is a Module,
-// which is created before file initialization and
-// populated by execution of the top-level statements in a file;
-// every UserDefinedFunction value should hold a reference to its Module.
+// The Frame interface should eliminated.
// As best I can tell, all the skyframe serialization
// as it applies to LexicalFrames is redundant, as these are transient
// and should not exist after loading.
// We will remove the FuncallExpression parameter from StarlarkFunction.call.
// Clients should use getCallerLocation instead.
-// The Continuation class should be deleted.
+// The only place that still needs an AST is Bazel's generator_name.
// Once the API is small and sound, we can start to represent all
// the lexical frames within a single function using just an array,
// indexed by a small integer computed during the validation pass.
@@ -167,16 +163,6 @@
}
interface LexicalFrame extends Frame {
- static LexicalFrame create(Mutability mutability) {
- return mutability.isFrozen()
- ? ImmutableEmptyLexicalFrame.INSTANCE
- : new MutableLexicalFrame(mutability);
- }
-
- static LexicalFrame create(Mutability mutability, int numArgs) {
- Preconditions.checkState(!mutability.isFrozen());
- return new MutableLexicalFrame(mutability, /*initialCapacity=*/ numArgs);
- }
}
private static final class ImmutableEmptyLexicalFrame implements LexicalFrame {
@@ -288,36 +274,38 @@
return v == null ? null : key.cast(v);
}
- /**
- * A Continuation contains data saved during a function call and restored when the function exits.
- */
- private static final class Continuation {
- /** The {@link BaseFunction} being evaluated that will return into this Continuation. */
- final BaseFunction function;
+ /** A CallFrame records information about an active function call. */
+ // TODO(adonovan): merge LexicalFrame into CallFrame. Every function call should have a frame,
+ // but only Starlark functions need local variables.
+ private static final class CallFrame {
+ final StarlarkCallable fn; // the called function
- /** The {@link FuncallExpression} to which this Continuation will return. */
- @Nullable final FuncallExpression caller;
+ // Note that the inherited design is off-by-one:
+ // the following fields are logically facts about the _enclosing_ frame.
+ // This is a consequence of not representing toplevel statements as a function.
+ // TODO(adonovan): fix that.
- /** The next Continuation after this Continuation. */
- @Nullable final Continuation continuation;
+ final Location callerLoc; // location of the enclosing call (may be Location.BUILTIN)
+ @Nullable final FuncallExpression call; // syntax of the enclosing call
+ final Frame savedLexicals; // the saved lexicals of the parent
+ final Module savedModule; // the saved module of the parent (TODO(adonovan): eliminate)
- /** The lexical Frame of the caller. */
- final Frame lexicalFrame;
+ CallFrame(
+ StarlarkCallable fn,
+ Location callerLoc,
+ @Nullable FuncallExpression call,
+ Frame savedLexicals,
+ Module savedModule) {
+ this.fn = fn;
+ this.callerLoc = callerLoc;
+ this.call = call;
+ this.savedLexicals = savedLexicals;
+ this.savedModule = savedModule;
+ }
- /** The global Frame of the caller. */
- final Module globalFrame;
-
- Continuation(
- @Nullable Continuation continuation,
- BaseFunction function,
- @Nullable FuncallExpression caller,
- Frame lexicalFrame,
- Module globalFrame) {
- this.continuation = continuation;
- this.function = function;
- this.caller = caller;
- this.lexicalFrame = lexicalFrame;
- this.globalFrame = globalFrame;
+ @Override
+ public String toString() {
+ return fn.getName() + "@" + callerLoc;
}
}
@@ -483,18 +471,15 @@
}
}
- /**
- * Static Frame for lexical variables that are always looked up in the current StarlarkThread or
- * for the definition StarlarkThread of the function currently being evaluated.
- */
+ // Local environment of the current active call,
+ // or an alias for globalFrame if no calls are active.
+ // TODO(adonovan): redundant with callstack; eliminate once we fix off-by-one problem.
private Frame lexicalFrame;
- /**
- * Static Frame for global variables; either the current lexical Frame if evaluation is currently
- * happening at the global scope of a BUILD file, or the global Frame at the time of function
- * definition if evaluation is currently happening in the body of a function. Thus functions can
- * close over other functions defined in the same file.
- */
+ // Global environment of the current topmost call frame,
+ // or of the file about to be initialized if no calls are active.
+ // TODO(adonovan): eliminate once we represent even toplevel statements
+ // as a StarlarkFunction that closes over its Module.
private Module globalFrame;
/** The semantics options that affect how Skylark code is evaluated. */
@@ -511,39 +496,51 @@
*/
private final Map<String, Extension> importedExtensions;
- /**
- * When in a lexical (Skylark) frame, this lists the names of the functions in the call stack. We
- * currently use it to artificially disable recursion.
- */
- @Nullable private Continuation continuation;
+ /** Stack of active function calls. */
+ // TODO(adonovan): currently off by one because top-level statements don't have a CallFrame.
+ private final ArrayList<CallFrame> callstack = new ArrayList<>();
/** A hook for notifications of assignments at top level. */
PostAssignHook postAssignHook;
/**
- * Enters a scope by saving state to a new Continuation
+ * Pushes a function onto the call stack.
*
- * @param function the function whose scope to enter
- * @param lexical the lexical frame to use
- * @param caller the source AST node for the caller
- * @param globals the global Frame that this function closes over from its definition
- * StarlarkThread
+ * @param fn the function whose scope to enter
+ * @param loc the source location of the function call.
*/
- void enterScope(
- BaseFunction function, Frame lexical, @Nullable FuncallExpression caller, Module globals) {
- continuation = new Continuation(continuation, function, caller, lexicalFrame, globalFrame);
- lexicalFrame = lexical;
- globalFrame = globals;
+ void push(StarlarkCallable fn, Location loc, @Nullable FuncallExpression call) {
+ callstack.add(new CallFrame(fn, loc, call, this.lexicalFrame, this.globalFrame));
+
+ if (fn instanceof StarlarkFunction) {
+ StarlarkFunction sfn = (StarlarkFunction) fn;
+ this.lexicalFrame =
+ new MutableLexicalFrame(
+ this.mutability(), /*initialCapacity=*/ sfn.getSignature().numParameters());
+ this.globalFrame = sfn.getModule();
+ } else {
+ // built-in function
+ this.lexicalFrame = DUMMY_LEXICAL_FRAME;
+ // this.globalFrame is left as is.
+ // For built-ins, thread.globals() returns the module
+ // of the file from which the built-in was called.
+ // Really they have no business knowing about that.
+ }
}
- /** Exits a scope by restoring state from the current continuation */
- void exitScope() {
- Preconditions.checkNotNull(continuation);
- lexicalFrame = continuation.lexicalFrame;
- globalFrame = continuation.globalFrame;
- continuation = continuation.continuation;
+ /** Pops a function off the call stack. */
+ void pop() {
+ int last = callstack.size() - 1;
+ CallFrame top = callstack.get(last);
+ callstack.remove(last); // pop
+ this.lexicalFrame = top.savedLexicals;
+ this.globalFrame = top.savedModule;
}
+ // Builtins cannot create or modify variable bindings,
+ // so it's sufficient to use a shared instance.
+ private static final LexicalFrame DUMMY_LEXICAL_FRAME = ImmutableEmptyLexicalFrame.INSTANCE;
+
private final String transitiveHashCode;
/**
@@ -583,33 +580,29 @@
* supplied function is already on the stack.
*/
boolean isRecursiveCall(StarlarkFunction function) {
- for (Continuation k = continuation; k != null; k = k.continuation) {
+ for (CallFrame fr : callstack) {
// TODO(adonovan): compare code, not closure values, otherwise
// one can defeat this check by writing the Y combinator.
- if (k.function.equals(function)) {
+ if (fr.fn.equals(function)) {
return true;
}
}
return false;
}
- /** Returns the current function call, if it exists. */
- @Nullable
- BaseFunction getCurrentFunction() {
- return continuation != null ? continuation.function : null;
+ /** Returns the current called function, which must exist. */
+ StarlarkCallable getCurrentFunction() {
+ return Iterables.getLast(callstack).fn;
}
- /** Returns the FuncallExpression and the BaseFunction for the top-level call being evaluated. */
- // TODO(adonovan): replace this by an API for walking the call stack.
- public Pair<FuncallExpression, BaseFunction> getTopCall() {
- Continuation continuation = this.continuation;
- if (continuation == null) {
+ /** Returns the call expression and called function for the outermost call being evaluated. */
+ // TODO(adonovan): replace this by an API for walking the call stack, then move to lib.packages.
+ public Pair<FuncallExpression, StarlarkCallable> getOutermostCall() {
+ if (callstack.isEmpty()) {
return null;
}
- while (continuation.continuation != null) {
- continuation = continuation.continuation;
- }
- return new Pair<>(continuation.caller, continuation.function);
+ CallFrame outermost = callstack.get(0);
+ return new Pair<>(outermost.call, outermost.fn);
}
/**
@@ -864,8 +857,7 @@
* Returns a set of all names of variables that are accessible in this {@code StarlarkThread}, in
* a deterministic order.
*/
- // TODO(adonovan): eliminate sole external call from docgen.
- public Set<String> getVariableNames() {
+ Set<String> getVariableNames() {
LinkedHashSet<String> vars = new LinkedHashSet<>();
vars.addAll(lexicalFrame.getTransitiveBindings().keySet());
// No-op when globalFrame = lexicalFrame
@@ -886,33 +878,29 @@
* current context. The innermost frame's location must be supplied as {@code currentLocation} by
* the caller.
*/
- public ImmutableList<DebugFrame> listFrames(Location currentLocation) {
+ public ImmutableList<DebugFrame> listFrames(Location loc) {
ImmutableList.Builder<DebugFrame> frameListBuilder = ImmutableList.builder();
- Continuation currentContinuation = continuation;
- Frame currentFrame = lexicalFrame;
-
- // if there's a continuation then the current frame is a lexical frame
- while (currentContinuation != null) {
+ Frame lex = this.lexicalFrame;
+ for (CallFrame fr : Lists.reverse(callstack)) {
frameListBuilder.add(
DebugFrame.builder()
- .setLexicalFrameBindings(ImmutableMap.copyOf(currentFrame.getTransitiveBindings()))
+ .setLexicalFrameBindings(ImmutableMap.copyOf(lex.getTransitiveBindings()))
.setGlobalBindings(ImmutableMap.copyOf(getGlobals().getTransitiveBindings()))
- .setFunctionName(currentContinuation.function.getName())
- .setLocation(currentLocation)
+ .setFunctionName(fr.fn.getName())
+ .setLocation(loc)
.build());
-
- currentFrame = currentContinuation.lexicalFrame;
- currentLocation =
- currentContinuation.caller != null ? currentContinuation.caller.getLocation() : null;
- currentContinuation = currentContinuation.continuation;
+ lex = fr.savedLexicals;
+ loc = fr.callerLoc;
}
-
+ // TODO(adonovan): simplify by fixing the callstack's off-by-one problem.
+ // We won't need to pass in 'loc' nor add a fake <top level> frame, nor
+ // suffer a loop-carried dependence.
frameListBuilder.add(
DebugFrame.builder()
.setGlobalBindings(ImmutableMap.copyOf(getGlobals().getTransitiveBindings()))
.setFunctionName("<top level>")
- .setLocation(currentLocation)
+ .setLocation(loc)
.build());
return frameListBuilder.build();
@@ -930,8 +918,7 @@
*/
@Nullable
public ReadyToPause stepControl(Stepping stepping) {
- final Continuation pausedContinuation = continuation;
-
+ final int depth = callstack.size();
switch (stepping) {
case NONE:
return null;
@@ -939,10 +926,10 @@
// pause at the very next statement
return thread -> true;
case OVER:
- return thread -> isAt(thread, pausedContinuation) || isOutside(thread, pausedContinuation);
+ return thread -> thread.callstack.size() <= depth;
case OUT:
- // if we're at the outer-most frame, same as NONE
- return pausedContinuation == null ? null : thread -> isOutside(thread, pausedContinuation);
+ // if we're at the outermost frame, same as NONE
+ return depth == 0 ? null : thread -> thread.callstack.size() < depth;
}
throw new IllegalArgumentException("Unsupported stepping type: " + stepping);
}
@@ -975,17 +962,6 @@
OUT,
}
- /** Returns true if {@code thread} is in a parent frame of {@code pausedContinuation}. */
- private static boolean isOutside(
- StarlarkThread thread, @Nullable Continuation pausedContinuation) {
- return pausedContinuation != null && thread.continuation == pausedContinuation.continuation;
- }
-
- /** Returns true if {@code thread} is at the same frame as {@code pausedContinuation. */
- private static boolean isAt(StarlarkThread thread, @Nullable Continuation pausedContinuation) {
- return thread.continuation == pausedContinuation;
- }
-
@Override
public int hashCode() {
throw new UnsupportedOperationException(); // avoid nondeterminism