v4 Bytecode: lowering control flow
- Derive loop targets and scope cleanup for every exit path.
- Preserve exactly one boolean result across short-circuit joins.
- Distinguish emitted RHS code from runtime evaluation.
1 / 6
Use this labeled shape for a for loop:
ENTER_SCOPE # initializer scope
init
condition: cond-or-true
JUMP_IF_FALSE cleanup
ENTER_SCOPE # body block
body
EXIT_SCOPE
post: post-statement
JUMP condition
cleanup: EXIT_SCOPE # initializer scope
Suppose a continue occurs in an extra nested block inside the body. At that point the runtime has initializer, body, and nested scopes. Emit two EXIT_SCOPE instructions to discard nested/body scopes, then jump to post. Preserve the initializer scope so post can still read i. Break emits the same local cleanup but jumps to cleanup, which also exits the initializer scope. Jumping after that last cleanup instruction would leak it.
The normal lexical EXIT_SCOPE instructions remain in emitted code, but a taken jump skips them. Do not decrement the compiler's syntactic scope-depth counter when emitting cleanup for one runtime edge: later source statements are still compiled in the same lexical nesting.
A compiler changes its own scopeDepth while emitting cleanup for a break in one branch. Subsequent code emits too few exits on a continue in the other branch. Why is source nesting different from one runtime path?
Draw a two-level loop with an inner break and an outer continue after the inner loop. Label each jump target and count runtime scope exits. Then trace both paths of false || true && false to verify one boolean at each join.
Carry a path-by-path stack/scope trace into the v4 capstone. An output-only test should be supplemented by a case that repeats continue many times to expose leaked scopes.