@asmlift/core 0.5.0 → 0.6.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 (86) 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 +238 -167
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +194 -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 +54 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +337 -2
  16. package/src/ir/opcodes.ts +140 -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/argbase.ts +2 -1
  24. package/src/l3/ast.ts +464 -57
  25. package/src/l3/basecse.ts +664 -76
  26. package/src/l3/coalesce.ts +429 -43
  27. package/src/l3/dce.ts +31 -9
  28. package/src/l3/gates.ts +21 -0
  29. package/src/l3/hoist.ts +293 -14
  30. package/src/l3/homesplit.ts +285 -0
  31. package/src/l3/initfirst.ts +301 -0
  32. package/src/l3/inlinebase.ts +193 -0
  33. package/src/l3/mentions.ts +113 -0
  34. package/src/l3/mulfirst.ts +42 -0
  35. package/src/l3/nearbase.ts +152 -0
  36. package/src/l3/offmember.ts +371 -0
  37. package/src/l3/parkfirst.ts +96 -0
  38. package/src/l3/pollguard.ts +154 -0
  39. package/src/l3/ptrfield.ts +227 -0
  40. package/src/l3/regspell.ts +110 -85
  41. package/src/l3/reindex.ts +715 -78
  42. package/src/l3/scopebase.ts +644 -218
  43. package/src/l3/sinkinit.ts +40 -0
  44. package/src/l3/slotorder.ts +123 -0
  45. package/src/l3/storage.ts +48 -0
  46. package/src/l3/symbol-refs.ts +41 -8
  47. package/src/l3/tailmerge.ts +15 -0
  48. package/src/l3/typing.ts +198 -9
  49. package/src/l3/unmerge.ts +263 -0
  50. package/src/l3/unreduce.ts +971 -0
  51. package/src/l3/volatileptr.ts +207 -0
  52. package/src/l3/volatileval.ts +130 -0
  53. package/src/l3/volstore.ts +229 -0
  54. package/src/l3/zerosub.ts +62 -0
  55. package/src/pattern/engine.ts +236 -13
  56. package/src/pipeline.ts +157 -56
  57. package/src/proto.ts +112 -14
  58. package/src/raise/arrays.ts +6 -1
  59. package/src/raise/divpow2.ts +2 -2
  60. package/src/raise/globalshape.ts +1038 -0
  61. package/src/raise/gvn.ts +33 -18
  62. package/src/raise/latch.ts +126 -0
  63. package/src/raise/memberarrays.ts +594 -0
  64. package/src/raise/narrow.ts +124 -0
  65. package/src/raise/narrowlocal.ts +556 -0
  66. package/src/raise/paramwidth.ts +179 -0
  67. package/src/raise/pre-recovery.ts +97 -14
  68. package/src/raise/recover.ts +56 -23
  69. package/src/raise/retsink.ts +210 -10
  70. package/src/raise/shortcircuit.ts +474 -74
  71. package/src/raise/struct-arrays.ts +19 -2
  72. package/src/raise/structs.ts +33 -3
  73. package/src/rank-axes.ts +630 -0
  74. package/src/rank-declare.ts +256 -0
  75. package/src/rank.ts +1723 -272
  76. package/src/structure/analysis.ts +1392 -141
  77. package/src/structure/bitfields.ts +332 -0
  78. package/src/structure/globalaccess.ts +274 -0
  79. package/src/structure/hazards.ts +411 -20
  80. package/src/structure/loops.ts +2 -49
  81. package/src/structure/namecoalesce.ts +435 -0
  82. package/src/structure/structure.ts +2678 -526
  83. package/src/structure/switch-recover.ts +616 -144
  84. package/src/symbols.ts +62 -1
  85. package/src/target.ts +367 -24
  86. package/src/trace.ts +111 -32
package/src/l3/ast.ts CHANGED
@@ -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,68 @@ 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 axis 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
+ k: 'index';
101
+ base: Expr;
102
+ idx: Expr;
103
+ width: number;
104
+ signed: boolean;
105
+ lead?: Expr[];
106
+ operandOff?: number;
107
+ baseOrdered?: true;
108
+ }
48
109
  // A named struct-field access `base->name` (raise/structs.ts recovered `base` as a struct
49
110
  // pointer, so the byte offset resolves to a named field instead of a scaled array index).
50
111
  // Unlike `index`, this carries the field NAME (which encodes the byte offset, `field_<off>`),
@@ -59,31 +120,41 @@ export type Expr =
59
120
  // default) never produces this node; it keeps the `"?"` sentinel → ContractError behavior.
60
121
  | { k: 'marker'; reason: string; args: Expr[] };
