Skip to content

Fix #137: for-loop continue skips update on --target web#178

Merged
proggeramlug merged 1 commit intomainfrom
fix-137-web-continue-if-else
Apr 24, 2026
Merged

Fix #137: for-loop continue skips update on --target web#178
proggeramlug merged 1 commit intomainfrom
fix-137-web-continue-if-else

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Root cause

In crates/perry-codegen-wasm/src/emit.rs, Stmt::For lowered the update expression at the end of the loop body, just before the explicit Br(0) restart:

;; BEFORE (broken)
block $break
  loop $loop_top               ;; continue_d = this label
    <condition>; br_if 1 ($break)
    <body>                     ;; continue → Br(rel) targets $loop_top
    <update>                   ;; SKIPPED when continue fires
    br 0 ($loop_top)
  end
end

When continue fired (as Br(rel) targeting the loop label), it jumped back to the loop start and skipped the update — causing the iterator to pin forever on the last active index.

The bug was latent before v0.5.161 because continue was hardcoded to Br(0) (always targeting the innermost block, i.e. the loop). After #135's fix made continue use the formula block_depth - loop_depth.last(), continue correctly escaped nested if blocks — but now exposed that the for update was always in the wrong place relative to where continue landed.

Fix

Wrap the loop body in an inner block so that continue exits that inner block and falls through to the update before the loop restarts:

;; AFTER (fixed)
block $break
  loop $loop_top
    <condition>; br_if 1 ($break)    ;; loop directly inside block, rel=1 unchanged
    block $body_end                  ;; continue_d = this label (pushed to loop_depth)
      <body>                         ;; continue → Br(rel) exits $body_end
    end                              ;; fall through here
    <update>                         ;; always runs before restart
    br 0 ($loop_top)
  end
end

loop_depth now tracks $body_end instead of $loop_top, so the existing Stmt::Continue formula (block_depth - loop_depth.last()) produces the correct relative branch without any changes to the Stmt::Continue handler. break_depth still tracks $break, so Stmt::Break is unaffected.

Verification

WAT disassembly of the repro from #137 (via wasm-tools print) confirms:

block           ;; @1 ($break)
  loop          ;; @2 ($loop_top)
    ...
    br_if 1 (;@1;)      ;; condition exit — unchanged
    block       ;; @3 ($body_end) ← NEW
      ...
      if        ;; @4 (EA[i] < 0.5 check)
        br 1 (;@3;)     ;; continue A — exits $body_end ✓
      end
      ...if/else-if/else chain...
      if        ;; (type > 99.0 check)
        br 1 (;@3;)     ;; continue C — exits $body_end ✓
      end
    end                 ;; $body_end — falls through to update
    ...update (i = i + 1)...
    br 0 (;@2;)         ;; restart loop ✓
  end
end

Both continue statements emit br 1 (;@3;) targeting the inner body block, the update always runs before br 0 restarts the loop. Native target output (walker 2 / done) matches — no non-WASM code paths touched.

Scope

Single-file change in crates/perry-codegen-wasm/src/emit.rs. No version bump, no CLAUDE.md changes (external contributor PR per project rules).


Generated by Claude Code

In the WASM emitter's Stmt::For lowering, the update expression was
placed at the end of the loop body just before the explicit Br(0)
restart.  When a continue statement fired (via Br(rel) targeting the
loop label), it jumped back to the loop start and skipped both the rest
of the body AND the update — causing the iterator to pin forever on the
last active index.

Fix: wrap the loop body in an inner block so that continue targets the
inner block's exit rather than the loop top.  The new structure is:

  block $break
    loop $loop_top
      <condition>; br_if 1 ($break)
      block $body_end        ← continue targets this block's exit
        <body>
      end                    ← continue falls through here
      <update>               ← now always runs before loop restart
      br 0 ($loop_top)
    end
  end

loop_depth now tracks $body_end instead of $loop_top, so the existing
Stmt::Continue formula (block_depth - loop_depth.last()) correctly emits
a relative branch that exits $body_end and falls through to the update.
break_depth still tracks $break, so Stmt::Break is unaffected.

Verified with wasm-tools: both continue statements in the repro emit
br 1 (;@3;) — targeting the inner body block — and br 0 (;@2;) at
the bottom correctly restarts the loop after the update runs.

https://claude.ai/code/session_01BBbsJYwY1ZWTjESXffRC6C
@proggeramlug proggeramlug merged commit fed05c7 into main Apr 24, 2026
8 checks passed
@proggeramlug proggeramlug deleted the fix-137-web-continue-if-else branch April 24, 2026 09:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants