Progress / July 2026

Written by Reuben RoesslerMon Aug 03 2026

Status Update

The month of July has been productive with a baseline rewrite soon to be ready ⚔️1 for a canary release. This month I have been rewriting the interpreter to be tail-called instead of directed-threaded ⚔️2 which has lead to some intriguing results. Additionally, I have started investigating the viability of directly compiled various functions to a baseline JIT on initial compilation.

Interpreter Rework

For some background, bytecode interpreters typically dispatch a sequence of instructions where a basic implementation would use a switch case:

// The program, a list of encoded instructions
let bytecode: List[Number] = [...];

// A simple decoder for instructions
let decode = fn (encoded: Number): Instruction => Todo();

// A basic handler for dispatching instructions using `match`
let dispatch = fn (offset: Number) {
    // Decode the incoming instruction
    let instruction = decode(bytecode.get(offset));

    // Other languages could use `switch` statements
    match (instruction.opcode()) {
        Opcode.ADD: Todo(),
        Opcode.SUB: Todo(),
        *: panic "Unknown Opcode",
    }
}

// Iterate over the available encoded instructions
for (offset in Iterator.range(bytecode.size())) dispatch(offset);

Previously ⚔️2, Sabre implemented the interpreter using direct-threading. Although this solution has great performance, it could be relatively finicky to fine-tune since it relied on all cases being perfectly inlined ⚔️3 to maintain this performance. During the current rewrite, I decided to instead opt for a tail-call interpreter since this should come with similar benefits to the direct-thread approach whilst also simplifying compilation heuristics.

Note:

The match statement is currently unimplemented in v0.1.2 and will be available when v0.2.0 is released.

Baseline JIT Compilation

Many modern dynamic programming languages implement some form a JIT within their compilation/optimization process. For Sabre, I wanted to make this JIT opt-in through a specialized attribute:

#[Internal.Optimize]
let foo = fn => 42;

Since this is only a baseline JIT, it should be utilized sparingly to improve performance. This is because the current downside to this implementation is that the outputs require storage space with simple functions requiring a minimum of ~256 bytes ⚔️4. Statistics of jitted functions is availble through the hidden --dump-assembly flag.

Upcoming Work

After work on the baseline JIT has been completed, the next goal is to prepare a canary release for Sabre from the work that has been implemented thus far. This canary release will also include the foundations for the match statement.

Footnotes

⚔️ 1  —  Currently GitHub CI integration needs to be revised for releases.

⚔️ 2  —  Only up to v0.1.2 was a direct-thread interpreter used.

⚔️ 3  —  Direct-thread interpreters work best with small functions as compiler heuristics may remove the indirect-jump if a function is too large effectively reversing the performance gain.

⚔️ 4  —  Functions that return constants/strings resolve to much smaller values (eg: around 16-36 bytes), whilst method calls can be much larger (around 200+ bytes).