61
122
 
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.
123
+ // THE SIGNEDNESS-CARRYING PAIRS. `>>` is the ARITHMETIC right shift and `>>>` the LOGICAL one;
124
+ // `/`/`%` are the SIGNED quotient and remainder and `/u`/`%u` the unsigned ones. C spells each pair
125
+ // with one token and picks between them from the operand types, so the C backend synthesizes the
126
+ // cast that pins the choice exactly as it already synthesizes scalar deref casts from an `index`
127
+ // node's width. A backend with no spelling for one of them (IDO Pascal) declines LOUDLY on the
128
+ // operation itself, rather than on whatever artifact another language's spelling happened to leave
129
+ // in the tree.
130
+ //
131
+ // WHY THESE SPLITS AND NOT THE OTHERS. "The machine distinguishes them" is NOT the rule — the
132
+ // machine distinguishes `sltu`/`slt` too, and CMP_TO_BIN deliberately collapses `icmp_u*`→`<` etc.,
133
+ // noting that "unsignedness is in the operand types". Taking the machine as the rule would license
134
+ // more splits with no inhabitant, which is what "earn the level" forbids. The rule is the repo's
135
+ // own: a split is earned by a real, byte-load-bearing divergence WITH inhabitants that no other
136
+ // channel can carry. The shifts earned it first (~20 rows, 5 projects, 4 compilers) because the
137
+ // operand type could not carry them — a promoted narrow value is signed whatever it was loaded as.
67
138
  //
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.
139
+ // The divides earned it second, on pokeemerald:GetAnchorCoord `(u32)(coord * a1) / (u32)a0`
140
+ // standing beside two arithmetic shifts of the same values. Their only other channel is the operand
141
+ // TYPES, reached by flipping a declaration, and there that flip is unreachable and unsound at
142
+ // once: the divisor also feeds a signed compare, so the /uns-cmp reconciliation correctly refuses
143
+ // it, and forcing it anyway makes agbcc delete the comparison as always-false. A per-operand pin is
144
+ // the only spelling that says "this division alone is unsigned".
76
145
  //
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.
146
+ // The COMPARISONS stay collapsed, and that asymmetry is the rule applying rather than an omission:
147
+ // which side a compare was spelled from genuinely underdetermines a signed spelling that
148
+ // byte-matched was proved non-negative by the compiler so it is refereed as an axis, while a
149
+ // division helper is a pure function of the expression's C type with no such proof available.
81
150
  export type BinOp =
82
151
  | '+'
83
152
  | '-'
84
153
  | '*'
85
154
  | '/'
155
+ | '/u'
86
156
  | '%'
157
+ | '%u'
87
158
  | '<'
88
159
  | '<='
89
160
  | '>'
@@ -130,7 +201,20 @@ export type Stmt =
130
201
  | { k: 'continue' }
131
202
  // A multi-way `switch` over an integer scrutinee (recovered from a comparison tree — Regime A — or
132
203
  // a jump-table `switch_br` — Regime B). `cases` are emitted IN ARRAY ORDER; `default` (if present)
133
- // is emitted last.
204
+ // is emitted after `defaultAt` of them, or after all of them when that is absent.
205
+ //
206
+ // `defaultAt` exists because C lets `default:` sit BETWEEN case labels and a compiler that lays
207
+ // case bodies out in source order shows where the source put it. It is a COUNT of preceding arms,
208
+ // not an index into an array a later pass may rebuild, and a count past the arms is a producer bug
209
+ // a backend refuses. Setting it is legal only when the arm before the label does not fall through:
210
+ // moving the label in front of a falling arm would divert that arm into the default. The C-family
211
+ // printer terminates a non-final default with `break;` for the mirror-image reason.
212
+ //
213
+ // Unlike `fallsThrough` below, `defaultAt` is a SPELLING: the arm BEFORE the label is closed (the
214
+ // rule above) and so is the default body itself (the C-family printer terminates a non-final one),
215
+ // so a backend with no positional default (Pascal's `otherwise`) may ignore the count and still
216
+ // emit the same program. The arm AFTER the label is under no such rule and MAY fall through — it
217
+ // falls into the arm below it, which the label does not stand between.
134
218
  //
135
219
  // NON-NEUTRALITY NOTE (like the `index` node above): `fallsThrough` encodes a C/C++ control-flow
136
220
  // concept POSITIONALLY — `cases[i].fallsThrough === true` means control continues into
@@ -139,7 +223,12 @@ export type Stmt =
139
223
  // Pascal backend MUST loud-fail a `fallsThrough` case (it has no faithful spelling), exactly as it
140
224
  // loud-fails `field`/`cast`. Recovery must therefore only set `fallsThrough` when the fall-through
141
225
  // target is the emission-adjacent case.
