Interpreter

class Interpreter(val runtime: AvailRuntime)

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

Constructors

Link copied to clipboard
fun Interpreter(runtime: AvailRuntime)

Types

Link copied to clipboard
object Companion
Link copied to clipboard
class SuspensionHelper<A>(toSucceed: (A) -> Unit, toFail: (A_BasicObject) -> Unit)

A helper class for making fiber suspension syntax more articulate. It provides succeed and fail methods that client code can invoke.

Functions

Link copied to clipboard
fun abortFiber()

Abort the current fiber.

Link copied to clipboard
fun adjustUnreifiedCallDepthBy(delta: Int)

Add the delta to the current count of how many frames would be reified into continuations at the current execution point.

Link copied to clipboard
fun afterAttemptPrimitive(    primitive: Primitive,     timeBefore: Long,     success: Primitive.Result): Primitive.Result

The given primitive has just executed; do any necessary post-processing.

Link copied to clipboard
fun argument(zeroBasedIndex: Int): AvailObject

Answer the specified element of argsBuffer.

Link copied to clipboard
fun attemptInlinePrimitive(primitiveFunction: A_Function, primitive: Primitive): StackReifier?

Attempt the inlineableprimitive.

Link copied to clipboard
fun attemptNonInlinePrimitive(primitiveFunction: A_Function?, primitive: Primitive): StackReifier
Link copied to clipboard
fun attemptThePrimitive(primitiveFunction: A_Function, primitive: Primitive): StackReifier?

Attempt the primitive, dynamically checking whether it is an inlineable primitive.

Link copied to clipboard
fun availLoader(): AvailLoader

Answer the AvailLoader associated with the fiber currently running on this interpreter. This interpreter must be bound to a fiber having an AvailLoader.

Link copied to clipboard
fun availLoaderOrNull(): AvailLoader?

Answer the AvailLoader associated with the fiber currently running on this interpreter. Answer null if there is no AvailLoader for the current fiber.

Link copied to clipboard
fun beforeAttemptPrimitive(primitive: Primitive): Long

Prepare to execute the given primitive. Answer the current time in nanoseconds.

Link copied to clipboard
fun callerIsReified(): Boolean

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.

Link copied to clipboard
fun checkArgumentCount(expectedCount: Int)

Assert that the number of arguments in the argsBuffer agrees with the given expected number.

Link copied to clipboard
fun checkValidity(offsetInDefaultChunkIfInvalid: Int): Boolean

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.

Link copied to clipboard
fun describeForDebugger(): Array<AvailObjectFieldHelper?>

Utility method for decomposing this object in the debugger. See AvailObjectFieldHelper for instructions to enable this functionality in IntelliJ.

Link copied to clipboard
fun fiber(): A_Fiber

Return the current fiber.

fun fiber(newFiber: A_Fiber?, tempDebug: String?)

Bind the specified runningfiber to the Interpreter, or unbind the current fiber.

Link copied to clipboard
fun fiberOrNull(): A_Fiber?

Answer the current fiber bound to this interpreter, or null if there is none.

Link copied to clipboard
fun getLatestResult(): AvailObject

Answer the latest result produced by a successfulprimitive, or the latest error code number produced by a failed primitive.

Link copied to clipboard
fun getReifiedContinuation(): AvailObject?

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.

Link copied to clipboard
fun invokeFunction(aFunction: A_Function): StackReifier?

Prepare the interpreter to execute the given A_Function with the arguments provided in argsBuffer.

Link copied to clipboard
fun latestResultOrNull(): AvailObject?

Answer the latest result produced by a successfulprimitive, 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.

Link copied to clipboard
fun <T> lockFiberWhile(aFiber: A_Fiber, supplier: () -> T): T

Lock the specified fiber for the duration of evaluation of the provided Supplier. Answer the result produced by the supplier.

Link copied to clipboard
fun markGuardVariable(guardVariable: A_Variable, marker: A_Number): Primitive.Result

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.

Link copied to clipboard
fun markNearestGuard(marker: A_Number): Primitive.Result

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.

