v4 Bytecode: lowering control flow
Lesson, slides, and applied problem sets.
View SlidesLesson
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:
initexecutes exactly once at loop entrycondexecutes on each iteration before bodypostexecutes after each body iterationbreak/continuetargets 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.
breakjump -> patched toloop_endcontinuejump -> patched topost(or condition check target)
Use a loop context stack to handle nesting:
- push
{start, end, continue, breakList, continueList} - pop after loop end
Pitfall:
- emitting
continueinside 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 orbresult
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/continueedges 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