142
- | { k: 'switch'; scrutinee: Expr; cases: SwitchCase[]; default?: Stmt[] }
226
+ //
227
+ // Recovery COMPUTES this flag rather than spelling it, so a source grep for `fallsThrough: true`
228
+ // finds hand-written fixtures and nothing else, whatever the corpus does — count its inhabitants
229
+ // by instrumenting the printer. Both regimes produce them: the jump table spells `case 4:` of
230
+ // `kleod:UpdateWorldMapNodeAnim`, the comparison tree `synthetic:sw_fallmem:agbcc`.
231
+ | { k: 'switch'; scrutinee: Expr; cases: SwitchCase[]; default?: Stmt[]; defaultAt?: number }
143
232
  | { k: 'return'; value?: Expr };
144
233
 
145
234
  /** One arm of a `switch`. `values` stacks multiple `case K:` labels onto one body (`case 1: case 2:`).
@@ -153,7 +242,59 @@ export interface SwitchCase {
153
242
  export interface SFn {
154
243
  name: string;
155
244
  params: { name: string; type: IrType }[];
156
- locals: { name: string; type: IrType; volatile?: true }[]; // recovered locals, declared at function top
245
+ /** Recovered locals, declared at function top. Two INDEPENDENT volatility facts, mirroring
246
+ * symbols.ts's cell-vs-pointee split: `volatile` = the local OBJECT is volatile (the
247
+ * address-escaped frame scratch; dce.ts treats reads of it as observable), `pointeeVolatile`
248
+ * = the local is a pointer TO volatile data (the l3/volatileptr.ts lever; a declaration
249
+ * spelling only — nothing about the local itself is observable).
250
+ *
251
+ * `frame` is present on a local the structurer recovered from an `laddr` — the asm
252
+ * MATERIALIZED the slot's address into a register, so the object provably lives in memory —
253
+ * and carries the machine's static access counts for it. Under Thumb that envelope is a
254
+ * SUB-WORD frame object: `strh/ldrh/strb/ldrb` have no `[sp,#imm]` form, so a compiler must
255
+ * copy `sp` first, while a word spill goes straight to `[sp,#imm]` and is recovered as an
256
+ * SSA value with no local of its own. So `frame` is NOT the set of every value the machine
257
+ * slotted. `loads`/`stores` are the yardstick a qualifier lever must match before it may
258
+ * declare every access to the object observable: the readability passes between here and L3
259
+ * may drop a store or render one machine load as two reads, and `volatile` over an access
260
+ * set asmlift did not preserve is a source that contradicts itself. ABSENT where the counts
261
+ * would be a floor rather than the set: an address reaching anything but a direct load/store
262
+ * leaves accesses the count cannot see.
263
+ *
264
+ * ORDER MATTERS, and it means two different things at two different times. As the structurer
265
+ * builds it and as every L3 pass sees it, this is the RECOVERED DECLARATION ORDER — the naming
266
+ * walk's order — and passes reason about it as such: `l3/coalesce.ts` picks the arm-disjoint
267
+ * survivor by position in THIS list, so the earlier declaration wins the way a shared source
268
+ * local reads. The EMITTED order is this list re-sorted by `l3/slotorder.ts` inside `emit`,
269
+ * which happens after every pass and returns a copy. A pass that sorted the list any earlier
270
+ * would silently change which local survives every arm-disjoint merge.
271
+ *
272
+ * ONE COMMENT, DELIBERATELY. Five sites tell a reader to consult "the SFn.locals doc"
273
+ * (structure/structure.ts, l3/inlinebase.ts, l3/volatileval.ts, l3/reindex.ts, l3/unreduce.ts),
274
+ * and TypeScript attaches only the LAST doc block before a declaration — so a second block
275
+ * added in front of this one would silently orphan everything above and those five references
276
+ * would point at half a paragraph. Append here; do not add a neighbour. */
277
+ locals: {
278
+ name: string;
279
+ type: IrType;
280
+ volatile?: true;
281
+ pointeeVolatile?: true;
282
+ frame?: { loads: number; stores: number };
283
+ /** the local stands on an `undef` — storage the asm reads without ever writing it, where the
284
+ * MISSING assignment is the recovery. Marked because a local read and never assigned is
285
+ * otherwise a dropped statement (contracts.ts assertLocalsWritten). */
286
+ uninit?: true;
287
+ /** every `[sp,#k]` the machine homed this local at, when the asm spilled it — ascending, and
288
+ * usually one. Several when the naming walk put several spilled values under this one name,
289
+ * or a coalesce absorbed a second homed local into it; the list is the UNION and picks
290
+ * nothing, because which offset is the earlier DECLARATION RANK depends on the frame's
291
+ * direction and only `l3/slotorder.ts` holds it (ir/core.ts `SlotHomes`).
292
+ *
293
+ * Present ONLY on a local recovered from word-spill VALUES. A `frame` local and a `uninit`
294
+ * one are deliberately left unstamped even where the offset is in hand — see the refusal at
295
+ * the structurer's build site for the measurement that decided it. */
296
+ slots?: number[];
297
+ }[];
157
298
  /** project globals referenced with a known declaration shape (symbol map) — typed for the
158
299
  * legalization env (exprCType) but NEVER declared by a backend: the project's own headers
159
300
  * declare them, exactly like every other global name asmlift emits. */
