Master the fundamental concepts of jit compilation 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 worksJIT IR sits between bytecode and machine code: low enough to map quickly, high enough to optimize. JVM bytecodes, JavaScriptCore's DFG SSA, and V8's TurboFan IR are purpose-built for fast tier-up and guard insertion.
Explicit types or tags for guard insertion. SSA for quick DCE and constant fold. Block structure mirrors control flow for patchable branches. Operands close to machine registers.
cLoading…
Production compilers embed this step inside a longer pipeline. GCC flows through cpp, cc1, assembly, and ld; Clang uses the driver, Sema, LLVM IR passes, and a target backend. LLVM bitcode, JVM bytecode, and WASM are other familiar IRs at the same layer. The exercise isolates one pass so you can test it alone before chaining it to the next stage.
You will design IR suited for JIT compilation. This exercise asks you to define instructions that lower quickly to native code while supporting guards, type specialization, and deoptimization metadata.
Design the IR of a tracing JIT: translate a recorded bytecode trace into SSA instructions with explicit type guards, then optimise it the way LuaJIT-style JITs do: forward loads, drop guards whose outcome is already known, and sink stores to the end of the trace.
One bytecode per line (a stack machine), with the types the recorder observed:
| Bytecode | Meaning |
|---|---|
LOAD var TYPE | push variable var; it had type TYPE (int or float) when recorded |
CONST n | push an integer constant |
ADD / SUB / MUL / LT TYPE | pop b, pop a, push a OP b; operands had type TYPE |
STORE var | pop into var |
BRANCH taken|not-taken | pop a condition; the recorder saw the branch go this way |
Values are v1, v2, … in creation order:
LOAD: vN = load var then guard_type vN, TYPECONST n: vN = const nvN = add_TYPE vA, vB (sub_, mul_, lt_ likewise)STORE var: store var, vA (emitted immediately)BRANCH: guard_true vA if taken, guard_false vA if not takenv1)LOAD of a variable that already has one (from an earlier load or store in the trace) emits nothing and reuses it.add_int produces int, lt_* produces int). A new load still gets its guard_type; a forwarded value never needs one.STORE only updates the variable's current value. At the end of the trace emit one store var, vA per variable that was stored, in order of each variable's first STORE.CONST, arithmetic and branch guards are emitted as in the naive IR.cLoading…
Input:
cLoading…
Output:
cLoading…
Hidden tests cover several variables stored in interleaved order (a stored variable read back later), and float arithmetic with a not-taken branch.