@asmlift/core 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
@@ -0,0 +1,367 @@
1
+ // asmlift — A NARROW READ OF A WIDER FIELD, which the struct recognizers see as a second field at
2
+ // the same offset.
3
+ //
4
+ // WHAT IS MISSING WITHOUT THIS. A compiler asked for the low byte of a halfword member emits a byte
5
+ // load at the halfword's own address:
6
+ //
7
+ // BlendPalette(..., task->data[6], ...) // data[] is s16, the parameter is u8
8
+ // ldrh r0, [r4, #0xc] @ the member, read whole
9
+ // ldrb r2, [r4, #0xc] @ the same member, narrowed for the call
10
+ //
11
+ // The pass that groups a base's constant-offset accesses reads that as ONE OFFSET AT TWO WIDTHS:
12
+ // `raise/structs.ts` declares the pair a union member (on a parameter or a loaded pointer). That
13
+ // reading is right about the SHAPE and wrong about the CAUSE — there is no second object here, only
14
+ // a cast — so the function would carry a union its source never declared. (`raise/memberarrays.ts`'s
15
+ // same-sounding `member-conflict` judges VARIABLE-INDEX accesses, `aload`/`astore`, and a base with
16
+ // any constant-offset load is already outside that pass by `direct-access`. It is a different shape
17
+ // and this fold does not reach it.)
18
+ //
19
+ // THE ASM DOES NOT SAY WHICH IT IS, and that is the point: a union, a cast, a bitfield and two
20
+ // adjacent bytes the compiler fused all produce the same two instructions. What this pass claims is
21
+ // narrower than any of those readings. `(u8)x` and a byte load of `x`'s low-order byte denote THE
22
+ // SAME BYTES AND THE SAME VALUE, so rewriting one into the other invents no type and loses no
23
+ // fidelity; what the struct recognizers then do with a base carrying one width instead of two is
24
+ // their existing job, refereed by the differ as before.
25
+ //
26
+ // MEASURED, not argued, on the row that motivated it
27
+ // (`pokeemerald:AnimTask_FlashHealthboxOnLevelUp_Step:agbcc`): the reference source declares ONE
28
+ // `s16` member at offset 12 and passes it to a `u8` parameter, and agbcc compiles that to the
29
+ // `ldrh`/`ldrb` pair above — `.text` sha256 `e0524eb341119a21`, 204 bytes, byte-identical to the
30
+ // target object. So the single-field layout this pass makes reachable is the layout the ROM was
31
+ // built from.
32
+ //
33
+ // WHAT IT DOES. Over the L1 fn, for every base with constant-offset accesses: a LOAD whose byte
34
+ // range is the LOW-ORDER END of a strictly wider access's range at the same base becomes a
35
+ // `zext`/`sext` — matching the narrow load's own signedness — of a fresh load at the wider range.
36
+ // No opcode, no representation, no level: `zext`/`sext` with a `width` attr already exist
37
+ // (`ir/opcodes.ts`), they already print as a C cast, and the backend already recompiles that cast
38
+ // to the narrow instruction on the compilers that emit it.
39
+ //
40
+ // THE FOLD WIDENS A READ, which is the one thing worth arguing about, and the two rules that bound
41
+ // it are `fixed-cell` and `covering-dominates` below. Neither is a restatement of what
42
+ // raise/structs.ts already claims, and the difference is what makes them rules rather than
43
+ // decoration: that pass declares a SINGLE LAYOUT covering every offset the function touches, which
44
+ // is a claim the compiler reads and no instruction executes, while this one emits a DEREFERENCE.
45
+ // Widening a read where the covering access is on the other arm of a branch would have the emitted
46
+ // program load bytes that no execution reaching that point ever loaded — a compile-time claim
47
+ // escalated into a runtime one. Widening a read of a cell at a constant address would change the
48
+ // event a device sees. So the range this fold widens to must be a range some access at the same
49
+ // base read ON THIS PATH, at a cell whose declaration cannot make its access width observable.
50
+ //
51
+ // LOW-ORDER END IS AN ENDIANNESS QUESTION, read off `capabilities.endianness` rather than assumed:
52
+ // the low bytes of a big-endian field sit at its TOP address (`lw 20(a0)` covering `lhu 22(a0)`),
53
+ // the low bytes of a little-endian one at its base. Both directions have a corpus inhabitant and
54
+ // the benchmark's `unitrunc` row carries both.
55
+ //
56
+ // WHICH GATE BOUNDS THE CORPUS PATH — `pnpm bench gates --pass truncload [--toolchain <id>]`, and
57
+ // the answer is the SOUND ones, per population. Over the synthetic tier on agbcc (337 rows) the
58
+ // census reads `covering-store 12, covering-dominates 4, fixed-cell 2`; on ido7.1 (179 rows) and
59
+ // gcc2.7.2kmc (183) `covering-store 12, high-order-read 4, fixed-cell 2`, the big-endian rows
60
+ // reaching the skew rule because `utag`'s union members are read at the field's TOP; on
61
+ // mwcc_242_81 (189) `covering-store 12, fixed-cell 2`. The union rows (`uhalf`, `ureread`, `utag`)
62
+ // are most of it, and they are counted whole only because raise/structs.ts
63
+ // recovers them rather than declining part-way through the enumeration — the next paragraph.
64
+ //
65
+ // READ THE `did not lift` LINE WITH THE COUNTS, because the census runs the whole RANKED
66
+ // enumeration per row and a row that THROWS part-way through it contributes a TRUNCATED count
67
+ // rather than none. `synthetic:unidev` is the worked example: while `raise/structs.ts` declined it
68
+ // the agbcc census read `fixed-cell 1` with 13 rows unlifted, and once that pass stopped declining
69
+ // a literal-address base it read `fixed-cell 2` with 12 — the same refusals, all of them now
70
+ // reached. `--only synthetic:unidev:agbcc` shows the whole delta on that one row.
71
+ //
72
+ // `high-order-read` is STARVED on agbcc — not because nothing
73
+ // skews there, but because `synthetic:uhalf`'s skewed read sits on a base whose widest access is
74
+ // the `u->w = v` STORE, so the earlier rule answers first. Real rows show two of them alone:
75
+ // `--only sa3:sub_804DC38:agbcc` reads `high-order-read 4`, an `ldrb` at byte 5 of a word at 4,
76
+ // and `--only kleod:WorldMapScreenUnlockNewWorld:agbcc` reads `fixed-cell 90` — a named global's
77
+ // cells, which is the same shape a named hardware register has. Two rules are UNINHABITED and say
78
+ // so rather than being left to look load-bearing: `unresolved-base` and `cast-width` are refusals
79
+ // no corpus row reaches, and both are pinned by a hand-written shape in the unit tests instead.
80
+ //
81
+ // A THIRD SHAPE IS ADMITTED AND SAYS SO RATHER THAN BEING CLAIMED SOUND: a cell reached at a
82
+ // RUNTIME index from a literal or a named base — `((vu16 *)0x4000000)[i]`, the GBA palette and DMA
83
+ // loops. `fixed-cell` does not fire there, because no constant address exists to read, and nothing
84
+ // else can: the IR for that loop and the IR for an element of an ordinary global array are the
85
+ // SAME IR, so refusing it would refuse every indexed access into a named object — including the
86
+ // map-less spelling of the row this pass was written for. Where the access is qualified — the lift
87
+ // pinned it as a device register's, or the map declares its object volatile — `qualified-access`
88
+ // refuses it; any other one is a residue, it is unpriced, and the place it would be settled is the
89
+ // symbol map, not this pass.
90
+ //
91
+ // A NARROW STORE IS NOT A CANDIDATE AT ALL — the refusal is in the candidate builder below rather
92
+ // than in the gate table, and this is the table's named residue. Widening a write clobbers the
93
+ // bytes past it, and no cast spelling expresses a partial write: `(u8)p->f = v` is not C. A base
94
+ // whose conflict is a narrow STORE therefore keeps both widths — two views of a union member where
95
+ // raise/structs.ts synthesizes the layout, two casts at a declared address.
96
+ //
97
+ // THE SECOND RESIDUE. A union read through members of three widths on three arms of a `switch`
98
+ // (`synthetic:utag`) is refused by `covering-dominates`, and the overlap it leaves is declared as a
99
+ // union member by raise/structs.ts — which is what that source wrote. Widening its reads DOES
100
+ // compile byte-exactly on agbcc, which is exactly why the rule has to be a rule: the differ scores
101
+ // the sound and the unsound spelling the same, so nothing downstream would ever refuse it.
102
+ import { constAddressOf, globalCellOf } from '../ir/alias';
103
+ import { type Block, type Fn, type Op, type Value, defOpMap, dominators, mkOp, mkValue } from '../ir/core';
104
+ import { qualified } from '../ir/discipline';
105
+ import { CAST_WIDTHS } from '../ir/opcodes';
106
+ import { T } from '../ir/types';
107
+ import { type Gate, firstRejection } from '../l3/gates';
108
+ import { type Access, constOffsetAccesses } from './structs';
109
+
110
+ /** Ops through which a base address is still the SAME address question — the arithmetic a lift
111
+ * produces between a root and a load's operand. Anything else (a `load`, a `call`, a `laddr`) is
112
+ * a value the function obtained, which ENDS the walk: a pointer it was handed is an object the
113
+ * source declared, and there is nothing further to resolve. */
114
+ const ADDRESS_ARITHMETIC: ReadonlySet<string> = new Set(['add', 'sub', 'or', 'and', 'xor', 'shl', 'shr_u', 'shr_s']);
115
+
116
+ /** Does this base's origin RESOLVE — does the walk above reach a root it can name on every branch?
117
+ * An entry parameter, a literal, a named global and a loaded pointer all resolve; a block param
118
+ * joining two edges and an `undef` do not.
119
+ *
120
+ * It answers ONE question and `fixed-cell` answers the other: whether the cell is at a constant
121
+ * address is asked of the resolved base, and a base that never resolved cannot be asked at all.
122
+ * WHY THE WALK IS HERE rather than composed out of ir/alias.ts's `constAddressOf` /
123
+ * `globalBaseOf`: both of those return null for "not one of these" AND for "cannot tell", so a
124
+ * caller composing them cannot separate a parameter from a join — and the join is the case that
125
+ * has to refuse. */
126
+ function baseResolves(defs: Map<Value, Op>, entryParams: ReadonlySet<Value>, v: Value): boolean {
127
+ const d = defs.get(v);
128
+ if (!d) {
129
+ return entryParams.has(v);
130
+ }
131
+ if (d.opcode === 'undef') {
132
+ return false;
133
+ }
134
+ return !ADDRESS_ARITHMETIC.has(d.opcode) || d.operands.every((o) => baseResolves(defs, entryParams, o));
135
+ }
136
+
137
+ /** What the gates below judge: one narrow LOAD and the access at its base that covers it. */
138
+ export interface TruncatedLoad {
139
+ /** the narrow load reads a cell at a COMPILE-TIME CONSTANT address */
140
+ fixedCell: boolean;
141
+ /** the narrow load or its cover is a qualified access: pinned by the lift, or of an object the
142
+ * map declares volatile */
143
+ qualified: boolean;
144
+ /** the base's origin resolves — it is not a join the walk could not finish */
145
+ resolvedBase: boolean;
146
+ /** the covering access is a LOAD, so its signedness is read rather than invented */
147
+ coveringLoad: boolean;
148
+ /** the narrow bytes are the low-order end of the covering range on THIS target's endianness */
149
+ lowOrderEnd: boolean;
150
+ /** the covering access runs, on EVERY path that reaches the narrow load, before it */
151
+ coveringDominates: boolean;
152
+ /** the width in BITS the cast would carry */
153
+ castWidth: number;
154
+ }
155
+
156
+ export const TRUNC_LOAD_GATES: readonly Gate<TruncatedLoad>[] = [
157
+ {
158
+ // SOUND, and it is the ACCESS WIDTH that is at stake rather than the value: a hardware register
159
+ // answers a byte read and a halfword read differently, so reading two bytes where the machine
160
+ // read one is an event the device sees. A memory-mapped register is a FIXED cell, and both
161
+ // spellings a source has for one land here: the bare cast of a number (`*(vu16 *)0x4000004`)
162
+ // through `constAddressOf`, and the named register (`REG_DISPSTAT`) through `globalCellOf`,
163
+ // whose `volatile` is a declaration in the project's headers that no raise pass can read.
164
+ // The two helpers are ir/alias.ts's. A dead read this rule and `qualified-access` admit is one
165
+ // nobody spells: a read the raw lift never used is stamped by the dead-read pin
166
+ // (frontend/dead-reads.ts) before this pass runs, `qualified-access` refuses it, and
167
+ // structure.ts `volatileQualifiable` spells only a qualified read.
168
+ //
169
+ // NOT `capabilities.deviceRegisters`. That window is documented as a question about SPELLING
170
+ // and may be approximate (target.ts), so a soundness rule may not rest on it; asking whether
171
+ // the address is constant at all is wider and needs nothing approximate.
172
+ id: 'fixed-cell',
173
+ why: 'a cell at a constant address can be a hardware register, where the access WIDTH is itself observable',
174
+ sound: true,
175
+ guardedBy: 'truncload.test.ts: a cell at a constant address is not narrowed',
176
+ rejects: (c) => c.fixedCell,
177
+ },
178
+ {
179
+ // SOUND, and the residue of the rule above rather than a rule of its own: a base that joins two
180
+ // edges has no definition to walk, so `fixed-cell` cannot ask its question and the honest
181
+ // answer is that this pass does not know what the base is. An `undef` base is the same shape —
182
+ // storage nothing was entitled to write.
183
+ id: 'unresolved-base',
184
+ why: 'a base whose origin does not resolve cannot be shown not to be a device cell',
185
+ sound: true,
186
+ guardedBy: 'truncload.test.ts: a base joined on two edges is not narrowed',
187
+ rejects: (c) => !c.resolvedBase,
188
+ },
189
+ {
190
+ // SOUND, and `fixed-cell`'s question answered by the passes that knew it: the device pin
191
+ // (frontend/device-pins.ts) marks a register's access whatever its address is, including the
192
+ // runtime-indexed channel `(vu32 *)(0x40000B0 + ch * 12)` no constant address reaches, and the
193
+ // `declared` stamp (raise/declared-volatile.ts) marks a read of a volatile object through a
194
+ // runtime index into it. Widened, the narrow access becomes a cast of a fresh load that carries
195
+ // no placement, at a width the machine never used on the object.
196
+ id: 'qualified-access',
197
+ why: 'a qualified access is made at its own width, and the fold would also drop its placement',
198
+ sound: true,
199
+ guardedBy: 'truncload.test.ts: a pinned narrow read through a runtime-indexed base keeps its width and its pin',
200
+ rejects: (c) => c.qualified,
201
+ },
202
+ {
203
+ // The sibling narrowing passes' rule, under the sibling name (raise/paramwidth.ts,
204
+ // raise/narrowlocal.ts): a width `CAST_WIDTHS` does not carry has no C cast to spell it, so the
205
+ // fold would mint a `zext` the backend can only gap on. No corpus row reaches it today — every
206
+ // frontend load is 1, 2 or 4 bytes — and it costs a line rather than an argument about which
207
+ // widths a frontend may acquire.
208
+ id: 'cast-width',
209
+ why: 'only 8 and 16 are widths a `zext`/`sext` — and so a C declaration — carries',
210
+ sound: true,
211
+ guardedBy: 'truncload.test.ts: a width no C type spells is refused',
212
+ rejects: (c) => !CAST_WIDTHS.has(c.castWidth),
213
+ },
214
+ {
215
+ // A covering STORE fixes the bytes but not the field's signedness, and the fold has to put one
216
+ // on the load it synthesizes. Not sound — the bytes are the same either way, so the worst case
217
+ // is a field declared `u16` where the source wrote `s16` — but inventing a fact the asm did not
218
+ // carry is not how a default spelling is chosen, and the differ never sees this one because the
219
+ // synthesized load is not enumerated.
220
+ id: 'covering-store',
221
+ why: 'a covering store carries no signedness, so the field type would be invented rather than read',
222
+ sound: false,
223
+ rejects: (c) => !c.coveringLoad,
224
+ },
225
+ {
226
+ // SOUND: a read of bytes ABOVE the field's low-order end is `(u16)(x >> 16)`, not `(u16)x`.
227
+ // The cast spelling would read the field's other half — a different value from the one the
228
+ // machine loaded, silently. The shifted form is expressible, but it is not what the narrow
229
+ // instruction spells and nothing would recompile to it, so the honest answer is to leave the
230
+ // base declining.
231
+ id: 'high-order-read',
232
+ why: 'bytes above the low-order end of a field are a shift, and a plain cast would read the wrong ones',
233
+ sound: true,
234
+ guardedBy: 'truncload.test.ts: a read above the low-order end is not narrowed',
235
+ rejects: (c) => !c.lowOrderEnd,
236
+ },
237
+ {
238
+ // SOUND, and the rule that separates this fold from the layout claim it feeds. Widening the
239
+ // read puts a WIDER ACCESS at the point where the machine executed a narrower one, so the bytes
240
+ // past the narrow range have to be bytes this execution already read — not merely bytes the
241
+ // FUNCTION reads somewhere. raise/structs.ts declares one layout per base across the whole
242
+ // function, which is a claim about the object's TYPE and costs no instruction; this fold emits
243
+ // a dereference, so the same function-wide reasoning would have the program read three bytes it
244
+ // never read on the path it is on. Dominance is what makes the two claims different sizes.
245
+ id: 'covering-dominates',
246
+ why: 'the bytes past the narrow read must be bytes this execution already read, not bytes some other path reads',
247
+ sound: true,
248
+ guardedBy: 'truncload.test.ts: a cover on the other arm of a branch is not narrowed',
249
+ rejects: (c) => !c.coveringDominates,
250
+ },
251
+ ];
252
+
253
+ /** One admitted-or-refused fold: the narrow load, the access it truncates, and what the gates see. */
254
+ export interface TruncLoadCandidate {
255
+ narrow: Access;
256
+ covering: Access;
257
+ c: TruncatedLoad;
258
+ }
259
+
260
+ /** Every narrow load an access at the same base covers, in program order.
261
+ *
262
+ * WHICH COVER, when several cover the same narrow read: the pick is a TOTAL ORDER over facts, not
263
+ * a reduction over the order the accesses happen to be emitted in. A cover the sound rules can
264
+ * admit outranks one they cannot — `{w4@8, w4@6, w1@8}` folds whichever of the two wide reads was
265
+ * emitted first otherwise, so an identical access set would admit or refuse on instruction
266
+ * scheduling alone. After that the WIDEST wins, because a base read at 4, 2 and 1 bytes from one
267
+ * offset collapses to ONE field in a single pass that way where the closest cover would need a
268
+ * fixpoint; then a load over a store; then the earlier access. A cover is never itself a
269
+ * candidate, since a candidate is strictly narrower than its own cover. */
270
+ export function truncatedLoadCandidates(fn: Fn, littleEndian: boolean): TruncLoadCandidate[] {
271
+ const { accessesOf } = constOffsetAccesses(fn);
272
+ const defs = defOpMap(fn);
273
+ const entryParams = new Set(fn.blocks[0].params);
274
+ const dom = dominators(fn);
275
+ const blockDominates = (a: number, b: number): boolean => dom.get(fn.blocks[b] as Block)!.has(fn.blocks[a] as Block);
276
+ const runsBefore = (a: Access, b: Access): boolean =>
277
+ a.block === b.block ? a.at < b.at : blockDominates(a.block, b.block);
278
+ const isLowOrderEnd = (narrow: Access, cover: Access): boolean =>
279
+ littleEndian ? narrow.off === cover.off : narrow.off + narrow.width === cover.off + cover.width;
280
+
281
+ const out: TruncLoadCandidate[] = [];
282
+ for (const b of fn.blocks) {
283
+ for (const op of b.ops as Op[]) {
284
+ if (op.opcode !== 'load') {
285
+ continue;
286
+ }
287
+ const base = op.operands[0];
288
+ const all = accessesOf.get(base)!;
289
+ const narrow = all.find((a) => a.op === op)!;
290
+ const covers = all.filter(
291
+ (a) => a.width > narrow.width && a.off <= narrow.off && a.off + a.width >= narrow.off + narrow.width,
292
+ );
293
+ if (covers.length === 0) {
294
+ continue;
295
+ }
296
+ const preference = (a: Access): number[] => [
297
+ isLowOrderEnd(narrow, a) ? 1 : 0,
298
+ runsBefore(a, narrow) ? 1 : 0,
299
+ a.width,
300
+ a.isLoad ? 1 : 0,
301
+ ];
302
+ const covering = covers.reduce((best, a) => {
303
+ const [x, y] = [preference(a), preference(best)];
304
+ const i = x.findIndex((v, k) => v !== y[k]);
305
+ return i >= 0 && x[i] > y[i] ? a : best;
306
+ });
307
+ out.push({
308
+ narrow,
309
+ covering,
310
+ c: {
311
+ fixedCell: constAddressOf(defs, base, narrow.off) !== null || globalCellOf(defs, base, narrow.off) !== null,
312
+ qualified: [op, covering.op].some(qualified),
313
+ resolvedBase: baseResolves(defs, entryParams, base),
314
+ coveringLoad: covering.isLoad,
315
+ lowOrderEnd: isLowOrderEnd(narrow, covering),
316
+ coveringDominates: runsBefore(covering, narrow),
317
+ castWidth: narrow.width * 8,
318
+ },
319
+ });
320
+ }
321
+ }
322
+ return out;
323
+ }
324
+
325
+ /** Rewrite every admitted narrow load into a cast of a wider one. Returns the number of rewrites.
326
+ *
327
+ * `gates` is a REQUIRED argument, not an option with a default: the table is how the refusals are
328
+ * ablated, and a second caller that could omit it would switch the channel off silently. */
329
+ export function foldTruncatedLoads(fn: Fn, littleEndian: boolean, gates: readonly Gate<TruncatedLoad>[]): number {
330
+ const admitted = truncatedLoadCandidates(fn, littleEndian).filter((k) => firstRejection(gates, k.c) === null);
331
+ if (admitted.length === 0) {
332
+ return 0;
333
+ }
334
+ const byBlock = new Map<number, TruncLoadCandidate[]>();
335
+ for (const k of admitted) {
336
+ const list = byBlock.get(k.narrow.block);
337
+ if (list) {
338
+ list.push(k);
339
+ } else {
340
+ byBlock.set(k.narrow.block, [k]);
341
+ }
342
+ }
343
+ for (const [block, ks] of byBlock) {
344
+ const b = fn.blocks[block];
345
+ const inserted = new Map<number, Op>();
346
+ for (const k of ks) {
347
+ const wide = mkOp('load', {
348
+ operands: [k.narrow.op.operands[0]],
349
+ results: [mkValue(T.unk(32))],
350
+ attrs: { off: k.covering.off, width: k.covering.width, signed: k.covering.signed },
351
+ });
352
+ inserted.set(k.narrow.at, wide);
353
+ // REWRITTEN IN PLACE, so the result `Value` identity survives and every existing use — and
354
+ // any slot home stamped on it (`ir/core.ts` SlotHomes) — keeps reading the same value. The
355
+ // operand it drops is the base, which the fresh load above still reads, so nothing is
356
+ // orphaned and the pass declares `dce: false`.
357
+ k.narrow.op.opcode = k.narrow.signed ? 'sext' : 'zext';
358
+ k.narrow.op.operands = [wide.results[0]];
359
+ k.narrow.op.attrs = { width: k.narrow.width * 8 };
360
+ }
361
+ b.ops = b.ops.flatMap((op, at) => {
362
+ const wide = inserted.get(at);
363
+ return wide ? [wide, op] : [op];
364
+ });
365
+ }
366
+ return admitted.length;
367
+ }