Master the fundamental concepts of build a bytecode vm through this focused micro-challenge.
You have read the whole brief, and the concepts above stay free on every task. Writing and running the code needs a plan.
Three hints are available for this task, revealed one at a time inside the code workspace so you can struggle productively before seeing them.
Every task includes starter code, theory, and hidden tests so you can implement and verify locally in the browser.
How it worksInterpretation pays dispatch tax every opcode. A template JIT copies pre-built machine-code snippets into an executable buffer, patches immediates, and jumps to native code. LuaJIT and V8 sit farther along this spectrum; template JIT is the entry point.
Each bytecode maps to a native template:
Executable pages come from `mmap` with `PROT_READ | PROT_WRITE | PROT_EXEC`, or `mprotect` after writing. Modern OS hardening (W^X on iOS, SELinux policies) restricts this, which is why browser JITs need special entitlements.
A switch-based interpreter might spend 5-10 cycles per opcode on fetch and dispatch. JIT'd straight-line code runs at near-native speed for hot sequences.
For example, `PUSH 10; PUSH 20; ADD` becomes a few `mov` instructions and one `add` without a central dispatch loop.
Flush the instruction cache after writing machine code on architectures that require it. Template JIT is not peak performance, but it teaches the exact steps production engines hide inside megabytes of optimization logic.
This exercise asks you to emit x86-64 machine code for a short bytecode program using `mmap`. You will implement byte emitters and demonstrate calling the generated function from C.
A template JIT translates each bytecode instruction into a fixed snippet of machine code and concatenates the snippets. Write the code generator for x86-64: map the VM's operand stack onto the hardware stack (push/pop/[rsp]), emit the exact instruction bytes, verify stack depths statically before emitting anything, and optionally run a constant-folding peephole first. The generated function returns the final value in rax. A reference interpreter computes the expected result, so the program runs anywhere.
cLoading…
| Bytecode | x86-64 |
|---|---|
| PUSH n (−128..127) | 6a ib push imm8 |
| PUSH n (other int32) | 68 id push imm32 (little-endian) |
| POP | 48 83 c4 08 add rsp, 8 |
| ADD | 58 pop rax; 48 01 04 24 add [rsp], rax |
| SUB | 58 pop rax; 48 29 04 24 sub [rsp], rax |
| MUL | 58 pop rax; 48 0f af 04 24 imul rax, [rsp]; 48 89 04 24 mov [rsp], rax |
| NEG | 48 f7 1c 24 neg qword [rsp] |
| DUP | ff 34 24 push qword [rsp] |
| SWAP | 58 pop rax; 5a pop rdx; 50 push rax; 52 push rdx |
| HALT | 58 pop rax; c3 ret |
PUSH a; PUSH b; ADD|SUB|MUL with PUSH (a op b), using 64-bit wrap-around, but only when the result fits in int32. Report folded K constant operation(s).error: OP at I would underflow the stackerror: HALT must leave exactly one value on the stack (found D)error: code after HALTerror: program must end with HALT%02x , padded to 7 byte slots) and the assembly. Finally N bytes of machine code, max stack depth D, result R, where R is computed with 64-bit wrap-around.unsupported by the JIT: X and PUSH needs a 32-bit immediate.cLoading…
Input:
cLoading…
Output:
cLoading…
push imm8 encoding whenever the value fits.Hidden tests cover a negative immediate, a large immediate, fold results that do not fit in 32 bits (left unfolded), -128 and 128 at the imm8 boundary, stack underflow, a missing HALT, extra values at HALT, and code after HALT.