Compilation model
The MysoreScript is a simple two-tier implementation of a language. The first tier is an interpreter, which can be extended to collect profiling information. The second tier is an LLVM-based JIT.
The basic implementation does not perform on-stack replacement (also known as deoptimisation or side exits) and transfers control between the interpreter only at function boundaries.
The compiler follows an object oriented style, with each AST node knowing how to interpret and compile itself.
The root class for all AST nodes is AST::Statement
.
This defines interpret
and compile
methods.
Subclasses of AST::Expression
share an implementation of interpret
which calls evaluate
, allowing them to return a value.
This method calls isConstantExpression
and caches the return value (effectively performing constant folding) and calls the evaluateExpr
, allowing subclasses to perform the evaluation.
Initially, all functions are executed by the interpreter.
After a ClosureDecl
has executed ClosureDecl::compileThreshold
times, the compiler will run and provide a compiled implementation of the function (in MysoreScript, closures and methods are represented by the same class).
Calling methods and Closures
The evaluateExpr
method of the AST::Call
expression class always performs a normal C function call in the same way, irrespective of whether the target function is compiled or interpreted.
The compiled code works in the same way, so we’ll just look at the interpreter for now.
Calling a method is two-stage process:
- The interpreter calls
compiledMethodForSelector
, which returns a pointer to the method. - The interpreter calls
callCompiledMethod
, which casts the function pointer to one accepting the provided number of arguments and then calls it.
If the method has been compiled, then the function pointer is the one returned by the LLVM JIT.
When calling a method that has not yet been compiled, the returned function pointer will be a trampoline, for example methodTrampoline0
(the zero-argument method trampoline), which looks up the correct AST node and then invokes the interpreter.
This mechanism currently limits the interpreter to handling methods with no more than 10 arguments, a limit that could be improved with a small amount of run-time code generation.
You could try replacing the static trampolines with some simple JIT’d functions generated with LLVM.