@asmlift/core 0.5.0 → 0.7.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 (94) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +270 -171
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +243 -39
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +11 -0
  10. package/src/frontend/ppc.ts +43 -7
  11. package/src/frontend/ssa.ts +404 -29
  12. package/src/frontend/thumb.ts +2176 -686
  13. package/src/ir/alias.ts +78 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +345 -2
  16. package/src/ir/opcodes.ts +176 -21
  17. package/src/ir/parse.ts +19 -2
  18. package/src/ir/print.ts +27 -2
  19. package/src/ir/simplify.ts +190 -3
  20. package/src/ir/struct-names.ts +42 -0
  21. package/src/ir/verify.ts +43 -49
  22. package/src/l3/address.ts +62 -0
  23. package/src/l3/advance.ts +373 -0
  24. package/src/l3/argbase.ts +6 -5
  25. package/src/l3/ast.ts +510 -59
  26. package/src/l3/basecse.ts +686 -78
  27. package/src/l3/coalesce.ts +432 -46
  28. package/src/l3/dce.ts +31 -9
  29. package/src/l3/gates.ts +96 -1
  30. package/src/l3/hoist.ts +293 -14
  31. package/src/l3/homesplit.ts +285 -0
  32. package/src/l3/initfirst.ts +301 -0
  33. package/src/l3/inlinebase.ts +193 -0
  34. package/src/l3/mentions.ts +176 -0
  35. package/src/l3/mulfirst.ts +42 -0
  36. package/src/l3/nearbase.ts +152 -0
  37. package/src/l3/offmember.ts +371 -0
  38. package/src/l3/parkfirst.ts +96 -0
  39. package/src/l3/pollguard.ts +154 -0
  40. package/src/l3/ptrfield.ts +227 -0
  41. package/src/l3/regspell.ts +114 -89
  42. package/src/l3/reindex.ts +722 -80
  43. package/src/l3/scopebase.ts +649 -220
  44. package/src/l3/sinkinit.ts +40 -0
  45. package/src/l3/slotorder.ts +123 -0
  46. package/src/l3/storage.ts +48 -0
  47. package/src/l3/symbol-refs.ts +41 -8
  48. package/src/l3/tailmerge.ts +16 -1
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +687 -0
  51. package/src/l3/unreduce.ts +971 -0
  52. package/src/l3/volatileptr.ts +207 -0
  53. package/src/l3/volatileval.ts +130 -0
  54. package/src/l3/volstore.ts +229 -0
  55. package/src/l3/zerosub.ts +62 -0
  56. package/src/pattern/engine.ts +239 -16
  57. package/src/pipeline.ts +173 -60
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/const.ts +203 -3
  61. package/src/raise/divpow2.ts +4 -4
  62. package/src/raise/extscale.ts +342 -0
  63. package/src/raise/globalshape.ts +1058 -0
  64. package/src/raise/gvn.ts +33 -18
  65. package/src/raise/latch.ts +126 -0
  66. package/src/raise/magicdiv.ts +2 -2
  67. package/src/raise/memberarrays.ts +594 -0
  68. package/src/raise/narrow.ts +124 -0
  69. package/src/raise/narrowlocal.ts +572 -0
  70. package/src/raise/paramwidth.ts +201 -0
  71. package/src/raise/pre-recovery.ts +169 -21
  72. package/src/raise/recover.ts +56 -23
  73. package/src/raise/retsink.ts +585 -19
  74. package/src/raise/shortcircuit.ts +1050 -89
  75. package/src/raise/struct-arrays.ts +19 -2
  76. package/src/raise/structs.ts +34 -4
  77. package/src/raise/tailsink.ts +126 -0
  78. package/src/rank-declare.ts +256 -0
  79. package/src/rank-variations.ts +760 -0
  80. package/src/rank.ts +2122 -326
  81. package/src/structure/analysis.ts +1398 -150
  82. package/src/structure/bitfields.ts +432 -0
  83. package/src/structure/globalaccess.ts +300 -0
  84. package/src/structure/hazards.ts +411 -20
  85. package/src/structure/loops.ts +2 -49
  86. package/src/structure/namecoalesce.ts +454 -0
  87. package/src/structure/structure.ts +3979 -612
  88. package/src/structure/switch-recover.ts +710 -145
  89. package/src/symbols.ts +188 -6
  90. package/src/target.ts +495 -32
  91. package/src/trace.ts +112 -33
  92. package/src/variation-definitions.ts +1540 -0
  93. package/src/variation-gates.ts +89 -0
  94. package/src/variation-tokens.ts +355 -0
package/src/l3/ast.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // asmlift L3 — the language-NEUTRAL structured AST. A LanguageBackend lowers this to a
2
2
  // concrete language (C / Pascal / C++) and prints it. "Return a value" and binary ops
3
3
  // are neutral nodes here; each backend owns its own spelling.
4
- import type { IrType } from '../ir/types';
4
+ import { type IrType, typeEquals } from '../ir/types';
5
5
 
6
6
  export type Expr =
7
7
  | { k: 'var'; name: string }
@@ -15,7 +15,13 @@ export type Expr =
15
15
  // `3 & (s32)p`). Scalar deref casts are backend-owned — the C-family printer synthesizes
16
16
  // them from the `index` node's width. Each backend spells the cast in its own syntax
17
17
  // (C: `(u8)e`; Pascal: no spelling yet → fails loud).
18
- | { k: 'cast'; to: IrType; e: Expr }
18
+ //
19
+ // `volatile` qualifies the POINTEE of a pointer cast (`(volatile u16 *)0x4000208`) — the one
20
+ // place asmlift can say "this access is to a volatile object" when there is no declaration to
21
+ // hang it on, which is exactly the raw-address case (l3/inlinebase.ts). IrType models no
22
+ // cv-qualifier, deliberately: volatility is a SPELLING, carried at the declaration or the
23
+ // cast, the same split SFn.locals makes for `volatile`/`pointeeVolatile`.
24
+ | { k: 'cast'; to: IrType; e: Expr; volatile?: true }
19
25
  | { k: 'call'; fn: string; args: Expr[] }
20
26
  // The ADDRESS of a named global, `&gSym` (agbcc pool `.word gSym`, frontend `gaddr` op). A
21
27
  // DEREF of it collapses to the bare global: memAccess/arrayAccess spell `*(&gSym)` as `gSym`
@@ -38,13 +44,105 @@ export type Expr =
38
44
  // Variable-index `a[i]` is recovered at the IR level (`aload`/`astore` carry elemSize;
39
45
  // raise/arrays.ts) but still LOWERS to this one C-shaped `index` node, so it stays C-only
40
46
  // (a Pascal array-access spelling is future work). Treat `index` with idx ≠ 0 as C-shaped.
41
- // `lead` prefixes CONSTANT subscripts before `idx` — `g[0][i]` rather than `g[i]`. It exists for
42
- // exactly one inhabitant: the bare-name spelling of a MULTIDIMENSIONAL array global, where one
43
- // subscript reaches a row and the element needs the leading dimensions pinned first. The node
44
- // still denotes ONE `width`-byte element, so its type, its legalization and its stride contract
45
- // are unchanged — this is a spelling of the same address, not a new kind of access. Absent for
46
- // every rank-1 access, which is why it is optional rather than an empty array.
47
- | { k: 'index'; base: Expr; idx: Expr; width: number; signed: boolean; lead?: number[] }
47
+ // `lead` prefixes the LEADING subscripts before `idx` — `g[0][i]` or `g[r][i]` rather than
48
+ // `g[i]`. It exists for exactly one inhabitant: the bare-name spelling of a MULTIDIMENSIONAL
49
+ // array global, where one subscript reaches a row and the element needs the leading dimensions
50
+ // pinned first. The node still denotes ONE `width`-byte element, so its type, its legalization
51
+ // and its stride contract are unchanged — this is a spelling of the same address, not a new kind
52
+ // of access. Absent for every rank-1 access, which is why it is optional rather than an empty
53
+ // array. A leading subscript is an EXPRESSION because a row index the asm computed is a value,
54
+ // not a literal (`g[gRow][i]`), so every generic walk descends into it: a name mentioned there
55
+ // is a real use, and a rewrite that skipped it would rename half an address. `exprChildren` and
56
+ // `mapExprChildren` below carry it, which is what makes that true for every walk DERIVED from
57
+ // them; the one walk that is not — l3/mentions.ts, which hand-rolls a traversal to tell an
58
+ // `index` BASE from every other position — enumerates the positions itself and has to be
59
+ // extended by hand when one is added here.
60
+ // `operandOff` is the one field here that is EVIDENCE rather than spelling: it is the BYTE
61
+ // DISPLACEMENT this access's constant offset arrived in through the instruction's MEMORY
62
+ // OPERAND (`ldrb [r0, #0x3]` records 3), as opposed to through the address the pool word
63
+ // materialized (`.word gSym+0x3`, which records nothing). Both denote the same cell and print
64
+ // the same subscript, which is why `exprEquals` ignores it — but on a compiler that folds a
65
+ // constant subscript into the literal, only one C spelling could have put it there, so
66
+ // `l3/basecse.ts` reads its PRESENCE as the evidence its `unfoldedOffset` rule is about and
67
+ // `l3/offmember.ts` reads its VALUE as the member offset to re-spell. Absent whenever the
68
+ // offset was 0 or came from the address expression, so absence is never proof of anything — and
69
+ // a displacement can be NEGATIVE, so every reader tests `!== undefined`, never truthiness.
70
+ //
71
+ // THE FIELD IS HALF OF A TWO-PART FACT, and the other half has no field because it needs none.
72
+ // A displacement that reached the instruction says the compiler did not fold it; WHAT TO DO
73
+ // about that depends on the BASE'S KIND, which is read off the base expression at each reader:
74
+ // • a local (`var`) — nothing to reassociate into; already right, and no reader fires.
75
+ // • a plus tree — the fold pulls the displacement into the tree, costing an `add` and a
76
+ // register. The repair is to home the base in a local so it becomes a
77
+ // `var`, which is the ADDRESS-HOME variation and lives one level down at L2
78
+ // (`structure/analysis.ts`'s `sharedBaseClasses`) because materializing
79
+ // a value is a structuring decision, not a spelling.
80
+ // • a leaf const/addr — the fold bakes the displacement into the literal, changing the `.word`.
81
+ // The repair is a spelling: `/basefold`'s named base or
82
+ // `/offmember`'s aggregate member, both at L3.
83
+ // The two repairs are deliberately NOT dispatched from one place. They sit at different levels
84
+ // and partition the space by their own pre-existing predicates (a non-const pure def with no
85
+ // gaddr in its cone, versus `l3/offmember.ts`'s leaf base), so a shared dispatch would be
86
+ // scaffolding over two rules that already disagree about what a base is. What the reader needs
87
+ // is the map, which is this paragraph.
88
+ //
89
+ // `baseOrdered` is the SECOND evidence field, and it answers about the base rather than the
90
+ // offset: the input assembly materialized this access's base BEFORE it scaled the index
91
+ // (raise/globalshape.ts `orderLicensedGlobals`, stamped at the structure seam; that module's
92
+ // header carries the compiles saying which source spellings produce which order). It is what
93
+ // says a base may be given a HOME — `l3/basecse.ts`'s `order-licensed`. Absent means "no such
94
+ // evidence", never "the index came first": a scaling in another block is not comparable, and a
95
+ // compiler that has not opted in stamps nothing. `exprEquals` ignores it for `operandOff`'s
96
+ // reason — both spellings denote the same cell — and it is per SYMBOL, so every access of one
97
+ // name carries the same answer, which is right for agbcc because one CSEd pool load serves
98
+ // them all.
99
+ //
100
+ // `baseAdvanced` is the THIRD, and it answers about the ADDRESS COMPUTATION: this access's
101
+ // address was reached by ADDING this many bytes to a register that already held — and had just
102
+ // been used as — another address (`adds r3, #2` between two stores), rather than by a second
103
+ // pool word or a memory-operand displacement. `raise/const.ts` records it on the literal its
104
+ // fold produces, because the fold is what makes the three shapes indistinguishable; the
105
+ // structure seam copies it here. `l3/advance.ts` is what reads it, to offer a pointer local
106
+ // advanced in place. `exprEquals` ignores it for `operandOff`'s reason. Absence is never proof:
107
+ // a target whose frontend does not lift the advance stamps nothing, and the value is a byte
108
+ // count that can be NEGATIVE, so readers test `!== undefined`.
109
+ | {
110
+ k: 'index';
111
+ base: Expr;
112
+ idx: Expr;
113
+ width: number;
114
+ signed: boolean;
115
+ lead?: Expr[];
116
+ /** The ELEMENT type the base's own DECLARATION gives it, for the one base whose stride the
117
+ * C type walk cannot reconstruct: a map-declared array MEMBER (`gPtr->arr`), which
118
+ * `exprCType` types `undefined` because the `field` node hangs off an untyped `var`. Without
119
+ * it the C backend legalizes the base through a reinterpret cast (`((u8 *)gPtr->arr)[i]`),
120
+ * which is the CAST form's object again and defeats the whole point of naming the member.
121
+ *
122
+ * IT IS A PRODUCER INVARIANT: the backend cannot vet a stated type. `derefStrideOk` tests
123
+ * the STATED type against the access width, never against the base, so it is true by
124
+ * construction for anything a producer could state AND for a statement gone stale. What
125
+ * the consumer does guard is PRECEDENCE — this field is consulted only where `exprCType`
126
+ * answers nothing (cfamily.ts `legalizedIndexBase`), so wherever the walk can read the base
127
+ * it corrects a stale statement instead of being overridden by it. The one producer,
128
+ * structure/structure.ts `pointeeElement`, sets it from the same `elemSize`/`elemSigned` it
129
+ * has just passed `spellsAccessType` on — and that predicate IS
130
+ * `typeEquals(T.int(width*8, elemSigned), scalarTypeForAccess(width, signed))`, so
131
+ * `derefStrideOk` over the stated pointer is true by construction at every width (4 by
132
+ * `width === 4`, 1 and 2 by `to.signed === signed`). The obligation is on the PRODUCER:
133
+ * state the type the access's own width and signedness agree with.
134
+ *
135
+ * THE ALTERNATIVE REJECTED: teach `exprCType` the pointee layout, the way
136
+ * `sym.noteGlobal` types the bare-array spelling —
137
+ * the printer already renders `u8 grid[6][8]` for this member from this same layout. It was
138
+ * rejected because `SFn.globals` is also the ADDRESSABLE-BASE list, so typing the symbol
139
+ * there admits it as a `/livebase` base (measured on the probe: 8 extra base locals). Typing
140
+ * the one node costs no candidates. */
141
+ baseElem?: IrType;
142
+ operandOff?: number;
143
+ baseOrdered?: true;
144
+ baseAdvanced?: number;
145
+ }
48
146
  // A named struct-field access `base->name` (raise/structs.ts recovered `base` as a struct
49
147
  // pointer, so the byte offset resolves to a named field instead of a scaled array index).
50
148
  // Unlike `index`, this carries the field NAME (which encodes the byte offset, `field_<off>`),
@@ -59,31 +157,41 @@ export type Expr =
59
157
  // default) never produces this node; it keeps the `"?"` sentinel → ContractError behavior.
60
158
  | { k: 'marker'; reason: string; args: Expr[] };
61
159
 
62
- // `>>` is the ARITHMETIC right shift and `>>>` the LOGICAL one. C spells both `>>` and picks from
63
- // the left operand's type, so the C backend synthesizes the cast that pins the choice — exactly as
64
- // it already synthesizes scalar deref casts from an `index` node's width. A backend with no
65
- // spelling for one of them (IDO Pascal) declines LOUDLY on the operation itself, rather than on
66
- // whatever artifact another language's spelling happened to leave in the tree.
160
+ // THE SIGNEDNESS-CARRYING PAIRS. `>>` is the ARITHMETIC right shift and `>>>` the LOGICAL one;
161
+ // `/`/`%` are the SIGNED quotient and remainder and `/u`/`%u` the unsigned ones. C spells each pair
162
+ // with one token and picks between them from the operand types, so the C backend synthesizes the
163
+ // cast that pins the choice exactly as it already synthesizes scalar deref casts from an `index`
164
+ // node's width. A backend with no spelling for one of them (IDO Pascal) declines LOUDLY on the
165
+ // operation itself, rather than on whatever artifact another language's spelling happened to leave
166
+ // in the tree.
167
+ //
168
+ // WHY THESE SPLITS AND NOT THE OTHERS. "The machine distinguishes them" is NOT the rule — the
169
+ // machine distinguishes `sltu`/`slt` too, and CMP_TO_BIN deliberately collapses `icmp_u*`→`<` etc.,
170
+ // noting that "unsignedness is in the operand types". Taking the machine as the rule would license
171
+ // more splits with no inhabitant, which is what "earn the level" forbids. The rule is the repo's
172
+ // own: a split is earned by a real, byte-load-bearing divergence WITH inhabitants that no other
173
+ // channel can carry. The shifts earned it first (~20 rows, 5 projects, 4 compilers) because the
174
+ // operand type could not carry them — a promoted narrow value is signed whatever it was loaded as.
67
175
  //
68
- // WHY THIS ONE SPLIT AND NOT THE OTHERS. "The machine distinguishes them" is NOT the rule — the
69
- // machine distinguishes `divu`/`div` and `sltu`/`slt` too, and ARITH_TO_BIN deliberately collapses
70
- // `udiv`→`/`, `umod`→`%`, `icmp_u*`→`<` etc., noting that "unsignedness is in the operand types".
71
- // Taking the machine as the rule would license four more splits with no inhabitant, which is what
72
- // "earn the level" forbids. The rule is the repo's own: the shift split because a real,
73
- // byte-load-bearing divergence HAD inhabitants (~20 rows, 5 projects, 4 compilers) and no other
74
- // channel could carry it — the operand type could not, since a promoted narrow value is signed
75
- // whatever it was loaded as.
176
+ // The divides earned it second, on pokeemerald:GetAnchorCoord `(u32)(coord * a1) / (u32)a0`
177
+ // standing beside two arithmetic shifts of the same values. Their only other channel is the operand
178
+ // TYPES, reached by flipping a declaration, and there that flip is unreachable and unsound at
179
+ // once: the divisor also feeds a signed compare, so the /uns-cmp reconciliation correctly refuses
180
+ // it, and forcing it anyway makes agbcc delete the comparison as always-false. A per-operand pin is
181
+ // the only spelling that says "this division alone is unsigned".
76
182
  //
77
- // The collapsed operators lean on exactly that channel, so they carry the same latent hazard:
78
- // `*(u16 *)p / 3` renders as a signed division of a promoted `int` where the asm did `divu`. It is
79
- // tolerated because no row has produced such a divergence. When one does, the fix is this same
80
- // split not a per-site patch.
183
+ // The COMPARISONS stay collapsed, and that asymmetry is the rule applying rather than an omission:
184
+ // which side a compare was spelled from genuinely underdetermines a signed spelling that
185
+ // byte-matched was proved non-negative by the compiler so it is refereed as a variation, while a
186
+ // division helper is a pure function of the expression's C type with no such proof available.
81
187
  export type BinOp =
82
188
  | '+'
83
189
  | '-'
84
190
  | '*'
85
191
  | '/'
192
+ | '/u'
86
193
  | '%'
194
+ | '%u'
87
195
  | '<'
88
196
  | '<='
89
197
  | '>'
@@ -130,7 +238,20 @@ export type Stmt =
130
238
  | { k: 'continue' }
131
239
  // A multi-way `switch` over an integer scrutinee (recovered from a comparison tree — Regime A — or
132
240
  // a jump-table `switch_br` — Regime B). `cases` are emitted IN ARRAY ORDER; `default` (if present)
