Step 1 of 3 · Reading · ~3 min
Jump Instructions for Control Flow
Compiling to Bytecode
Control Flow Is Just Jumps
A stack-based VM has no notion of if or while at runtime — only a flat
array of opcodes and an instruction pointer (ip) that normally increments
by one. Control flow is implemented entirely with three jump instructions
that manipulate ip directly:
OP_JUMP <offset>— unconditional:ip += offsetOP_JUMP_IF_FALSE <offset>— peek the top of stack; if falsey,ip += offset, otherwise fall throughOP_LOOP <offset>— unconditional, but backward:ip -= offset
Offsets are 16-bit operands (two bytes), giving jumps a decent range without bloating every instruction.
The Backpatching Trick
Here's the catch: when you compile if (cond) { thenBranch }, you need to
emit OP_JUMP_IF_FALSE <offset> before you know how long thenBranch's
compiled bytecode will be. The fix is backpatching:
- Emit the jump opcode with a placeholder offset (
0xFFFF) and remember the byte position of that placeholder. - Compile the body — however many bytes that takes.
- Now that you know the current chunk length, go back and patch the placeholder with the real offset.
if / else
Notice the condition value is popped on both branches, once inside the
"then" path and once at the top of the "else" path — that's what keeps the
stack balanced regardless of which branch executes. This same shape (jump,
pop, body, jump-over-else, patch, pop, else-body, patch) is exactly how
short-circuiting and/or get compiled too, if your language has them.
while — Jumping Backward
OP_LOOP doesn't need backpatching because loop_start is already known
when you emit it — you're jumping to a point in the past, not the future.
for Desugars at Compile Time
A for (init; cond; incr) body never needs its own opcode. The compiler
just emits, in order: the initializer once, then a while-shaped loop
where the increment is compiled after the body but before jumping back
to re-test the condition. There's no runtime concept of "for" at all — by
the time bytecode exists, it's indistinguishable from a hand-written
while.
What to build
Implement if/else and while using emit_jump/patch_jump/
emit_loop, then desugar for into the same primitives. Verify the stack
stays balanced regardless of branch taken — a classic bug is popping the
condition on only one path, which silently corrupts every subsequent
instruction's operand offsets since the stack "leaks" one slot per
iteration.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…