kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 67344f35ae4b8850515b5741595103ef9bbdd65b
parent 104b3914e3d72c60eef8824d69b03f6ec726aa30
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 27 May 2026 11:38:29 -0700

wasm: de-rotate for-loop CFG blocks before structurization

The C frontend emits for-loops in a rotated layout: step block appears
before body in WIR, with the body's closing edge jumping backward to the
step.  The structurizer misclassifies the step block as a loop header and
emits a spurious inner `loop`, causing the program to spin forever.

Add derotate_for_loops(), run before scope-depth analysis, that detects
the pattern (JUMP L_body; LABEL L_step; ...; JUMP L_top; LABEL L_body)
and moves the step block to after the body.  This turns the body->step
edge into a forward edge so the sole back-edge is body/step -> L_top,
matching the wasm `loop` semantics.  The pass re-scans to a fixed point
to handle nested for-loops.

Diffstat:
Msrc/arch/wasm/structure.c | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 108 insertions(+), 0 deletions(-)

diff --git a/src/arch/wasm/structure.c b/src/arch/wasm/structure.c @@ -408,6 +408,113 @@ static void unroll_switch_islands(WSCtx* x) { } } +/* For-loop de-rotation. + * + * The C frontend lowers `for (init; cond; step) body` in a single pass, so + * the step clause (which appears before the body in source) is emitted into + * a block placed BEFORE the body, with the body jumping back to it: + * + * LABEL L_top // header + * <cond>; CMP_BRANCH L_end // (cond optional) + * JUMP L_body // skip the step on entry + * LABEL L_step // continue target + * <step> + * JUMP L_top // genuine loop back-edge + * LABEL L_body + * <body> + * JUMP L_step // body -> step (continue path) + * LABEL L_end + * + * In WIR textual order the body's `JUMP L_step` is a backward edge, so the + * structurer misclassifies L_step as a loop header and emits a second, + * unintended `loop` — the program spins forever. The CFG is reducible; only + * the block layout is rotated. We restore a structure-compatible order by + * moving the step block to AFTER the body, mirroring how + * unroll_switch_islands reorders the switch shape: + * + * LABEL L_top + * <cond>; CMP_BRANCH L_end + * LABEL L_body // JUMP L_body dropped; falls through + * <body> + * JUMP L_step // now a forward edge + * LABEL L_step + * <step> + * JUMP L_top // sole back-edge -> L_top is the loop + * LABEL L_end + * + * The step block ends in an unconditional `JUMP L_top` (no fall-through out) + * and is entered only via `JUMP L_step`, so relocating it preserves + * semantics. Nested for-loops live entirely inside the body region and are + * handled by re-scanning to a fixed point. */ +static int derotate_one_for_loop(WSCtx* x) { + WTarget* t = x->t; + Heap* h = t->c->ctx->heap; + for (u32 j = 0; j < t->nwir; ++j) { + if (t->wir[j].op != WIR_JUMP) continue; + /* JUMP L_body, with L_body a forward label. */ + Label xb = t->wir[j].labels[0]; + if (xb == LABEL_NONE || xb - 1u >= t->nlabels) continue; + WLabel* lx = &t->labels[xb - 1u]; + if (!lx->placed || lx->wir_index <= j) continue; + /* Immediately followed by LABEL L_step. */ + if (j + 1u >= t->nwir || t->wir[j + 1u].op != WIR_LABEL) continue; + Label s = t->wir[j + 1u].labels[0]; + if (s == LABEL_NONE || s == xb) continue; + /* Scan the step block for `JUMP L_top; LABEL L_body`, L_top backward. */ + u32 jt = UINT32_MAX; + for (u32 m = j + 2u; m + 1u < t->nwir; ++m) { + u8 mop = t->wir[m].op; + if (mop == WIR_RET || mop == WIR_UNREACHABLE) break; + if (mop != WIR_JUMP) continue; + Label cand = t->wir[m].labels[0]; + if (cand == LABEL_NONE || cand - 1u >= t->nlabels) continue; + WLabel* lc = &t->labels[cand - 1u]; + if (lc->placed && lc->wir_index < j && t->wir[m + 1u].op == WIR_LABEL && + t->wir[m + 1u].labels[0] == xb) { + jt = m; + break; + } + } + if (jt == UINT32_MAX) continue; + /* k = last `JUMP L_step` (the body's closing/continue edges all target + * L_step; the textually-last one closes the body, just before L_end). */ + u32 k = UINT32_MAX; + for (u32 m = jt + 1u; m < t->nwir; ++m) { + if (t->wir[m].op == WIR_JUMP && t->wir[m].labels[0] == s) k = m; + } + if (k == UINT32_MAX || k <= jt) continue; + + /* Rebuild: [0..j) drop JUMP at j, [jt+1..k] body, [j+1..jt] step, + * [k+1..nwir). */ + WIR* nw = NULL; + u32 nn = 0, ncap = 0; + for (u32 m = 0; m < j; ++m) *wir_append(t, &nw, &nn, &ncap) = t->wir[m]; + for (u32 m = jt + 1u; m <= k; ++m) + *wir_append(t, &nw, &nn, &ncap) = t->wir[m]; + for (u32 m = j + 1u; m <= jt; ++m) + *wir_append(t, &nw, &nn, &ncap) = t->wir[m]; + for (u32 m = k + 1u; m < t->nwir; ++m) + *wir_append(t, &nw, &nn, &ncap) = t->wir[m]; + + if (t->wir) h->free(h, t->wir, sizeof(WIR) * t->wir_cap); + t->wir = nw; + t->nwir = nn; + t->wir_cap = ncap; + for (u32 m = 0; m < t->nwir; ++m) { + if (t->wir[m].op != WIR_LABEL) continue; + Label l = t->wir[m].labels[0]; + if (l != LABEL_NONE && l - 1u < t->nlabels) + t->labels[l - 1u].wir_index = m; + } + return 1; + } + return 0; +} + +static void derotate_for_loops(WSCtx* x) { + while (derotate_one_for_loop(x)) { /* re-scan after each reorder */ } +} + static void pass_preds(WSCtx* x) { WTarget* t = x->t; for (u32 i = 0; i < t->nwir; ++i) { @@ -648,6 +755,7 @@ void wasm_structurize(WTarget* t) { memset(&x, 0, sizeof x); x.t = t; unroll_switch_islands(&x); + derotate_for_loops(&x); pass_scope_depth(&x); pass_preds(&x); pass_decide(&x);