v4 Bytecode: lowering control flow

Lesson, slides, and applied problem sets.

View Slides

Lesson

v4 Bytecode: lowering control flow

Why control-flow lowering is the hardest compiler step so far

Expressions mostly map to linear instructions. Loops, branches, and boolean connectives require two extra skills:

  • control-target management
  • placeholder patching for forward jumps

This module is where “good parser” is not enough; emit order now determines execution correctness.


1) Lowering for as an explicit jump graph

Source:

for (init; cond; post) { body }

Canonical lowering:

ENTER_SCOPE
init
loop_start:
cond
JUMP_IF_FALSE loop_end
body
post
JUMP loop_start
loop_end:
EXIT_SCOPE

Rules to preserve:

  • init executes exactly once at loop entry
  • cond executes on each iteration before body
  • post executes after each body iteration
  • break/continue targets resolve inside this scope’s loop metadata

In many implementations, this is built from a couple labels (loop_start, loop_end) and patch lists for unresolved jumps.


2) break and continue with patching lists

Both compile to jumps before their final offsets are known.

  • break jump -> patched to loop_end
  • continue jump -> patched to post (or condition check target)

Use a loop context stack to handle nesting:

  • push {start, end, continue, breakList, continueList}
  • pop after loop end

Pitfall:

  • emitting continue inside nested loop must target inner loop’s continue point, not outer.

3) Short-circuit && and || lowering

a && b

  • evaluate a
  • if falsey => jump to false label and push false
  • otherwise evaluate b
  • final value is either a-false result or b result

a || b

  • evaluate a
  • if truthy => jump to true label and push true
  • otherwise evaluate b

This style avoids adding dedicated boolean-only bytecode and preserves expression truthiness semantics.


4) Boolean falsey contract across control flow

In this pack, falsey = false or nil.

Every branch instruction should test this contract consistently.

If your && lowering uses strict identity to false only, you will incorrectly treat nil as truthy.


5) Block scoping in loops

A for has two kinds of scoping interactions:

  • initializer scope rules (if any)
  • loop body scope with break/continue edges not leaking locals

Emit ENTER_SCOPE before init and EXIT_SCOPE after end unless your language defines a different scoping nuance.


6) Control-flow correctness checks

Before finishing, verify:

  • every emitted jump has valid target
  • no unresolved patch entries remain
  • post-loop fallthrough target is well-defined
  • boolean lowering leaves exactly one result on stack per expression

7) Practice checkpoints (almost solved)

1) For-loop shape + Lower for (i = 0; i < 3; i = i + 1) {} + Expected high-level shape: +init -> cond -> body -> post -> cond -> ... -> end

2) continue target + Inside a loop body, continue should jump to increment/post step, not condition check if your compiler architecture requires one.

3) break target + break should jump to loop end and skip post-step.

4) Short-circuit && + false && sideEffect() should never compile/evaluate sideEffect().

5) Short-circuit || + true || sideEffect() should never evaluate sideEffect().

6) Nested loops + break in inner loop must target inner loop end; continue inner must not escape to outer increment.

7) Empty for condition + If condition is omitted (for(;;)), model it as literal true loop with explicit jump-back behavior.

This module’s quality is in deterministic branching, not just “it terminates.”


Module Items

  • Compile to Bytecode v4

    Lower a v4 program AST into stack-based bytecode with loops and logic.

    hard Upgrade to Pro to access hard problems
Join Discord