JetScheme
git clone https://github.com/pinecone/JetScheme
cd JetScheme && make
A small, fast Scheme interpreter in ~10k lines of C++. It has lambdas, proper tail calls, escape continuations, coroutines, structs, tuples, hash sets and maps, and mutation-safe iteration. JetScheme is in pre-alpha. It is thus unfit for any particular use (other than working on JetScheme).
Run make ab-cross-bench to compare Jet with popular language
runtimes on your machine. (All runtimes except jsc run in interpreter-mode.)
| benchmark | LLInt | python3.14 | lua5.5 | luajit | luau | jet | jsc(jit) | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 🍎 | 🎮 | 🍎 | 🎮 | 🍎 | 🎮 | 🍎 | 🎮 | 🍎 | 🎮 | 🍎 | 🎮 | 🍎 | 🎮 | |
| astar | 76 | 273 | 155 | 452 | 62 | 215 | 57 | 177 | 77 | 206 | 51 | 117 | 33 | 96 |
| basic | 596 | 2205 | 498 | 1126 | 411 | 1121 | 320 | 651 | 353 | 787 | 156 | 455 | 63 | 189 |
| cdjs | 97 | 306 | 164 | 355 | 110 | 337 | 106 | 213 | 105 | 222 | 48 | 119 | 23 | 51 |
| deltablue | 250 | 819 | 233 | 561 | 125 | 568 | 140 | 422 | 70 | 310 | 62 | 189 | 28 | 72 |
| havlak | 245 | 1199 | 279 | 792 | 324 | 1461 | 221 | 763 | 219 | 725 | 174 | 781 | 67 | 250 |
| json | 343 | 1144 | 148 | 312 | 136 | 395 | 111 | 250 | 113 | 345 | 64 | 183 | 37 | 108 |
| nbody | 404 | 1168 | 597 | 1294 | 330 | 900 | 190 | 465 | 147 | 382 | 136 | 298 | 23 | 61 |
| raytrace | 640 | 2443 | 514 | 1301 | 520 | 1482 | 354 | 844 | 275 | 679 | 115 | 424 | 42 | 113 |
| richards | 119 | 374 | 191 | 402 | 108 | 257 | 107 | 140 | 94 | 206 | 53 | 137 | 19 | 35 |
| splay | 25 | 93 | 42 | 67 | 44 | 132 | 27 | 57 | 25 | 66 | 20 | 47 | 15 | 32 |
| total | 2795 | 10024 | 2821 | 6662 | 2170 | 6868 | 1633 | 3982 | 1478 | 3928 | 879 | 2750 | 350 | 1007 |
🍎MacBook Air (M5)
🎮Steam Deck OG (Zen 2)
ms, median of 20 runsmake ab-cross-bench at e7653ec↗
Impl
JetScheme is built on techniques described in:
- Tail-threaded dispatch (Bell, 1973) — each opcode is its own small function, and each function ends by tail-calling the next one directly. There is no central dispatch loop and no callee-save spills between opcodes, so hot VM state stays in registers across the whole run.
- Inline caching (Deutsch & Schiffman, 1984) — the first time through a call site, the slow path patches the opcode in place to point at a specialized fast version of itself. Later passes through that site skip the lookup, key compare, and type check entirely.
- Flat closures (Dybvig, 1987) — captures live in an inline array on the closure. Immutable ones are copied by value; a capture that is both mutated and shared gets a heap indirection.
- A-normal form (Flanagan et al., 1993) — every operand is a variable or a literal, and every intermediate result gets an explicit name. The later passes — inlining, register allocation, instruction selection — then rewrite one flat shape instead of arbitrary nested trees.
- NaN-boxing (Gudeman, 1993) — every value fits in 8 bytes. Numbers are raw doubles, while immediates and tagged heap pointers hide in the IEEE 754 quiet-NaN payload. Type tests are cheap bit ops, and any value passes in a single register.
- Superinstructions (Proebsting, 1995) — common back-to-back opcode pairs are emitted as one fused opcode. That is a single chain step instead of two, and the body can skip the intermediate register move.
- Opcode replication for inline caches (Ertl & Gregg, 2003) — each cached opcode has several near-identical copies that rotate per call site. Distinct sites then land on distinct branch-target-predictor entries instead of all colliding on one.