Bytecode-VM Turtle
Games & Classics
A tiny stack-machine bytecode interpreter, running live on a Super
Nintendo, drawing LOGO turtle graphics.
The interpreter is the whole point: its main switch(op) over a dense opcode range
compiles to a jump table — a JMP (abs,X) computed branch through a table
of code addresses — and the arithmetic opcodes dispatch through a
function-pointer opcode table
(jsr __call_indir). Indirect, data-driven control flow — the corner no
other demo on this site runs. A correct VM walks the bytecode into this woven multi-colour rosette;
one wrong jump-table entry would scrawl garbage. The path is bit-for-bit identical to host
x86, in every codegen mode. Runs automatically; no joypad needed. Written in C and compiled with the
llvm-mos-based 65816 toolchain
(+mos-a16).
Self-running demo — the turtle interprets the bytecode and traces the rosette, a few segments per frame. (A title card shows first; the drawing fades in after it.)
Click the screen to focus, then watch. Tab away and it pauses.
What it is
A bytecode program — a flat array of opcodes — drives a turtle (a pen with a position and a heading). The opcodes push numbers, do arithmetic, move the turtle forward, turn it, set the pen colour, and loop. The interpreter reads one opcode at a time and dispatches it; the turtle’s trail is the picture. The program here is a single loop:
REP 180: // 180 segments ITER PUSH 3 ALU % // i % 3 ← function-pointer ALU table PUSH 1 ALU + // + 1 → pen colour 1..3 PEN PUSH 75 FWD // forward 75 (8.8 fixed-point, sine LUT) PUSH 67 TURN // turn 67/256 of a circle ENDREP
The dispatch is what stresses the compiler. The main switch(op) becomes a
JMP (abs,X) jump table, and ALU indexes a
static const array of function pointers and calls through it
(jsr __call_indir). All the data — bytecode, stacks, turtle, line buffer — lives
in bank-0 RAM with near function pointers, so the same VM compiles and runs
five ways, all asserted equal.
Compiler stress-test #29a — indirect dispatch
A Round 2 demo — each Round-2 entry targets a codegen corner the first twenty never execute. Here it is computed / indirect control flow: a jump table and a function-pointer call table. Every other demo branches on simple conditions; this one branches on data, the way every real interpreter, VM, and dynamic dispatch does.
| Item | What it exercises |
|---|---|
| Jump-table dispatch (JMP (abs,X)) | The interpreter’s main switch(op) over a dense opcode range lowers to a computed branch through a table of code addresses — the JMPIdxIndir pseudo. It is the exact dispatch the toolchain’s recent xy16 index-width hardening singled out, so the demo is a live cross-mode test of that fix |
| Function-pointer opcode table | The binary ALU ops (+ − × % min max) live in a static const array of function pointers and are invoked indirectly (jsr __call_indir through a .rodata table) — the classic VM “opcode table”, never built before in this battery |
| Indirect / computed control flow | Every other demo here is straight-line arithmetic with simple branches. This is the first to dispatch on data — a wrong jump-table entry or a corrupt function pointer runs the wrong opcode and the turtle scrawls garbage. The two indirect dispatches are the whole point |
| Bit-exact differential | The VM is deterministic integer fixed-point (turtle position Q8.8, heading 0..255 into an 8.8 sine LUT; segment math widened to 32-bit → __mulsi3). Exact ops ⇒ host x86 == console bit-for-bit. Verified host == default-8bit == +mos-a16 == +mos-xy16 == path CRC 0x4007 |
| Cross-mode in xy16 | The 5-way pass includes +mos-xy16 (16-bit index registers) — where a JMP (abs,X) jump table indexes with a 16-bit X. That it dispatches identically in all three codegen modes is the live confirmation the index-width fix holds |
The VM is a portable C header (examples/65816/turtle_vm.h) linked into the SNES ROM, a
host oracle, and the corpus differential slice alike. The gate runs the program and folds every
line segment of the turtle’s path into corpus_result. Because the math is exact
integer fixed-point, that 16-bit hash must be identical on host x86 and on the console — and it is,
across every target codegen mode (default 8-bit, +mos-a16,
+mos-xy16).
Written in C with the llvm-mos 65816 toolchain. Hit Verify fidelity to
reproduce the build gate’s WRAM assert (path CRC 0x4007) live in this tab — the
same hash the host oracle and the cycle-accurate bsnes-jg core agree on, bit for bit. The disasm
gate proves the shape: a JMP (abs,X) jump table, a jsr __call_indir
through the opcode table, and rep/sep — indirect dispatch in native-16 mode.