@asmlift/core 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
@@ -4,8 +4,10 @@
4
4
  // factory takes its dependencies EXPLICITLY (`SwitchRecoverDeps`); `expr`/`structureRegion` are
5
5
  // late-bound callbacks into the emission phase, so case bodies reuse the ordinary structuring
6
6
  // machinery (loops/ifs inside cases, the onStack guard).
7
+ import { constWord } from '../ir/bits';
7
8
  import { Block, Fn, Op, Value, forwardingTarget, isBodyless, successorsOf } from '../ir/core';
8
9
  import { orderSensitive } from '../ir/discipline';
10
+ import { SWAPPED_ICMP } from '../ir/opcodes';
9
11
  import { Expr, Stmt, SwitchCase } from '../l3/ast';
10
12
 
11
13
  /** Which side of a relational test a compiler's dispatch lands a pinned case body on. */
@@ -48,6 +50,11 @@ export interface SwitchRecoverDeps {
48
50
  * of "where is this defined" `defs` does not answer. Taken from `structure.ts`, which builds
49
51
  * both halves for def-site anchoring, rather than indexing the parameters a second time. */
50
52
  blockOf: (v: Value) => Block | undefined;
53
+ /** The header and exit of the loop the switch is being structured in, or null outside any loop.
54
+ * Read LIVE: the structurer's loop frame is the one current when the switch is met. An edge to
55
+ * the exit from inside a case arm is a `break` of that loop, which C binds to the switch instead
56
+ * (`analyzeArmExit`), save where the exit is the switch's own merge. */
57
+ enclosingLoop: () => { header: Block; exit: Block | null } | null;
51
58
  /** THE DISPATCH HOIST: the copies of every edge the tree walk collapsed, merged and re-emitted
52
59
  * ONCE ahead of the `switch`. `structure.ts hoistedDispatchAssigns` owns the emission
53
60
  * (suppression, identity elision, `undef`, the write-order sort, `sequentialize`), so this
@@ -60,6 +67,10 @@ export interface SwitchRecoverDeps {
60
67
  ) => Stmt[] | null;
61
68
  expr: (v: Value) => Expr;
62
69
  structureRegion: (b: Block, stop: Block | null) => Stmt[];
70
+ /** One arm's body, from `entry` to `stop`, inside the switch whose code resumes at `merge`. The
71
+ * structurer owns what `break;` means there (structure.ts `structureArm`), so both regimes hand
72
+ * it every arm and the exit verdict they read for it. */
73
+ structureArm: (entry: Block, stop: Block | null, merge: Block | null, exit: ArmExit | undefined) => Stmt[];
63
74
  }
64
75
 
65
76
  /** Where ONE switch arm's region leaves it — the fact that decides whether the arm can be spelled
@@ -70,18 +81,54 @@ export interface SwitchRecoverDeps {
70
81
  * - `fallthrough` every path out leaves into exactly ONE sibling arm's entry: C's fall-through.
71
82
  * Only spellable when that sibling is the arm emitted NEXT, which `chainArms`
72
83
  * below arranges and both regimes then re-read off the emission array (see the
73
- * l3/ast.ts non-neutrality note).
74
- * - `unstructurable` anything else: two different siblings, or a mix of "into a sibling" and
75
- * "out to the merge". C needs a `goto` for those. Regime A declines to
76
- * if-recovery on this verdict; Regime B, having no fallback, fails LOUD. */
77
- export type ArmExit = { kind: 'break' } | { kind: 'fallthrough'; to: Block } | { kind: 'unstructurable'; why: string };
84
+ * l3/ast.ts non-neutrality note). With `breaks`, some paths out reach the
85
+ * switch's merge instead: `case 1: if (c) break; case 2: …`, the arm's own
86
+ * `break;` on those paths.
87
+ * - `goto` a `fallthrough` exit whose sibling another arm already falls into: the same
88
+ * paths, ending in `goto` to that sibling's case label (l3/ast.ts) instead of
89
+ * running on into it. Only `jumpSharedFallTargets` below makes one, and only
90
+ * Regime B asks it.
91
+ * - `unstructurable` anything else: two different siblings, or a sibling beside a block the
92
+ * switch does not dominate. C needs a `goto` for those. Regime A declines to
93
+ * if-recovery on this verdict; Regime B, having no fallback, fails LOUD.
94
+ *
95
+ * `region` is the arm's own blocks: every block the walk below reached, the entry included. It is
96
+ * what the structurer needs to find the arm's own joins (`structure.ts` `armJoin`) and to spell a
97
+ * path to the merge in the middle of the arm as `break;` (`switchBreakAt`), in a closed arm as in a
98
+ * falling one: `case 1: if (a) { if (b) break; g(1); } h(2); break;`. A closed arm that leaves
99
+ * sideways, into a block the switch does not dominate, has none: its paths through such a block
100
+ * are not the arm's, and it structures to the CFG's joins. It names those blocks instead
101
+ * (`escapedTo`, the first block outside the region on each such path), which decide whether its
102
+ * body can still be copied into the arms that fall into it (`copySharedFallTargets`). An arm that
103
+ * took such a copy carries the SIBLING's `escapedTo`, not its own first outside block: the blocks
104
+ * where the copy steps sideways, which is all the copies read of it. */
105
+ export type ArmExit =
106
+ | { kind: 'break'; region?: ReadonlySet<Block>; escapedTo?: ReadonlySet<Block> }
107
+ | { kind: 'fallthrough'; to: Block; breaks: boolean; region: ReadonlySet<Block> }
108
+ | { kind: 'goto'; to: Block; breaks: boolean; region: ReadonlySet<Block> }
109
+ | { kind: 'unstructurable'; why: string };
78
110
 
