Interpreter
This class is used to execute Level Two code, which is a translation of the Level One nybblecodes found in raw functions.
Level One nybblecodes are designed to be compact and very simple, but not particularly efficiently executable. Level Two is designed for a clean model for optimization, including:
primitive folding.
register coloring/allocation.
inlining.
common sub-expression elimination.
side effect analysis.
object escape analysis.
a variant of keyhole optimization that involves building the loosest possible Level Two instruction dependency graph, then "pulling" eligible instruction sequences that are profitably rewritten.
further translation to native code – the L1Translator and L2Generator produce Level Two code, which is immediately translated to JVM bytecodes. This leverages the enormous amount of effort that has gone into the bytecode verifier, concurrency semantics, and HotSpot's low-level optimizations.
To accomplish these goals, the stack-oriented architecture of Level One maps onto a register transfer language for Level Two. At runtime the idealized interpreter has an arbitrarily large bank of pointer registers (that point to Avail objects), plus a separate bank for Ints (unboxed 32-bit signed integers), and a similar bank for Doubles (unboxed double-precision floating point numbers). We leave it to HotSpot to determine how best to map these registers to CPU registers.
One of the less intuitive aspects of the Level One / Level Two mapping is how to handle the call stack. The Level One view is of a chain of continuations, but Level Two doesn't even have a stack! We bridge this disconnect by using a field in the interpreter to hold the caller of the current continuation. The L1InstructionStepper holds arrays of pointers, ints, and doubles for the current continuation.
We also use a technique called "semi-stackless". Under this scheme, most continuations run for a while, use local variables (within the call stack), make normal Java-stack calls of their own, and eventually return their result. However, if while running a function, the need arises to empty the Java call stack into a chain of continuations, we return a StackReifier. When a call to an Avail function returns a StackReifier, the caller records information about its local variables within a (heap-allocated) lambda, adds the lambda to a list within the StackReifier, then returns to its caller. When it returns from the outermost Avail call, the StackReifier's list of lambdas are run in the reverse order, each adding a continuation to the chain.
Later, when one of those continuations has to be "returned into" (calling in Java but returning in Avail), the JVM entry point for that function is invoked in such a way that it restores the register values from the continuation, then continues executing where it left off.
Note that unlike languages like C and C++, optimizations below Level One are always transparent – other than observations about performance and memory use. Also note that this was a design constraint for Avail as far back as 1993, after Self, but before its technological successor Java. The way in which this is accomplished (or will be more fully accomplished) in Avail is by allowing the generated level two code itself to define how to maintain the "accurate fiction" of a level one interpreter. If a method is inlined ten layers deep inside an outer method, a non-inlined call from that inner method requires ten layers of continuations to be constructed prior to the call (to accurately maintain the fiction that it was always simply interpreting Level One nybblecodes). There are ways to avoid or at least postpone this phase transition, but I don't have any solid plans for introducing such a mechanism any time soon.
Finally, note that the Avail control structures are defined in terms of multimethod dispatch and continuation resumption. Multimethod dispatch is implemented in terms of type-tests and conditional jumps in Level Two, so conditional control flow ends up being similar to branches in traditional languages. Loops and exits are accomplished by restarting or exiting continuations. The Level Two optimizer generally identifies situations where a label is created and then used for a restart within the same function, and rewrites that as a backward jump, usually allowing the continuation creation to be elided entirely.
Author
Mark van Gulik
Todd L Smith
Properties
A reusable temporary buffer used to hold arguments during method invocations.
Used by the L2SimpleTranslator. It's fine that it's per-interpreter, since it doesn't have to perfectly canonicalize the arrays, just reduce greatly the amount of repetition of equivalent arrays. The key is a List, just to get the right equality and hash semantics.
A fiber's debugger can only change during a safe point, but at that time no interpreters are bound to fibers, so this can be cached when binding the fiber to the interpreter, and cleared when unbinding.
A fiber's debuggerRunCondition can only change during a safe point, but at that time no interpreters are bound to fibers, so this can be cached when binding the fiber to the interpreter, and cleared when unbinding.
Text to show at the starts of lines in debug traces.
The A_Function being executed. This is only volatile so that the AvailRuntime.clock thread can safely pollActiveRawFunction, then navigate from the A_Function to the A_RawFunction inside it.
Capture a unique ID between 0 and maxInterpreters minus one.
An indication that a reification action is running.
The L1InstructionStepper used to simulate execution of Level One nybblecodes.
An action to run after a fiber exits and is unbound.
A field that captures which A_Function is returning. This is used for statistics collection and reporting errors when returning a value that disagrees with semantic restrictions.
Functions
Add the delta to the current count of how many frames would be reified into continuations at the current execution point.
The given primitive has just executed; do any necessary post-processing.
Answer the specified element of argsBuffer.
Attempt the inlineable primitive.
Attempt the non-inlineable primitive.
Attempt the primitive, dynamically checking whether it is an inlineable primitive.
Answer the AvailLoader associated with the fiber currently running on this interpreter. This interpreter must be bound to a fiber having an AvailLoader.
Answer the AvailLoader associated with the fiber currently running on this interpreter. Answer null if there is no AvailLoader for the current fiber.
Prepare to execute the given primitive. Answer the current time in nanoseconds.
Answer whether the current frame's caller has been fully reified at this time, and is therefore at the top of the getReifiedContinuation call stack.
Assert that the number of arguments in the argsBuffer agrees with the given expected number.
Check if the current chunk is still valid. If so, return true. Otherwise, set the current chunk to the unoptimizedChunk, set the offset to the specified offset within that chunk, and return false.
Utility method for decomposing this object in the debugger. See AvailObjectFieldHelper for instructions to enable this functionality in IntelliJ.
Answer the current fiber bound to this interpreter, or null if there is none.
Answer the latest result produced by a successful primitive, or the latest error code number produced by a failed primitive.
Answer the (bottom) portion of the call stack that has been reified. This must always be either an A_Continuation, nil, or null. It's typed as AvailObject to avoid potential JVM runtime checks.
Prepare the interpreter to execute the given A_Function with the arguments provided in argsBuffer.
Answer the latest result produced by a successful primitive, or the latest error code number produced by a failed primitive. Answer null if no such value is available. This is useful for saving/restoring without knowing whether the value is valid.
Update the guard A_Variable with the new marker number. The variable is a failure variable of a primitive function for P_CatchException, and is used to track exception/unwind states.
Assume the entire stack has been reified. Scan the stack of continuations until one is found for a function whose code specifies P_CatchException. Write the specified marker into its primitive failure variable to indicate the current exception handling state.
Present the name in the debugger.
The primitive was just invoked, producing the result, which must be either SUCCESS or CONTINUATION_CHANGED. Answer null if the primitive indicated success, otherwise answer a StackReifier that will discard the JVM call stack and continue running whatever the getReifiedContinuation was when this method was called.
As the system runs, the clock thread periodically wakes up and samples the running interpreters to get an indication of which A_RawFunctions are taking up time. Those raw functions have their countdowns decreased by a big jump, being careful not to reach or cross zero. That way, the logic for creating an optimized L2Chunks for it remains within the execution mechanism.
Replace the getReifiedContinuation with its caller.
Set the post-exit continuation. The affected fiber will be locked around the evaluation of this continuation.
Do what's necessary after a function invocation, leaving just the given StackReifier on the stack.
Prepare to run a function invocation with an array of arguments.
Prepare to run a function invocation with zero arguments.
Prepare to run a function invocation with one argument.
Prepare to run a function invocation with two arguments.
Prepare to run a function invocation with three arguments.
Set the resulting value of a primitive invocation. Answer primitive failure.
Set the resulting value of a primitive invocation to the numeric code of the specified AvailErrorCode. Answer primitive failure.
Set the resulting value of a primitive invocation to the numeric code of the AvailErrorCode embedded within the specified exception. Answer primitive failure.
Set the resulting value of a primitive invocation to the numeric code of the AvailRuntimeException. Answer primitive failure.
Park the current A_Fiber from within a Primitive invocation. The reified A_Continuation will be available in getReifiedContinuation, and will be installed into the current fiber.
Set the resulting value of a primitive invocation. Answer primitive success.
Suspend the current A_Fiber from within a Primitive invocation. The reified A_Continuation will be available in getReifiedContinuation, and will be installed into the current fiber.
The current fiber has been asked to temporarily cease running for an inter-nybblecode interrupt for some reason. It has possibly executed several more L2 instructions since that time, to place the fiber into a state that's consistent with naive Level One execution semantics. That is, a naive Level One interpreter should be able to resume the fiber later (although most of the time the Level Two interpreter will kick in).
Record the fact that a statement of the given module just took some number of nanoseconds to run.
A primitive has switched the continuation, but it's unknown whether the JVM stack has already been cleared of calls that are no longer in effect. Answer a StackReifier that will discard the frames and then continue with theReifiedContinuation.
Obtain an appropriate StackReifier for restarting the specified continuation.
Obtain an appropriate StackReifier for restarting the specified continuation with the given arguments.
Answer a StackReifier which can be used for reifying the current stack by returning it out to the run loop. When it reaches there, a lambda embedded in this reifier will run, performing an action suitable to the provided flags.
Handle having attempted to read from a variable that does not currently have a value. This shrinks the control flow graph in L2, which is not just a time saving during creation and memory saving ongoing, but may also increase HotSpot's effectiveness.
Handle a return value that doesn't satisfy its expected type out-of-line. This shrinks the control flow graph in L2, which is not just a time saving during creation and memory saving ongoing, but may also increase HotSpot's effectiveness.
Run the current L2Chunk to completion. Note that a reification request may cut this short. Also note that this interpreter indicates the offset at which to start executing. For an initial invocation, the argsBuffer will have been set up for the call. For a return into this continuation, the offset will refer to code that will rebuild the register set from the top reified continuation, using the latestResult. For resuming the continuation, the offset will point to code that also rebuilds the register set from the top reified continuation, but it won't expect a return value. These re-entry points should perform validity checks on the chunk, allowing an orderly off-ramp into the unoptimizedChunk (which simply interprets the L1 nybblecodes).
Raise an exception. Scan the stack of continuations (which must have been reified already) until one is found for a function whose code specifies P_CatchException. Get that continuation's second argument (a handler block of one argument), and check if that handler block will accept the exceptionValue. If not, keep looking. If it accepts it, unwind the continuation stack so that the primitive catch method is the top entry, and invoke the handler block with exceptionValue. If there is no suitable handler block, fail the primitive.
Set the latest result due to a successful primitive, or the latest error code A_Number produced by a failed primitive.
Set the current reified A_Continuation.
Set the variable trace flag.
Set the variable trace flag.
Suspend the current fiber, evaluating the provided action. The action is passed two additional actions, one indicating how to resume from the suspension in the future (taking the result of the primitive), and the other indicating how to cause the primitive to fail (taking an AvailErrorCode).
Suspend the interpreter in the middle of running a primitive (which must be marked as CanSuspend). The supplied action can invoke succeed or fail when it has determined its fate.
Should the Interpreter record which A_Variables are read before written while running its current A_Fiber?
Should the Interpreter record which A_Variables are written while running its current A_Fiber?
Answer how many continuations would be created from Java stack frames at the current execution point (or the nearest place reification may be triggered).