@@ -163,6 +304,18 @@ export interface SFn {
163
304
  /** Struct types this function's fields reference, declared above it by the backend. Empty
164
305
  * unless raise/structs.ts recovered a struct. Sorted by name for deterministic output. */
165
306
  structs?: StructType[];
307
+ /** Which way this compiler hands out frame slots against DECLARATION RANK, when it is known:
308
+ *
309
+ * THE ONE COMPILER DATUM ON THE NEUTRAL TREE, and it is here rather than in a backend on
310
+ * purpose: `LanguageBackend.emit(fn: SFn): string` is the only seam a backend has, and widening
311
+ * it to `emit(fn, opts)` would hand the datum back to the seven `.emit(` CALL SITES this design
312
+ * exists to keep it out of. So the tree is neutral in its NODES — every node still spells the
313
+ * same thing in C, C++ and Pascal — and not in its emission policy, which this field is.
314
+ * `ascending` = the earlier-declared spilled local takes the LOWER `[sp,#k]`. Set by the
315
+ * structurer from `StructureOptions.spillSlotOrder`, itself a per-compiler default declared in
316
+ * `TargetDescription.compilerBehaviors`. ABSENT means the direction is unknown for this target
317
+ * and `l3/slotorder.ts` is the identity — never "ascending by default". */
318
+ slotOrder?: 'ascending' | 'descending';
166
319
  }
167
320
 
168
321
  /** A struct declaration surfaced to the backend (name + field list). Mirrors the IR struct
@@ -177,6 +330,14 @@ export interface StructType {
177
330
  * comment spelling. */