133
- // is emitted last.
241
+ // is emitted after `defaultAt` of them, or after all of them when that is absent.
242
+ //
243
+ // `defaultAt` exists because C lets `default:` sit BETWEEN case labels and a compiler that lays
244
+ // case bodies out in source order shows where the source put it. It is a COUNT of preceding arms,
245
+ // not an index into an array a later pass may rebuild, and a count past the arms is a producer bug
246
+ // a backend refuses. Setting it is legal only when the arm before the label does not fall through:
247
+ // moving the label in front of a falling arm would divert that arm into the default. The C-family
248
+ // printer terminates a non-final default with `break;` for the mirror-image reason.
249
+ //
250
+ // Unlike `fallsThrough` below, `defaultAt` is a SPELLING: the arm BEFORE the label is closed (the
251
+ // rule above) and so is the default body itself (the C-family printer terminates a non-final one),
252
+ // so a backend with no positional default (Pascal's `otherwise`) may ignore the count and still
253
+ // emit the same program. The arm AFTER the label is under no such rule and MAY fall through — it
254
+ // falls into the arm below it, which the label does not stand between.
134
255
  //
135
256
  // NON-NEUTRALITY NOTE (like the `index` node above): `fallsThrough` encodes a C/C++ control-flow
136
257
  // concept POSITIONALLY — `cases[i].fallsThrough === true` means control continues into
@@ -139,7 +260,12 @@ export type Stmt =
139
260
  // Pascal backend MUST loud-fail a `fallsThrough` case (it has no faithful spelling), exactly as it
140
261
  // loud-fails `field`/`cast`. Recovery must therefore only set `fallsThrough` when the fall-through
141
262
  // target is the emission-adjacent case.
142
- | { k: 'switch'; scrutinee: Expr; cases: SwitchCase[]; default?: Stmt[] }
263
+ //
264
+ // Recovery COMPUTES this flag rather than spelling it, so a source grep for `fallsThrough: true`
265
+ // finds hand-written fixtures and nothing else, whatever the corpus does — count its inhabitants
266
+ // by instrumenting the printer. Both regimes produce them: the jump table spells `case 4:` of
267
+ // `kleod:UpdateWorldMapNodeAnim`, the comparison tree `synthetic:sw_fallmem:agbcc`.
268
+ | { k: 'switch'; scrutinee: Expr; cases: SwitchCase[]; default?: Stmt[]; defaultAt?: number }
143
269
  | { k: 'return'; value?: Expr };
144
270
 
145
271
  /** One arm of a `switch`. `values` stacks multiple `case K:` labels onto one body (`case 1: case 2:`).
@@ -153,7 +279,59 @@ export interface SwitchCase {
153
279
  export interface SFn {
154
280
  name: string;
155
281
  params: { name: string; type: IrType }[];
156
- locals: { name: string; type: IrType; volatile?: true }[]; // recovered locals, declared at function top
282
+ /** Recovered locals, declared at function top. Two INDEPENDENT volatility facts, mirroring
283
+ * symbols.ts's cell-vs-pointee split: `volatile` = the local OBJECT is volatile (the
284
+ * address-escaped frame scratch; dce.ts treats reads of it as observable), `pointeeVolatile`
285
+ * = the local is a pointer TO volatile data (the l3/volatileptr.ts variation; a declaration
286
+ * spelling only — nothing about the local itself is observable).
287
+ *
288
+ * `frame` is present on a local the structurer recovered from an `laddr` — the asm
289
+ * MATERIALIZED the slot's address into a register, so the object provably lives in memory —
290
+ * and carries the machine's static access counts for it. Under Thumb that envelope is a
291
+ * SUB-WORD frame object: `strh/ldrh/strb/ldrb` have no `[sp,#imm]` form, so a compiler must
292
+ * copy `sp` first, while a word spill goes straight to `[sp,#imm]` and is recovered as an
293
+ * SSA value with no local of its own. So `frame` is NOT the set of every value the machine
294
+ * slotted. `loads`/`stores` are the yardstick a qualifier variation must match before it may
295
+ * declare every access to the object observable: the readability passes between here and L3
296
+ * may drop a store or render one machine load as two reads, and `volatile` over an access
297
+ * set asmlift did not preserve is a source that contradicts itself. ABSENT where the counts
298
+ * would be a floor rather than the set: an address reaching anything but a direct load/store
299
+ * leaves accesses the count cannot see.
300
+ *
301
+ * ORDER MATTERS, and it means two different things at two different times. As the structurer
302
+ * builds it and as every L3 pass sees it, this is the RECOVERED DECLARATION ORDER — the naming
303
+ * walk's order — and passes reason about it as such: `l3/coalesce.ts` picks the arm-disjoint
304
+ * survivor by position in THIS list, so the earlier declaration wins the way a shared source
305
+ * local reads. The EMITTED order is this list re-sorted by `l3/slotorder.ts` inside `emit`,
306
+ * which happens after every pass and returns a copy. A pass that sorted the list any earlier
307
+ * would silently change which local survives every arm-disjoint merge.
308
+ *
309
+ * ONE COMMENT, DELIBERATELY. Five sites tell a reader to consult "the SFn.locals doc"
310
+ * (structure/structure.ts, l3/inlinebase.ts, l3/volatileval.ts, l3/reindex.ts, l3/unreduce.ts),
311
+ * and TypeScript attaches only the LAST doc block before a declaration — so a second block
312
+ * added in front of this one would silently orphan everything above and those five references
313
+ * would point at half a paragraph. Append here; do not add a neighbour. */
314
+ locals: {
315
+ name: string;
316
+ type: IrType;
317
+ volatile?: true;
318
+ pointeeVolatile?: true;
319
+ frame?: { loads: number; stores: number };
320
+ /** the local stands on an `undef` — storage the asm reads without ever writing it, where the
321
+ * MISSING assignment is the recovery. Marked because a local read and never assigned is
322
+ * otherwise a dropped statement (contracts.ts assertLocalsWritten). */
323
+ uninit?: true;
324
+ /** every `[sp,#k]` the machine homed this local at, when the asm spilled it — ascending, and
325
+ * usually one. Several when the naming walk put several spilled values under this one name,
326
+ * or a coalesce absorbed a second homed local into it; the list is the UNION and picks
327
+ * nothing, because which offset is the earlier DECLARATION RANK depends on the frame's
328
+ * direction and only `l3/slotorder.ts` holds it (ir/core.ts `SlotHomes`).
329
+ *
330
+ * Present ONLY on a local recovered from word-spill VALUES. A `frame` local and a `uninit`
331
+ * one are deliberately left unstamped even where the offset is in hand — see the refusal at
332
+ * the structurer's build site for the measurement that decided it. */
333
+ slots?: number[];
334
+ }[];
157
335
  /** project globals referenced with a known declaration shape (symbol map) — typed for the
158
336
  * legalization env (exprCType) but NEVER declared by a backend: the project's own headers
159
337
  * declare them, exactly like every other global name asmlift emits. */
