Skip to content

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 += offset
  • OP_JUMP_IF_FALSE <offset> — peek the top of stack; if falsey, ip += offset, otherwise fall through
  • OP_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:

  1. Emit the jump opcode with a placeholder offset (0xFFFF) and remember the byte position of that placeholder.
  2. Compile the body — however many bytes that takes.
  3. Now that you know the current chunk length, go back and patch the placeholder with the real offset.
python

if / else

python

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

python

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.

Up nextCompiling Functions & ClosuresCompiling to Bytecode

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…