Link copied to clipboard
fun module(): A_Module

Answer the A_Module being loaded by this interpreter's loader. If there is no loader then answer nil.

Link copied to clipboard
fun nameForDebugger(): String

Present the name in the debugger.

Link copied to clipboard
fun optionalReifierIfCanSwitchContinuations(primitive: Primitive, result: Primitive.Result): StackReifier?

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.

Link copied to clipboard
fun pollActiveRawFunction(): A_RawFunction?

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.

Link copied to clipboard
fun popContinuation()

Replace the getReifiedContinuation with its caller.

Link copied to clipboard
fun postExitContinuation(continuation: () -> Unit?)

Set the post-exit continuation. The affected fiber will be locked around the evaluation of this continuation.

Link copied to clipboard
fun postinvoke(    callingChunk: L2Chunk,     callingFunction: A_Function,     reifier: StackReifier?): StackReifier?

Do what's necessary after a function invocation, leaving just the given StackReifier on the stack.

Link copied to clipboard
fun preinvoke(calledFunction: A_Function, args: Array<AvailObject>): AvailObject

Prepare to run a function invocation with an array of arguments.

Link copied to clipboard
fun preinvoke0(calledFunction: A_Function): AvailObject

Prepare to run a function invocation with zero arguments.

Link copied to clipboard
fun preinvoke1(calledFunction: A_Function, arg1: AvailObject): AvailObject

Prepare to run a function invocation with one argument.

Link copied to clipboard
fun preinvoke2(    calledFunction: A_Function,     arg1: AvailObject,     arg2: AvailObject): AvailObject

Prepare to run a function invocation with two arguments.

Link copied to clipboard
fun preinvoke3(    calledFunction: A_Function,     arg1: AvailObject,     arg2: AvailObject,     arg3: AvailObject): AvailObject

Prepare to run a function invocation with three arguments.

Link copied to clipboard
fun primitiveFailure(result: A_BasicObject): Primitive.Result

Set the resulting value of a primitive invocation. Answer primitive failure.

fun primitiveFailure(code: AvailErrorCode): Primitive.Result

Set the resulting value of a primitive invocation to the numeric code of the specified AvailErrorCode. Answer primitive failure.

fun primitiveFailure(exception: AvailException): Primitive.Result

Set the resulting value of a primitive invocation to the numeric code of the AvailErrorCode embedded within the specified exception. Answer primitive failure.

fun primitiveFailure(exception: AvailRuntimeException): Primitive.Result

Set the resulting value of a primitive invocation to the numeric code of the AvailRuntimeException. Answer primitive failure.

Link copied to clipboard
fun primitivePark(suspendingFunction: A_Function): Primitive.Result

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.

Link copied to clipboard
fun primitiveSuccess(result: A_BasicObject): Primitive.Result

Set the resulting value of a primitive invocation. Answer primitive success.

Link copied to clipboard
fun primitiveSuspend(suspendingFunction: A_Function): Primitive.Result

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.

Link copied to clipboard
fun processInterrupt(continuation: A_Continuation)

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).

Link copied to clipboard
fun recordTopStatementEvaluation(sample: Double, module: A_Module)

Record the fact that a statement of the given module just took some number of nanoseconds to run.

Link copied to clipboard
fun reifierForChangedContinuation(primitive: Primitive): StackReifier

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.

Link copied to clipboard
fun reifierToRestart(continuation: A_Continuation): StackReifier

Obtain an appropriate StackReifier for restarting the specified continuation.

Link copied to clipboard
fun reifierToRestartWithArguments(continuation: A_Continuation, arguments: Array<AvailObject>): StackReifier

Obtain an appropriate StackReifier for restarting the specified continuation with the given arguments.

Link copied to clipboard
fun reify(    actuallyReify: Boolean,     processInterrupt: Boolean,     statistic: Statistic): StackReifier

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.

Link copied to clipboard
fun reportUnassignedVariableRead(    pc: Int,     stackp: Int,     vararg slots: A_BasicObject): StackReifier

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.

Link copied to clipboard
fun reportWrongReturnType(    returnedValueOrNil: A_BasicObject,     expectedReturnType: A_Type,     pc: Int,     stackp: Int,     vararg slots: A_BasicObject): StackReifier

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.

Link copied to clipboard
fun run()

Run the interpreter until it completes the fiber, is suspended, or is interrupted, perhaps by exceeding its time-slice.

Link copied to clipboard
fun runChunk(): StackReifier?

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).

Link copied to clipboard
fun searchForExceptionHandler(exceptionValue: AvailObject): Primitive.Result

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.

Link copied to clipboard
fun setLatestResult(newResult: A_BasicObject?)

Set the latest result due to a successfulprimitive, or the latest error codeA_Number produced by a failed primitive.

Link copied to clipboard
fun setOffset(newOffset: Int)

Jump to a new position in the L2 instruction stream.

Link copied to clipboard
fun setReifiedContinuation(continuation: A_Continuation?)

Set the current reified A_Continuation.

Link copied to clipboard
fun setTraceVariableReadsBeforeWrites(traceVariableReadsBeforeWrites: Boolean)

Set the variable trace flag.

Link copied to clipboard
fun setTraceVariableWrites(traceVariableWrites: Boolean)

Set the variable trace flag.

Link copied to clipboard

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).

Link copied to clipboard

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.

Link copied to clipboard
fun terminateFiber(value: A_BasicObject)

Terminate the current fiber, using the specified object as its final result.

Link copied to clipboard
open override fun toString(): String
Link copied to clipboard
fun traceVariableReadsBeforeWrites(): Boolean

Should the Interpreter record which A_Variables are read before written while running its current A_Fiber?

Link copied to clipboard
fun traceVariableWrites(): Boolean

Should the Interpreter record which A_Variables are written while running its current A_Fiber?

Link copied to clipboard
fun unreifiedCallDepth(): Int

Answer how many continuations would be created from Java stack frames at the current execution point (or the nearest place reification may be triggered).

Properties

Link copied to clipboard
val argsBuffer: MutableList<AvailObject>

A reusable temporary buffer used to hold arguments during method invocations.

Link copied to clipboard
val arraysForL2Simple: MutableMap<List<Int>, IntArray>

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.

Link copied to clipboard
var chunk: L2Chunk? = null

The L2Chunk being executed.

Link copied to clipboard
var debugger: AvailDebuggerModel? = null

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.

Link copied to clipboard
var debuggerRunCondition: (A_Fiber) -> Boolean? = null

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.

Link copied to clipboard
var debugModeString: String

Text to show at the starts of lines in debug traces.

Link copied to clipboard
var exitNow: Boolean = true

Should the Interpreter exit its run loop? This can happen when the fiber has completed, failed, or been suspended.

Link copied to clipboard
var function: A_Function? = null

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.

Link copied to clipboard
val interpreterIndex: Int

Capture a unique ID between 0 and maxInterpreters minus one.

Link copied to clipboard
val isInterruptRequested: Boolean

Answer true if an interrupt has been requested. The interrupt may be specific to the current fiber or global to the runtime. There are several reasons why an interrupt might be requested:

Link copied to clipboard
var isReifying: Boolean = false

An indication that a reification action is running.

Link copied to clipboard
val levelOneStepper: L1InstructionStepper

The L1InstructionStepper used to simulate execution of Level One nybblecodes.

Link copied to clipboard
var offset: Int = 0

The current zero-based L2 offset within the current L2Chunk's instructions.

Link copied to clipboard
var postExitContinuation: () -> Unit? = null

An action to run after a fiber exits and is unbound.

Link copied to clipboard
var returningFunction: A_Function? = null

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.

Link copied to clipboard
var returnNow: Boolean = false

Should the current executing chunk return to its caller? The value to return is in latestResult. If the outer interpreter loop detects this, it should resume the top reified continuation's chunk, giving it an opportunity to accept the return value and de-reify.

Link copied to clipboard
val runtime: AvailRuntime