178
331
  export interface LanguageBackend {
179
332
  readonly id: 'c' | 'cpp' | 'pascal';
333
+ /** Can this language spell a `switch` arm that RUNS ON into the next one (`fallsThrough`)?
334
+ * C and C++ can; Pascal's `case-of` cannot, and its backend loud-fails the node (see the
335
+ * non-neutrality note on `Stmt`'s `switch`). Declared here rather than inferred from `id`
336
+ * because it is what RECOVERY must ask: a comparison-tree switch has a second, behaviourally
337
+ * identical recovery (plain if-nesting), so minting a `fallsThrough` arm for a backend that
338
+ * cannot print it turns a whole function that used to decompile into a loud stub. The
339
+ * structurer reads it through `StructureOptions.spellSwitchFallthrough`. */
340
+ readonly spellsSwitchFallthrough: boolean;
180
341
  emit(fn: SFn): string;
181
342
  // Spell ONE LINE of text as a comment in this language (C block comments, Pascal `(* … *)`).
182
343
  // Used by the annotate-mode stub path to carry the failure reason + the original asm
@@ -201,8 +362,17 @@ export function fieldSpellsDot(f: Extract<Expr, { k: 'field' }>): boolean {
201
362
  }
202
363
 
203
364
  /** 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. */
365
+ * fieldSpellsDot/derefStrideOk), exhaustive under noImplicitReturns like the walkers below.
366
+ *
367
+ * Key-order-independent for every EXPR field — a switch, not a stringify. NOT for the `cast`
368
+ * arm's TARGET TYPE, which is compared by serialization and so is order-sensitive: two `IrType`
369
+ * objects with the same fields written in a different order compare UNEQUAL. That is why the
370
+ * inline `IrType` literals elsewhere in the codebase are written in `T.int`/`T.ptr` key order —
371
+ * matching the constructors keeps them comparable against constructed types.
372
+ *
373
+ * `typeEquals` (ir/types.ts) is NOT the fix: it ignores `struct.size`, so it is strictly more
374
+ * permissive, and swapping it in here would let a CSE collapse two accesses whose struct stride
375
+ * differs. */
206
376
  export function exprEquals(a: Expr, b: Expr): boolean {
207
377
  if (a.k !== b.k) {
208
378
  return false;
@@ -224,7 +394,14 @@ export function exprEquals(a: Expr, b: Expr): boolean {
224
394
  }
225
395
  case 'cast': {
226
396
  const bb = b as typeof a;
227
- return JSON.stringify(a.to) === JSON.stringify(bb.to) && exprEquals(a.e, bb.e);
397
+ // `volatile` is part of the SPELLING, compared for the same reason `lead` and `dot` are: a
398
+ // CSE or dedup that treats these as equal keeps one node and drops the other, silently
399
+ // respelling a volatile access as a plain one.
400
+ return (
401
+ JSON.stringify(a.to) === JSON.stringify(bb.to) &&
402
+ (a.volatile ?? false) === (bb.volatile ?? false) &&
403
+ exprEquals(a.e, bb.e)
404
+ );
228
405
  }
229
406
  case 'call': {
230
407
  const bb = b as typeof a;
@@ -234,13 +411,16 @@ export function exprEquals(a: Expr, b: Expr): boolean {
234
411
  const bb = b as typeof a;
235
412
  // `lead` is part of the ADDRESS (`g[0][i]` and `g[1][i]` are different elements), so it
236
413
  // must be compared — an omission here would let CSE/dedup collapse two distinct accesses.
414
+ // `operandOff` deliberately is NOT: two accesses agreeing on everything else denote the same
415
+ // cell and print the same subscript however the machine spelled the offset, so a CSE that
416
+ // collapses them respells nothing.
237
417
  const lead = a.lead ?? [];
238
418
  const bLead = bb.lead ?? [];
239
419
  return (
240
420
  a.width === bb.width &&
241
421
  a.signed === bb.signed &&
242
422
  lead.length === bLead.length &&
243
- lead.every((v, i) => v === bLead[i]) &&
423
+ lead.every((v, i) => exprEquals(v, bLead[i])) &&
244
424
  exprEquals(a.base, bb.base) &&
245
425
  exprEquals(a.idx, bb.idx)
246
426
  );
@@ -263,14 +443,6 @@ export function exprEquals(a: Expr, b: Expr): boolean {
263
443
  }
264
444
  }
265
445
 
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
446
  /** THE spelling of an unmodelled instruction's gap reason, in one place: `structure.ts` writes it
275
447
  * into the marker, `contracts.ts` matches on it to prove the gap was not dropped, and the benchmark
276
448
  * classifies declines by it. Two spellings make that contract silently vacuous — enforced-looking
@@ -279,6 +451,14 @@ export function gapReasonFor(mnemonic: unknown): string {
279
451
  return `unmodelled instruction '${typeof mnemonic === 'string' ? mnemonic : '?'}'`;
280
452
  }
281
453
 
454
+ // ── the ONE traversal vocabulary ───────────────────────────────────────────────────────────────
455
+ // Every generic walker derives from these helpers, so a NEW node kind is a compile error in
456
+ // exactly one place per union (the switches are exhaustive under noImplicitReturns) — a
457
+ // hand-rolled walker that misses a node kind is a silent bug. Specialized walkers with per-kind
458
+ // SEMANTICS (loop-boundary scans like hasEnclosingContinue, rebuilding transforms like
459
+ // recognizeForLoops) rightly keep their own switches.
460
+
461
+ /** The direct sub-expressions of `e`, in syntactic order. */
282
462
  export function exprChildren(e: Expr): Expr[] {
283
463
  switch (e.k) {
284
464
  case 'var':
@@ -293,7 +473,7 @@ export function exprChildren(e: Expr): Expr[] {
293
473
  case 'call':
294
474
  return e.args;
295
475
  case 'index':
296
- return [e.base, e.idx];
476
+ return [e.base, ...(e.lead ?? []), e.idx];
297
477
  case 'field':
298
478
  return [e.base];
299
479
  case 'marker':
@@ -316,7 +496,7 @@ export function mapExprChildren(e: Expr, f: (c: Expr) => Expr): Expr {
316
496
  case 'call':
317
497
  return { ...e, args: e.args.map(f) };
318
498
  case 'index':
319
- return { ...e, base: f(e.base), idx: f(e.idx) };
499
+ return { ...e, base: f(e.base), ...(e.lead ? { lead: e.lead.map(f) } : {}), idx: f(e.idx) };
320
500
  case 'field':
321
501
  return { ...e, base: f(e.base) };
322
502
  case 'marker':
@@ -349,7 +529,132 @@ export function stmtExprs(s: Stmt): Expr[] {
349
529
  }
350
530
  }
351
531
 
352
- /** The statements a statement DIRECTLY contains. NOTE for document-order walks: a `for`'s
532
+ /** Rebuild `s` with EVERY expression position mapped through `f` store lvalues and loop/switch
533
+ * heads included, nested statements recursively. The rewrite dual of stmtExprs/stmtChildren, for
534
+ * the PURE 1:1 case: one expression in, one expression out, every statement kept.
535
+ *
536
+ * A LEVER THAT HAND-ROLLS ITS OWN MAPPER IS NOT A MISSED MIGRATION. Several do, because their
537
+ * contract is not this one — a rewrite that may DECLINE, one that INSERTS statements, one that
538
+ * rewrites assign TARGETS as well as expressions, one that turns a statement into a list. Check
539
+ * the contract before pointing one of them here. */
540
+ export function mapStmtExprs(s: Stmt, f: (e: Expr) => Expr): Stmt {
541
+ const mapS = (x: Stmt): Stmt => mapStmtExprs(x, f);
542
+ switch (s.k) {
543
+ case 'assign':
544
+ return { ...s, value: f(s.value) };
545
+ case 'store':
546
+ return { ...s, lval: f(s.lval), value: f(s.value) };
547
+ case 'exprstmt':
548
+ return { ...s, value: f(s.value) };
549
+ case 'return':
550
+ return s.value ? { ...s, value: f(s.value) } : s;
551
+ case 'if':
552
+ return { ...s, cond: f(s.cond), then: s.then.map(mapS), else: s.else.map(mapS) };
553
+ case 'while':
554
+ case 'dowhile':
555
+ return { ...s, cond: f(s.cond), body: s.body.map(mapS) };
556
+ case 'for':
557
+ return { ...s, init: mapS(s.init), cond: f(s.cond), inc: mapS(s.inc), body: s.body.map(mapS) };
558
+ case 'switch':
559
+ return {
560
+ ...s,
561
+ scrutinee: f(s.scrutinee),
562
+ cases: s.cases.map((c) => ({ ...c, body: c.body.map(mapS) })),
563
+ default: s.default?.map(mapS),
564
+ };
565
+ case 'break':
566
+ case 'continue':
567
+ return s;
568
+ }
569
+ }
570
+
571
+ /** An address the target can REMATERIALIZE: a constant expression, reading no variable and no
572
+ * memory. Which ENCODING the compiler picked for it is not a property of the source — agbcc
573
+ * spells a pool word `(s32 *)33569456` but a shift-encodable one `(s32 *)(128 << 18)`, and every
574
+ * GBA hardware region (EWRAM 0x2000000, I/O 0x4000000, VRAM 0x6000000 …) takes the second form —
575
+ * so both must reach the same admission or the whole MMIO/VRAM fill family declines on its
576
+ * address. A bare `(T *)0` is excluded — a null base is not a walk — but the test is on the
577
+ * LITERALS the expression mentions, not on the value they fold to, so `(T *)(5 - 5)` passes.
578
+ * Folding would need a constant evaluator no consumer has another use for, and both consumers
579
+ * keep the expression verbatim, so no decision downstream reads the value.
580
+ *
581
+ * Two ask it: the walk re-index (l3/reindex.ts) about a walk base, and the `volatile` qualifier
582
+ * (l3/volatileptr.ts) about what feeds a pointer local. They must agree — a MMIO fill whose base
583
+ * one admits and the other refuses can be re-indexed but never qualified, so the paired
584
+ * `/indexed/volatile` spelling is unreachable at exactly the hardware addresses it is for.
585
+ *
586
+ * Two levers reading the same initializers are deliberately NOT here: l3/inlinebase.ts
587
+ * substitutes the address at each use, l3/nearbase.ts clusters neighbours by distance, and both
588
+ * need the VALUE, which is the evaluator above. Declining a shift-encoded base there costs a
589
+ * lever that does not fire, and the population is small: over klonoa's 531 lifting functions,
590
+ * one inlinebase-shaped local with no symbol map and none with it; a folded nearbase would form
591
+ * a new cluster in 4 functions mapless and 1 with the map. Both counts were zero over the agbcc
592
+ * benchmark rows that lifted when the levers landed — a MEASUREMENT over a corpus that grows, so
593
+ * re-take it rather than quoting it: a new row falsifies the number, not the argument. */
594
+ export function rematerializableAddress(e: Expr): boolean {
595
+ let nonZero = false;
596
+ let ok = true;
597
+ const visit = (x: Expr): void => {
598
+ switch (x.k) {
599
+ case 'const':
600
+ nonZero ||= x.value !== 0;
601
+ break;
602
+ case 'cast':
603
+ case 'bin':
604
+ case 'un':
605
+ break;
606
+ default:
607
+ ok = false; // var, addr, index, field, call, marker
608
+ return;
609
+ }
610
+ for (const c of exprChildren(x)) {
611
+ visit(c);
612
+ }
613
+ };
614
+ visit(e);
615
+ return ok && nonZero;
616
+ }
617
+
618
+ /** Whether the tree contains a node with an EFFECT no re-ordering may move: a call, or a marker
619
+ * standing in for an unmodelled instruction (annotate mode). */
620
+ export function exprHasEffect(e: Expr): boolean {
621
+ return e.k === 'call' || e.k === 'marker' || exprChildren(e).some(exprHasEffect);
622
+ }
623
+
624
+ /** Whether the tree performs an access to a `volatile` object — the OTHER thing no re-ordering may
625
+ * move, and the one `exprHasEffect` deliberately does not answer: that one is about a call, this
626
+ * about a QUALIFIER, and a pass that asks the first where it means the second reorders device
627
+ * accesses while its gate reports clean.
628
+ *
629
+ * Three spellings assert one thing, and all three are here because a lever that knew only the cast
630
+ * would miss the two a later lever writes: a `volatile` cast (where a raw address carries it), a
631
+ * read through a pointer local declared to point at volatile data (l3/volatileptr.ts), and a read
632
+ * of a `volatile` local object (l3/volatileval.ts). A bare cast counts even with no deref under it
633
+ * — the qualifier is on the ACCESS the cast exists to spell, and every caller so far is asking
634
+ * whether it may move the expression rather than how many accesses it holds. */
635
+ export function exprReadsVolatile(e: Expr, sfn: SFn): boolean {
636
+ const pointee = new Set(sfn.locals.filter((l) => l.pointeeVolatile).map((l) => l.name));
637
+ const object = new Set(sfn.locals.filter((l) => l.volatile).map((l) => l.name));
638
+ const namesUnder = (x: Expr): string[] =>
639
+ [...subterms(x)].filter((y): y is Extract<Expr, { k: 'var' }> => y.k === 'var').map((y) => y.name);
640
+ return [...subterms(e)].some(
641
+ (x) =>
642
+ (x.k === 'cast' && x.volatile === true) ||
643
+ (x.k === 'var' && object.has(x.name)) ||
644
+ ((x.k === 'index' || x.k === 'field') && namesUnder(x).some((n) => pointee.has(n))),
645
+ );
646
+ }
647
+
648
+ /** every node of an expression tree, itself included */
649
+ function* subterms(e: Expr): Generator<Expr> {
650
+ yield e;
651
+ for (const c of exprChildren(e)) {
652
+ yield* subterms(c);
653
+ }
654
+ }
655
+
656
+ /** The statements a statement DIRECTLY contains, in the order a backend prints them — a `switch`
657
+ * splices its default in at `defaultAt` for that reason. NOTE for document-order walks: a `for`'s
353
658
  * init/inc are listed here while its cond is in stmtExprs — a walker visiting exprs-then-stmts
354
659
  * sees the cond before the init. */
355
660
  export function stmtChildren(s: Stmt): Stmt[] {
@@ -368,18 +673,117 @@ export function stmtChildren(s: Stmt): Stmt[] {
368
673
  return s.body;
369
674
  case 'for':
370
675
  return [s.init, s.inc, ...s.body];
676
+ case 'switch': {
677
+ const arms = s.cases.map((c) => c.body);
678
+ arms.splice(s.defaultAt ?? s.cases.length, 0, s.default ?? []);
679
+ return arms.flat();
680
+ }
681
+ }
682
+ }
683
+
684
+ /** The nested statement LISTS of a statement — the SCOPES it opens.
685
+ *
686
+ * Deliberately not `stmtChildren`, which flattens a `for`'s `init`/`inc` in with its body: those
687
+ * are single statements, not lists. A `for`'s body is the only list here, which is what a caller
688
+ * that PLACES into a list wants (`l3/scopebase.ts` — before the loop changes when a statement
689
+ * runs, inside the body repeats it) and not what a caller that reads the init as a DEF wants
690
+ * (`contracts.ts`'s dominance walk, which descends into the loop's parts itself).
691
+ *
692
+ * It lives here so a new `Stmt` kind carrying a list is one compile error rather than a silent
693
+ * miss in each caller's own recursion. Exhaustive on purpose: no `default`.
694
+ */
695
+ export function stmtLists(s: Stmt): Stmt[][] {
696
+ switch (s.k) {
697
+ case 'if':
698
+ return [s.then, s.else];
699
+ case 'while':
700
+ case 'dowhile':
701
+ case 'for':
702
+ return [s.body];
703
+ case 'switch':
704
+ return [...s.cases.map((c) => c.body), ...(s.default ? [s.default] : [])];
705
+ case 'assign':
706
+ case 'store':
707
+ case 'exprstmt':
708
+ case 'return':
709
+ case 'break':
710
+ case 'continue':
711
+ return [];
712
+ }
713
+ }
714
+
715
+ /** The SETTER half of {@link stmtLists}: `s` with each of its nested lists replaced by `f` applied
716
+ * to it, in the same order `stmtLists` yields them. A caller that rebuilds a tree around one
717
+ * nested list therefore writes no `Stmt`-kind switch of its own — the pair lives here so a new
718
+ * kind carrying a list is a compile error in both halves rather than a silent miss in a caller's
719
+ * hand-rolled recursion. Exhaustive on purpose: no `default`. */
720
+ export function mapStmtLists(s: Stmt, f: (list: Stmt[]) => Stmt[]): Stmt {
721
+ switch (s.k) {
722
+ case 'if':
723
+ return { ...s, then: f(s.then), else: f(s.else) };
724
+ case 'while':
725
+ case 'dowhile':
726
+ case 'for':
727
+ return { ...s, body: f(s.body) };
371
728
  case 'switch':
372
- return [...s.cases.flatMap((c) => c.body), ...(s.default ?? [])];
729
+ return {
730
+ ...s,
731
+ cases: s.cases.map((c) => ({ ...c, body: f(c.body) })),
732
+ ...(s.default ? { default: f(s.default) } : {}),
733
+ };
734
+ case 'assign':
735
+ case 'store':
736
+ case 'exprstmt':
737
+ case 'return':
738
+ case 'break':
739
+ case 'continue':
740
+ return s;
741
+ }
742
+ }
743
+
744
+ /** Every expression node in a body, statements nested and children included — the whole-tree walk
745
+ * `stmtChildren`, `stmtExprs` and `exprChildren` compose into, kept here so a new node kind is a
746
+ * compile error in one of them rather than a silent miss in each caller's own recursion.
747
+ *
748
+ * An EXPLICIT stack, not `yield*` recursion. A delegated generator costs a frame per nesting
749
+ * level on every value it forwards, so an expression ten levels down was handed off ten times; this
750
+ * walk runs over a whole function body once per emitted candidate. The stack
751
+ * holds either a statement list still to expand or an expression still to visit — `Expr` is
752
+ * never an array, so the two are told apart without a tag — and both are pushed in reverse so
753
+ * they pop in document order, which is the order the recursion produced. */
754
+ export function* walkExprs(body: Stmt[]): Generator<Expr> {
755
+ const stack: (Expr | Stmt[])[] = [body];
756
+ while (stack.length > 0) {
757
+ const top = stack.pop()!;
758
+ if (Array.isArray(top)) {
759
+ for (let i = top.length - 1; i >= 0; i--) {
760
+ const s = top[i];
761
+ const children = stmtChildren(s);
762
+ if (children.length > 0) {
763
+ stack.push(children);
764
+ }
765
+ const exprs = stmtExprs(s);
766
+ for (let j = exprs.length - 1; j >= 0; j--) {
767
+ stack.push(exprs[j]);
768
+ }
769
+ }
770
+ continue;
771
+ }
772
+ yield top;
773
+ const children = exprChildren(top);
774
+ for (let i = children.length - 1; i >= 0; i--) {
775
+ stack.push(children[i]);
776
+ }
373
777
  }
374
778
  }
375
779
 
376
780
  // THE negation of a CONDITION — the one implementation, shared by every L3 pass that flips one.
377
781
  //
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.
782
+ // WHY IT IS SHARED RATHER THAN COPIED PER PASS: the passes that flip a condition do not run at one
783
+ // time. The structurer's empty-then peephole flips one, and `eliminateDeadStores` flips another
784
+ // AFTER structuring so a copy that lacked a rule the other had would re-introduce the exact
785
+ // spelling the earlier pass just removed, and nothing downstream could tell that apart from a
786
+ // spelling the input really had. The negation therefore lives with the AST vocabulary.
383
787
  //
384
788
  // Three rules, in order:
385
789
  // 1. a relational operator flips directly (`!=` → `==`, `<` → `>=`, …), exact over C's total
@@ -389,7 +793,7 @@ export function stmtChildren(s: Stmt): Stmt[] {
389
793
  // holds. Same operands, same inputs — which is what makes it safe over a `b` that loads. It
390
794
  // matters because a source `&&` and its dual `||` compile to the SAME branch graph, so the
391
795
  // 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;
796
+ // distributing is what lets the other one be spelled at all;
393
797
  // 3. `!!x` collapses to `x`, reachable only from a double flip that rule 2 now produces.
394
798
  //
395
799
  // CONTEXT REQUIREMENT, and it is the reason this is `negateCond` and not `negate`: rule 3 is valid
@@ -397,13 +801,16 @@ export function stmtChildren(s: Stmt): Stmt[] {
397
801
  // so this must never be used to negate a general integer expression — only an `if`/loop test or an
398
802
  // operand of one of the connectives above.
399
803
  //
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>> = {
804
+ // SCOPE of rule 2: it fires wherever a branch-sense lever negates the condition, which is both `if`
805
+ // classes `preserveDivergentBranchSense` on divergent ifs, `negateJoinedBranchSense` on
806
+ // reconverging ones and on the joined class it is a DEFAULT emission, not a differ-only
807
+ // alternative, so a source `&&` can come out as its `||` dual with no lever asked for
808
+ // (`synthetic:ifand_far`, where the branch range put the fold on the other arm). Neither
809
+ // lever reaches a LOOP test, so a connective that ended up as one has no dual candidate at all —
810
+ // the differ never sees the other form, and on such a row this rule changes how the code READS and
811
+ // nothing else. Widening a branch-sense lever to loop tests is what would make it a matching lever
812
+ // there, and that is a separate change.
813
+ export const NEGATE_REL: Partial<Record<BinOp, BinOp>> = {
407
814
  '<': '>=',
408
815
  '>=': '<',
409
816
  '>': '<=',