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

loading core…

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.

ItemWhat 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 tableThe 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 flowEvery 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 differentialThe 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 xy16The 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.