Master the fundamental concepts of webassembly internals 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 worksOnce you have an interpreter, compiling hot Wasm functions to native code removes dispatch overhead. Template or trace JITs map Wasm opcodes to machine instructions, reusing techniques from your bytecode VM track.
Approaches:
Executable memory via \`mmap\` + \`mprotect\` matches what you built for the bytecode VM JIT task.
For example, \`i32.add\` becomes x86 \`add\` after two loads from the Wasm stack slots mapped to registers or a native stack frame.
Fallback to the interpreter keeps rare opcodes simple.
Map Wasm locals to native stack slots or registers consistently for the whole function. Changing mapping mid-function makes debugging generated code nearly impossible without a side table.
This exercise asks you to add a basic JIT to your Wasm runtime using prior JIT knowledge. You will emit native code for a short Wasm function and call it instead of interpreting opcode by opcode.
You will use the same mental model here when reading production interpreter source later in the track. Sketch one concrete input on paper, predict the outcome, then confirm with code. That discipline catches logic errors early and makes debugging far faster when you extend the implementation in follow-on tasks.
Write a baseline JIT for straight-line WebAssembly (i32 only) that emits x86-64 assembly in two ways:
push/pop for each operand.Print both listings with their instruction counts, so the difference is visible. invoke runs the function in an interpreter as a reference.
;; starts a comment.
cLoading…
At end, the function is checked first:
NAME: stack underflow at I (OP));NAME: stack deeper than 7 values);NAME: must leave exactly one i32 (leaves N));i32.shl directly preceded by i32.const (NAME: i32.shl at I needs an i32.const count right before it).Only then are the listings printed.
push rbp, mov rbp, rsp, sub rsp, F (F = 4·(params+locals) rounded up to 16; omitted if 0). Then param i is stored with mov [rbp-4(i+1)], REG from edi esi edx ecx r8d r9d, and each local j is zeroed with mov dword [rbp-4(np+j+1)], 0. The code ends with leave and ret, and the result goes in eax.
| Wasm | x86-64 |
|---|---|
local.get I | mov eax, [rbp-S]; push rax |
i32.const V | push V |
local.set I | pop rax; mov [rbp-S], eax |
local.tee I | mov rax, [rsp]; mov [rbp-S], eax |
drop | add rsp, 8 |
| binary op | pop rcx; pop rax; OP eax, ecx (add sub imul and or xor, or shl eax, cl); push rax |
| end | pop rax, then leave/ret |
eax ecx edx esi edi r8d r9d.mov R, V for a constant or mov R, [rbp-S] for a local, and turns it into a register.local.get and i32.const only push entries, with no code.drop frees the entry's register.OP R, b, where b is written as V, [rbp-S] or a register. Multiplying by a constant uses imul R, R, V, and shl uses shl R, V&31;mov dword [rbp-S], V or mov [rbp-S], R;mov eax, V, mov eax, [rbp-S], mov eax, R (if R is not eax), or nothing.cLoading…
Errors:
line N: unsupported instruction Xline N: bad operand for OP, line N: OP takes no operandline N: only i32 is supported (T), line N: at most 6 params (register arguments)line N: outside a funcNAME: not compiled (a function with line errors)invoke: no compiled function, invoke: expected N argument(s)Input:
cLoading…
Output:
cLoading…
Hidden tests cover constant folding into an immediate, local.tee with a later reuse, the swap pattern that needs the stale-local flush, a register-register operation, drop, and the error checks.