kit

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

structure.c (28130B)


      1 /* Wasm CGTarget structurer.
      2  *
      3  * Walks the WIR list produced by CG record callbacks and rewrites it so
      4  * every label is bound to a structured Wasm scope. Free WIR_LABELs (those
      5  * placed via wasm_label_place outside any CG scope's break/continue role)
      6  * are wrapped in synthetic SCOPE_BLOCK (forward jumps) or SCOPE_LOOP
      7  * (backward jumps) records; jumps to the formerly-free labels resolve via
      8  * the existing scope-bound br_to_label machinery in emit.c.
      9  *
     10  * Switch islands — the C/toy `switch` shape `JUMP dispatch; case bodies;
     11  * LABEL dispatch; selector; WIR_SWITCH; LABEL end` — are pre-processed
     12  * by unroll_switch_islands, which reorders the WIR to put selector +
     13  * SWITCH first and the case bodies after. That conversion turns the
     14  * case labels into forward refs the structurer handles uniformly with
     15  * other forward gotos.
     16  *
     17  * Diagnostics (hard wfail):
     18  *   - irreducible: a label has both forward and backward predecessors
     19  *     (multi-entry loop or hybrid; v1 declines node splitting).
     20  *   - cross-scope: a free label sits at scope depth D, but a predecessor
     21  *     sits inside an extra CG scope; the synthetic block's start cannot
     22  *     lift across the scope boundary cleanly. */
     23 
     24 #include <stdarg.h>
     25 #include <string.h>
     26 
     27 #include "arch/wasm/internal.h"
     28 #include "core/heap.h"
     29 
     30 static _Noreturn void sfail(WTarget* t, const char* fmt, ...) {
     31   va_list ap;
     32   SrcLoc l = {0, 0, 0};
     33   va_start(ap, fmt);
     34   if (t->cur_fn_desc) l = t->cur_fn_desc->loc;
     35   compiler_panicv(t->c, l, fmt, ap);
     36 }
     37 
     38 /* Per-free-label working state. */
     39 typedef struct WSLbl {
     40   Label label;
     41   u32 wir_idx;     /* WIR_LABEL position */
     42   u32 scope_depth; /* CG-scope nesting depth at the label */
     43   /* Forward predecessor stats: min adjusted start (so the synthetic block
     44    * opens at the same scope depth as L). UINT32_MAX = no forward pred. */
     45   u32 fwd_start_idx;
     46   u32 nfwd;
     47   /* Backward predecessor stats: max adjusted end (exclusive) for the
     48    * synthetic loop close. UINT32_MAX = no backward pred. */
     49   u32 bwd_end_idx;
     50   u32 nbwd;
     51   /* After pass_decide: actual synthetic scope range. open_idx is mutable
     52    * by merge_ranges (extended backward to nest overlapping forward blocks);
     53    * close_idx is fixed at the label's natural position so jumps to the
     54    * label still land at the right place. */
     55   u32 open_idx;
     56   u32 close_idx;
     57   u8 is_loop;
     58   u8 valid; /* 1 if structurer will wrap this label */
     59   u8 pad[2];
     60 } WSLbl;
     61 
     62 /* Event inserted at a WIR position. Sorted primarily by wir_idx; within the
     63  * same wir_idx, closes fire before opens. The pair_idx field carries the
     64  * partner event's wir_idx so we can break ties: at a shared OPEN wir_idx,
     65  * the outer (larger pair_idx = later close) opens first; at a shared CLOSE
     66  * wir_idx, the inner (larger pair_idx = later open) closes first. */
     67 typedef enum WSEvKind {
     68   WSEV_CLOSE_LOOP = 0,
     69   WSEV_CLOSE_BLOCK = 1,
     70   WSEV_OPEN_BLOCK = 2,
     71   WSEV_OPEN_LOOP = 3,
     72 } WSEvKind;
     73 
     74 typedef struct WSEvent {
     75   u32 wir_idx;
     76   u32 pair_idx;
     77   u8 kind; /* WSEvKind */
     78   u8 pad[3];
     79   u32 scope_id;
     80   Label label; /* break (BLOCK) or continue (LOOP) */
     81 } WSEvent;
     82 
     83 /* Per-idx scope chain entry. */
     84 typedef struct WSScopeFrame {
     85   u32 scope_id;
     86   u32 open_idx;
     87   u32 depth; /* 1-based */
     88 } WSScopeFrame;
     89 
     90 typedef struct WSCtx {
     91   WTarget* t;
     92   WSLbl* lbls;
     93   u32 nlbls;
     94   u32 lbls_cap;
     95   /* Per-WIR-idx scope depth (immediately AT this index — for SCOPE_OPEN,
     96    * the depth AFTER pushing). */
     97   u32* depth_at;
     98   u32 ndepth;
     99   /* Stack of currently-open scopes during the WIR walk; recycled per
    100    * pass since CG scopes never go above 32. */
    101   WSScopeFrame stack[64];
    102   u32 nstack;
    103   WSEvent* events;
    104   u32 nevents;
    105   u32 events_cap;
    106 } WSCtx;
    107 
    108 /* ---- label index ---- */
    109 
    110 static WSLbl* find_lbl(WSCtx* x, Label l) {
    111   for (u32 i = 0; i < x->nlbls; ++i) {
    112     if (x->lbls[i].label == l) return &x->lbls[i];
    113   }
    114   return NULL;
    115 }
    116 
    117 static WSLbl* push_lbl(WSCtx* x, Label l) {
    118   Heap* h = x->t->c->ctx->heap;
    119   if (x->nlbls == x->lbls_cap) {
    120     u32 nc = x->lbls_cap ? x->lbls_cap * 2u : 8u;
    121     void* p = h->realloc(h, x->lbls, sizeof(WSLbl) * x->lbls_cap,
    122                          sizeof(WSLbl) * nc, _Alignof(WSLbl));
    123     if (!p) sfail(x->t, "wasm: out of memory in structurer");
    124     x->lbls = (WSLbl*)p;
    125     x->lbls_cap = nc;
    126   }
    127   WSLbl* L = &x->lbls[x->nlbls++];
    128   memset(L, 0, sizeof *L);
    129   L->label = l;
    130   L->fwd_start_idx = UINT32_MAX;
    131   L->bwd_end_idx = UINT32_MAX;
    132   L->open_idx = UINT32_MAX;
    133   L->close_idx = UINT32_MAX;
    134   return L;
    135 }
    136 
    137 /* ---- pass 1: scope-depth map + free-label discovery ---- */
    138 
    139 static void pass_scope_depth(WSCtx* x) {
    140   WTarget* t = x->t;
    141   Heap* h = t->c->ctx->heap;
    142   x->depth_at =
    143       (u32*)h->alloc(h, sizeof(u32) * (t->nwir ? t->nwir : 1u), _Alignof(u32));
    144   if (!x->depth_at) sfail(t, "wasm: out of memory in structurer");
    145   x->ndepth = t->nwir;
    146   x->nstack = 0;
    147   for (u32 i = 0; i < t->nwir; ++i) {
    148     WIR* w = &t->wir[i];
    149     if (w->op == WIR_SCOPE_OPEN) {
    150       if (x->nstack >= 64u) sfail(t, "wasm: scope nesting too deep");
    151       x->stack[x->nstack].scope_id = w->scope_id;
    152       x->stack[x->nstack].open_idx = i;
    153       x->stack[x->nstack].depth = x->nstack + 1u;
    154       x->nstack++;
    155     }
    156     x->depth_at[i] = x->nstack;
    157     if (w->op == WIR_SCOPE_CLOSE) {
    158       if (x->nstack == 0)
    159         sfail(t, "wasm: structurer saw SCOPE_CLOSE with empty stack");
    160       x->nstack--;
    161       /* Depth AT a CLOSE is the depth INSIDE the scope (we haven't popped
    162        * yet at the time the record sits in WIR). The pop above is for the
    163        * NEXT idx. Restore. */
    164       x->depth_at[i] = x->nstack + 1u;
    165     }
    166     if (w->op == WIR_LABEL) {
    167       WLabel* lbl = &t->labels[w->labels[0] - 1u];
    168       if (lbl->kind == WLBL_FORWARD || lbl->kind == WLBL_UNBOUND) {
    169         WSLbl* L = find_lbl(x, w->labels[0]);
    170         if (!L) L = push_lbl(x, w->labels[0]);
    171         L->wir_idx = i;
    172         L->scope_depth = x->depth_at[i];
    173       }
    174     }
    175   }
    176   if (x->nstack != 0)
    177     sfail(t, "wasm: structurer finished with %u scopes still open", x->nstack);
    178 }
    179 
    180 /* ---- pass 2: locate the smallest "outside" idx for a pred at depth `pd`
    181  *      whose target sits at depth `ld`. Walks backward through WIR's scope
    182  *      transitions to find the open_idx of the outermost CG scope that
    183  *      contains the pred but not the target. ---- */
    184 static u32 lift_to_label_depth(WSCtx* x, u32 pred_idx, u32 ld) {
    185   WTarget* t = x->t;
    186   u32 cur = x->depth_at[pred_idx];
    187   /* We need cur to drop to ld. Walk backward; each SCOPE_OPEN we cross
    188    * (going up the depth) corresponds to an extra scope above the target's
    189    * depth. We stop AT the outermost extra SCOPE_OPEN. */
    190   if (cur <= ld) return pred_idx;
    191   u32 i = pred_idx;
    192   u32 best = pred_idx;
    193   while (i > 0 && cur > ld) {
    194     --i;
    195     if (t->wir[i].op == WIR_SCOPE_OPEN) {
    196       cur--;
    197       if (cur == ld) {
    198         best = i; /* place synthetic OPEN right before this CG OPEN */
    199         break;
    200       }
    201     } else if (t->wir[i].op == WIR_SCOPE_CLOSE) {
    202       cur++;
    203     }
    204   }
    205   return best;
    206 }
    207 
    208 /* Symmetric: for a backward pred at idx pred_idx targeting label at depth ld,
    209  * return the WIR position (exclusive end) where the synthetic LOOP must
    210  * close so that it sits at the same scope depth as the label. */
    211 static u32 drop_to_label_depth_after(WSCtx* x, u32 pred_idx, u32 ld) {
    212   WTarget* t = x->t;
    213   u32 cur = x->depth_at[pred_idx];
    214   /* Walk forward across closes until we drop to ld. The synthetic close
    215    * happens AFTER the SCOPE_CLOSE that takes us back to ld. */
    216   if (cur <= ld) return pred_idx + 1u;
    217   u32 i = pred_idx;
    218   while (i < t->nwir && cur > ld) {
    219     ++i;
    220     if (i >= t->nwir) break;
    221     if (t->wir[i].op == WIR_SCOPE_OPEN) {
    222       cur++;
    223     } else if (t->wir[i].op == WIR_SCOPE_CLOSE) {
    224       cur--;
    225       if (cur == ld) {
    226         return i + 1u;
    227       }
    228     }
    229   }
    230   /* Couldn't drop — back-edge spans out of the function body? Treat as
    231    * unstructurable. */
    232   return UINT32_MAX;
    233 }
    234 
    235 /* ---- pass 3: record pred contributions for each free label. Walk WIR
    236  *      and for each JUMP/CMP_BRANCH/SWITCH that targets a tracked free
    237  *      label, update fwd_start_idx / bwd_end_idx / nfwd / nbwd. ---- */
    238 static void verify_well_nested_fwd(WSCtx* x, u32 start, u32 end, u32 ld) {
    239   /* Every WIR index in [start, end) must have scope depth >= ld so the
    240    * synthetic block can sit at L's depth without crossing a CG-scope
    241    * boundary outward. */
    242   for (u32 i = start; i < end; ++i) {
    243     if (x->depth_at[i] < ld) {
    244       sfail(x->t,
    245             "wasm: forward jump crosses out of structured scope at WIR[%u]; "
    246             "kit's CFG structurer does not split scopes in v1",
    247             i);
    248     }
    249   }
    250 }
    251 
    252 static void note_pred(WSCtx* x, u32 pred_idx, Label tgt) {
    253   WSLbl* L = find_lbl(x, tgt);
    254   if (!L) return;
    255   if (pred_idx < L->wir_idx) {
    256     /* Forward pred. */
    257     u32 lifted = lift_to_label_depth(x, pred_idx, L->scope_depth);
    258     verify_well_nested_fwd(x, lifted, L->wir_idx, L->scope_depth);
    259     if (lifted < L->fwd_start_idx) L->fwd_start_idx = lifted;
    260     L->nfwd++;
    261   } else if (pred_idx > L->wir_idx) {
    262     /* Backward pred. */
    263     u32 dropped = drop_to_label_depth_after(x, pred_idx, L->scope_depth);
    264     if (dropped == UINT32_MAX) {
    265       sfail(x->t,
    266             "wasm: backward jump at WIR[%u] to label outside function body; "
    267             "structurer cannot place enclosing loop",
    268             pred_idx);
    269     }
    270     if (L->bwd_end_idx == UINT32_MAX || dropped > L->bwd_end_idx)
    271       L->bwd_end_idx = dropped;
    272     L->nbwd++;
    273   }
    274 }
    275 
    276 /* Switch-island reorder.
    277  *
    278  * Frontends (C, toy) emit `switch` as:
    279  *     JUMP dispatch          // forward, to skip past case bodies on entry
    280  *     LABEL case_1; body; JUMP end
    281  *     LABEL case_2; body; JUMP end
    282  *     LABEL dispatch
    283  *     selector
    284  *     WIR_SWITCH cases: [case_1, case_2], default: end
    285  *     LABEL end
    286  *
    287  * The case labels are placed BEFORE the WIR_SWITCH that targets them — a
    288  * backward edge in WIR order that the natural structurer cannot wrap as
    289  * forward blocks. We rewrite the WIR to move the dispatch (selector +
    290  * SWITCH) up to the position of the original JUMP and let the case
    291  * bodies follow:
    292  *
    293  *     selector
    294  *     WIR_SWITCH cases: [case_1, case_2], default: end
    295  *     LABEL case_1; body; JUMP end
    296  *     LABEL case_2; body; JUMP end
    297  *     LABEL end
    298  *
    299  * Now case_i is forward from SWITCH and the structurer wraps it in a
    300  * synthetic SCOPE_BLOCK like any other forward goto. The JUMP and the
    301  * dispatch LABEL are dropped (the dispatch label is no longer reached).
    302  *
    303  * The pattern is detected by scanning forward from each WIR_JUMP. We
    304  * support multiple non-nested switch islands per function and stop on
    305  * the first intervening terminator that breaks the dispatch shape. */
    306 typedef struct WSIsland {
    307   u32 j; /* JUMP dispatch idx */
    308   u32 d; /* LABEL dispatch idx */
    309   u32 s; /* WIR_SWITCH idx */
    310 } WSIsland;
    311 
    312 static WIR* wir_append(WTarget* t, WIR** out, u32* nout, u32* cap);
    313 
    314 /* One pass of switch-island detection + reordering. Returns the number of
    315  * islands rewritten (0 = nothing to do). Nested switches need multiple passes:
    316  * the outer JUMP-to-dispatch sits before the inner switch's JUMP-to-dispatch
    317  * in WIR order, so the outer scan advances past the inner JUMP after recording
    318  * the outer island. Re-scanning the rewritten WIR brings the now-exposed inner
    319  * island into view. */
    320 static int unroll_switch_islands_once(WSCtx* x) {
    321   WTarget* t = x->t;
    322   Heap* h = t->c->ctx->heap;
    323   WSIsland islands[16];
    324   u32 nislands = 0;
    325   u32 scan = 0;
    326   while (scan < t->nwir) {
    327     if (t->wir[scan].op != WIR_JUMP) {
    328       scan++;
    329       continue;
    330     }
    331     Label disp = t->wir[scan].labels[0];
    332     if (disp == LABEL_NONE || disp - 1u >= t->nlabels) {
    333       scan++;
    334       continue;
    335     }
    336     WLabel* dlbl = &t->labels[disp - 1u];
    337     if (dlbl->kind != WLBL_FORWARD || !dlbl->placed ||
    338         dlbl->wir_index <= scan) {
    339       scan++;
    340       continue;
    341     }
    342     /* Find WIR_SWITCH just past dispatch label; bail on intervening
    343      * terminator or scope boundary. The C/toy frontends emit the
    344      * selector expression as a small straight-line sequence between
    345      * LABEL dispatch and WIR_SWITCH, with no SCOPE_OPEN/CLOSE — those
    346      * only appear at switch entry/exit, never around the dispatcher.
    347      * Rejecting SCOPE_OPEN here keeps a second pass from mistaking a
    348      * for-loop body label (which sits just before a `[switch open]`)
    349      * for a switch dispatch label. */
    350     u32 sw_i = UINT32_MAX;
    351     for (u32 k = dlbl->wir_index + 1u; k < t->nwir; ++k) {
    352       if (t->wir[k].op == WIR_SWITCH) {
    353         sw_i = k;
    354         break;
    355       }
    356       if (t->wir[k].op == WIR_JUMP || t->wir[k].op == WIR_CMP_BRANCH ||
    357           t->wir[k].op == WIR_RET || t->wir[k].op == WIR_UNREACHABLE ||
    358           t->wir[k].op == WIR_SCOPE_OPEN || t->wir[k].op == WIR_SCOPE_CLOSE)
    359         break;
    360     }
    361     if (sw_i == UINT32_MAX) {
    362       scan++;
    363       continue;
    364     }
    365     if (nislands >= 16u)
    366       sfail(t,
    367             "wasm: too many switch islands per function (max 16 supported in "
    368             "v1)");
    369     islands[nislands].j = scan;
    370     islands[nislands].d = dlbl->wir_index;
    371     islands[nislands].s = sw_i;
    372     nislands++;
    373     scan = sw_i + 1u;
    374   }
    375   if (nislands == 0) return 0;
    376 
    377   /* Build a rewritten WIR list. For each island, emit pre + (selector +
    378    * SWITCH) + (case bodies). Then everything after the last island's
    379    * SWITCH is copied through. */
    380   WIR* new_wir = NULL;
    381   u32 new_n = 0;
    382   u32 new_cap = 0;
    383   u32 cursor = 0;
    384   for (u32 ii = 0; ii < nislands; ++ii) {
    385     WSIsland* I = &islands[ii];
    386     /* pre-island: WIR[cursor .. I->j) */
    387     for (u32 k = cursor; k < I->j; ++k) {
    388       WIR* w = wir_append(t, &new_wir, &new_n, &new_cap);
    389       *w = t->wir[k];
    390     }
    391     /* selector + SWITCH: WIR[I->d+1 .. I->s] */
    392     for (u32 k = I->d + 1u; k <= I->s; ++k) {
    393       WIR* w = wir_append(t, &new_wir, &new_n, &new_cap);
    394       *w = t->wir[k];
    395     }
    396     /* case bodies: WIR[I->j+1 .. I->d) — drops the JUMP at j and LABEL at d */
    397     for (u32 k = I->j + 1u; k < I->d; ++k) {
    398       WIR* w = wir_append(t, &new_wir, &new_n, &new_cap);
    399       *w = t->wir[k];
    400     }
    401     cursor = I->s + 1u;
    402   }
    403   /* trailing region after the last island */
    404   for (u32 k = cursor; k < t->nwir; ++k) {
    405     WIR* w = wir_append(t, &new_wir, &new_n, &new_cap);
    406     *w = t->wir[k];
    407   }
    408 
    409   if (t->wir) h->free(h, t->wir, sizeof(WIR) * t->wir_cap);
    410   t->wir = new_wir;
    411   t->nwir = new_n;
    412   t->wir_cap = new_cap;
    413 
    414   /* Refresh WLabel.wir_index from the new layout so any downstream
    415    * consumer of t->labels[].wir_index sees the rewritten positions.
    416    * (pass_scope_depth re-discovers free-label positions on its own walk.) */
    417   for (u32 k = 0; k < t->nwir; ++k) {
    418     if (t->wir[k].op != WIR_LABEL) continue;
    419     Label l = t->wir[k].labels[0];
    420     if (l != LABEL_NONE && l - 1u < t->nlabels) t->labels[l - 1u].wir_index = k;
    421   }
    422   return (int)nislands;
    423 }
    424 
    425 /* Re-run the single-pass unroller to a fixed point so nested switches all
    426  * get reordered. Each pass reorders at most the outermost island in a chain
    427  * of nested switches; the next pass exposes the next inner one. The loop
    428  * terminates because every rewritten island drops one JUMP-to-dispatch from
    429  * the WIR (and a function has finitely many). */
    430 static void unroll_switch_islands(WSCtx* x) {
    431   while (unroll_switch_islands_once(x) > 0) { /* re-scan after each rewrite */
    432   }
    433 }
    434 
    435 /* For-loop de-rotation.
    436  *
    437  * The C frontend lowers `for (init; cond; step) body` in a single pass, so
    438  * the step clause (which appears before the body in source) is emitted into
    439  * a block placed BEFORE the body, with the body jumping back to it:
    440  *
    441  *     LABEL L_top                 // header
    442  *     <cond>; CMP_BRANCH L_end    // (cond optional)
    443  *     JUMP L_body                 // skip the step on entry
    444  *     LABEL L_step                // continue target
    445  *     <step>
    446  *     JUMP L_top                  // genuine loop back-edge
    447  *     LABEL L_body
    448  *     <body>
    449  *     JUMP L_step                 // body -> step (continue path)
    450  *     LABEL L_end
    451  *
    452  * In WIR textual order the body's `JUMP L_step` is a backward edge, so the
    453  * structurer misclassifies L_step as a loop header and emits a second,
    454  * unintended `loop` — the program spins forever. The CFG is reducible; only
    455  * the block layout is rotated. We restore a structure-compatible order by
    456  * moving the step block to AFTER the body, mirroring how
    457  * unroll_switch_islands reorders the switch shape:
    458  *
    459  *     LABEL L_top
    460  *     <cond>; CMP_BRANCH L_end
    461  *     LABEL L_body                // JUMP L_body dropped; falls through
    462  *     <body>
    463  *     JUMP L_step                 // now a forward edge
    464  *     LABEL L_step
    465  *     <step>
    466  *     JUMP L_top                  // sole back-edge -> L_top is the loop
    467  *     LABEL L_end
    468  *
    469  * The step block ends in an unconditional `JUMP L_top` (no fall-through out)
    470  * and is entered only via `JUMP L_step`, so relocating it preserves
    471  * semantics. Nested for-loops live entirely inside the body region and are
    472  * handled by re-scanning to a fixed point. */
    473 static int derotate_one_for_loop(WSCtx* x) {
    474   WTarget* t = x->t;
    475   Heap* h = t->c->ctx->heap;
    476   for (u32 j = 0; j < t->nwir; ++j) {
    477     if (t->wir[j].op != WIR_JUMP) continue;
    478     /* JUMP L_body, with L_body a forward label. */
    479     Label xb = t->wir[j].labels[0];
    480     if (xb == LABEL_NONE || xb - 1u >= t->nlabels) continue;
    481     WLabel* lx = &t->labels[xb - 1u];
    482     if (!lx->placed || lx->wir_index <= j) continue;
    483     /* Immediately followed by LABEL L_step. */
    484     if (j + 1u >= t->nwir || t->wir[j + 1u].op != WIR_LABEL) continue;
    485     Label s = t->wir[j + 1u].labels[0];
    486     if (s == LABEL_NONE || s == xb) continue;
    487     /* Scan the step block for `JUMP L_top; LABEL L_body`, L_top backward. */
    488     u32 jt = UINT32_MAX;
    489     for (u32 m = j + 2u; m + 1u < t->nwir; ++m) {
    490       u8 mop = t->wir[m].op;
    491       if (mop == WIR_RET || mop == WIR_UNREACHABLE) break;
    492       if (mop != WIR_JUMP) continue;
    493       Label cand = t->wir[m].labels[0];
    494       if (cand == LABEL_NONE || cand - 1u >= t->nlabels) continue;
    495       WLabel* lc = &t->labels[cand - 1u];
    496       if (lc->placed && lc->wir_index < j && t->wir[m + 1u].op == WIR_LABEL &&
    497           t->wir[m + 1u].labels[0] == xb) {
    498         jt = m;
    499         break;
    500       }
    501     }
    502     if (jt == UINT32_MAX) continue;
    503     /* k = last `JUMP L_step` (the body's closing/continue edges all target
    504      * L_step; the textually-last one closes the body, just before L_end). */
    505     u32 k = UINT32_MAX;
    506     for (u32 m = jt + 1u; m < t->nwir; ++m) {
    507       if (t->wir[m].op == WIR_JUMP && t->wir[m].labels[0] == s) k = m;
    508     }
    509     if (k == UINT32_MAX || k <= jt) continue;
    510 
    511     /* Rebuild: [0..j) drop JUMP at j, [jt+1..k] body, [j+1..jt] step,
    512      * [k+1..nwir). */
    513     WIR* nw = NULL;
    514     u32 nn = 0, ncap = 0;
    515     for (u32 m = 0; m < j; ++m) *wir_append(t, &nw, &nn, &ncap) = t->wir[m];
    516     for (u32 m = jt + 1u; m <= k; ++m)
    517       *wir_append(t, &nw, &nn, &ncap) = t->wir[m];
    518     for (u32 m = j + 1u; m <= jt; ++m)
    519       *wir_append(t, &nw, &nn, &ncap) = t->wir[m];
    520     for (u32 m = k + 1u; m < t->nwir; ++m)
    521       *wir_append(t, &nw, &nn, &ncap) = t->wir[m];
    522 
    523     if (t->wir) h->free(h, t->wir, sizeof(WIR) * t->wir_cap);
    524     t->wir = nw;
    525     t->nwir = nn;
    526     t->wir_cap = ncap;
    527     for (u32 m = 0; m < t->nwir; ++m) {
    528       if (t->wir[m].op != WIR_LABEL) continue;
    529       Label l = t->wir[m].labels[0];
    530       if (l != LABEL_NONE && l - 1u < t->nlabels)
    531         t->labels[l - 1u].wir_index = m;
    532     }
    533     return 1;
    534   }
    535   return 0;
    536 }
    537 
    538 static void derotate_for_loops(WSCtx* x) {
    539   while (derotate_one_for_loop(x)) { /* re-scan after each reorder */
    540   }
    541 }
    542 
    543 static void pass_preds(WSCtx* x) {
    544   WTarget* t = x->t;
    545   for (u32 i = 0; i < t->nwir; ++i) {
    546     WIR* w = &t->wir[i];
    547     switch (w->op) {
    548       case WIR_JUMP:
    549       case WIR_CMP_BRANCH:
    550         if (w->labels[0] != LABEL_NONE) note_pred(x, i, w->labels[0]);
    551         break;
    552       case WIR_SWITCH:
    553         if (w->labels[0] != LABEL_NONE) note_pred(x, i, w->labels[0]);
    554         for (u32 c = 0; c < w->switch_ncases; ++c) {
    555           if (w->switch_cases[c].label != LABEL_NONE)
    556             note_pred(x, i, w->switch_cases[c].label);
    557         }
    558         break;
    559       default:
    560         break;
    561     }
    562   }
    563 }
    564 
    565 /* ---- pass 4: produce per-label decisions. ---- */
    566 static void pass_decide(WSCtx* x) {
    567   WTarget* t = x->t;
    568   for (u32 i = 0; i < x->nlbls; ++i) {
    569     WSLbl* L = &x->lbls[i];
    570     if (L->nfwd == 0 && L->nbwd == 0) {
    571       /* Dead label — placed but never branched to. Drop quietly. */
    572       L->valid = 0;
    573       continue;
    574     }
    575     if (L->nfwd > 0 && L->nbwd > 0) {
    576       sfail(t,
    577             "wasm: label has both forward and backward gotos; kit's CFG "
    578             "structurer does not split hybrid labels in v1");
    579     }
    580     L->valid = 1;
    581     if (L->nfwd > 0) {
    582       L->is_loop = 0;
    583       L->open_idx = L->fwd_start_idx;
    584       L->close_idx = L->wir_idx;
    585     } else {
    586       L->is_loop = 1;
    587       L->open_idx = L->wir_idx;
    588       L->close_idx = L->bwd_end_idx;
    589     }
    590   }
    591 }
    592 
    593 /* ---- pass 5: emit event list and validate well-nesting. ---- */
    594 
    595 static void push_event(WSCtx* x, u32 wir_idx, u32 pair_idx, u8 kind,
    596                        u32 scope_id, Label label) {
    597   Heap* h = x->t->c->ctx->heap;
    598   if (x->nevents == x->events_cap) {
    599     u32 nc = x->events_cap ? x->events_cap * 2u : 16u;
    600     void* p = h->realloc(h, x->events, sizeof(WSEvent) * x->events_cap,
    601                          sizeof(WSEvent) * nc, _Alignof(WSEvent));
    602     if (!p) sfail(x->t, "wasm: out of memory in structurer");
    603     x->events = (WSEvent*)p;
    604     x->events_cap = nc;
    605   }
    606   WSEvent* e = &x->events[x->nevents++];
    607   e->wir_idx = wir_idx;
    608   e->pair_idx = pair_idx;
    609   e->kind = kind;
    610   e->scope_id = scope_id;
    611   e->label = label;
    612 }
    613 
    614 static int ev_cmp(const WSEvent* a, const WSEvent* b) {
    615   if (a->wir_idx != b->wir_idx) return a->wir_idx < b->wir_idx ? -1 : 1;
    616   /* At a tie: closes fire before opens (so we close existing scopes before
    617    * opening new ones). CLOSE kinds are 0..1, OPEN kinds are 2..3. */
    618   int a_close = a->kind < WSEV_OPEN_BLOCK;
    619   int b_close = b->kind < WSEV_OPEN_BLOCK;
    620   if (a_close != b_close) return a_close ? -1 : 1;
    621   if (a_close) {
    622     /* CLOSE: innermost first. pair_idx = open_idx; the more-recently-opened
    623      * scope has a LATER open_idx, so larger pair_idx closes first. */
    624     if (a->pair_idx != b->pair_idx) return a->pair_idx > b->pair_idx ? -1 : 1;
    625     return 0;
    626   }
    627   /* OPEN: outer first. pair_idx = close_idx; the larger range (later close)
    628    * encloses the smaller, so larger pair_idx opens first. */
    629   if (a->pair_idx != b->pair_idx) return a->pair_idx > b->pair_idx ? -1 : 1;
    630   return 0;
    631 }
    632 
    633 static void ev_sort(WSEvent* arr, u32 n) {
    634   /* Small n — insertion sort. */
    635   for (u32 i = 1; i < n; ++i) {
    636     WSEvent k = arr[i];
    637     u32 j = i;
    638     while (j > 0 && ev_cmp(&arr[j - 1u], &k) > 0) {
    639       arr[j] = arr[j - 1u];
    640       --j;
    641     }
    642     arr[j] = k;
    643   }
    644 }
    645 
    646 /* Resolve overlapping synthetic ranges into a properly-nested set.
    647  *
    648  * Close positions are FIXED — they correspond to the label's natural WIR
    649  * placement (for forward labels) or the last back-edge end (for loop
    650  * headers). Moving a close would change where a br to the label lands.
    651  *
    652  * So we only adjust OPEN positions, pushing them backward as needed: if
    653  * range A's close happens inside range B (a_o < b_o < a_c < b_c), pull
    654  * B's open back to A's open so A nests inside B (both open at a_o; A's
    655  * inner close at a_c fires before B's outer close at b_c).
    656  *
    657  * Iterate to a fixed point since extending one range can introduce new
    658  * overlaps with a third. The number of distinct opens is bounded by the
    659  * label count, so iteration terminates in O(nlbls^2). */
    660 static void merge_ranges(WSCtx* x) {
    661   for (;;) {
    662     int changed = 0;
    663     for (u32 i = 0; i < x->nlbls; ++i) {
    664       if (!x->lbls[i].valid) continue;
    665       for (u32 j = 0; j < x->nlbls; ++j) {
    666         if (j == i || !x->lbls[j].valid) continue;
    667         WSLbl* a = &x->lbls[i];
    668         WSLbl* b = &x->lbls[j];
    669         u32 a_o = a->open_idx, a_c = a->close_idx;
    670         u32 b_o = b->open_idx, b_c = b->close_idx;
    671         /* Disjoint or nested — fine. */
    672         if (a_c <= b_o || b_c <= a_o) continue;
    673         if (a_o <= b_o && b_c <= a_c) continue;
    674         if (b_o <= a_o && a_c <= b_c) continue;
    675         /* Overlap. Two cases:
    676          *   a_o < b_o < a_c < b_c  -> pull b's open back to a_o
    677          *   b_o < a_o < b_c < a_c  -> handled by the j-then-i symmetric
    678          *                              iteration.
    679          */
    680         if (a_o < b_o && b_o < a_c && a_c < b_c) {
    681           b->open_idx = a_o;
    682           changed = 1;
    683         }
    684       }
    685     }
    686     if (!changed) break;
    687   }
    688 }
    689 
    690 static void pass_events(WSCtx* x) {
    691   WTarget* t = x->t;
    692   for (u32 i = 0; i < x->nlbls; ++i) {
    693     WSLbl* L = &x->lbls[i];
    694     if (!L->valid) continue;
    695     u32 scope_id = ++t->next_scope_id;
    696     if (L->is_loop) {
    697       push_event(x, L->open_idx, L->close_idx, WSEV_OPEN_LOOP, scope_id,
    698                  L->label);
    699       push_event(x, L->close_idx, L->open_idx, WSEV_CLOSE_LOOP, scope_id,
    700                  L->label);
    701     } else {
    702       push_event(x, L->open_idx, L->close_idx, WSEV_OPEN_BLOCK, scope_id,
    703                  L->label);
    704       push_event(x, L->close_idx, L->open_idx, WSEV_CLOSE_BLOCK, scope_id,
    705                  L->label);
    706     }
    707     /* Re-bind the WLabel. */
    708     WLabel* wl = &t->labels[L->label - 1u];
    709     wl->scope_id = scope_id;
    710     wl->kind = L->is_loop ? WLBL_SCOPE_CONT : WLBL_SCOPE_BREAK;
    711   }
    712   ev_sort(x->events, x->nevents);
    713 }
    714 
    715 /* ---- pass 6: rebuild the WIR list, splicing in synthetic scope opens
    716  *      and closes at their event positions. ---- */
    717 
    718 static WIR* wir_append(WTarget* t, WIR** out, u32* nout, u32* cap) {
    719   Heap* h = t->c->ctx->heap;
    720   if (*nout == *cap) {
    721     u32 nc = *cap ? *cap * 2u : 64u;
    722     void* p = h->realloc(h, *out, sizeof(WIR) * *cap, sizeof(WIR) * nc,
    723                          _Alignof(WIR));
    724     if (!p) sfail(t, "wasm: out of memory in structurer");
    725     *out = (WIR*)p;
    726     *cap = nc;
    727   }
    728   WIR* w = &(*out)[(*nout)++];
    729   memset(w, 0, sizeof *w);
    730   return w;
    731 }
    732 
    733 static void emit_synthetic_scope(WTarget* t, WIR** out, u32* nout, u32* cap,
    734                                  u8 ev_kind, u32 scope_id) {
    735   WIR* w = wir_append(t, out, nout, cap);
    736   if (ev_kind == WSEV_OPEN_BLOCK || ev_kind == WSEV_OPEN_LOOP) {
    737     w->op = WIR_SCOPE_OPEN;
    738     w->cgop = (ev_kind == WSEV_OPEN_LOOP) ? SCOPE_LOOP : SCOPE_BLOCK;
    739   } else {
    740     w->op = WIR_SCOPE_CLOSE;
    741   }
    742   w->scope_id = scope_id;
    743   w->dst = REG_NONE;
    744 }
    745 
    746 static void pass_rewrite(WSCtx* x) {
    747   WTarget* t = x->t;
    748   WIR* new_wir = NULL;
    749   u32 new_n = 0;
    750   u32 new_cap = 0;
    751   u32 ev_i = 0;
    752   for (u32 i = 0; i <= t->nwir; ++i) {
    753     /* Fire all events whose wir_idx == i (closes first, then opens). */
    754     while (ev_i < x->nevents && x->events[ev_i].wir_idx == i) {
    755       emit_synthetic_scope(t, &new_wir, &new_n, &new_cap, x->events[ev_i].kind,
    756                            x->events[ev_i].scope_id);
    757       ev_i++;
    758     }
    759     if (i == t->nwir) break;
    760     WIR* src = &t->wir[i];
    761     WIR* dst = wir_append(t, &new_wir, &new_n, &new_cap);
    762     *dst = *src;
    763   }
    764   if (ev_i != x->nevents)
    765     sfail(t, "wasm: structurer left %u events unplaced", x->nevents - ev_i);
    766   /* Swap. */
    767   Heap* h = t->c->ctx->heap;
    768   if (t->wir) h->free(h, t->wir, sizeof(WIR) * t->wir_cap);
    769   t->wir = new_wir;
    770   t->nwir = new_n;
    771   t->wir_cap = new_cap;
    772 }
    773 
    774 /* ---- entry point ---- */
    775 
    776 void wasm_structurize(WTarget* t) {
    777   if (t->nwir == 0) return;
    778   Heap* h = t->c->ctx->heap;
    779   WSCtx x;
    780   memset(&x, 0, sizeof x);
    781   x.t = t;
    782   unroll_switch_islands(&x);
    783   derotate_for_loops(&x);
    784   pass_scope_depth(&x);
    785   pass_preds(&x);
    786   pass_decide(&x);
    787   if (x.nlbls > 0) {
    788     int any = 0;
    789     for (u32 i = 0; i < x.nlbls; ++i)
    790       if (x.lbls[i].valid) {
    791         any = 1;
    792         break;
    793       }
    794     if (any) {
    795       merge_ranges(&x);
    796       pass_events(&x);
    797       pass_rewrite(&x);
    798     }
    799   }
    800   if (x.depth_at) h->free(h, x.depth_at, sizeof(u32) * x.ndepth);
    801   if (x.lbls) h->free(h, x.lbls, sizeof(WSLbl) * x.lbls_cap);
    802   if (x.events) h->free(h, x.events, sizeof(WSEvent) * x.events_cap);
    803 }