79
111
  export interface SwitchRecovery {
80
- recognizeSwitch: (b: Block, stop: Block | null) => Stmt[] | null;
112
+ /** `mergeAt`, given the tree's case and default blocks, may name the switch's end ahead of the
113
+ * CFG's (structure.ts's join past early returns); `undefined` keeps `ipdom ?? stop`. */
114
+ recognizeSwitch: (
115
+ b: Block,
116
+ stop: Block | null,
117
+ mergeAt?: (arms: readonly Block[]) => Block | undefined,
118
+ ) => Stmt[] | null;
81
119
  /** shared with the Regime-B (`switch_br`) path in structure.ts. Both regimes recover the
82
120
  * fall-through this returns; on an `unstructurable` verdict Regime A declines to if-recovery and
83
121
  * Regime B, which has no fallback, fails loud. */
84
- analyzeArmExit: (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>) => ArmExit;
122
+ analyzeArmExit: (
123
+ entry: Block,
124
+ b: Block,
125
+ merge: Block | null,
126
+ siblings: Set<Block>,
127
+ stopAtLoop?: boolean,
128
+ jumps?: ReadonlySet<Block>,
129
+ ) => ArmExit;
130
+ /** may the copies write out a sibling that leaves sideways into `s`? (`copySharedFallTargets`) */
131
+ forwardToMerge: (s: Block, b: Block, merge: Block | null, siblings: ReadonlySet<Block>) => boolean;
85
132
  /** a block's position in the ASSEMBLY — the arm-order evidence, shared with Regime B so the two
86
133
  * regimes read it from one definition (and one statement of what it rests on). */
87
134
  layoutIndex: (blk: Block) => number;
@@ -158,18 +205,6 @@ const INT_MIN = -0x80000000;
158
205
  const INT_MAX = 0x7fffffff;
159
206
  const ALL: Ranges = [[INT_MIN, INT_MAX]];
160
207
 
161
- /** The same opcode with its operands swapped: `k < x` is `x > k`. */
162
- const MIRRORED: Record<string, string> = {
163
- icmp_slt: 'icmp_sgt',
164
- icmp_sgt: 'icmp_slt',
165
- icmp_sle: 'icmp_sge',
166
- icmp_sge: 'icmp_sle',
167
- icmp_ult: 'icmp_ugt',
168
- icmp_ugt: 'icmp_ult',
169
- icmp_ule: 'icmp_uge',
170
- icmp_uge: 'icmp_ule',
171
- };
172
-
173
208
  /** `[lo, hi]` of the UNSIGNED order as signed intervals: one, or two where it straddles 2^31. */
174
209
  function fromUnsigned(lo: number, hi: number): Ranges {
175
210
  if (lo > hi) {
@@ -214,7 +249,7 @@ function intersect(a: Ranges, b: Ranges): Ranges {
214
249
 
215
250
  /** The register values that take the test's BRANCH. */
216
251
  function takenBy(ti: TestInfo): Ranges {
217
- const op = ti.xOnLeft ? ti.opcode : (MIRRORED[ti.opcode] ?? ti.opcode);
252
+ const op = ti.xOnLeft ? ti.opcode : (SWAPPED_ICMP[ti.opcode] ?? ti.opcode);
218
253
  if (op === 'icmp_eq' || op === 'icmp_ne') {
219
254
  const k = ti.k | 0;
220
255
  return op === 'icmp_eq' ? [[k, k]] : complement([[k, k]]);
@@ -254,6 +289,204 @@ function singleton(r: Ranges): number | null {
254
289
  return r.length === 1 && r[0][0] === r[0][1] ? r[0][0] : null;
255
290
  }
256
291
 
292
+ /** TWO OR MORE ARMS ENTERING ONE SIBLING'S BODY, put back as copies of it. A compiler that
293
+ * cross-jumps (agbcc's and gcc 2.7.2's `jump.c` cross-jumping, KMC's with it) finds two arms
294
+ * ending in the same code as a sibling arm and keeps one copy, so `case 1: g(v); gOut = v;
295
+ * break; case 2: gOut = v; break; case 3: h(v); gOut = v; break;` comes back with case 1 and
296
+ * case 3 both entering case 2's `gOut = v`. C drops into an arm from above along one edge only,
297
+ * so read as written that is two fall-throughs into one arm and `chainArms` refuses it — but the
298
+ * source had one fall-through at most.
299
+ *
300
+ * WHICH ENTRANT FALLS IS NOT IN THE ASM, so it is a ranked choice (`/tail-copies`,
301
+ * rank-variations.ts), never a fixed default. The cross-jump keeps ONE copy of the tail and
302
+ * says nothing about which arm the source wrote it under, and one compiler at one set of flags
303
+ * answers both ways: KMC `-O2` and gcc 2.7.2 `-O1` lay arm bodies out in SOURCE order, and a
304
+ * source whose fall-through arm is written directly above its target in case order recompiles
305
+ * only with that arm falling, while one whose arms are written out of case order (`case 3`,
306
+ * `case 2`, `case 1`) recompiles nearer with every entrant holding its own copy. Neither the
307
+ * layout nor the case order decides it: the copy the cross-jump kept is not the source's
308
+ * fall-through, so the arm that physically runs into the tail can be the wrong faller too.
309
+ *
310
+ * - `faller` (the default): at most one entrant keeps its fall-through, the one the caller's
311
+ * arm-order policy (`order`, before the chain moves anything) puts DIRECTLY ABOVE the sibling —
312
+ * on `switchArmsFollowLayout` targets (agbcc) its layout neighbour, elsewhere the arm before it in
313
+ * case order. Every other entrant copies; no entrant above it ⇒ every entrant copies.
314
+ * - not `faller`: every entrant copies, whichever arm sits above the sibling.
315
+ *
316
+ * Either way each copying entrant gets the sibling's region written out again under its own arm:
317
+ * its exit becomes a closed `break` over the two regions.
318
+ *
319
+ * A `default:` SIBLING takes the LAST arm of `order` as the arm above it: that is where `chainArms`
320
+ * puts a default that an arm falls into, at the end. It is not always where the label is printed —
321
+ * a target that prints `default:` at its layout position (`defaultLayoutPos`) can put it between two
322
+ * cases, and then the arm the source wrote above it is not `order`'s last, and its entrants all copy
323
+ * (a measured agbcc shape of that kind still recompiles to its target).
324
+ *
325
+ * A `default:` SIBLING MAY BE CODE THE SOURCE WROTE AFTER THE SWITCH. Where the other arms
326
+ * `return` from a void function they reach the same epilogue the default block does, so
327
+ * post-dominance ends the switch there, and arms that `break` to the code after the switch read as
328
+ * falling into the default block. The copies then write that code into each of them: the same
329
+ * program, not the source's spelling (which would end the switch at the default block and spell
330
+ * the other arms `return`, a reading nothing makes yet).
331
+ *
332
+ * ONLY OVER A CLOSED SIBLING. The copy is the sibling's region walked again inside the entrant's
333
+ * arm, so it is spelled only where that region ends the switch: the sibling's own exit a `break`
334
+ * WITH a region — it reaches no sibling of its own and leaves through no block the switch does
335
+ * not dominate. A sibling that falls on stays two-into-one for `chainArms` to refuse. PURE: the
336
+ * closed exit is exactly what `analyzeArmExit` reads for the entrant once the sibling is no
337
+ * longer a sibling — its walk reaches the sibling's blocks, and they reach the merge and nothing
338
+ * else — so it is assembled here rather than walked again.
339
+ *
340
+ * OR A SIBLING THAT LEAVES SIDEWAYS ONLY FORWARD. A closed sibling with no region stepped into
341
+ * blocks the switch does not dominate (`escapedTo`). Its body is still the switch's end on every
342
+ * path where each of those blocks runs only on to the merge or a `ret` — never back into the
343
+ * dispatch, a sibling (the entrants among them) or round a cycle; a path out through the
344
+ * enclosing loop's exit is one more path to a `ret`. `sidewaysEnds` answers that per block
345
+ * (`forwardToMerge`, the caller's to hand in; absent, no such sibling is copied). Nothing in it is
346
+ * particular to one compiler: it is asked of every jump table whose shared tail lost its region
347
+ * because a block on the tail's way to the merge is also reached from outside the switch. Where it
348
+ * shows:
349
+ * - agbcc's gcse PRE puts the next switch's index in the dispatch block, which then no longer
350
+ * dominates the default the bounds test reaches too, and the default's tail runs on through
351
+ * blocks other arms cross-jumped into (`synthetic:jt_xjdefpre`, kleod EntityCreate);
352
+ * - with no PRE at all (IDO 7.1 has no gcse), the tail runs on into a second jump table after the
353
+ * switch that the other branch of an `if` around it reaches too, and from there to the switch's
354
+ * end: in a `for` whose tail `continue`s (switch-arms.test.ts, the ido7.1 `jl_cont` fixture),
355
+ * or where a `return` in the tail makes the epilogue the merge, and the copies then write that
356
+ * whole second switch into each entrant.
357
+ * Each copying entrant then has no region either: like every arm that leaves sideways, it
358
+ * structures to the CFG's joins, which walks the sibling's blocks once inside it and stops at the
359
+ * merge.
360
+ *
361
+ * `onFallerKept` fires each time `faller` leaves an entrant its fall-through — exactly where the
362
+ * two settings of `faller` return different exits. Where it never fires over a whole structuring,
363
+ * the other setting builds the same tree, which is what rank.ts reads to skip `/tail-copies`.
364
+ * `onCopied` fires with the sibling each time an entrant takes a copy of a CASE arm (not the
365
+ * `default:` arm's), which is where `jumpSharedFallTargets` can read a `goto` instead: rank.ts's
366
+ * witness for `/case-goto`, and the siblings that variation hands it. */
367
+ export function copySharedFallTargets(
368
+ order: readonly Block[],
369
+ dflt: Block | null,
370
+ exitOf: ReadonlyMap<Block, ArmExit>,
371
+ faller: boolean,
372
+ onFallerKept?: () => void,
373
+ sidewaysEnds?: (s: Block) => boolean,
374
+ onCopied?: (to: Block) => void,
375
+ ): Map<Block, ArmExit> {
376
+ const out = new Map(exitOf);
377
+ for (const { to, movers } of sharedFallTargets(order, dflt, exitOf, faller)) {
378
+ const own = exitOf.get(to);
379
+ if (own?.kind !== 'break') {
380
+ continue;
381
+ }
382
+ const { region, escapedTo } = own;
383
+ if (
384
+ region === undefined &&
385
+ (escapedTo === undefined || sidewaysEnds === undefined || ![...escapedTo].every(sidewaysEnds))
386
+ ) {
387
+ continue;
388
+ }
389
+ for (const [e, x] of movers(onFallerKept)) {
390
+ if (to !== dflt) {
391
+ onCopied?.(to);
392
+ }
393
+ out.set(
394
+ e,
395
+ region === undefined
396
+ ? { kind: 'break', escapedTo: escapedTo! }
397
+ : { kind: 'break', region: new Set([...x.region, ...region]) },
398
+ );
399
+ }
400
+ }
401
+ return out;
402
+ }
403
+
404
+ /** TWO OR MORE ARMS ENTERING ONE SIBLING'S BODY, put back as one fall-through and `goto`s to the
405
+ * sibling's case label — where the copies above do not take them (they are read first, and this
406
+ * is read over what they leave). It is the source's own spelling of the shape: `case 1: if (c)
407
+ * continue; goto L; case 2: if (d) continue; goto L; case 3: case 4: L: …` (kleod sub_0800D188's
408
+ * `goto block_1643;`), which every compiler that does not cross-jump a copy into it compiles with
409
+ * the sibling's code once. Unlike the copies it needs nothing of the sibling: the sibling's own exit
410
+ * is read for itself and may fall on, break, or leave sideways, since the jump does not write it
411
+ * out again.
412
+ *
413
+ * WHO FALLS is read from the one place the copies ask it too (`sharedFallTargets`), but with no
414
+ * choice in it: the entrant the caller's arm-order policy puts directly above the sibling always
415
+ * keeps its fall-through, and every other entrant jumps. A `goto` from that arm would land on the
416
+ * label directly below it, a path its plain fall-through already spells, so it is a jump goto-free
417
+ * C does not need (asmlift-101 §2.7). `/tail-copies`' setting of `faller` asks which entrant the
418
+ * SOURCE wrote the body under, a question a jump does not have: it writes no body.
419
+ *
420
+ * ONLY INTO A CASE ARM: the sibling is in `order`, never `dflt`, since only a case arm carries a
421
+ * label (l3/ast.ts `SwitchCase.label`) — and, given `only`, only into those siblings (`/case-goto`
422
+ * hands it the case arms the copies wrote back).
423
+ *
424
+ * WHAT MAKES THE JUMP THE SAME PROGRAM is the exit it replaces. A `goto` exit is a `fallthrough`
425
+ * exit with its last edge spelled as a jump: the arm is structured to stop at the sibling's entry
426
+ * exactly as a falling arm is (structure.ts `structureArm`), so every path that leaves the arm's
427
+ * body at its end is a path into the sibling, with the edge's own copies already run, and the
428
+ * `goto` is the arm's last top-level statement. The jump lands right after the sibling's case
429
+ * labels, which is where a fall-through lands; the emission refuses a sibling that opens with
430
+ * switch-edge copies, as it does for a fall-through: entering it by its own case value runs those,
431
+ * and the jump, landing above them, would run them again. No loop stands between the jump and the
432
+ * label: both are the switch's, in the loop the switch is in. */
433
+ export function jumpSharedFallTargets(
434
+ order: readonly Block[],
435
+ dflt: Block | null,
436
+ exitOf: ReadonlyMap<Block, ArmExit>,
437
+ only?: ReadonlySet<Block>,
438
+ ): Map<Block, ArmExit> {
439
+ const out = new Map(exitOf);
440
+ for (const { to, movers } of sharedFallTargets(order, dflt, exitOf, true)) {
441
+ if (to === dflt || !order.includes(to) || (only !== undefined && !only.has(to))) {
442
+ continue;
443
+ }
444
+ for (const [e, x] of movers()) {
445
+ out.set(e, { kind: 'goto', to, breaks: x.breaks, region: x.region });
446
+ }
447
+ }
448
+ return out;
449
+ }
450
+
451
+ type Entrant = [Block, Extract<ArmExit, { kind: 'fallthrough' }>];
452
+
453
+ /** WHO FALLS, the one answer both rewrites above read: each arm two or more arms fall into (`to`,
454
+ * in the order its first entrant comes in `order` and then `dflt`), and `movers` — its entrants
455
+ * other than the one that keeps its fall-through, with their exits, which each rewrite spells its
456
+ * own way (a copy, a jump). The keeper is the arm the caller's arm-order policy puts directly
457
+ * above `to`, where `faller` is set (`dflt` sits below the last of `order`); `movers` fires
458
+ * `onFallerKept` when that arm is an entrant, which is where the two settings of `faller` differ.
459
+ * It fires on the call, so a rewrite that refuses `to` before asking reports nothing. */
460
+ function sharedFallTargets(
461
+ order: readonly Block[],
462
+ dflt: Block | null,
463
+ exitOf: ReadonlyMap<Block, ArmExit>,
464
+ faller: boolean,
465
+ ): { to: Block; movers: (onFallerKept?: () => void) => Entrant[] }[] {
466
+ const entrants = new Map<Block, Entrant[]>();
467
+ for (const e of [...order, ...(dflt ? [dflt] : [])]) {
468
+ const x = exitOf.get(e);
469
+ if (x?.kind === 'fallthrough') {
470
+ entrants.set(x.to, [...(entrants.get(x.to) ?? []), [e, x]]);
471
+ }
472
+ }
473
+ return [...entrants]
474
+ .filter(([, from]) => from.length >= 2)
475
+ .map(([to, from]) => {
476
+ const at = to === dflt ? order.length : order.indexOf(to);
477
+ const above = faller && at > 0 ? order[at - 1] : undefined;
478
+ return {
479
+ to,
480
+ movers: (onFallerKept?: () => void) => {
481
+ if (above !== undefined && from.some(([e]) => e === above)) {
482
+ onFallerKept?.();
483
+ }
484
+ return from.filter(([e]) => e !== above);
485
+ },
486
+ };
487
+ });
488
+ }
489
+
257
490
  /** Re-thread `order` so every FALLING arm sits directly above the arm it falls into. Each
258
491
  * fall-through chain is emitted contiguously and takes the position of its HEAD in `order`,
259
492
  * which is the caller's own arm-order policy — so with no fall-through every chain is a
@@ -321,9 +554,11 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
321
554
  spellSwitchFallthrough,
322
555
  emitsOwnStatement,
323
556
  blockOf,
557
+ enclosingLoop,
324
558
  hoistDispatchCopies,
325
559
  expr,
326
560
  structureRegion,
561
+ structureArm,
327
562
  } = deps;
328
563
 
329
564
  // A block's index in `fn.blocks` as its position in the ASSEMBLY — the warrant for every reading
@@ -333,8 +568,12 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
333
568
  // the list for every conditional-return branch, wherever in the stream that branch sits. So
334
569
  // its real blocks keep address order among themselves, and each synthetic one — a bare
335
570
  // return, never a test — sorts after all of them;
336
- // - raising only ever REMOVES blocks from the list (raise/{divpow2,latch,retsink,shortcircuit}
337
- // .ts all `filter`), never inserts or reorders, so the frontend's order is what survives.
571
+ // - raising mostly REMOVES blocks from the list (raise/{divpow2,latch,retsink,shortcircuit}.ts
572
+ // all `filter`), but not only: raise/reroll.ts moves a rerolled loop's new head ahead of the
573
+ // old one and inserts a block of its own before it. So this order is the frontend's only
574
+ // where no reroll fired. A reader that needs the asm's own order wherever a pass rewrote the
575
+ // list reads the frontend's record instead (ir/core.ts `AsmLayout`, which the return-tail
576
+ // chooser reads); this one has not been moved onto it.
338
577
  // TWO READERS, both target-gated, and they need DIFFERENT strengths of the compiler half:
339
578
  // - `switchArmsFollowLayout` (arm ORDER, the sort below) needs the full claim — no block moved
340
579
  // at all — because it PLACES the arms from the layout;
@@ -427,75 +666,12 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
427
666
  // has declared that its layout answers it — which SPELLING the source wrote.
428
667
 
429
668
  // Fold a value that is a compile-time constant (a `const`, or a synthesized immediate like agbcc's
430
- // `250 << 2` for a large sparse case) to a number — else null.
431
- const evalConst = (v: Value): number | null => {
432
- if (isNamed(v)) {
433
- return null;
434
- } // a named variable is not a constant
435
- const d = defs.get(v);
436
- if (!d) {
437
- return null;
438
- }
439
- if (d.opcode === 'const') {
440
- return (d.attrs.value as number) | 0;
441
- }
442
- // Resolve the two operands of a binary op to constants (2-operand → both; 1-operand → operand +
443
- // `imm` attr, which MUST be present, else decline — a missing imm would wrongly fold `and x` to 0).
444
- const operands2 = (): [number, number] | null => {
445
- const a = evalConst(d.operands[0]);
446
- if (a === null) {
447
- return null;
448
- }
449
- let c: number | null;
450
- if (d.operands.length === 2) {
451
- c = evalConst(d.operands[1]);
452
- } else if (typeof d.attrs.imm === 'number') {
453
- c = d.attrs.imm | 0;
454
- } else {
455
- return null;
456
- }
457
- return c === null ? null : [a, c];
458
- };
459
- const bin = (f: (a: number, c: number) => number): number | null => {
460
- const p = operands2();
461
- return p === null ? null : f(p[0], p[1]) | 0;
462
- };
463
- const shift = (f: (a: number, c: number) => number): number | null => {
464
- const p = operands2();
465
- if (p === null || p[1] < 0 || p[1] >= 32) {
466
- return null;
467
- } // out-of-range shift amount → decline
468
- return f(p[0], p[1]) | 0;
469
- };
470
- switch (d.opcode) {
471
- case 'shl':
472
- return shift((a, c) => a << c);
473
- case 'shr_u':
474
- return shift((a, c) => a >>> c);
475
- case 'shr_s':
476
- return shift((a, c) => a >> c);
477
- case 'or':
478
- return bin((a, c) => a | c);
479
- case 'and':
480
- return bin((a, c) => a & c);
481
- case 'xor':
482
- return bin((a, c) => a ^ c);
483
- case 'add':
484
- return bin((a, c) => a + c);
485
- case 'sub':
486
- return bin((a, c) => a - c);
487
- case 'neg': {
488
- const a = evalConst(d.operands[0]);
489
- return a === null ? null : -a | 0;
490
- }
491
- case 'not': {
492
- const a = evalConst(d.operands[0]);
493
- return a === null ? null : ~a | 0;
494
- }
495
- default:
496
- return null;
497
- }
498
- };
669
+ // `250 << 2` for a large sparse case) to a number — else null. The shared word evaluator
670
+ // (ir/bits.ts), which declines an opcode it has no rule for: a `neg_s` (Thumb `cmn` under a signed
671
+ // branch) is no word, so a guard against it is never read as a case bound. A named variable is
672
+ // not a constant, whatever set it.
673
+ const words = { defs, opaque: isNamed };
674
+ const evalConst = (v: Value): number | null => constWord(words, v);
499
675
 
500
676
  // TWO QUESTIONS about a block, asked separately because the answers diverge and the walk needs
501
677
  // both: WHAT does it test (`testInfo`), and may this recovery DISCARD it (`collapsible`)?
@@ -541,6 +717,191 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
541
717
  return { x, k, cls, opcode: cmp.opcode, xOnLeft };
542
718
  };
543
719
 
720
+ // Does `blk` leave the enclosing loop for its exit, other than by the loop's own bottom test (a
721
+ // block that also edges back to the header, which spells as the loop's condition)?
722
+ const breaksLoop = (blk: Block): boolean => {
723
+ const loop = enclosingLoop();
724
+ const succ = successorsOf(blk);
725
+ return loop !== null && loop.exit !== null && succ.includes(loop.exit) && !succ.includes(loop.header);
726
+ };
727
+
728
+ // Does a block some arm structures edge to `target`? Walked as `structureArm` walks an arm: every
729
+ // block reached from the arm's entry without stepping through `merge` or back into `header`. That
730
+ // is wider than `analyzeArmExit`'s dominated region on purpose: when Regime A recognises a sub-tree
731
+ // of the comparison tree, an arm can run on into the code of a case the tree's root already took,
732
+ // which the dispatch does not dominate, and `structureArm` structures that code inside the arm.
733
+ // A block that also edges back to `header` is the loop's bottom test, which spells as the loop's
734
+ // condition, not as the arm's (`breaksLoop`) — save where `header` IS the merge (a top-tested
735
+ // loop): no bottom test is then reachable without passing the merge, and an arm's
736
+ // `if (c) goto out;` is a block that edges to the exit and straight back into the header.
737
+ const armsReach = (entries: Iterable<Block>, merge: Block | null, header: Block, target: Block): boolean => {
738
+ const seen = new Set<Block>(entries);
739
+ for (const q = [...seen]; q.length;) {
740
+ const succ = successorsOf(q.pop()!);
741
+ if (succ.includes(target) && (header === merge || !succ.includes(header))) {
742
+ return true;
743
+ }
744
+ for (const s of succ) {
745
+ if (s !== target && s !== merge && s !== header && !seen.has(s)) {
746
+ seen.add(s);
747
+ q.push(s);
748
+ }
749
+ }
750
+ }
751
+ return false;
752
+ };
753
+
754
+ // Can a path from `from` reach `to` without passing `through`? False when `to` is null (the end
755
+ // of the function) or is `through` itself.
756
+ const reachesAvoiding = (from: Block, to: Block | null, through: Block | null): boolean => {
757
+ if (to === null || to === through) {
758
+ return false;
759
+ }
760
+ const seen = new Set<Block>([from]);
761
+ for (const q = [from]; q.length;) {
762
+ for (const s of successorsOf(q.pop()!)) {
763
+ if (s === to) {
764
+ return true;
765
+ }
766
+ if (s !== through && !seen.has(s)) {
767
+ seen.add(s);
768
+ q.push(s);
769
+ }
770
+ }
771
+ }
772
+ return false;
773
+ };
774
+
775
+ // THE LOOP-EXIT MERGE, WHERE IT SPELLS. A switch whose merge is its enclosing loop's exit puts
776
+ // the exit region after the `switch`, INSIDE the loop body, and reaches the loop bottom only
777
+ // through the arms that do not break. That is the source's text on two conditions, both read off
778
+ // the CFG here, and on either failing the merge-past-stop gate below declines to if-recovery:
779
+ // - the exit region RETURNS: no path from `merge` reaches a block that dominates it (other
780
+ // than `merge` itself, a loop of the exit region's own) — the enclosing loop's header, or an
781
+ // OUTER loop's, whose back edge the inner body cannot spell
782
+ // (`for (j...) { for (;;) { switch ... } out: u += prev; }` runs on to the outer latch);
783
+ // - every loop-bottom TEST the arms reach (a block that edges back to the header) leaves for
784
+ // the merge itself, which the arm then spells as its `break;` (`latchexit`). One that leaves
785
+ // for any other block outside the loop is past what an arm structured against `merge` can
786
+ // say: the merge is then the loop's exit only because an early `return` made a shared
787
+ // epilogue the dispatch's post-dominator, while the code the bottom test leaves for runs
788
+ // before it (`for (w = 0; w < 4; w++) { if (c) return w; switch ... } r(t);` on
789
+ // gcc2.7.2kmc).
790
+ // And the loop's HEADER must not leave for the merge itself. One that does makes the loop
791
+ // top-tested on it, with the merge as the loop's own exit, which the exit region rendered after
792
+ // the switch inside the body would leave nothing to reach
793
+ // (`for (k = 0; k < 3; k++) { if (gC == 7) goto out; switch ... }` becomes `while (gC != 7)`).
794
+ // A test lower in the body that leaves for the merge ahead of the dispatch spells as its own
795
+ // `goto` or `break`, and keeps the switch.
796
+ const exitMergeSpells = (from: Block, merge: Block | null): boolean => {
797
+ if (merge === null) {
798
+ return false;
799
+ }
800
+ const above = dom.get(merge)!;
801
+ const exitSeen = new Set<Block>([merge]);
802
+ for (const q = [merge]; q.length;) {
803
+ for (const s of successorsOf(q.pop()!)) {
804
+ if (s !== merge && above.has(s)) {
805
+ return false;
806
+ }
807
+ if (!exitSeen.has(s)) {
808
+ exitSeen.add(s);
809
+ q.push(s);
810
+ }
811
+ }
812
+ }
813
+ const header = enclosingLoop()?.header ?? null;
814
+ if (header !== null && successorsOf(header).includes(merge)) {
815
+ return false;
816
+ }
817
+ const seen = new Set<Block>([from]);
818
+ for (const q = [from]; q.length;) {
819
+ const succ = successorsOf(q.pop()!);
820
+ if (
821
+ header !== null &&
822
+ succ.includes(header) &&
823
+ succ.some((s) => s !== header && s !== merge && !reachesAvoiding(s, header, merge))
824
+ ) {
825
+ return false;
826
+ }
827
+ for (const s of succ) {
828
+ if (s !== header && s !== merge && !seen.has(s)) {
829
+ seen.add(s);
830
+ q.push(s);
831
+ }
832
+ }
833
+ }
834
+ return true;
835
+ };
836
+
837
+ // A SIDEWAYS PATH THAT ONLY RETURNS. An arm can step into a block the switch does not dominate
838
+ // and still go nowhere but the end of the function: every path from `s` runs forward to a `ret`,
839
+ // never back into the switch, its merge or a sibling, and never round a cycle. Such an arm spells
840
+ // the escaping path in place as its `return;` (`case 0: if (c) { k(a); return; } gOut = 1;`
841
+ // falling into `case 1:`), and a falling arm's join already reads that path as a jump
842
+ // (`regionJoins`, "a `return k` through a shared epilogue"). It is a property of the ARM, not of
843
+ // any loop, so it is asked of every jump-table switch (Regime B; `analyzeArmExit` says why not of
844
+ // Regime A's comparison trees). Where it shows:
845
+ // - in a loop body, where an arm `return`s into the one epilogue past the loop (agbcc and IDO
846
+ // when the code after the loop returns early too, mwcc always) and the switch ends at the
847
+ // body's own join (structure.ts, THE LOOP CLAMP);
848
+ // - outside any loop, where an arm `goto`s a returning tail it shares with code the switch does
849
+ // not dominate: a guard above the switch cross-jumped into the same `k(7); return;`, a shared
850
+ // `out:` label, the other branch of an `if` around the switch, or a switch with no merge.
851
+ // The cycle test is a walk of the paths themselves: a block met again while it is still on the
852
+ // path being walked is a cycle, reducible or not (an IDO `goto mid;` into a `while` body makes an
853
+ // irreducible one, which a dominance test does not see). An edge to a block that dominates its
854
+ // source is refused too, since that block may lie above `s` where the walk never put it.
855
+ //
856
+ // WITH `mergeEnds` it is the copies' question (`forwardToMerge`): a path may also END at the
857
+ // merge, and nothing more is admitted. A path out through the enclosing loop's exit is one more
858
+ // forward path to a `ret`, wherever it falls on the path: the copy writes the code past the loop
859
+ // and its `return;` inside the case arm. A step round the loop to its header needs no test of its
860
+ // own: the header dominates every block of the loop, so the dominance test refuses it from inside,
861
+ // and the cycle test from a path that entered at the header.
862
+ //
863
+ // There the SIBLING and CYCLE refusals are conservative, not needed for soundness: forced off, a
864
+ // tail that steps into a sibling (agbcc, the `jt_xjdefpre` shape with `if (v == 9) return;` in the
865
+ // tail, so the epilogue is the merge and the empty `case 4:` is a sibling) or round a cycle lifts
866
+ // correct, that sibling's body or the loop written out in each entrant. They spare that
867
+ // duplication; dropping one needs a differential over the shapes it then admits.
868
+ const onlyReturns = (
869
+ s: Block,
870
+ b: Block,
871
+ merge: Block | null,
872
+ siblings: ReadonlySet<Block>,
873
+ mergeEnds = false,
874
+ ): boolean => {
875
+ const done = new Set<Block>();
876
+ const onPath = new Set<Block>([s]);
877
+ const stack: { x: Block; next: number }[] = [{ x: s, next: 0 }];
878
+ while (stack.length) {
879
+ const top = stack[stack.length - 1];
880
+ const succ = successorsOf(top.x);
881
+ if (top.next === 0 && succ.length === 0 && top.x.ops[top.x.ops.length - 1]?.opcode !== 'ret') {
882
+ return false;
883
+ }
884
+ if (top.next === succ.length) {
885
+ stack.pop();
886
+ onPath.delete(top.x);
887
+ done.add(top.x);
888
+ continue;
889
+ }
890
+ const y = succ[top.next++];
891
+ if (mergeEnds && y === merge && merge !== null) {
892
+ continue; // a path that reaches the switch's end
893
+ }
894
+ if (y === b || y === merge || siblings.has(y) || onPath.has(y) || dom.get(top.x)!.has(y)) {
895
+ return false; // back into the switch, or a cycle
896
+ }
897
+ if (!done.has(y)) {
898
+ onPath.add(y);
899
+ stack.push({ x: y, next: 0 });
900
+ }
901
+ }
902
+ return true;
903
+ };
904
+
544
905
  // Where does one arm's region LEAVE? Walk it from `entry`, never stepping THROUGH the merge or a
545
906
  // sibling arm's entry, and classify what it steps INTO. `siblings` is every OTHER arm entry the
546
907
  // caller can emit a `case`/`default` label for — the merge is deliberately not among them, so a
@@ -552,30 +913,62 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
552
913
  // sideways does not fall into the next case — and only the FALL-THROUGH verdict consults that
553
914
  // record, so an arm that reaches no sibling still closes with a plain `break`.
554
915
  //
916
+ // With `stopAtLoop` (Regime A), a FALLING arm that also reaches the merge must not break the loop
917
+ // the switch sits in. When the dispatch IS the loop header (agbcc and IDO rotate
918
+ // `while (k < 4) { k++; switch … }` so), it dominates the loop's exit and all the code after the
919
+ // loop, and the merge is the header itself, so dominance cannot tell the arm's region from the
920
+ // code past the loop. Such an arm that falls into a sibling, reaches the merge by the back edge,
921
+ // and leaves for the loop's exit on a third path (a `goto out`, or a sibling that IS the exit, as
922
+ // a `default: goto out;` makes it) would read as falling with `breaks`, Regime A would commit,
923
+ // and the loop-exit refusal in `structure.ts` would fail the whole function where if-recovery
924
+ // spells it. So it is `unstructurable`. What counts is the block the refusal fires on
925
+ // (`breaksLoop`): the loop's own bottom test, reached after a `continue`, exits too but is the
926
+ // loop's condition, and a block that also edges to the merge is the switch's own end. Regime B,
927
+ // which has no fallback, does not ask: it lifts a jump table whose arms share the rotated latch
928
+ // and its exit (`do { … switch … t++; } while (t < 15)`).
929
+ //
555
930
  // A CONSEQUENCE, not a hole: a sibling reachable only THROUGH such a block is never seen, so the
556
931
  // arm reads as closed and `structureRegion` walks into the sibling's blocks and emits them again
557
932
  // under this arm. That is duplication, not a wrong dispatch — the same duplication the structurer
558
933
  // already does for any tail two arms share, and how the case bodies agbcc tail-merged are put
559
934
  // back. Costly for matching, correct to run.
560
- const analyzeArmExit = (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>): ArmExit => {
935
+ //
936
+ // `jumps` are blocks an edge into which is a jump that ends its path where it sits, whatever the
937
+ // switch: the latch of a loop spelled `for`, which the structurer writes as `continue;`. Such an
938
+ // edge is neither a fall into a sibling nor an escape, and an arm that IS one is a bare jump.
939
+ const analyzeArmExit = (
940
+ entry: Block,
941
+ b: Block,
942
+ merge: Block | null,
943
+ siblings: Set<Block>,
944
+ stopAtLoop = false,
945
+ jumps: ReadonlySet<Block> = new Set(),
946
+ ): ArmExit => {
561
947
  if (entry === merge) {
562
948
  return { kind: 'break' }; // an empty arm (a table slot pointing straight at the switch's end)
563
949
  }
950
+ if (jumps.has(entry)) {
951
+ return { kind: 'break', region: new Set([entry]) };
952
+ }
564
953
  const into = new Set<Block>(); // sibling entries this arm flows into
954
+ const escapedTo = new Set<Block>(); // the blocks outside the switch's region it steps into
565
955
  let toMerge = false,
566
- escapes = false;
956
+ leavesLoop = false;
567
957
  const seen = new Set<Block>([entry]);
568
958
  const q = [entry];
569
959
  while (q.length) {
570
960
  const cur = q.pop()!;
961
+ leavesLoop ||= !successorsOf(cur).includes(merge as Block) && breaksLoop(cur);
571
962
  for (const s of successorsOf(cur)) {
572
963
  if (s === merge) {
573
964
  toMerge = true;
965
+ } else if (jumps.has(s)) {
966
+ continue;
574
967
  } else if (s !== entry && siblings.has(s)) {
575
968
  into.add(s);
576
969
  } else if (s !== entry && !seen.has(s)) {
577
970
  if (!dom.get(s)!.has(b)) {
578
- escapes = true;
971
+ escapedTo.add(s);
579
972
  continue;
580
973
  }
581
974
  seen.add(s);
@@ -584,36 +977,50 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
584
977
  }
585
978
  }
586
979
  if (into.size === 0) {
587
- return { kind: 'break' };
588
- }
589
- if (into.size === 1 && !toMerge && !escapes) {
590
- return { kind: 'fallthrough', to: [...into][0] };
980
+ return escapedTo.size > 0 ? { kind: 'break', escapedTo } : { kind: 'break', region: seen };
591
981
  }
592
- // Name what is actually missing. These three are different facts, and only the first is a shape
593
- // C has no spelling for — the other two are asmlift's own limits, so say so rather than blame C.
594
- const names = () => [...into].map((x) => `#${layoutIndex(x)}`).join(', ');
595
- if (into.size > 1) {
982
+ // A path to the merge beside the fall-through is the arm's own `break;` — switch-scoped, so it
983
+ // spells only where nothing breakable sits between that edge and the arm, which the structurer
984
+ // checks where it emits it (`structure.ts` `switchBreakAt`) and refuses loud otherwise.
985
+ if (into.size === 1 && toMerge && leavesLoop && stopAtLoop) {
596
986
  return {
597
987
  kind: 'unstructurable',
598
- why: `a case body reaches several sibling cases (${names()}) — C fall-through reaches only one, so this needs a goto`,
988
+ why: `a case body falls into sibling case ${[...into].map((x) => `#${layoutIndex(x)}`).join(', ')}, reaches the end of the switch, and leaves the loop the switch sits in`,
599
989
  };
600
990
  }
601
- if (escapes) {
991
+ // A sideways path that only RETURNS does not stop the fall-through (`onlyReturns`): it is a
992
+ // jump that ends where it sits, as a `ret` inside the arm is. Regime B only. Regime A
993
+ // (`stopAtLoop`) commits to its `switch` on gates that read an arm's `breaks` alone, and has
994
+ // if-recovery to fall back on, so a falling arm beside an escape stays `unstructurable` there:
995
+ // admitted, it commits Regime A to switches whose emission then refuses the whole function (an
996
+ // escape to a `goto out` past a loop exit, or out of a nested switch) where if-recovery spells
997
+ // them.
998
+ if (
999
+ into.size === 1 &&
1000
+ (escapedTo.size === 0 || (!stopAtLoop && [...escapedTo].every((s) => onlyReturns(s, b, merge, siblings))))
1001
+ ) {
1002
+ return { kind: 'fallthrough', to: [...into][0], breaks: toMerge, region: seen };
1003
+ }
1004
+ // Name what is actually missing. These are two different facts: the first is a shape C's
1005
+ // fall-through cannot spell, the second leaves the switch's region sideways.
1006
+ const names = () => [...into].map((x) => `#${layoutIndex(x)}`).join(', ');
1007
+ if (into.size > 1) {
602
1008
  return {
603
1009
  kind: 'unstructurable',
604
- why: `a case body reaches sibling case ${names()} on one path and, on another, a block the switch does not dominate`,
1010
+ why: `a case body reaches several sibling cases (${names()}) — C fall-through reaches only one, so this needs a goto`,
605
1011
  };
606
1012
  }
607
1013
  return {
608
1014
  kind: 'unstructurable',
609
- // `case 0: if (c) { …; break; } /* fall through */ case 1:` is the C for this, and the reason
610
- // asmlift cannot write it is its own: `{k:'break'}` is emitted only for the innermost LOOP
611
- // (l3/ast.ts), never switch-scoped. That is the capability this shape is waiting on.
612
- why: `a case body reaches sibling case ${names()} on one path and the end of the switch on another — a switch-scoped \`break\` inside a case body is not emitted yet`,
1015
+ why: `a case body reaches sibling case ${names()} on one path and, on another, a block the switch does not dominate`,
613
1016
  };
614
1017
  };
615
1018
 
616
- const recognizeSwitch = (b: Block, stop: Block | null): Stmt[] | null => {
1019
+ const recognizeSwitch = (
1020
+ b: Block,
1021
+ stop: Block | null,
1022
+ mergeAt?: (arms: readonly Block[]) => Block | undefined,
1023
+ ): Stmt[] | null => {
617
1024
  const root = testInfo(b);
618
1025
  if (!root) {
619
1026
  return null;
@@ -831,29 +1238,73 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
831
1238
  }
832
1239
 
833
1240
  // PRE2 (arm exits), per SITE. A `break` arm closes; a `fallthrough` arm runs on into the one
834
- // sibling it reaches, which `chainArms` then places directly under it. `unstructurable` is a
835
- // shape no single linear switch spells — a body reaching two siblings, or a sibling on one path
836
- // and the switch's end on another — and Regime A declines to if-recovery, which spells every
837
- // one of those edges. (Regime B reads the same verdicts and fails LOUD on either.)
838
- const merge = ipdom.get(b) ?? stop;
1241
+ // sibling it reaches, which `chainArms` then places directly under it — on every path, or on
1242
+ // some while the others reach the switch's end, which the arm's own `break;` spells (`breaks`).
1243
+ // `unstructurable` is a shape no single linear switch spells — a body reaching two siblings, or
1244
+ // a sibling beside a block the switch does not dominate — and Regime A declines to if-recovery,
1245
+ // which spells every one of those edges. (Regime B reads the same verdicts and fails LOUD on
1246
+ // either.)
839
1247
  const targets = new Set<Block>([...caseBlocks, ...(defaultBlk ? [defaultBlk] : [])]);
1248
+ const merge = mergeAt?.([...targets]) ?? ipdom.get(b) ?? stop;
840
1249
  const siblings = new Set([...targets].filter((t) => t !== merge));
841
1250
  const exitOf = new Map<Block, ArmExit>();
842
1251
  for (const t of siblings) {
843
- exitOf.set(t, analyzeArmExit(t, b, merge, siblings));
1252
+ exitOf.set(t, analyzeArmExit(t, b, merge, siblings, true));
844
1253
  }
845
1254
  // A language with no fall-through in its `case` (Pascal) cannot print a falling arm at all, so
846
1255
  // for it a falling arm is exactly as unspellable as an `unstructurable` one — and takes the
847
1256
  // same exit: if-recovery, which that backend prints fine. Asked HERE rather than left to the
848
1257
  // backend because the backend's refusal is terminal (the whole function becomes a stub) while
849
1258
  // this one costs nothing but the `switch` spelling.
1259
+ //
1260
+ // A falling arm that also breaks is admitted wherever it falls, the `default:` arm included:
1261
+ // for `case 1: if (c) break; default: f(); break;` the ladder if-recovery writes copies `f()`
1262
+ // into the arm and misses the object the `switch` reaches.
850
1263
  if (
851
1264
  [...exitOf.values()].some(
852
- (e) => e.kind === 'unstructurable' || (!spellSwitchFallthrough && e.kind === 'fallthrough'),
1265
+ (e) => e.kind === 'unstructurable' || (e.kind === 'fallthrough' && !spellSwitchFallthrough),
853
1266
  )
854
1267
  ) {
855
1268
  return null;
856
1269
  }
1270
+ // AN ARM'S `break;` NEEDS THE MERGE TO BE THE SWITCH'S END IN THIS REGION. The code after the
1271
+ // `switch` is `structureRegion(merge, stop)`, which reads as that only where every path from
1272
+ // the dispatch reaches `stop` through the merge. A sub-tree whose post-dominator lies PAST the
1273
+ // region's `stop` breaks that: inside a loop body whose dispatch reaches both the latch (`stop`)
1274
+ // and an early `return` block, ipdom is the return, so an arm's path to it would read as a
1275
+ // switch `break` while a sibling arm runs through the latch and back to the header, which
1276
+ // fails the whole function loud. Declined here, to if-recovery, which spells each edge as the
1277
+ // `return` or the fall to the loop bottom it is. A merge that is the enclosing loop's EXIT is
1278
+ // the exception the structurer spells (`exitMergeSpells`): the code after the switch is then
1279
+ // the exit region, and an arm's path to it is the arm's `break;` (`structure.ts`, the loop-exit
1280
+ // rules).
1281
+ if (
1282
+ [...exitOf.values()].some((e) => e.kind === 'fallthrough' && e.breaks) &&
1283
+ !(merge === enclosingLoop()?.exit && exitMergeSpells(b, merge)) &&
1284
+ reachesAvoiding(b, stop, merge)
1285
+ ) {
1286
+ return null;
1287
+ }
1288
+ // AN ARM'S EDGE TO THE LOOP'S EXIT, WHEN THAT EXIT IS NOT THE MERGE. A switch admitted on a
1289
+ // falling arm's `break;` hands every arm to `structureArm`, where an edge to the enclosing loop's
1290
+ // exit can be spelled only as the switch's own end or as a copied returning tail; any other is
1291
+ // refused there, loud, for the whole function (`structure.ts`, "leaves its switch for a loop
1292
+ // exit"). With an early `return` above the dispatch, the merge is the loop bottom, and a sibling
1293
+ // arm's `goto out` to an exit that runs on to an OUTER loop's latch is such an edge. Declined
1294
+ // here to if-recovery, which spells that `goto` as the inner loop's `break`. The exemption is
1295
+ // the exit that is itself a `ret`; the structurer's other one, an early-return arm owning the
1296
+ // edge, is not asked here, which costs only the `switch` spelling.
1297
+ const loop = enclosingLoop();
1298
+ if (
1299
+ [...exitOf.values()].some((e) => e.kind === 'fallthrough' && e.breaks) &&
1300
+ loop !== null &&
1301
+ loop.exit !== null &&
1302
+ loop.exit !== merge &&
1303
+ loop.exit.ops[loop.exit.ops.length - 1]?.opcode !== 'ret' &&
1304
+ armsReach(siblings, merge, loop.header, loop.exit)
1305
+ ) {
1306
+ return null;
1307
+ }
857
1308
 
858
1309
  // PRE3 (concrete interval consistency): a `case k` is only sound if the ORIGINAL tree
859
1310
  // actually routes x==k to its recorded body. A relational guard can make an `x==k` test DEAD (e.g.
@@ -1006,6 +1457,13 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
1006
1457
  // The `default:` arm is a chain member too — an arm may run on into it, and C prints it last.
1007
1458
  const dfltArm = defaultBlk !== null && defaultBlk !== merge ? defaultBlk : null;
1008
1459
  const preChain = [...armsByBlock.keys()];
1460
+ // NO COPIES HERE. Arms a cross-jump sent into one sibling's body do not linearize, and Regime
1461
+ // B reads them back as copies (structure.ts, THE COPIES) because its only alternative is a loud
1462
+ // failure. Regime A's alternative is if-recovery, which LIFTS: a copy reading here would replace
1463
+ // one lift with another, the choice between two legal spellings of one CFG that only a ranked
1464
+ // variation may make (docs/level-tower.md). On comparison trees agbcc and KMC cross-jumped,
1465
+ // the switch spelling measured nearer the target on some and further on others, and no corpus
1466
+ // row reaches this point, so the refusal stands and if-recovery answers.
1009
1467
  const entries = chainArms(preChain, dfltArm, exitOf);
1010
1468
  if (entries === null) {
1011
1469
  return null;
@@ -1085,13 +1543,13 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
1085
1543
  // built, and no row asks for it (`sw_jtfall`/`sw_jtfalldesc` match today).
1086
1544
  outCases.push({
1087
1545
  values: armsByBlock.get(blk)!,
1088
- body: structureRegion(blk, to ?? merge),
1546
+ body: structureArm(blk, to ?? merge, merge, exitOf.get(blk)),
1089
1547
  fallsThrough: to !== null,
1090
1548
  });
1091
1549
  }
1092
1550
  // An empty default arm is not a default (see the Regime-B note in structure.ts): the label
1093
1551
  // would carry no statement, which says nothing and is not valid C89.
1094
- const defBody = defaultBlk ? structureRegion(defaultBlk, merge) : [];
1552
+ const defBody = defaultBlk ? structureArm(defaultBlk, merge, merge, exitOf.get(defaultBlk)) : [];
1095
1553
  // The `default:` arm is an ARM: where its block is laid out is read exactly as a case body's is
1096
1554
  // (`defaultLayoutPos`). The dispatch placed that block itself when the last test simply RAN OUT
1097
1555
  // into it and no other subtree jumps there — the two references the tree walk can count.
@@ -1135,7 +1593,12 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
1135
1593
  }
1136
1594
  return out;
1137
1595
  };
1138
- return { recognizeSwitch, analyzeArmExit, layoutIndex, defaultLayoutPos, chainArms };
1596
+ /** `copySharedFallTargets`' `sidewaysEnds`: does every path from `s`, a block outside the
1597
+ * switch's region, run only forward to `merge` or a `ret` (`onlyReturns` with the merge as an
1598
+ * end)? */
1599
+ const forwardToMerge = (s: Block, b: Block, merge: Block | null, siblings: ReadonlySet<Block>): boolean =>
1600
+ merge !== null && onlyReturns(s, b, merge, siblings, true);
1601
+ return { recognizeSwitch, analyzeArmExit, forwardToMerge, layoutIndex, defaultLayoutPos, chainArms };
1139
1602
  }
1140
1603
 
1141
1604
  /** Internal surface for this module's own tests, and for nothing else: PRE3 reads each test