@asmlift/core 0.8.1 → 0.10.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 (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
@@ -4,10 +4,14 @@
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
- import { ORDER_SENSITIVE_OPS } from '../ir/opcodes';
9
+ import { orderSensitive } from '../ir/discipline';
9
10
  import { Expr, Stmt, SwitchCase } from '../l3/ast';
10
11
 
12
+ /** Which side of a relational test a compiler's dispatch lands a pinned case body on. */
13
+ export type SwitchBoundCase = 'taken' | 'either';
14
+
11
15
  export interface SwitchRecoverDeps {
12
16
  fn: Fn;
13
17
  defs: Map<Value, Op>;
@@ -19,8 +23,9 @@ export interface SwitchRecoverDeps {
19
23
  /** is this opcode an integer comparison? */
20
24
  isCmpOpcode: (opcode: string) => boolean;
21
25
  switchAllowsNeqCase: boolean;
22
- /** read a relational test whose BRANCH admits exactly one scrutinee value as that case */
23
- switchAllowsBoundCase: boolean;
26
+ /** read a relational test as a case where the values that can reach it leave exactly one on
27
+ * this side of it — its branch, or either side — or never (null) */
28
+ switchBoundCase: SwitchBoundCase | null;
24
29
  /** emit the case arms in the ASSEMBLY's block-layout order rather than by ascending case value */
25
30
  switchArmsFollowLayout: boolean;
26
31
  /** DECLINE a recovered tree whose own layout INTERLEAVES a test with a case body. A source
@@ -44,6 +49,11 @@ export interface SwitchRecoverDeps {
44
49
  * of "where is this defined" `defs` does not answer. Taken from `structure.ts`, which builds
45
50
  * both halves for def-site anchoring, rather than indexing the parameters a second time. */
46
51
  blockOf: (v: Value) => Block | undefined;
52
+ /** The header and exit of the loop the switch is being structured in, or null outside any loop.
53
+ * Read LIVE: the structurer's loop frame is the one current when the switch is met. An edge to
54
+ * the exit from inside a case arm is a `break` of that loop, which C binds to the switch instead
55
+ * (`analyzeArmExit`), save where the exit is the switch's own merge. */
56
+ enclosingLoop: () => { header: Block; exit: Block | null } | null;
47
57
  /** THE DISPATCH HOIST: the copies of every edge the tree walk collapsed, merged and re-emitted
48
58
  * ONCE ahead of the `switch`. `structure.ts hoistedDispatchAssigns` owns the emission
49
59
  * (suppression, identity elision, `undef`, the write-order sort, `sequentialize`), so this
@@ -56,6 +66,10 @@ export interface SwitchRecoverDeps {
56
66
  ) => Stmt[] | null;
57
67
  expr: (v: Value) => Expr;
58
68
  structureRegion: (b: Block, stop: Block | null) => Stmt[];
69
+ /** One arm's body, from `entry` to `stop`, inside the switch whose code resumes at `merge`. The
70
+ * structurer owns what `break;` means there (structure.ts `structureArm`), so both regimes hand
71
+ * it every arm and the exit verdict they read for it. */
72
+ structureArm: (entry: Block, stop: Block | null, merge: Block | null, exit: ArmExit | undefined) => Stmt[];
59
73
  }
60
74
 
61
75
  /** Where ONE switch arm's region leaves it — the fact that decides whether the arm can be spelled
@@ -66,18 +80,30 @@ export interface SwitchRecoverDeps {
66
80
  * - `fallthrough` every path out leaves into exactly ONE sibling arm's entry: C's fall-through.
67
81
  * Only spellable when that sibling is the arm emitted NEXT, which `chainArms`
68
82
  * below arranges and both regimes then re-read off the emission array (see the
69
- * l3/ast.ts non-neutrality note).
70
- * - `unstructurable` anything else: two different siblings, or a mix of "into a sibling" and
71
- * "out to the merge". C needs a `goto` for those. Regime A declines to
72
- * if-recovery on this verdict; Regime B, having no fallback, fails LOUD. */
73
- export type ArmExit = { kind: 'break' } | { kind: 'fallthrough'; to: Block } | { kind: 'unstructurable'; why: string };
83
+ * l3/ast.ts non-neutrality note). With `breaks`, some paths out reach the
84
+ * switch's merge instead: `case 1: if (c) break; case 2: …`, the arm's own
85
+ * `break;` on those paths.
86
+ * - `unstructurable` anything else: two different siblings, or a sibling beside a block the
87
+ * switch does not dominate. C needs a `goto` for those. Regime A declines to
88
+ * if-recovery on this verdict; Regime B, having no fallback, fails LOUD.
89
+ *
90
+ * `region` is the arm's own blocks: every block the walk below reached, the entry included. It is
91
+ * what the structurer needs to find the arm's own joins (`structure.ts` `armJoin`) and to spell a
92
+ * path to the merge in the middle of the arm as `break;` (`switchBreakAt`), in a closed arm as in a
93
+ * falling one: `case 1: if (a) { if (b) break; g(1); } h(2); break;`. A closed arm that leaves
94
+ * sideways, into a block the switch does not dominate, has none: its paths through such a block
95
+ * are not the arm's, and it structures to the CFG's joins. */
96
+ export type ArmExit =
97
+ | { kind: 'break'; region?: ReadonlySet<Block> }
98
+ | { kind: 'fallthrough'; to: Block; breaks: boolean; region: ReadonlySet<Block> }
99
+ | { kind: 'unstructurable'; why: string };
74
100
 
75
101
  export interface SwitchRecovery {
76
102
  recognizeSwitch: (b: Block, stop: Block | null) => Stmt[] | null;
77
103
  /** shared with the Regime-B (`switch_br`) path in structure.ts. Both regimes recover the
78
104
  * fall-through this returns; on an `unstructurable` verdict Regime A declines to if-recovery and
79
105
  * Regime B, which has no fallback, fails loud. */
80
- analyzeArmExit: (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>) => ArmExit;
106
+ analyzeArmExit: (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>, stopAtLoop?: boolean) => ArmExit;
81
107
  /** a block's position in the ASSEMBLY — the arm-order evidence, shared with Regime B so the two
82
108
  * regimes read it from one definition (and one statement of what it rests on). */
83
109
  layoutIndex: (blk: Block) => number;
@@ -128,66 +154,126 @@ export interface TestInfo {
128
154
  xOnLeft: boolean;
129
155
  }
130
156
 
131
- // Evaluate a test predicate for a CONCRETE scrutinee value — used to SIMULATE the decision tree and
132
- // verify recovered case values (below). Returns true iff the `taken` (successors[0]) edge is followed.
133
- // Signed/unsigned per the icmp opcode (PRE3, done concretely rather than via interval lattices).
134
- function evalCmp(opcode: string, xOnLeft: boolean, xv: number, k: number): boolean {
135
- const uns = opcode.startsWith('icmp_u');
136
- const [xn, kn] = uns ? [xv >>> 0, k >>> 0] : [xv | 0, k | 0];
137
- const [l, r] = xOnLeft ? [xn, kn] : [kn, xn]; // put the scrutinee where it textually appears
138
- switch (opcode) {
139
- case 'icmp_eq':
140
- return l === r;
141
- case 'icmp_ne':
142
- return l !== r;
143
- case 'icmp_slt':
144
- case 'icmp_ult':
145
- return l < r;
146
- case 'icmp_sle':
147
- case 'icmp_ule':
148
- return l <= r;
149
- case 'icmp_sgt':
150
- case 'icmp_ugt':
151
- return l > r;
152
- case 'icmp_sge':
153
- case 'icmp_uge':
154
- return l >= r;
155
- default:
156
- return false;
157
+ // WHICH SCRUTINEE VALUES CAN REACH A TEST, AND WHICH ONE A SIDE OF IT PINS. A comparison dispatch
158
+ // pins its last value with a RELATIONAL test once the tests above it have narrowed the scrutinee to
159
+ // one value on that side: mwcc's `switch (x) { case 0: … case 1: … default: … }` is `cmpwi r0,1;
160
+ // beq- case1; bge- default; cmpwi r0,0; bge- case0; b default`, where `x >= 0` is `case 0` only
161
+ // because `x != 1` and `x < 1` came first. agbcc's `emit_case_nodes` does the same on the branch it
162
+ // takes once `node_is_bounded` holds, and its `x <u 1` for `case 0` of an unsigned switch is the
163
+ // instance that needs nothing from the tests above: one value at an end of the 32-bit domain. Read
164
+ // as navigation instead, the pinned arm's body becomes a second default candidate and the whole
165
+ // tree declines. Which SIDE of a test may pin a case is the compiler's (`switchBoundCase`,
166
+ // target.ts).
167
+ //
168
+ // EXACT, not a bound. A range is the set of 32-bit register values, as disjoint signed intervals in
169
+ // ascending order, and every test the walk narrows it by — `==`, `!=`, and a relational test under
170
+ // either signedness — is an exact set over that register. An unsigned interval straddling 2^31 is
171
+ // two signed ones (`fromUnsigned`). So a side that comes out a singleton names the only value that
172
+ // CAN take it, and a side that comes out empty is one no value takes. Whether the value DOES reach
173
+ // the body is still PRE3's to say, simulating the original tree case by case.
174
+ //
175
+ // THE DOMAIN IS THE REGISTER's, not the scrutinee's recovered type, so a narrower type has nearer
176
+ // ends this misses. That costs a case and never invents one.
177
+ type Ranges = readonly (readonly [number, number])[];
178
+
179
+ const INT_MIN = -0x80000000;
180
+ const INT_MAX = 0x7fffffff;
181
+ const ALL: Ranges = [[INT_MIN, INT_MAX]];
182
+
183
+ /** The same opcode with its operands swapped: `k < x` is `x > k`. */
184
+ const MIRRORED: Record<string, string> = {
185
+ icmp_slt: 'icmp_sgt',
186
+ icmp_sgt: 'icmp_slt',
187
+ icmp_sle: 'icmp_sge',
188
+ icmp_sge: 'icmp_sle',
189
+ icmp_ult: 'icmp_ugt',
190
+ icmp_ugt: 'icmp_ult',
191
+ icmp_ule: 'icmp_uge',
192
+ icmp_uge: 'icmp_ule',
193
+ };
194
+
195
+ /** `[lo, hi]` of the UNSIGNED order as signed intervals: one, or two where it straddles 2^31. */
196
+ function fromUnsigned(lo: number, hi: number): Ranges {
197
+ if (lo > hi) {
198
+ return [];
157
199
  }
200
+ return hi <= INT_MAX || lo > INT_MAX
201
+ ? [[lo | 0, hi | 0]]
202
+ : [
203
+ [INT_MIN, hi | 0],
204
+ [lo, INT_MAX],
205
+ ];
158
206
  }
159
207
 
160
- // Which single scrutinee value does this relational test's BRANCH admit, if exactly one? A
161
- // relational side is a HALF-LINE in the compare's own ordering, so it can hold one value only at
162
- // a domain endpoint — which is why testing the two endpoints and their neighbours decides it,
163
- // with no interval lattice. `x < 1` over an unsigned scrutinee admits `{0}` and is agbcc's
164
- // spelling of `case 0` in a balanced search: `emit_case_nodes` tests the subtree BOUND, not the
165
- // value, whenever the remaining range has collapsed to one. Read as navigation instead, that
166
- // arm's body becomes a second default candidate and the whole tree declines.
167
- //
168
- // THE BRANCH, never the fall-through. Every jump in `emit_case_nodes` that lands on a case body
169
- // is its test's BRANCH — for a single-valued node, LT to `node->left->code_label` and GT to
170
- // `node->right->code_label`, each guarded by `node_is_bounded` on that side — while the
171
- // fall-through always continues into more dispatch, so a fall-side reading has no producer in
172
- // this dispatch — and none turns up in 3176 generated agbcc dispatches.
173
- //
174
- // TWO PREMISES ABOUT THE DOMAIN. It is the 32-bit REGISTER's, not the scrutinee's recovered
175
- // type, so a narrower type has a nearer endpoint this misses — which costs a case and never
176
- // invents one. And it is the WHOLE of that domain, so an ancestor that already excluded the
177
- // value makes the reading wrong; PRE3 is what catches that, simulating the original tree for
178
- // every recovered case value and declining unless it lands on the recorded body, exactly as it
179
- // does for the `eq` cases. Null when the branch admits none, several, or the whole domain.
180
- function singletonTaken(ti: TestInfo): number | null {
181
- const [min, max] = ti.opcode.startsWith('icmp_u') ? [0, -1] : [-0x80000000, 0x7fffffff];
182
- for (const [v, next] of [
183
- [min, min + 1],
184
- [max, max - 1],
185
- ]) {
186
- if (evalCmp(ti.opcode, ti.xOnLeft, v, ti.k) && !evalCmp(ti.opcode, ti.xOnLeft, next, ti.k)) {
187
- return v;
208
+ function complement(r: Ranges): Ranges {
209
+ const out: [number, number][] = [];
210
+ let next = INT_MIN;
211
+ for (const [lo, hi] of r) {
212
+ if (lo > next) {
213
+ out.push([next, lo - 1]);
188
214
  }
215
+ next = hi + 1;
216
+ }
217
+ if (next <= INT_MAX) {
218
+ out.push([next, INT_MAX]);
189
219
  }
190
- return null;
220
+ return out;
221
+ }
222
+
223
+ function intersect(a: Ranges, b: Ranges): Ranges {
224
+ const out: [number, number][] = [];
225
+ for (const [alo, ahi] of a) {
226
+ for (const [blo, bhi] of b) {
227
+ const lo = Math.max(alo, blo);
228
+ const hi = Math.min(ahi, bhi);
229
+ if (lo <= hi) {
230
+ out.push([lo, hi]);
231
+ }
232
+ }
233
+ }
234
+ return out.sort((x, y) => x[0] - y[0]);
235
+ }
236
+
237
+ /** The register values that take the test's BRANCH. */
238
+ function takenBy(ti: TestInfo): Ranges {
239
+ const op = ti.xOnLeft ? ti.opcode : (MIRRORED[ti.opcode] ?? ti.opcode);
240
+ if (op === 'icmp_eq' || op === 'icmp_ne') {
241
+ const k = ti.k | 0;
242
+ return op === 'icmp_eq' ? [[k, k]] : complement([[k, k]]);
243
+ }
244
+ const uns = op.startsWith('icmp_u');
245
+ const [min, max, k] = uns ? [0, 0xffffffff, ti.k >>> 0] : [INT_MIN, INT_MAX, ti.k | 0];
246
+ const rel = op.slice('icmp_s'.length);
247
+ const [lo, hi] =
248
+ rel === 'lt'
249
+ ? [min, k - 1]
250
+ : rel === 'le'
251
+ ? [min, k]
252
+ : rel === 'gt'
253
+ ? [k + 1, max]
254
+ : rel === 'ge'
255
+ ? [k, max]
256
+ : [min, max];
257
+ return uns ? fromUnsigned(lo, hi) : lo > hi ? [] : [[lo, hi]];
258
+ }
259
+
260
+ /** What of `path` follows the test's `taken` edge, or its fall-through. */
261
+ function narrow(path: Ranges, ti: TestInfo, taken: boolean): Ranges {
262
+ const t = takenBy(ti);
263
+ return intersect(path, taken ? t : complement(t));
264
+ }
265
+
266
+ /** Whether register value `v` is in `r`. PRE3's concrete simulation reads each test through this
267
+ * and {@link takenBy}, so the tree it walks and the ranges a bound case is read off are one
268
+ * definition of the icmp family, not two. */
269
+ function contains(r: Ranges, v: number): boolean {
270
+ const x = v | 0;
271
+ return r.some(([lo, hi]) => lo <= x && x <= hi);
272
+ }
273
+
274
+ /** The one value in `r`, if it holds exactly one. */
275
+ function singleton(r: Ranges): number | null {
276
+ return r.length === 1 && r[0][0] === r[0][1] ? r[0][0] : null;
191
277
  }
192
278
 
193
279
  /** Re-thread `order` so every FALLING arm sits directly above the arm it falls into. Each
@@ -251,23 +337,26 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
251
337
  isNamed,
252
338
  isCmpOpcode,
253
339
  switchAllowsNeqCase,
254
- switchAllowsBoundCase,
340
+ switchBoundCase,
255
341
  switchArmsFollowLayout,
256
342
  switchRequiresFrontLoadedTests,
257
343
  spellSwitchFallthrough,
258
344
  emitsOwnStatement,
259
345
  blockOf,
346
+ enclosingLoop,
260
347
  hoistDispatchCopies,
261
348
  expr,
262
349
  structureRegion,
350
+ structureArm,
263
351
  } = deps;
264
352
 
265
353
  // A block's index in `fn.blocks` as its position in the ASSEMBLY — the warrant for every reading
266
354
  // of layout below, and true PER FRONTEND rather than of the IR:
267
355
  // - thumb.ts and mips.ts build the list by scanning the instruction stream in address order;
268
- // - ppc.ts does not. It APPENDS a synthetic return block (`synthReturn`) at the end of the list
269
- // for every conditional-return branch, wherever in the stream that branch sits, so its list
270
- // is not address order at all;
356
+ // - ppc.ts does too, and then APPENDS a synthetic return block (`synthReturn`) at the end of
357
+ // the list for every conditional-return branch, wherever in the stream that branch sits. So
358
+ // its real blocks keep address order among themselves, and each synthetic one — a bare
359
+ // return, never a test — sorts after all of them;
271
360
  // - raising only ever REMOVES blocks from the list (raise/{divpow2,latch,retsink,shortcircuit}
272
361
  // .ts all `filter`), never inserts or reorders, so the frontend's order is what survives.
273
362
  // TWO READERS, both target-gated, and they need DIFFERENT strengths of the compiler half:
@@ -278,9 +367,14 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
278
367
  // `MIPS_GCC` declares `switchRequiresFrontLoadedTests` and NOT `switchArmsFollowLayout` for
279
368
  // exactly that reason — it has a scheduler and fills delay slots (target.ts).
280
369
  // Both are therefore claims about a target's FRONTEND as much as about its compiler, and a target
281
- // opts in on both halves — which is why PPC_MWCC, whose frontend fails the frontend half outright,
282
- // declares neither. Anything added below that reads `layoutIndex` inherits the frontend half and
283
- // owes a statement of which strength of the compiler half it needs.
370
+ // opts in on both halves. PPC_MWCC's frontend holds the weaker half only: a synthetic return body
371
+ // sorting last can make PRE5 keep a `switch` it should decline, never decline one it should keep,
372
+ // so mwcc declares `switchRequiresFrontLoadedTests` and could not declare the placing reading.
373
+ // That is the frontend's limit and not the compiler's: mwcc lays its case bodies in source order
374
+ // (the compiled pairs at target.ts `switchArmsFollowLayout`), so a synthetic block placed at its
375
+ // branch's address would give PPC both halves.
376
+ // Anything added below that reads `layoutIndex` inherits the frontend half and owes a statement
377
+ // of which strength of the compiler half it needs.
284
378
  const blockIndex = new Map(fn.blocks.map((blk, i) => [blk, i] as const));
285
379
  const layoutIndex = (blk: Block): number => blockIndex.get(blk) ?? -1;
286
380
 
@@ -357,82 +451,19 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
357
451
  // has declared that its layout answers it — which SPELLING the source wrote.
358
452
 
359
453
  // Fold a value that is a compile-time constant (a `const`, or a synthesized immediate like agbcc's
360
- // `250 << 2` for a large sparse case) to a number — else null.
361
- const evalConst = (v: Value): number | null => {
362
- if (isNamed(v)) {
363
- return null;
364
- } // a named variable is not a constant
365
- const d = defs.get(v);
366
- if (!d) {
367
- return null;
368
- }
369
- if (d.opcode === 'const') {
370
- return (d.attrs.value as number) | 0;
371
- }
372
- // Resolve the two operands of a binary op to constants (2-operand → both; 1-operand → operand +
373
- // `imm` attr, which MUST be present, else decline — a missing imm would wrongly fold `and x` to 0).
374
- const operands2 = (): [number, number] | null => {
375
- const a = evalConst(d.operands[0]);
376
- if (a === null) {
377
- return null;
378
- }
379
- let c: number | null;
380
- if (d.operands.length === 2) {
381
- c = evalConst(d.operands[1]);
382
- } else if (typeof d.attrs.imm === 'number') {
383
- c = d.attrs.imm | 0;
384
- } else {
385
- return null;
386
- }
387
- return c === null ? null : [a, c];
388
- };
389
- const bin = (f: (a: number, c: number) => number): number | null => {
390
- const p = operands2();
391
- return p === null ? null : f(p[0], p[1]) | 0;
392
- };
393
- const shift = (f: (a: number, c: number) => number): number | null => {
394
- const p = operands2();
395
- if (p === null || p[1] < 0 || p[1] >= 32) {
396
- return null;
397
- } // out-of-range shift amount → decline
398
- return f(p[0], p[1]) | 0;
399
- };
400
- switch (d.opcode) {
401
- case 'shl':
402
- return shift((a, c) => a << c);
403
- case 'shr_u':
404
- return shift((a, c) => a >>> c);
405
- case 'shr_s':
406
- return shift((a, c) => a >> c);
407
- case 'or':
408
- return bin((a, c) => a | c);
409
- case 'and':
410
- return bin((a, c) => a & c);
411
- case 'xor':
412
- return bin((a, c) => a ^ c);
413
- case 'add':
414
- return bin((a, c) => a + c);
415
- case 'sub':
416
- return bin((a, c) => a - c);
417
- case 'neg': {
418
- const a = evalConst(d.operands[0]);
419
- return a === null ? null : -a | 0;
420
- }
421
- case 'not': {
422
- const a = evalConst(d.operands[0]);
423
- return a === null ? null : ~a | 0;
424
- }
425
- default:
426
- return null;
427
- }
428
- };
454
+ // `250 << 2` for a large sparse case) to a number — else null. The shared word evaluator
455
+ // (ir/bits.ts), which declines an opcode it has no rule for: a `neg_s` (Thumb `cmn` under a signed
456
+ // branch) is no word, so a guard against it is never read as a case bound. A named variable is
457
+ // not a constant, whatever set it.
458
+ const words = { defs, opaque: isNamed };
459
+ const evalConst = (v: Value): number | null => constWord(words, v);
429
460
 
430
461
  // TWO QUESTIONS about a block, asked separately because the answers diverge and the walk needs
431
462
  // both: WHAT does it test (`testInfo`), and may this recovery DISCARD it (`collapsible`)?
432
463
  //
433
464
  // PRE4 (purity) is the second. A collapsed test block's ops re-render at whichever use inlines
434
465
  // them, at a point the switch decides — so the question is motion, not deletion, and
435
- // `ORDER_SENSITIVE_OPS` is the set that asks it. NOT the trapping divides: a use is dominated by
466
+ // `orderSensitive` is the question that asks it. NOT the trapping divides: a use is dominated by
436
467
  // its def, so the re-rendered op runs on a subset of the paths it already ran on — nothing is
437
468
  // speculated. `emitsOwnStatement` covers what motion cannot save: a statement belonging to the
438
469
  // block rather than to a use. The root is exempt from all of it — its ops are already emitted as
@@ -441,8 +472,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
441
472
  // A block that tests the scrutinee and is NOT collapsible is still dispatch, so the walk must
442
473
  // read it as dispatch and decline, never re-read it as a case body — that would spell an arm
443
474
  // whose guard the dispatch has already decided.
444
- const collapsible = (blk: Block): boolean =>
445
- !blk.ops.some((op) => ORDER_SENSITIVE_OPS.has(op.opcode)) && !emitsOwnStatement(blk);
475
+ const collapsible = (blk: Block): boolean => !blk.ops.some(orderSensitive) && !emitsOwnStatement(blk);
446
476
  const testInfo = (blk: Block): TestInfo | null => {
447
477
  const term = blk.ops[blk.ops.length - 1];
448
478
  if (term.opcode !== 'cond_br') {
@@ -472,6 +502,123 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
472
502
  return { x, k, cls, opcode: cmp.opcode, xOnLeft };
473
503
  };
474
504
 
505
+ // Does `blk` leave the enclosing loop for its exit, other than by the loop's own bottom test (a
506
+ // block that also edges back to the header, which spells as the loop's condition)?
507
+ const breaksLoop = (blk: Block): boolean => {
508
+ const loop = enclosingLoop();
509
+ const succ = successorsOf(blk);
510
+ return loop !== null && loop.exit !== null && succ.includes(loop.exit) && !succ.includes(loop.header);
511
+ };
512
+
513
+ // Does a block some arm structures edge to `target`? Walked as `structureArm` walks an arm: every
514
+ // block reached from the arm's entry without stepping through `merge` or back into `header`. That
515
+ // is wider than `analyzeArmExit`'s dominated region on purpose: when Regime A recognises a sub-tree
516
+ // of the comparison tree, an arm can run on into the code of a case the tree's root already took,
517
+ // which the dispatch does not dominate, and `structureArm` structures that code inside the arm.
518
+ // A block that also edges back to `header` is the loop's bottom test, which spells as the loop's
519
+ // condition, not as the arm's (`breaksLoop`) — save where `header` IS the merge (a top-tested
520
+ // loop): no bottom test is then reachable without passing the merge, and an arm's
521
+ // `if (c) goto out;` is a block that edges to the exit and straight back into the header.
522
+ const armsReach = (entries: Iterable<Block>, merge: Block | null, header: Block, target: Block): boolean => {
523
+ const seen = new Set<Block>(entries);
524
+ for (const q = [...seen]; q.length;) {
525
+ const succ = successorsOf(q.pop()!);
526
+ if (succ.includes(target) && (header === merge || !succ.includes(header))) {
527
+ return true;
528
+ }
529
+ for (const s of succ) {
530
+ if (s !== target && s !== merge && s !== header && !seen.has(s)) {
531
+ seen.add(s);
532
+ q.push(s);
533
+ }
534
+ }
535
+ }
536
+ return false;
537
+ };
538
+
539
+ // Can a path from `from` reach `to` without passing `through`? False when `to` is null (the end
540
+ // of the function) or is `through` itself.
541
+ const reachesAvoiding = (from: Block, to: Block | null, through: Block | null): boolean => {
542
+ if (to === null || to === through) {
543
+ return false;
544
+ }
545
+ const seen = new Set<Block>([from]);
546
+ for (const q = [from]; q.length;) {
547
+ for (const s of successorsOf(q.pop()!)) {
548
+ if (s === to) {
549
+ return true;
550
+ }
551
+ if (s !== through && !seen.has(s)) {
552
+ seen.add(s);
553
+ q.push(s);
554
+ }
555
+ }
556
+ }
557
+ return false;
558
+ };
559
+
560
+ // THE LOOP-EXIT MERGE, WHERE IT SPELLS. A switch whose merge is its enclosing loop's exit puts
561
+ // the exit region after the `switch`, INSIDE the loop body, and reaches the loop bottom only
562
+ // through the arms that do not break. That is the source's text on two conditions, both read off
563
+ // the CFG here, and on either failing the merge-past-stop gate below declines to if-recovery:
564
+ // - the exit region RETURNS: no path from `merge` reaches a block that dominates it (other
565
+ // than `merge` itself, a loop of the exit region's own) — the enclosing loop's header, or an
566
+ // OUTER loop's, whose back edge the inner body cannot spell
567
+ // (`for (j...) { for (;;) { switch ... } out: u += prev; }` runs on to the outer latch);
568
+ // - every loop-bottom TEST the arms reach (a block that edges back to the header) leaves for
569
+ // the merge itself, which the arm then spells as its `break;` (`latchexit`). One that leaves
570
+ // for any other block outside the loop is past what an arm structured against `merge` can
571
+ // say: the merge is then the loop's exit only because an early `return` made a shared
572
+ // epilogue the dispatch's post-dominator, while the code the bottom test leaves for runs
573
+ // before it (`for (w = 0; w < 4; w++) { if (c) return w; switch ... } r(t);` on
574
+ // gcc2.7.2kmc).
575
+ // And the loop's HEADER must not leave for the merge itself. One that does makes the loop
576
+ // top-tested on it, with the merge as the loop's own exit, which the exit region rendered after
577
+ // the switch inside the body would leave nothing to reach
578
+ // (`for (k = 0; k < 3; k++) { if (gC == 7) goto out; switch ... }` becomes `while (gC != 7)`).
579
+ // A test lower in the body that leaves for the merge ahead of the dispatch spells as its own
580
+ // `goto` or `break`, and keeps the switch.
581
+ const exitMergeSpells = (from: Block, merge: Block | null): boolean => {
582
+ if (merge === null) {
583
+ return false;
584
+ }
585
+ const above = dom.get(merge)!;
586
+ const exitSeen = new Set<Block>([merge]);
587
+ for (const q = [merge]; q.length;) {
588
+ for (const s of successorsOf(q.pop()!)) {
589
+ if (s !== merge && above.has(s)) {
590
+ return false;
591
+ }
592
+ if (!exitSeen.has(s)) {
593
+ exitSeen.add(s);
594
+ q.push(s);
595
+ }
596
+ }
597
+ }
598
+ const header = enclosingLoop()?.header ?? null;
599
+ if (header !== null && successorsOf(header).includes(merge)) {
600
+ return false;
601
+ }
602
+ const seen = new Set<Block>([from]);
603
+ for (const q = [from]; q.length;) {
604
+ const succ = successorsOf(q.pop()!);
605
+ if (
606
+ header !== null &&
607
+ succ.includes(header) &&
608
+ succ.some((s) => s !== header && s !== merge && !reachesAvoiding(s, header, merge))
609
+ ) {
610
+ return false;
611
+ }
612
+ for (const s of succ) {
613
+ if (s !== header && s !== merge && !seen.has(s)) {
614
+ seen.add(s);
615
+ q.push(s);
616
+ }
617
+ }
618
+ }
619
+ return true;
620
+ };
621
+
475
622
  // Where does one arm's region LEAVE? Walk it from `entry`, never stepping THROUGH the merge or a
476
623
  // sibling arm's entry, and classify what it steps INTO. `siblings` is every OTHER arm entry the
477
624
  // caller can emit a `case`/`default` label for — the merge is deliberately not among them, so a
@@ -483,22 +630,44 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
483
630
  // sideways does not fall into the next case — and only the FALL-THROUGH verdict consults that
484
631
  // record, so an arm that reaches no sibling still closes with a plain `break`.
485
632
  //
633
+ // With `stopAtLoop` (Regime A), a FALLING arm that also reaches the merge must not break the loop
634
+ // the switch sits in. When the dispatch IS the loop header (agbcc and IDO rotate
635
+ // `while (k < 4) { k++; switch … }` so), it dominates the loop's exit and all the code after the
636
+ // loop, and the merge is the header itself, so dominance cannot tell the arm's region from the
637
+ // code past the loop. Such an arm that falls into a sibling, reaches the merge by the back edge,
638
+ // and leaves for the loop's exit on a third path (a `goto out`, or a sibling that IS the exit, as
639
+ // a `default: goto out;` makes it) would read as falling with `breaks`, Regime A would commit,
640
+ // and the loop-exit refusal in `structure.ts` would fail the whole function where if-recovery
641
+ // spells it. So it is `unstructurable`. What counts is the block the refusal fires on
642
+ // (`breaksLoop`): the loop's own bottom test, reached after a `continue`, exits too but is the
643
+ // loop's condition, and a block that also edges to the merge is the switch's own end. Regime B,
644
+ // which has no fallback, does not ask: it lifts a jump table whose arms share the rotated latch
645
+ // and its exit (`do { … switch … t++; } while (t < 15)`).
646
+ //
486
647
  // A CONSEQUENCE, not a hole: a sibling reachable only THROUGH such a block is never seen, so the
487
648
  // arm reads as closed and `structureRegion` walks into the sibling's blocks and emits them again
488
649
  // under this arm. That is duplication, not a wrong dispatch — the same duplication the structurer
489
650
  // already does for any tail two arms share, and how the case bodies agbcc tail-merged are put
490
651
  // back. Costly for matching, correct to run.
491
- const analyzeArmExit = (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>): ArmExit => {
652
+ const analyzeArmExit = (
653
+ entry: Block,
654
+ b: Block,
655
+ merge: Block | null,
656
+ siblings: Set<Block>,
657
+ stopAtLoop = false,
658
+ ): ArmExit => {
492
659
  if (entry === merge) {
493
660
  return { kind: 'break' }; // an empty arm (a table slot pointing straight at the switch's end)
494
661
  }
495
662
  const into = new Set<Block>(); // sibling entries this arm flows into
496
663
  let toMerge = false,
497
- escapes = false;
664
+ escapes = false,
665
+ leavesLoop = false;
498
666
  const seen = new Set<Block>([entry]);
499
667
  const q = [entry];
500
668
  while (q.length) {
501
669
  const cur = q.pop()!;
670
+ leavesLoop ||= !successorsOf(cur).includes(merge as Block) && breaksLoop(cur);
502
671
  for (const s of successorsOf(cur)) {
503
672
  if (s === merge) {
504
673
  toMerge = true;
@@ -515,32 +684,32 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
515
684
  }
516
685
  }
517
686
  if (into.size === 0) {
518
- return { kind: 'break' };
519
- }
520
- if (into.size === 1 && !toMerge && !escapes) {
521
- return { kind: 'fallthrough', to: [...into][0] };
687
+ return escapes ? { kind: 'break' } : { kind: 'break', region: seen };
522
688
  }
523
- // Name what is actually missing. These three are different facts, and only the first is a shape
524
- // C has no spelling for — the other two are asmlift's own limits, so say so rather than blame C.
525
- const names = () => [...into].map((x) => `#${layoutIndex(x)}`).join(', ');
526
- if (into.size > 1) {
689
+ // A path to the merge beside the fall-through is the arm's own `break;` — switch-scoped, so it
690
+ // spells only where nothing breakable sits between that edge and the arm, which the structurer
691
+ // checks where it emits it (`structure.ts` `switchBreakAt`) and refuses loud otherwise.
692
+ if (into.size === 1 && toMerge && leavesLoop && stopAtLoop) {
527
693
  return {
528
694
  kind: 'unstructurable',
529
- why: `a case body reaches several sibling cases (${names()}) — C fall-through reaches only one, so this needs a goto`,
695
+ 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`,
530
696
  };
531
697
  }
532
- if (escapes) {
698
+ if (into.size === 1 && !escapes) {
699
+ return { kind: 'fallthrough', to: [...into][0], breaks: toMerge, region: seen };
700
+ }
701
+ // Name what is actually missing. These are two different facts: the first is a shape C's
702
+ // fall-through cannot spell, the second leaves the switch's region sideways.
703
+ const names = () => [...into].map((x) => `#${layoutIndex(x)}`).join(', ');
704
+ if (into.size > 1) {
533
705
  return {
534
706
  kind: 'unstructurable',
535
- why: `a case body reaches sibling case ${names()} on one path and, on another, a block the switch does not dominate`,
707
+ why: `a case body reaches several sibling cases (${names()}) — C fall-through reaches only one, so this needs a goto`,
536
708
  };
537
709
  }
538
710
  return {
539
711
  kind: 'unstructurable',
540
- // `case 0: if (c) { …; break; } /* fall through */ case 1:` is the C for this, and the reason
541
- // asmlift cannot write it is its own: `{k:'break'}` is emitted only for the innermost LOOP
542
- // (l3/ast.ts), never switch-scoped. That is the capability this shape is waiting on.
543
- 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`,
712
+ why: `a case body reaches sibling case ${names()} on one path and, on another, a block the switch does not dominate`,
544
713
  };
545
714
  };
546
715
 
@@ -588,12 +757,15 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
588
757
  }
589
758
  guard.add(cur);
590
759
  const term = cur.ops[cur.ops.length - 1];
591
- const taken = evalCmp(ti.opcode, ti.xOnLeft, xv, ti.k);
760
+ const taken = contains(takenBy(ti), xv);
592
761
  cur = forwardingTarget(term.successors[taken ? 0 : 1].block);
593
762
  }
594
763
  };
595
764
  const seen = new Set<Block>();
596
765
  const work: Block[] = [b];
766
+ // The values that can reach each test block, written as the walk pushes it. Only a bound case
767
+ // reads it; the tree is a tree (a second visit declines below), so each block has one entry.
768
+ const pathOf = new Map<Block, Ranges>([[b, ALL]]);
597
769
  while (work.length) {
598
770
  const blk = work.pop()!;
599
771
  if (seen.has(blk)) {
@@ -628,10 +800,13 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
628
800
  cases.set(k, child);
629
801
  return true;
630
802
  };
803
+ const path = pathOf.get(blk)!;
631
804
  /** Read `child` as a NAVIGATION edge: more dispatch to walk, or a non-test leaf, which is a
632
- * default candidate. Never declines — the leaf's own reading is settled below. */
633
- const asNav = (child: Block): boolean => {
805
+ * default candidate. Never declines — the leaf's own reading is settled below. `onTaken`
806
+ * says which of `blk`'s edges it is, which decides what the range narrows to. */
807
+ const asNav = (child: Block, onTaken: boolean): boolean => {
634
808
  if (isTestOn(child)) {
809
+ pathOf.set(child, narrow(path, ti, onTaken));
635
810
  work.push(child);
636
811
  return true;
637
812
  }
@@ -642,7 +817,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
642
817
  if (!asCase(taken, ti.k)) {
643
818
  return null;
644
819
  } // x==k → taken is case k
645
- if (!asNav(fall)) {
820
+ if (!asNav(fall, false)) {
646
821
  return null;
647
822
  }
648
823
  } else if (ti.cls === 'ne') {
@@ -652,12 +827,12 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
652
827
  if (!asCase(fall, ti.k)) {
653
828
  return null;
654
829
  } // x!=k → the EQUAL side (fall) is case k
655
- if (!asNav(taken)) {
830
+ if (!asNav(taken, true)) {
656
831
  return null;
657
832
  }
658
833
  } else {
659
- // relational → navigation, except where the BRANCH has collapsed to a single value and
660
- // lands on a BODY, on a compiler that declared the spelling. Two more refusals:
834
+ // relational → navigation, except where a side the compiler declared (`switchBoundCase`)
835
+ // admits a single value and lands on a BODY. Two more refusals:
661
836
  // - a bound test at the ROOT. `emit_case_nodes` emits a single-valued node's own
662
837
  // `do_jump_if_equal` before either descent test, so a bound test always sits under
663
838
  // another test of the same tree; one that OPENS the region did not come from this
@@ -665,11 +840,13 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
665
840
  // - a singleton branch onto another TEST of the scrutinee, which is the search
666
841
  // descending to pin the value. It is dispatch, so the walk reads it as dispatch —
667
842
  // recovering it, or declining at PRE4 if it is not collapsible.
668
- const k = switchAllowsBoundCase && blk !== b && !isTestOn(taken) ? singletonTaken(ti) : null;
669
- if (!(k === null ? asNav(taken) : asCase(taken, k))) {
843
+ const sides = blk !== b ? switchBoundCase : null;
844
+ const kTaken = sides && !isTestOn(taken) ? singleton(narrow(path, ti, true)) : null;
845
+ const kFall = sides === 'either' && !isTestOn(fall) ? singleton(narrow(path, ti, false)) : null;
846
+ if (!(kTaken === null ? asNav(taken, true) : asCase(taken, kTaken))) {
670
847
  return null;
671
848
  }
672
- if (!asNav(fall)) {
849
+ if (!(kFall === null ? asNav(fall, false) : asCase(fall, kFall))) {
673
850
  return null;
674
851
  }
675
852
  }
@@ -754,29 +931,73 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
754
931
  }
755
932
 
756
933
  // PRE2 (arm exits), per SITE. A `break` arm closes; a `fallthrough` arm runs on into the one
757
- // sibling it reaches, which `chainArms` then places directly under it. `unstructurable` is a
758
- // shape no single linear switch spells — a body reaching two siblings, or a sibling on one path
759
- // and the switch's end on another — and Regime A declines to if-recovery, which spells every
760
- // one of those edges. (Regime B reads the same verdicts and fails LOUD on either.)
934
+ // sibling it reaches, which `chainArms` then places directly under it — on every path, or on
935
+ // some while the others reach the switch's end, which the arm's own `break;` spells (`breaks`).
936
+ // `unstructurable` is a shape no single linear switch spells — a body reaching two siblings, or
937
+ // a sibling beside a block the switch does not dominate — and Regime A declines to if-recovery,
938
+ // which spells every one of those edges. (Regime B reads the same verdicts and fails LOUD on
939
+ // either.)
761
940
  const merge = ipdom.get(b) ?? stop;
762
941
  const targets = new Set<Block>([...caseBlocks, ...(defaultBlk ? [defaultBlk] : [])]);
763
942
  const siblings = new Set([...targets].filter((t) => t !== merge));
764
943
  const exitOf = new Map<Block, ArmExit>();
765
944
  for (const t of siblings) {
766
- exitOf.set(t, analyzeArmExit(t, b, merge, siblings));
945
+ exitOf.set(t, analyzeArmExit(t, b, merge, siblings, true));
767
946
  }
768
947
  // A language with no fall-through in its `case` (Pascal) cannot print a falling arm at all, so
769
948
  // for it a falling arm is exactly as unspellable as an `unstructurable` one — and takes the
770
949
  // same exit: if-recovery, which that backend prints fine. Asked HERE rather than left to the
771
950
  // backend because the backend's refusal is terminal (the whole function becomes a stub) while
772
951
  // this one costs nothing but the `switch` spelling.
952
+ //
953
+ // A falling arm that also breaks is admitted wherever it falls, the `default:` arm included:
954
+ // for `case 1: if (c) break; default: f(); break;` the ladder if-recovery writes copies `f()`
955
+ // into the arm and misses the object the `switch` reaches.
773
956
  if (
774
957
  [...exitOf.values()].some(
775
- (e) => e.kind === 'unstructurable' || (!spellSwitchFallthrough && e.kind === 'fallthrough'),
958
+ (e) => e.kind === 'unstructurable' || (e.kind === 'fallthrough' && !spellSwitchFallthrough),
776
959
  )
777
960
  ) {
778
961
  return null;
779
962
  }
963
+ // AN ARM'S `break;` NEEDS THE MERGE TO BE THE SWITCH'S END IN THIS REGION. The code after the
964
+ // `switch` is `structureRegion(merge, stop)`, which reads as that only where every path from
965
+ // the dispatch reaches `stop` through the merge. A sub-tree whose post-dominator lies PAST the
966
+ // region's `stop` breaks that: inside a loop body whose dispatch reaches both the latch (`stop`)
967
+ // and an early `return` block, ipdom is the return, so an arm's path to it would read as a
968
+ // switch `break` while a sibling arm runs through the latch and back to the header, which
969
+ // fails the whole function loud. Declined here, to if-recovery, which spells each edge as the
970
+ // `return` or the fall to the loop bottom it is. A merge that is the enclosing loop's EXIT is
971
+ // the exception the structurer spells (`exitMergeSpells`): the code after the switch is then
972
+ // the exit region, and an arm's path to it is the arm's `break;` (`structure.ts`, the loop-exit
973
+ // rules).
974
+ if (
975
+ [...exitOf.values()].some((e) => e.kind === 'fallthrough' && e.breaks) &&
976
+ !(merge === enclosingLoop()?.exit && exitMergeSpells(b, merge)) &&
977
+ reachesAvoiding(b, stop, merge)
978
+ ) {
979
+ return null;
980
+ }
981
+ // AN ARM'S EDGE TO THE LOOP'S EXIT, WHEN THAT EXIT IS NOT THE MERGE. A switch admitted on a
982
+ // falling arm's `break;` hands every arm to `structureArm`, where an edge to the enclosing loop's
983
+ // exit can be spelled only as the switch's own end or as a copied returning tail; any other is
984
+ // refused there, loud, for the whole function (`structure.ts`, "leaves its switch for a loop
985
+ // exit"). With an early `return` above the dispatch, the merge is the loop bottom, and a sibling
986
+ // arm's `goto out` to an exit that runs on to an OUTER loop's latch is such an edge. Declined
987
+ // here to if-recovery, which spells that `goto` as the inner loop's `break`. The exemption is
988
+ // the exit that is itself a `ret`; the structurer's other one, an early-return arm owning the
989
+ // edge, is not asked here, which costs only the `switch` spelling.
990
+ const loop = enclosingLoop();
991
+ if (
992
+ [...exitOf.values()].some((e) => e.kind === 'fallthrough' && e.breaks) &&
993
+ loop !== null &&
994
+ loop.exit !== null &&
995
+ loop.exit !== merge &&
996
+ loop.exit.ops[loop.exit.ops.length - 1]?.opcode !== 'ret' &&
997
+ armsReach(siblings, merge, loop.header, loop.exit)
998
+ ) {
999
+ return null;
1000
+ }
780
1001
 
781
1002
  // PRE3 (concrete interval consistency): a `case k` is only sound if the ORIGINAL tree
782
1003
  // actually routes x==k to its recorded body. A relational guard can make an `x==k` test DEAD (e.g.
@@ -1008,13 +1229,13 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
1008
1229
  // built, and no row asks for it (`sw_jtfall`/`sw_jtfalldesc` match today).
1009
1230
  outCases.push({
1010
1231
  values: armsByBlock.get(blk)!,
1011
- body: structureRegion(blk, to ?? merge),
1232
+ body: structureArm(blk, to ?? merge, merge, exitOf.get(blk)),
1012
1233
  fallsThrough: to !== null,
1013
1234
  });
1014
1235
  }
1015
1236
  // An empty default arm is not a default (see the Regime-B note in structure.ts): the label
1016
1237
  // would carry no statement, which says nothing and is not valid C89.
1017
- const defBody = defaultBlk ? structureRegion(defaultBlk, merge) : [];
1238
+ const defBody = defaultBlk ? structureArm(defaultBlk, merge, merge, exitOf.get(defaultBlk)) : [];
1018
1239
  // The `default:` arm is an ARM: where its block is laid out is read exactly as a case body's is
1019
1240
  // (`defaultLayoutPos`). The dispatch placed that block itself when the last test simply RAN OUT
1020
1241
  // into it and no other subtree jumps there — the two references the tree walk can count.
@@ -1060,3 +1281,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
1060
1281
  };
1061
1282
  return { recognizeSwitch, analyzeArmExit, layoutIndex, defaultLayoutPos, chainArms };
1062
1283
  }
1284
+
1285
+ /** Internal surface for this module's own tests, and for nothing else: PRE3 reads each test
1286
+ * through {@link takenBy}, so the ranges need a check that does not share their definition. */
1287
+ export const __testing = { takenBy, narrow, contains, ALL };