@@ -163,6 +341,18 @@ export interface SFn {
163
341
  /** Struct types this function's fields reference, declared above it by the backend. Empty
164
342
  * unless raise/structs.ts recovered a struct. Sorted by name for deterministic output. */
165
343
  structs?: StructType[];
344
+ /** Which way this compiler hands out frame slots against DECLARATION RANK, when it is known:
345
+ *
346
+ * THE ONE COMPILER DATUM ON THE NEUTRAL TREE, and it is here rather than in a backend on
347
+ * purpose: `LanguageBackend.emit(fn: SFn): string` is the only seam a backend has, and widening
348
+ * it to `emit(fn, opts)` would hand the datum back to the seven `.emit(` CALL SITES this design
349
+ * exists to keep it out of. So the tree is neutral in its NODES — every node still spells the
350
+ * same thing in C, C++ and Pascal — and not in its emission policy, which this field is.
351
+ * `ascending` = the earlier-declared spilled local takes the LOWER `[sp,#k]`. Set by the
352
+ * structurer from `StructureOptions.spillSlotOrder`, itself a compiler behavior declared in
353
+ * `TargetDescription.compilerBehaviors`. ABSENT means the direction is unknown for this target
354
+ * and `l3/slotorder.ts` is the identity — never "ascending by default". */
355
+ slotOrder?: 'ascending' | 'descending';
166
356
  }
167
357
 
168
358
  /** A struct declaration surfaced to the backend (name + field list). Mirrors the IR struct
@@ -177,6 +367,14 @@ export interface StructType {
177
367
  * comment spelling. */
