commit 9ebc085dfe3e925ecf2cf329beba8fe4d043c884
parent ad61e55784cc0159d881e48ba40fd38ad1590aa2
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 28 May 2026 12:31:31 -0700
wasm/structure: unroll nested switches to fix infinite-loop dispatch
unroll_switch_islands only reordered the outermost switch per invocation:
after recording the outer island, scan jumped to sw_i+1, skipping past
the inner switch's JUMP-to-dispatch (which sits inside the outer case
body). The inner case labels stayed as backward refs and the structurer
wrapped them in synthetic SCOPE_LOOPs — so the inner br_table targeted
loops and ran forever.
Split into unroll_switch_islands_once (returns island count) and a
wrapper that re-runs to a fixed point so nested switches all get
reordered.
Also bail the dispatch-search on WIR_SCOPE_OPEN/SCOPE_CLOSE. Without
this, a second pass mistakes a for-loop body label for a switch dispatch
label (the JUMP L_body; LABEL L_body; [switch SCOPE_LOOP open]; setup;
selector; WIR_SWITCH sequence has no terminator between the LABEL and
the WIR_SWITCH), and the resulting bogus rewrite buries the for-step
inside the switch's dispatcher dead-code region. Caught by
6_8_24_break_switch_in_loop and 6_8_25_continue_inside_switch.
Full W-path suite: 426 pass / 7 fail / 32 skip / 0 hang
(was 425 / 7 / 32 / 1).
Diffstat:
2 files changed, 39 insertions(+), 10 deletions(-)
diff --git a/doc/WASM_PARSE_CHECKLIST.md b/doc/WASM_PARSE_CHECKLIST.md
@@ -4,18 +4,24 @@ Status of the Wasm CGTarget against the `test/parse` C suite, path **W**
(`cfree cc -O0 -target wasm32-none -c case.c` → `cfree run -e test_main case.wasm`).
- Host: arm64 (native JIT for the re-lowering). Opt level 0.
-- 465 cases: **425 pass · 7 fail · 0 compile-fail · 32 skip · 1 hang**.
+- 465 cases: **426 pass · 7 fail · 0 compile-fail · 32 skip · 0 hang**.
- The skips below match run.sh's phased-rollout regex (reported SKIP).
The fails fall outside it and report as **FAIL** in the harness.
- Reproduce / re-probe: `build/wasm_probe.sh [filter]`; results in
`build/wasm_probe/results.tsv`, per-case logs alongside.
-## ⏳ Hang (blocks `make test-parse`)
+## ⏳ Hang — fixed (was 1)
-- [ ] **`6_8_19_switch_nested_dup_case`** — `.wasm` compiles clean; `cfree run`
- spins at ~100% CPU forever. Infinite loop is in JIT'd code (nested-switch
- lowering emits a backward branch turning the switch into a loop). One stalled
- parallel worker prevents the suite from completing.
+- [x] **`6_8_19_switch_nested_dup_case`** — the structurer's
+ `unroll_switch_islands` pass only reordered the outermost switch per
+ invocation: after recording the outer island its `scan` advanced past the
+ inner switch's `JUMP L_disp`, so the inner case labels stayed backward
+ refs and were wrapped as synthetic `SCOPE_LOOP`s. `br_table` targeting
+ those loops turned the inner switch into an infinite loop. Fix
+ (`src/arch/wasm/structure.c`): re-run the unroller to a fixed point, and
+ also bail the dispatch-search loop on `WIR_SCOPE_OPEN`/`SCOPE_CLOSE` so a
+ later pass doesn't mistake a for-loop body label (which sits just before
+ `[switch SCOPE_LOOP open]`) for a switch dispatch label.
## ❌ Fail — wrong exit code (7)
diff --git a/src/arch/wasm/structure.c b/src/arch/wasm/structure.c
@@ -311,7 +311,13 @@ typedef struct WSIsland {
static WIR* wir_append(WTarget* t, WIR** out, u32* nout, u32* cap);
-static void unroll_switch_islands(WSCtx* x) {
+/* One pass of switch-island detection + reordering. Returns the number of
+ * islands rewritten (0 = nothing to do). Nested switches need multiple passes:
+ * the outer JUMP-to-dispatch sits before the inner switch's JUMP-to-dispatch
+ * in WIR order, so the outer scan advances past the inner JUMP after recording
+ * the outer island. Re-scanning the rewritten WIR brings the now-exposed inner
+ * island into view. */
+static int unroll_switch_islands_once(WSCtx* x) {
WTarget* t = x->t;
Heap* h = t->c->ctx->heap;
WSIsland islands[16];
@@ -334,7 +340,13 @@ static void unroll_switch_islands(WSCtx* x) {
continue;
}
/* Find WIR_SWITCH just past dispatch label; bail on intervening
- * terminator. */
+ * terminator or scope boundary. The C/toy frontends emit the
+ * selector expression as a small straight-line sequence between
+ * LABEL dispatch and WIR_SWITCH, with no SCOPE_OPEN/CLOSE — those
+ * only appear at switch entry/exit, never around the dispatcher.
+ * Rejecting SCOPE_OPEN here keeps a second pass from mistaking a
+ * for-loop body label (which sits just before a `[switch open]`)
+ * for a switch dispatch label. */
u32 sw_i = UINT32_MAX;
for (u32 k = dlbl->wir_index + 1u; k < t->nwir; ++k) {
if (t->wir[k].op == WIR_SWITCH) {
@@ -342,7 +354,8 @@ static void unroll_switch_islands(WSCtx* x) {
break;
}
if (t->wir[k].op == WIR_JUMP || t->wir[k].op == WIR_CMP_BRANCH ||
- t->wir[k].op == WIR_RET || t->wir[k].op == WIR_UNREACHABLE)
+ t->wir[k].op == WIR_RET || t->wir[k].op == WIR_UNREACHABLE ||
+ t->wir[k].op == WIR_SCOPE_OPEN || t->wir[k].op == WIR_SCOPE_CLOSE)
break;
}
if (sw_i == UINT32_MAX) {
@@ -359,7 +372,7 @@ static void unroll_switch_islands(WSCtx* x) {
nislands++;
scan = sw_i + 1u;
}
- if (nislands == 0) return;
+ if (nislands == 0) return 0;
/* Build a rewritten WIR list. For each island, emit pre + (selector +
* SWITCH) + (case bodies). Then everything after the last island's
@@ -406,6 +419,16 @@ static void unroll_switch_islands(WSCtx* x) {
Label l = t->wir[k].labels[0];
if (l != LABEL_NONE && l - 1u < t->nlabels) t->labels[l - 1u].wir_index = k;
}
+ return (int)nislands;
+}
+
+/* Re-run the single-pass unroller to a fixed point so nested switches all
+ * get reordered. Each pass reorders at most the outermost island in a chain
+ * of nested switches; the next pass exposes the next inner one. The loop
+ * terminates because every rewritten island drops one JUMP-to-dispatch from
+ * the WIR (and a function has finitely many). */
+static void unroll_switch_islands(WSCtx* x) {
+ while (unroll_switch_islands_once(x) > 0) { /* re-scan after each rewrite */ }
}
/* For-loop de-rotation.