Stack reification via trampolines
21 February 2016 (programming language java)I've been spending a couple weeks now on getting Időrégész to Android in the best possible way. Időrégész is the Hungarian text adventure game from the '80s that I reverse-engineered and then re-implemented in Inform. My original plan was to just implement a Glulx interpreter; however, initial experiments with a Clojure implementation weren't too promising on the performance front.
I decided to turn to compilation instead of interpretation, then: take the Inform-emitted Glulx image, and compile that directly to the JVM. Of course, that approach would have its own problems with self-modifying code, but my goal is just to get it working well enough that I can compile Időrégész, which is very vanilla as far as Inform programs go.
Most instructions of Glulx map to JVM instructions in a relatively straightforward manner; some unsigned integer operations are implemented by hand in Java and then called via invokestatic. The rather baroque string compression is currently handled at compile time, by just emitting a single Java function that looks like this:
public String resolveString(int ptr)
{
switch (ptr) {
case 32851 : return "Class" ;
case 32857 : return "Object" ;
case 32863 : return "Routine" ;
// and so on...
}However, there is one aspect of Glulx that makes the compilation trickier than a straightforward mapping of procedures to procedures and calls to calls: some Glulx opcodes provide fairly detailed access to the runtime state. For example, save must be able to serialize enough of the state that it can later load that back and continue with the next instruction.
In an interpreter, this is a trival matter, since things like the VM's memory and the stack are already reified data structures. However, if it's compiled to JVM, the state of the Glulx program and the state of the JVM is one and the same; so how do you then save it — from the inside?
The solution is to not use the JVM's stack as the Glulx stack; rather, function calls are compiled to areturn instructions that return a small Java object describing which function to call and where to jump back once its result is available. Returns are also compiled to areturn instructions, but this time returning an object describing the result value. Function-local variables and the per-function stack are passed to the generated JVM code as function arguments:
Each Glulx function is compiled into a class that extends the Glulx.Fun class, defined in Kotlin as:
package Glulx
abstract class Fun {
abstract class Cont {
class Return(val value : Int) : Cont()
class Call(val args: IntArray, val nextFun : Fun, val contPC : Short) : Cont()
}
class Result(val value : Int, val contPC : Short)
abstract fun enter(stub: Stack.CallStub?, args: IntArray): Stack.CallFrame
abstract fun exec(result: Result?, localVars: IntArray): Cont
}Since the JVM doesn't support computed jumps, the continuation address contPC is handled by starting the exec of each Fun with a big tableswitch. Here's an example of a recursively defined factorial function (using the Krakatau JVM assembler's syntax):
.method public exec : (LGlulx/Fun$Result;[I)LGlulx/Fun$Cont;
.code stack 10 locals 10
aload_1
ifnull LSTART
aload_1
invokevirtual Method Glulx/Fun$Result getContPC ()S
tableswitch 0
LCONT0
default: LSTART
LSTART:
;; if V0=0, jump to base case
aload_2
ldc 0
iaload
ifeq L0
;; START call FACT(V0-1)
ldc 1
newarray int
dup
ldc 0
aload_2
ldc 0
iaload
ldc 1
isub
iastore
new Glulx/Fun$Cont$Call
swap
dup2
getstatic Field Glulx/Image/FACT fun LGlulx/Fun;
ldc 0
invokespecial Method Glulx/Fun$Cont$Call <init> ([ILGlulx/Fun;S)V
pop
areturn
LCONT0:
aload_1
invokevirtual Method Glulx/Fun$Result getValue ()I
;; END call FACT(V0-1)
;; Note the code generated for the call spans an areturn!
;; Multiply result by V0
aload_2
ldc 0
iaload
imul
;; Return result -- this is the "real" return
new Glulx/Fun$Cont$Return
swap
dup2
invokespecial Method Glulx/Fun$Cont$Return <init> (I)V
pop
areturn
L0:
;; For the base case, we just return 1
new Glulx/Fun$Cont$Return
dup
ldc 1
invokespecial Method Glulx/Fun$Cont$Return <init> (I)V
areturn
.end code
.end methodRunning these functions then becomes a matter of mere stack juggling, implemented again in Kotlin:
package Glulx
class Stack {
class CallFrame(val parent: CallStub?, val localVars: IntArray) {
constructor(parent: CallStub?, localVarCount: Int):
this(parent, IntArray(localVarCount))
fun storeArgs(args: IntArray) {
for(i in args.zip(localVars).indices)
localVars[i] = args[i]
}
}
class CallStub(val parent: CallFrame, val parentFun: Fun, val parentPC : Short)
}
fun run(startFun: Fun) {
var frame = startFun.enter(null, IntArray(0))
var fn = startFun
var result : Fun.Result? = null
while (true) {
val cont = fn.exec(result, frame.localVars)
when (cont) {
is Fun.Cont.Return -> {
val stub = frame.parent
if (stub == null) return
frame = stub.parent
fn = stub.parentFun
result = Fun.Result(cont.value, stub.parentPC)
}
is Fun.Cont.Call -> {
val stub = Stack.CallStub(frame, fn, cont.contPC)
fn = cont.nextFun
frame = fn.enter(stub, cont.args)
result = null
}
}
}
}In the real implementation, there's slightly more state to pass around: Fun.exec also gets as argument an instance of a generic Environment class which it can use to e.g. access the main Glulx memory, or to issue Glk calls; and there are some boring details about handling both 32-, 16- and 8-bit local variables.
Note that the exact same technique can also be used to implement tail recursion (even mutual tail recursion) on a platform that doesn't support it, like the JVM. I am not using it here, but Glulx actually has a tailcall instruction (not used by Időrégész, or sane Inform code in general), so it might come handy if I want to increase feature coverage.