178
368
  export interface LanguageBackend {
179
369
  readonly id: 'c' | 'cpp' | 'pascal';
370
+ /** Can this language spell a `switch` arm that RUNS ON into the next one (`fallsThrough`)?
371
+ * C and C++ can; Pascal's `case-of` cannot, and its backend loud-fails the node (see the
372
+ * non-neutrality note on `Stmt`'s `switch`). Declared here rather than inferred from `id`
373
+ * because it is what RECOVERY must ask: a comparison-tree switch has a second, behaviourally
374
+ * identical recovery (plain if-nesting), so minting a `fallsThrough` arm for a backend that
375
+ * cannot print it turns a whole function that used to decompile into a loud stub. The
376
+ * structurer reads it through `StructureOptions.spellSwitchFallthrough`. */
377
+ readonly spellsSwitchFallthrough: boolean;
180
378
  emit(fn: SFn): string;
181
379
  // Spell ONE LINE of text as a comment in this language (C block comments, Pascal `(* … *)`).
182
380
  // Used by the annotate-mode stub path to carry the failure reason + the original asm
@@ -201,8 +399,17 @@ export function fieldSpellsDot(f: Extract<Expr, { k: 'field' }>): boolean {
201
399
  }
202
400
 
203
401
  /** Structural equality of two expression trees. THE one copy of Expr deep-equal (like
204
- * fieldSpellsDot/derefStrideOk): key-order-independent by construction (a switch, not a
205
- * stringify), exhaustive under noImplicitReturns like the walkers below. */
402
+ * fieldSpellsDot/derefStrideOk), exhaustive under noImplicitReturns like the walkers below.
403
+ *
404
+ * Key-order-independent for every EXPR field — a switch, not a stringify. NOT for the `cast`
405
+ * arm's TARGET TYPE, which is compared by serialization and so is order-sensitive: two `IrType`
406
+ * objects with the same fields written in a different order compare UNEQUAL. That is why the
407
+ * inline `IrType` literals elsewhere in the codebase are written in `T.int`/`T.ptr` key order —
408
+ * matching the constructors keeps them comparable against constructed types.
409
+ *
410
+ * `typeEquals` (ir/types.ts) is NOT the fix: it ignores `struct.size`, so it is strictly more
411
+ * permissive, and swapping it in here would let a CSE collapse two accesses whose struct stride
412
+ * differs. */
206
413
  export function exprEquals(a: Expr, b: Expr): boolean {
207
414
  if (a.k !== b.k) {
208
415
  return false;
@@ -224,7 +431,14 @@ export function exprEquals(a: Expr, b: Expr): boolean {
224
431
  }
225
432
  case 'cast': {
226
433
  const bb = b as typeof a;
227
- return JSON.stringify(a.to) === JSON.stringify(bb.to) && exprEquals(a.e, bb.e);
434
+ // `volatile` is part of the SPELLING, compared for the same reason `lead` and `dot` are: a
435
+ // CSE or dedup that treats these as equal keeps one node and drops the other, silently
436
+ // rewriting a volatile access as a plain one.
437
+ return (
438
+ JSON.stringify(a.to) === JSON.stringify(bb.to) &&
439
+ (a.volatile ?? false) === (bb.volatile ?? false) &&
440
+ exprEquals(a.e, bb.e)
441
+ );
228
442
  }
229
443
  case 'call': {
230
444
  const bb = b as typeof a;
@@ -234,13 +448,23 @@ export function exprEquals(a: Expr, b: Expr): boolean {
234
448
  const bb = b as typeof a;
235
449
  // `lead` is part of the ADDRESS (`g[0][i]` and `g[1][i]` are different elements), so it
236
450
  // must be compared — an omission here would let CSE/dedup collapse two distinct accesses.
451
+ // The EVIDENCE fields (`operandOff`, `baseOrdered`, `baseAdvanced`) deliberately are NOT:
452
+ // two accesses agreeing on everything else denote the same cell and print the same subscript
453
+ // however the machine spelled the offset, so a CSE that collapses them respells nothing.
237
454
  const lead = a.lead ?? [];
238
455
  const bLead = bb.lead ?? [];
239
456
  return (
240
457
  a.width === bb.width &&
241
458
  a.signed === bb.signed &&
242
459
  lead.length === bLead.length &&
243
- lead.every((v, i) => v === bLead[i]) &&
460
+ lead.every((v, i) => exprEquals(v, bLead[i])) &&
461
+ // `baseElem` is part of the SPELLING for the same reason `lead` is: it decides whether the
462
+ // base takes the reinterpret cast, so two otherwise-equal nodes disagreeing about it print
463
+ // two different expressions. (Its one producer derives it from the base and the width, so
464
+ // nodes that agree on those agree here too — this cannot reject a real CSE.)
465
+ (a.baseElem === undefined
466
+ ? bb.baseElem === undefined
467
+ : bb.baseElem !== undefined && typeEquals(a.baseElem, bb.baseElem)) &&
244
468
  exprEquals(a.base, bb.base) &&
245
469
  exprEquals(a.idx, bb.idx)
246
470
  );
@@ -248,7 +472,7 @@ export function exprEquals(a: Expr, b: Expr): boolean {
248
472
  case 'field': {
249
473
  const bb = b as typeof a;
250
474
  // `dot` is part of the SPELLING, and for the same reason `lead` is compared above: a CSE or
251
- // dedup that treats these as equal keeps one node and discards the other, silently respelling
475
+ // dedup that treats these as equal keeps one node and discards the other, silently rewriting
252
476
  // `p->field_4` as `p.field_4` (or the reverse). Both compile only for the base type each
253
477
  // belongs to, so collapsing them is how a valid access becomes an invalid one — or worse, a
254
478
  // valid one against a different object.
@@ -263,14 +487,6 @@ export function exprEquals(a: Expr, b: Expr): boolean {
263
487
  }
264
488
  }
265
489
 
266
- // ── the ONE traversal vocabulary ───────────────────────────────────────────────────────────────
267
- // Every generic walker derives from these helpers, so a NEW node kind is a compile error in
268
- // exactly one place per union (the switches are exhaustive under noImplicitReturns) — a
269
- // hand-rolled walker that misses a node kind is a silent bug. Specialized walkers with per-kind
270
- // SEMANTICS (loop-boundary scans like hasEnclosingContinue, rebuilding transforms like
271
- // recognizeForLoops) rightly keep their own switches.
272
-
273
- /** The direct sub-expressions of `e`, in syntactic order. */
274
490
  /** THE spelling of an unmodelled instruction's gap reason, in one place: `structure.ts` writes it
275
491
  * into the marker, `contracts.ts` matches on it to prove the gap was not dropped, and the benchmark
276
492
  * classifies declines by it. Two spellings make that contract silently vacuous — enforced-looking
@@ -279,6 +495,14 @@ export function gapReasonFor(mnemonic: unknown): string {
279
495
  return `unmodelled instruction '${typeof mnemonic === 'string' ? mnemonic : '?'}'`;
280
496
  }
281
497
 
498
+ // ── the ONE traversal vocabulary ───────────────────────────────────────────────────────────────
499
+ // Every generic walker derives from these helpers, so a NEW node kind is a compile error in
500
+ // exactly one place per union (the switches are exhaustive under noImplicitReturns) — a
501
+ // hand-rolled walker that misses a node kind is a silent bug. Specialized walkers with per-kind
502
+ // SEMANTICS (loop-boundary scans like hasEnclosingContinue, rebuilding transforms like
503
+ // recognizeForLoops) rightly keep their own switches.
504
+
505
+ /** The direct sub-expressions of `e`, in syntactic order. */
282
506
  export function exprChildren(e: Expr): Expr[] {
283
507
  switch (e.k) {
284
508
  case 'var':
@@ -293,7 +517,7 @@ export function exprChildren(e: Expr): Expr[] {
293
517
  case 'call':
294
518
  return e.args;
295
519
  case 'index':
296
- return [e.base, e.idx];
520
+ return [e.base, ...(e.lead ?? []), e.idx];
297
521
  case 'field':
298
522
  return [e.base];
299
523
  case 'marker':
@@ -316,7 +540,7 @@ export function mapExprChildren(e: Expr, f: (c: Expr) => Expr): Expr {
316
540
  case 'call':
317
541
  return { ...e, args: e.args.map(f) };
318
542
  case 'index':
319
- return { ...e, base: f(e.base), idx: f(e.idx) };
543
+ return { ...e, base: f(e.base), ...(e.lead ? { lead: e.lead.map(f) } : {}), idx: f(e.idx) };
320
544
  case 'field':
321
545
  return { ...e, base: f(e.base) };
322
546
  case 'marker':
@@ -349,7 +573,132 @@ export function stmtExprs(s: Stmt): Expr[] {
349
573
  }
350
574
  }
351
575
 
352
- /** The statements a statement DIRECTLY contains. NOTE for document-order walks: a `for`'s
576
+ /** Rebuild `s` with EVERY expression position mapped through `f` store lvalues and loop/switch
577
+ * heads included, nested statements recursively. The rewrite dual of stmtExprs/stmtChildren, for
578
+ * the PURE 1:1 case: one expression in, one expression out, every statement kept.
579
+ *
580
+ * A PASS THAT HAND-ROLLS ITS OWN MAPPER IS NOT A MISSED MIGRATION. Several do, because their
581
+ * contract is not this one — a rewrite that may DECLINE, one that INSERTS statements, one that
582
+ * rewrites assign TARGETS as well as expressions, one that turns a statement into a list. Check
583
+ * the contract before pointing one of them here. */
584
+ export function mapStmtExprs(s: Stmt, f: (e: Expr) => Expr): Stmt {
585
+ const mapS = (x: Stmt): Stmt => mapStmtExprs(x, f);
586
+ switch (s.k) {
587
+ case 'assign':
588
+ return { ...s, value: f(s.value) };
589
+ case 'store':
590
+ return { ...s, lval: f(s.lval), value: f(s.value) };
591
+ case 'exprstmt':
592
+ return { ...s, value: f(s.value) };
593
+ case 'return':
594
+ return s.value ? { ...s, value: f(s.value) } : s;
595
+ case 'if':
596
+ return { ...s, cond: f(s.cond), then: s.then.map(mapS), else: s.else.map(mapS) };
597
+ case 'while':
598
+ case 'dowhile':
599
+ return { ...s, cond: f(s.cond), body: s.body.map(mapS) };
600
+ case 'for':
601
+ return { ...s, init: mapS(s.init), cond: f(s.cond), inc: mapS(s.inc), body: s.body.map(mapS) };
602
+ case 'switch':
603
+ return {
604
+ ...s,
605
+ scrutinee: f(s.scrutinee),
606
+ cases: s.cases.map((c) => ({ ...c, body: c.body.map(mapS) })),
607
+ default: s.default?.map(mapS),
608
+ };
609
+ case 'break':
610
+ case 'continue':
611
+ return s;
612
+ }
613
+ }
614
+
615
+ /** An address the target can REMATERIALIZE: a constant expression, reading no variable and no
616
+ * memory. Which ENCODING the compiler picked for it is not a property of the source — agbcc
617
+ * spells a pool word `(s32 *)33569456` but a shift-encodable one `(s32 *)(128 << 18)`, and every
618
+ * GBA hardware region (EWRAM 0x2000000, I/O 0x4000000, VRAM 0x6000000 …) takes the second form —
619
+ * so both must reach the same admission or the whole MMIO/VRAM fill family declines on its
620
+ * address. A bare `(T *)0` is excluded — a null base is not a walk — but the test is on the
621
+ * LITERALS the expression mentions, not on the value they fold to, so `(T *)(5 - 5)` passes.
622
+ * Folding would need a constant evaluator no consumer has another use for, and both consumers
623
+ * keep the expression verbatim, so no decision downstream reads the value.
624
+ *
625
+ * Two ask it: the walk re-index (l3/reindex.ts) about a walk base, and the `volatile` qualifier
626
+ * (l3/volatileptr.ts) about what feeds a pointer local. They must agree — a MMIO fill whose base
627
+ * one admits and the other refuses can be re-indexed but never qualified, so the paired
628
+ * `/indexed/volatile` spelling is unreachable at exactly the hardware addresses it is for.
629
+ *
630
+ * Two respell variations reading the same initializers are deliberately NOT here: l3/inlinebase.ts
631
+ * substitutes the address at each use, l3/nearbase.ts clusters neighbours by distance, and both
632
+ * need the VALUE, which is the evaluator above. Declining a shift-encoded base there costs a
633
+ * variation that does not fire, and the population is small: over klonoa's 531 lifting functions,
634
+ * one inlinebase-shaped local with no symbol map and none with it; a folded nearbase would form
635
+ * a new cluster in 4 functions mapless and 1 with the map. Both counts were zero over the agbcc
636
+ * benchmark rows that lifted when the variations landed — a MEASUREMENT over a corpus that grows, so
637
+ * re-take it rather than quoting it: a new row falsifies the number, not the argument. */
638
+ export function rematerializableAddress(e: Expr): boolean {
639
+ let nonZero = false;
640
+ let ok = true;
641
+ const visit = (x: Expr): void => {
642
+ switch (x.k) {
643
+ case 'const':
644
+ nonZero ||= x.value !== 0;
645
+ break;
646
+ case 'cast':
647
+ case 'bin':
648
+ case 'un':
649
+ break;
650
+ default:
651
+ ok = false; // var, addr, index, field, call, marker
652
+ return;
653
+ }
654
+ for (const c of exprChildren(x)) {
655
+ visit(c);
656
+ }
657
+ };
658
+ visit(e);
659
+ return ok && nonZero;
660
+ }
661
+
662
+ /** Whether the tree contains a node with an EFFECT no re-ordering may move: a call, or a marker
663
+ * standing in for an unmodelled instruction (annotate mode). */
664
+ export function exprHasEffect(e: Expr): boolean {
665
+ return e.k === 'call' || e.k === 'marker' || exprChildren(e).some(exprHasEffect);
666
+ }
667
+
668
+ /** Whether the tree performs an access to a `volatile` object — the OTHER thing no re-ordering may
669
+ * move, and the one `exprHasEffect` deliberately does not answer: that one is about a call, this
670
+ * about a QUALIFIER, and a pass that asks the first where it means the second reorders device
671
+ * accesses while its gate reports clean.
672
+ *
673
+ * Three spellings assert one thing, and all three are here because a pass that knew only the cast
674
+ * would miss the two a later variation writes: a `volatile` cast (where a raw address carries it), a
675
+ * read through a pointer local declared to point at volatile data (l3/volatileptr.ts), and a read
676
+ * of a `volatile` local object (l3/volatileval.ts). A bare cast counts even with no deref under it
677
+ * — the qualifier is on the ACCESS the cast exists to spell, and every caller so far is asking
678
+ * whether it may move the expression rather than how many accesses it holds. */
679
+ export function exprReadsVolatile(e: Expr, sfn: SFn): boolean {
680
+ const pointee = new Set(sfn.locals.filter((l) => l.pointeeVolatile).map((l) => l.name));
681
+ const object = new Set(sfn.locals.filter((l) => l.volatile).map((l) => l.name));
682
+ const namesUnder = (x: Expr): string[] =>
683
+ [...subterms(x)].filter((y): y is Extract<Expr, { k: 'var' }> => y.k === 'var').map((y) => y.name);
684
+ return [...subterms(e)].some(
685
+ (x) =>
686
+ (x.k === 'cast' && x.volatile === true) ||
687
+ (x.k === 'var' && object.has(x.name)) ||
688
+ ((x.k === 'index' || x.k === 'field') && namesUnder(x).some((n) => pointee.has(n))),
689
+ );
690
+ }
691
+
692
+ /** every node of an expression tree, itself included */
693
+ function* subterms(e: Expr): Generator<Expr> {
694
+ yield e;
695
+ for (const c of exprChildren(e)) {
696
+ yield* subterms(c);
697
+ }
698
+ }
699
+
700
+ /** The statements a statement DIRECTLY contains, in the order a backend prints them — a `switch`
701
+ * splices its default in at `defaultAt` for that reason. NOTE for document-order walks: a `for`'s
353
702
  * init/inc are listed here while its cond is in stmtExprs — a walker visiting exprs-then-stmts
354
703
  * sees the cond before the init. */
355
704
  export function stmtChildren(s: Stmt): Stmt[] {
@@ -368,18 +717,117 @@ export function stmtChildren(s: Stmt): Stmt[] {
368
717
  return s.body;
369
718
  case 'for':
370
719
  return [s.init, s.inc, ...s.body];
720
+ case 'switch': {
721
+ const arms = s.cases.map((c) => c.body);
722
+ arms.splice(s.defaultAt ?? s.cases.length, 0, s.default ?? []);
723
+ return arms.flat();
724
+ }
725
+ }
726
+ }
727
+
728
+ /** The nested statement LISTS of a statement — the SCOPES it opens.
729
+ *
730
+ * Deliberately not `stmtChildren`, which flattens a `for`'s `init`/`inc` in with its body: those
731
+ * are single statements, not lists. A `for`'s body is the only list here, which is what a caller
732
+ * that PLACES into a list wants (`l3/scopebase.ts` — before the loop changes when a statement
733
+ * runs, inside the body repeats it) and not what a caller that reads the init as a DEF wants
734
+ * (`contracts.ts`'s dominance walk, which descends into the loop's parts itself).
735
+ *
736
+ * It lives here so a new `Stmt` kind carrying a list is one compile error rather than a silent
737
+ * miss in each caller's own recursion. Exhaustive on purpose: no `default`.
738
+ */
739
+ export function stmtLists(s: Stmt): Stmt[][] {
740
+ switch (s.k) {
741
+ case 'if':
742
+ return [s.then, s.else];
743
+ case 'while':
744
+ case 'dowhile':
745
+ case 'for':
746
+ return [s.body];
747
+ case 'switch':
748
+ return [...s.cases.map((c) => c.body), ...(s.default ? [s.default] : [])];
749
+ case 'assign':
750
+ case 'store':
751
+ case 'exprstmt':
752
+ case 'return':
753
+ case 'break':
754
+ case 'continue':
755
+ return [];
756
+ }
757
+ }
758
+
759
+ /** The SETTER half of {@link stmtLists}: `s` with each of its nested lists replaced by `f` applied
760
+ * to it, in the same order `stmtLists` yields them. A caller that rebuilds a tree around one
761
+ * nested list therefore writes no `Stmt`-kind switch of its own — the pair lives here so a new
762
+ * kind carrying a list is a compile error in both halves rather than a silent miss in a caller's
763
+ * hand-rolled recursion. Exhaustive on purpose: no `default`. */
764
+ export function mapStmtLists(s: Stmt, f: (list: Stmt[]) => Stmt[]): Stmt {
765
+ switch (s.k) {
766
+ case 'if':
767
+ return { ...s, then: f(s.then), else: f(s.else) };
768
+ case 'while':
769
+ case 'dowhile':
770
+ case 'for':
771
+ return { ...s, body: f(s.body) };
371
772
  case 'switch':
372
- return [...s.cases.flatMap((c) => c.body), ...(s.default ?? [])];
773
+ return {
774
+ ...s,
775
+ cases: s.cases.map((c) => ({ ...c, body: f(c.body) })),
776
+ ...(s.default ? { default: f(s.default) } : {}),
777
+ };
778
+ case 'assign':
779
+ case 'store':
780
+ case 'exprstmt':
781
+ case 'return':
782
+ case 'break':
783
+ case 'continue':
784
+ return s;
785
+ }
786
+ }
787
+
788
+ /** Every expression node in a body, statements nested and children included — the whole-tree walk
789
+ * `stmtChildren`, `stmtExprs` and `exprChildren` compose into, kept here so a new node kind is a
790
+ * compile error in one of them rather than a silent miss in each caller's own recursion.
791
+ *
792
+ * An EXPLICIT stack, not `yield*` recursion. A delegated generator costs a frame per nesting
793
+ * level on every value it forwards, so an expression ten levels down was handed off ten times; this
794
+ * walk runs over a whole function body once per emitted candidate. The stack
795
+ * holds either a statement list still to expand or an expression still to visit — `Expr` is
796
+ * never an array, so the two are told apart without a tag — and both are pushed in reverse so
797
+ * they pop in document order, which is the order the recursion produced. */
798
+ export function* walkExprs(body: Stmt[]): Generator<Expr> {
799
+ const stack: (Expr | Stmt[])[] = [body];
800
+ while (stack.length > 0) {
801
+ const top = stack.pop()!;
802
+ if (Array.isArray(top)) {
803
+ for (let i = top.length - 1; i >= 0; i--) {
804
+ const s = top[i];
805
+ const children = stmtChildren(s);
806
+ if (children.length > 0) {
807
+ stack.push(children);
808
+ }
809
+ const exprs = stmtExprs(s);
810
+ for (let j = exprs.length - 1; j >= 0; j--) {
811
+ stack.push(exprs[j]);
812
+ }
813
+ }
814
+ continue;
815
+ }
816
+ yield top;
817
+ const children = exprChildren(top);
818
+ for (let i = children.length - 1; i >= 0; i--) {
819
+ stack.push(children[i]);
820
+ }
373
821
  }
374
822
  }
375
823
 
376
824
  // THE negation of a CONDITION — the one implementation, shared by every L3 pass that flips one.
377
825
  //
378
- // There were two, and they drifted: structure.ts's empty-then peephole learned to distribute over
379
- // the short-circuit connectives while l3/dce.ts's copy kept wrapping in `!`, and because
380
- // `eliminateDeadStores` runs AFTER structuring it re-introduced the very spelling the other one had
381
- // just removed. That is the l3/hoist.ts failure mode verbatim a copied helper silently losing the
382
- // newer rule so this lives with the AST vocabulary and the passes call it.
826
+ // WHY IT IS SHARED RATHER THAN COPIED PER PASS: the passes that flip a condition do not run at one
827
+ // time. The structurer's empty-then peephole flips one, and `eliminateDeadStores` flips another
828
+ // AFTER structuring so a copy that lacked a rule the other had would re-introduce the exact
829
+ // spelling the earlier pass just removed, and nothing downstream could tell that apart from a
830
+ // spelling the input really had. The negation therefore lives with the AST vocabulary.
383
831
  //
384
832
  // Three rules, in order:
385
833
  // 1. a relational operator flips directly (`!=` → `==`, `<` → `>=`, …), exact over C's total
@@ -389,7 +837,7 @@ export function stmtChildren(s: Stmt): Stmt[] {
389
837
  // holds. Same operands, same inputs — which is what makes it safe over a `b` that loads. It
390
838
  // matters because a source `&&` and its dual `||` compile to the SAME branch graph, so the
391
839
  // recognizers in raise/shortcircuit.ts can only pick whichever the asm's branch senses spell;
392
- // distributing is what lets the `/flip-branch` candidate reach the other one;
840
+ // distributing is what lets the other one be spelled at all;
393
841
  // 3. `!!x` collapses to `x`, reachable only from a double flip that rule 2 now produces.
394
842
  //
395
843
  // CONTEXT REQUIREMENT, and it is the reason this is `negateCond` and not `negate`: rule 3 is valid
@@ -397,13 +845,16 @@ export function stmtChildren(s: Stmt): Stmt[] {
397
845
  // so this must never be used to negate a general integer expression — only an `if`/loop test or an
398
846
  // operand of one of the connectives above.
399
847
  //
400
- // SCOPE of rule 2: it only gives the differ a second spelling where a candidate lever already flips
401
- // the condition, and `preserveDivergentBranchSense` covers divergent `if`s ONLY. A connective that
402
- // ended up as a LOOP test therefore has no dual candidate at all the differ never sees the other
403
- // form, so on such a row this rule changes how the code READS and nothing else. Widening the
404
- // branch-sense lever to loop tests is what would make it a matching lever there, and that is a
405
- // separate change.
406
- const NEGATE_REL: Partial<Record<BinOp, BinOp>> = {
848
+ // SCOPE of rule 2: it fires wherever a branch-sense variation negates the condition, which is both `if`
849
+ // classes `preserveDivergentBranchSense` on divergent ifs, `negateJoinedBranchSense` on
850
+ // reconverging ones and on the joined class it is a DEFAULT emission, not a differ-only
851
+ // alternative, so a source `&&` can come out as its `||` dual with no variation asked for
852
+ // (`synthetic:ifand_far`, where the branch range put the fold on the other arm). Neither
853
+ // variation reaches a LOOP test, so a connective that ended up as one has no dual candidate at all —
854
+ // the differ never sees the other form, and on such a row this rule changes how the code READS and
855
+ // nothing else. Widening a branch-sense variation to loop tests is what would make it a matching variation
856
+ // there, and that is a separate change.
857
+ export const NEGATE_REL: Partial<Record<BinOp, BinOp>> = {
407
858
  '<': '>=',
408
859
  '>=': '<',
409
860
  '>': '<=',