@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
@@ -0,0 +1,432 @@
1
+ // asmlift structurer — BITFIELD MEMBER SPELLING off a symbol map. Precomputes, for one function,
2
+ // which extracts read a declared bitfield and which stores write one; structure.ts renders from
3
+ // the three products and never re-derives them. Everything here is a REFUSAL machine: any fact
4
+ // that does not hold exactly leaves the honest shift/mask spelling in place.
5
+ //
6
+ // ── BITFIELD member READS ────────────────────────────────────────────────────────────────────
7
+ // The `(x << a) >> b` extract of a struct global's loaded bytes IS a bitfield access when the
8
+ // map declares a bitfield at exactly those bits: spelled `gSym.field`, the source form, whose
9
+ // declared `u32 field : n` then makes C's own integer promotion reproduce the signedness every
10
+ // downstream operator compiled with (a 7-bit unsigned field promotes to signed int — sdiv
11
+ // renders `/` and recompiles to __divsi3, where the raw-shift spelling stays u32).
12
+ //
13
+ // Semantically EXACT, never approximate: the window must lie inside the loaded bytes (so the
14
+ // load's extension bits cannot reach it), the field's position, width and signedness must all
15
+ // match the extract (a logical shift is an unsigned read, an arithmetic one a signed read —
16
+ // a signless field never matches), and the member must be nameable at all (memberQualsAllow;
17
+ // the map only carries bitfield facts for little-endian ELFs — see SymbolStructField). Any
18
+ // mismatch keeps the honest shift spelling.
19
+ //
20
+ // Precomputed over the ops (not folded during rendering) for the load's sake: a load whose
21
+ // EVERY use is a spelled extract chain must not also emit its materialized `v = *(u16 *)&g;`
22
+ // temp — the compiler CSEs the repeated member reads back to one load, but the leftover temp
23
+ // would be a second one. A VOLATILE container refuses the whole fold: N member reads are N
24
+ // volatile accesses where the asm did one load. (Byte-level residual, differ-refereed: a load
25
+ // only PARTIALLY absorbed — one extract spelled, another use kept — emits both the temp and
26
+ // the named reads, one load more than the asm; semantics hold, the score decides.)
27
+ //
28
+ // ORDERING GATE: the named spelling replaces a
29
+ // REGISTER value — the bits captured at the load's program position — with a fresh memory
30
+ // read at each render position. Every other memory read in this file goes through the
31
+ // materialization model (analysis.ts) for exactly that hazard, so the fold clears the SAME
32
+ // bar with the SAME machinery: `emitPos` resolves where each extract actually renders
33
+ // (transitively through its inlining consumers — an unresolvable position refuses), and
34
+ // `memWriteBetween` walks every def-avoiding load→render path for a call, an opaque, or a
35
+ // store not provably to a DIFFERENT named global. PATH-BASED, never a linear scan over the op
36
+ // list: `fn.blocks` is in ADDRESS order, not topological order, so a block laid out after the
37
+ // render can still execute between the load and the render on the taken path.
38
+ import { type GlobalCell, globalCellOf, mayWriteGlobal } from '../ir/alias';
39
+ import { type BitsCtx, constMask, provableBits } from '../ir/bits';
40
+ import { Block, Fn, Op, Value } from '../ir/core';
41
+ import { type DeclaredField, type SymbolInfo, type SymbolStructField, declaredFields } from '../symbols';
42
+
43
+ /** The slice of structure.ts's symbol-map rendering context this fold reads. Structural on purpose:
44
+ * the owner of that context stays in structure.ts, and nothing here can reach the rest of it. */
45
+ export interface BitfieldSymCtx {
46
+ info(name: string): SymbolInfo | undefined;
47
+ fieldsOf(name: string): DeclaredField[] | null;
48
+ }
49
+
50
+ export interface BitfieldDeps {
51
+ fn: Fn;
52
+ defs: Map<Value, Op>;
53
+ /** defs that emit as named temps at their own position (structure/analysis.ts). READ ONLY here:
54
+ * a materialized op is what several of the refusals below test for. */
55
+ materialize: Set<Op>;
56
+ useSitesOf: Map<Value, { op: Op }[]>;
57
+ opBlock: Map<Op, Block>;
58
+ opIndex: Map<Op, number>;
59
+ /** where an op's expression ultimately renders, transitively through its inlining consumers;
60
+ * null when there is no single such position. */
61
+ emitPos: (op: Op) => { blk: Block; idx: number } | null;
62
+ /** does any op matching `isWrite` lie on a def-avoiding path from `def` to `render`? */
63
+ memWriteBetween: (def: Op, render: { blk: Block; idx: number }, isWrite: (x: Op) => boolean) => boolean;
64
+ /** absent ⇒ no map, and then nothing here fires. */
65
+ sym: BitfieldSymCtx | undefined;
66
+ /** the map only carries bitfield facts for little-endian ELFs (see SymbolStructField). */
67
+ littleEndian: boolean;
68
+ /** `spellBitfieldMembers`, already normalized against the PROJECT map by the caller. */
69
+ enabled: boolean;
70
+ /** may a member be NAMED by an access of this direction, given its declared qualifiers? Taken as
71
+ * a dependency rather than duplicated: structure.ts owns the one statement of that rule, and
72
+ * both of its named-member spellings pass through the same predicate. */
73
+ memberQualsAllow: (f: SymbolStructField, containerConst: boolean | undefined, isStore: boolean) => boolean;
74
+ }
75
+
76
+ /** What a recognized bitfield store assigns: an inserted value, or the literal 0 the asm does not
77
+ * carry (the ALL-ZERO form — see the note in `makeBitfieldSpelling`). */
78
+ export type BitfieldAssigned = { k: 'zero' } | { k: 'value'; v: Value };
79
+
80
+ export interface BitfieldSpellings {
81
+ /** extract op → the `gSym.field` read it spells */
82
+ spelling: Map<Op, { global: string; field: string }>;
83
+ /** store op → the `gSym.field = …` write it spells */
84
+ stores: Map<Op, { global: string; field: string; value: BitfieldAssigned }>;
85
+ /** loads whose EVERY use is a spelled extract: the fold emits no temp for these */
86
+ absorbed: Set<Op>;
87
+ }
88
+
89
+ export function makeBitfieldSpelling(deps: BitfieldDeps): BitfieldSpellings {
90
+ const {
91
+ fn,
92
+ defs,
93
+ materialize,
94
+ useSitesOf,
95
+ opBlock,
96
+ opIndex,
97
+ emitPos,
98
+ memWriteBetween,
99
+ sym: symCtx,
100
+ littleEndian,
101
+ enabled: spellBitfieldMembers,
102
+ memberQualsAllow,
103
+ } = deps;
104
+ // the READ side: an extract op → the `gSym.field` it spells. Every rule and every refusal behind
105
+ // it is in this module's header.
106
+ const bitfieldSpelling = new Map<Op, { global: string; field: string }>();
107
+ // …and the WRITE side: a store the mask-and-insert idiom recognized (see the block below), with
108
+ // the value the source assigned. THE SECOND inhabitant of "a precomputed member spelling", which
109
+ // is what makes the shape shared rather than anticipated.
110
+ const bitfieldStore = new Map<Op, { global: string; field: string; value: BitfieldAssigned }>();
111
+ const absorbedLoads = new Set<Op>();
112
+ if (symCtx && littleEndian && spellBitfieldMembers) {
113
+ // the (name, byte) of a load's address when it resolves through defs alone — `gaddr` or
114
+ // `add(gaddr, const)`; anything else (a materialized base, a variable index) declines. THE
115
+ // shared L2 disjointness query (ir/alias.ts), which the materialization model consults with
116
+ // the same rule, so the fold and the model cannot disagree about what a store can reach.
117
+ const loadTargets = new Map<Op, GlobalCell>();
118
+ const addrOf = (v: Value, off: number): GlobalCell | null => globalCellOf(defs, v, off);
119
+ // A write for the fold's purposes: calls and opaques always; a store/astore unless its base
120
+ // resolves to a global PROVABLY different from the folded one.
121
+ const mayWrite = (sym: string) => mayWriteGlobal(defs, sym);
122
+ for (const blk of fn.blocks) {
123
+ for (const op of blk.ops) {
124
+ if ((op.opcode !== 'shr_u' && op.opcode !== 'shr_s') || op.operands.length !== 1) {
125
+ continue;
126
+ }
127
+ const b = op.attrs.imm as number | undefined;
128
+ const inner = defs.get(op.operands[0]);
129
+ if (typeof b !== 'number' || b <= 0 || b >= 32 || inner?.opcode !== 'shl' || inner.operands.length !== 1) {
130
+ continue;
131
+ }
132
+ const a = inner.attrs.imm as number | undefined;
133
+ if (typeof a !== 'number' || a < 0 || b < a) {
134
+ continue;
135
+ }
136
+ const w = 32 - b; // extract width
137
+ const lo = b - a; // low bit within the loaded value
138
+ const load = defs.get(inner.operands[0]);
139
+ if (load?.opcode !== 'load' || lo + w > (load.attrs.width as number) * 8) {
140
+ continue;
141
+ }
142
+ // a materialized shl would still emit its `v = x << a` temp reading the load — the fold
143
+ // would then ADD member reads on top of it; rare, refuse
144
+ if (materialize.has(inner)) {
145
+ continue;
146
+ }
147
+ const gb = addrOf(load.operands[0], load.attrs.off as number);
148
+ const si = gb ? symCtx.info(gb.name) : undefined;
149
+ if (!gb || si?.shape !== 'struct' || si.volatile) {
150
+ continue;
151
+ }
152
+ // where does the member read RENDER? at the extract's own position when materialized,
153
+ // else wherever each of its consumers ultimately renders (emitPos, transitively —
154
+ // unresolvable refuses); every load→render path must be write-free
155
+ const renders = materialize.has(op)
156
+ ? [{ blk: opBlock.get(op)!, idx: opIndex.get(op)! }]
157
+ : [...new Set((useSitesOf.get(op.results[0]) ?? []).map((s) => s.op))].map((c) => emitPos(c));
158
+ const writes = mayWrite(gb.name);
159
+ if (renders.some((r) => r === null) || renders.some((r) => memWriteBetween(load, r!, writes))) {
160
+ continue;
161
+ }
162
+ const signedRead = op.opcode === 'shr_s';
163
+ const fld = declaredFields(si.layout)?.find(
164
+ (f) => f.bitWidth === w && f.offset * 8 + f.bitOffset! === gb.byte * 8 + lo && f.signed === signedRead,
165
+ );
166
+ if (fld && memberQualsAllow(fld, si.const, false)) {
167
+ bitfieldSpelling.set(op, { global: gb.name, field: fld.name });
168
+ loadTargets.set(load, gb);
169
+ }
170
+ }
171
+ }
172
+ // a load is ABSORBED when every use is an shl whose every use is a spelled extract
173
+ for (const load of loadTargets.keys()) {
174
+ const shls = useSitesOf.get(load.results[0]) ?? [];
175
+ const absorbed =
176
+ shls.length > 0 &&
177
+ shls.every(
178
+ (u) =>
179
+ u.op.opcode === 'shl' && (useSitesOf.get(u.op.results[0]) ?? []).every((v) => bitfieldSpelling.has(v.op)),
180
+ );
181
+ if (absorbed) {
182
+ absorbedLoads.add(load);
183
+ }
184
+ }
185
+
186
+ // ── BITFIELD member WRITES: the mask-and-insert idiom ───────────────────────────────────
187
+ // `store(A, or(and(load(A), ~W), v << lo))` over a struct global's cell IS an assignment to
188
+ // the declared bitfield at bits W — `gSym.field = v;`, one statement where the recovered
189
+ // spelling is a read, a mask, a shift, an or and a store.
190
+ //
191
+ // EXACT, never approximate. The cleared bits must be exactly one declared field's window; the
192
+ // load must address the SAME cell at the same width; the insert must be that value shifted to
193
+ // the window's own position; and the load, the mask, the `and` and the `or` must each be
194
+ // single-use and unmaterialized, because the fold DELETES all of them — a second reader would
195
+ // keep the temp and the emitted C would do the work twice.
196
+ //
197
+ // THE ALL-ZERO FORM HAS NO `or`: agbcc's `expmed.c` skips the insert when the assigned value
198
+ // is all-zero (`:557-558`, `:606-608`), so `gSym.field = 0;` lowers to
199
+ // `store(A, and(load(A), ~W))` and the store's value operand IS the keep.
200
+ //
201
+ // ITS EVIDENCE IS THE MASK, NOT THE VALUE. `= 0` is also spellable raw, as
202
+ // `*(u8 *)&gSym = KEEP & *(u8 *)&gSym`, so keying on "the inserted value is 0" would name a
203
+ // member wherever the two spellings are the SAME OBJECT. Measured, pinned agbcc, `u8 a:4;
204
+ // u8 b:4`: the DECLARED store of the LOW nibble complements in the 32-bit domain — `~0xF` =
205
+ // -16, which no Thumb `mov #imm8` encodes, so `mov #0x10; neg` — where the byte-domain raw
206
+ // spelling narrows the same keep to one encodable `mov #0xF0`. The HIGH nibble's two
207
+ // spellings are byte-identical (`mov #0xf` on both sides). So the fold admits the zero form
208
+ // only where the keep mask is that 32-bit complement; the gate below says what each clause
209
+ // kills.
210
+ //
211
+ // THE MASK'S ENCODING IS NOT ITS DEFINING OP. `mov;neg` is only how the complement is built
212
+ // where it misses `mov #imm8`; agbcc spells the same complement for `u16 a : 12` as a pool
213
+ // `ldr` of `-0x1000` (compiled). A rule keyed on "defined by a `neg`" would refuse that real
214
+ // inhabitant, so the gate reads the mask's VALUE. The price: the Thumb frontend lowers
215
+ // `bic Rd,Rm` to `and(Rd, ~Rm)`, so a hand-written `mov #0xf; bic` arrives as the same IR as
216
+ // the accepted `mov #0x10; neg` and is admitted. agbcc emits no `bic` for this idiom
217
+ // (compiled: the or-form clear is `mov #0x4; neg`), and `/no-bitfield` co-enumerates the raw
218
+ // spelling, so that is one extra candidate for the differ, not a lost one.
219
+ //
220
+ // TARGET COUPLING, stated because the code cannot: every argument here is a THUMB encoding
221
+ // argument, and the fold's only target guard is `littleEndian`. That is sound only because
222
+ // armv4t+agbcc is the one little-endian target in `target.ts`; a second one inherits none of
223
+ // this reasoning.
224
+ //
225
+ // TRUNCATION is what makes an UNMASKED insert legal, and only sometimes: C truncates the
226
+ // assigned value to the field width, while the asm's `or` writes every bit of `v << lo` that
227
+ // the STORE keeps. The two agree when the field ends the stored cell — bits above it are
228
+ // dropped by the store either way — or when `v` provably has no more bits than the field.
229
+ // Anything else keeps the honest mask spelling.
230
+ //
231
+ // ORDERING is NOT this fold's to police, and the difference from the read fold above is the
232
+ // reason. That fold MOVES a read: its extract renders at the consumer, so a write in between
233
+ // changes what the extract sees. This one moves nothing — the spelling it replaces is a single
234
+ // statement AT THE STORE (`*(u8 *)&gS = v | *(u8 *)&gS & ~W;`), which reads the cell in exactly
235
+ // the position `gS.field = v` does. What keeps that read honest is the MATERIALIZATION model,
236
+ // and it is byte-granular where a symbol-wide alias query is not: a call, or a store this load
237
+ // may alias, forces the load to its own temp at its own position, and `!materialize.has(load)`
238
+ // below then refuses. A store to a DISJOINT byte of the same cell's symbol materializes
239
+ // nothing, and refusing there bought no ordering — it only spelled the same read as arithmetic.
240
+
241
+ // THE KNOWN-BITS QUESTION IS L2 AND LIVES THERE (ir/bits.ts) — this fold only supplies the
242
+ // one fact that layer cannot see: a bitfield READ this pass has already recognized, whose
243
+ // bound comes from the DECLARATION. And it supplies it signedness-first, because a signed
244
+ // field's read is sign-extended and carries all 32 bits however few bits the declaration
245
+ // allots it — bounding one by its own `bitWidth` folds `gS.dest = gS.delta` over a value whose
246
+ // high bits the asm's `or` writes and C's truncation does not.
247
+ const bits: BitsCtx = {
248
+ defs,
249
+ materialize,
250
+ bound: (d) => {
251
+ const bf = bitfieldSpelling.get(d);
252
+ if (!bf) {
253
+ return null;
254
+ }
255
+ const f = symCtx.fieldsOf(bf.global)?.find((x) => x.name === bf.field);
256
+ return f?.signed === false ? (f.bitWidth ?? 32) : 32;
257
+ },
258
+ };
259
+ const maskConst = (v: Value): number | null => constMask(bits, v);
260
+ /** The other operand of a 2-operand commutative op, or null when there is none — a
261
+ * 1-operand op carries its constant as `attrs.imm`, which is not a Value the caller can
262
+ * read a mask off, so the caller falls through to `attrs.imm` itself. */
263
+ const otherOperand = (d: Op, keep: Value): Value | null =>
264
+ d.operands.length === 2 ? (d.operands[0] === keep ? d.operands[1] : d.operands[0]) : null;
265
+
266
+ for (const blk of fn.blocks) {
267
+ for (const op of blk.ops) {
268
+ if (op.opcode !== 'store') {
269
+ continue;
270
+ }
271
+ const width = op.attrs.width as number;
272
+ const cell = globalCellOf(defs, op.operands[0], op.attrs.off as number);
273
+ const si = cell ? symCtx.info(cell.name) : undefined;
274
+ const valOp = defs.get(op.operands[1]);
275
+ // the ALL-ZERO form: no `or`, the keep `and` IS the store's value. Single-use and
276
+ // unmaterialized go untested here because the shared `andOp` gate tests the same op.
277
+ const zeroForm = valOp?.opcode === 'and';
278
+ if (
279
+ !cell ||
280
+ si?.shape !== 'struct' ||
281
+ si.volatile ||
282
+ (!zeroForm &&
283
+ (valOp?.opcode !== 'or' ||
284
+ valOp.operands.length !== 2 ||
285
+ materialize.has(valOp) ||
286
+ (useSitesOf.get(valOp.results[0]) ?? []).length !== 1))
287
+ ) {
288
+ continue;
289
+ }
290
+ const cellBits = width * 8;
291
+ const cellMask = width >= 4 ? -1 : (1 << cellBits) - 1;
292
+ // The `or` form's two pairs are the commutativity of `|`; the zero form contributes one,
293
+ // with `null` for the insert the asm does not contain.
294
+ const pairs: readonly (readonly [Value, Value | null])[] = zeroForm
295
+ ? [[op.operands[1], null]]
296
+ : [
297
+ [valOp!.operands[0], valOp!.operands[1]],
298
+ [valOp!.operands[1], valOp!.operands[0]],
299
+ ];
300
+ for (const [keepV, insV] of pairs) {
301
+ const andOp = defs.get(keepV);
302
+ if (
303
+ andOp?.opcode !== 'and' ||
304
+ materialize.has(andOp) ||
305
+ (useSitesOf.get(andOp.results[0]) ?? []).length !== 1
306
+ ) {
307
+ continue;
308
+ }
309
+ // `and` is commutative and may carry its constant as an immediate: find the operand that
310
+ // is the SAME cell's load, and read the mask off whatever is left.
311
+ const loadV = andOp.operands.find((o) => {
312
+ const l = defs.get(o);
313
+ const c = l?.opcode === 'load' ? globalCellOf(defs, l.operands[0], l.attrs.off as number) : null;
314
+ return c !== null && c.name === cell.name && c.byte === cell.byte && l!.attrs.width === width;
315
+ });
316
+ const load = loadV === undefined ? undefined : defs.get(loadV)!;
317
+ const maskV = loadV === undefined ? null : otherOperand(andOp, loadV);
318
+ const mask =
319
+ maskV !== null
320
+ ? maskConst(maskV)
321
+ : typeof andOp.attrs.imm === 'number'
322
+ ? (andOp.attrs.imm as number) | 0
323
+ : null;
324
+ if (
325
+ load === undefined ||
326
+ mask === null ||
327
+ materialize.has(load) ||
328
+ (useSitesOf.get(load.results[0]) ?? []).length !== 1
329
+ ) {
330
+ continue;
331
+ }
332
+ // The cleared bits must be ONE contiguous window inside the stored cell.
333
+ const clear = ~mask & cellMask;
334
+ if (clear === 0) {
335
+ continue;
336
+ }
337
+ const lo = 31 - Math.clz32(clear & -clear);
338
+ const w = 32 - Math.clz32(clear >>> lo);
339
+ if ((((w >= 32 ? -1 : (1 << w) - 1) << lo) & cellMask) !== clear) {
340
+ continue;
341
+ }
342
+ // …AND THE STORE'S WIDTH MUST BE THE ACCESS AN ASSIGNMENT TO THOSE BITS COMPILES TO:
343
+ // the NARROWEST aligned 1/2/4-byte cell that contains the whole window. agbcc picks the
344
+ // access from the field's own BITS, not from its container's declared type — measured,
345
+ // pinned agbcc, one compile per row:
346
+ //
347
+ // u16 a : 2 at bit 0 → `ldrb`/`strb` (narrower than the `u16` container)
348
+ // u32 a : 12 at bit 0 → `ldrh`/`strh` (narrower than the `u32` container)
349
+ // u32 x : 8 at bit 12 → `ldr`/`str` (WIDER: bits 12-19 straddle bytes 1-2)
350
+ // u32 a : 17 / a : 20 → `ldr`/`str` (WIDER: no 3-byte access exists)
351
+ //
352
+ // Both directions matter and only one of them is intuitive. Without the narrowing half a
353
+ // `strh` over `gState.hearts` (two bits inside byte 0) spells a member from bytes that
354
+ // spelling cannot reproduce. Without the WIDENING half every field whose bits straddle
355
+ // an aligned pair — the common packed-header shape — is refused, because the compiler
356
+ // had no choice but a word.
357
+ //
358
+ // So the test reads the WINDOW and never the map's `size`. `size` is the field's byte
359
+ // SPAN (@gba-kit/debug-info emits `ceil((bitsIntoByte + bitWidth) / 8)`), which for the
360
+ // two widening rows is 2 and 3 — neither an access this machine has, and 3 an access no
361
+ // machine has. Any bound expressed in `size` is therefore wrong for a whole band of real
362
+ // fields however it rounds. `bitOffset`/`bitWidth` state the same fact exactly.
363
+ const loBit = cell.byte * 8 + lo;
364
+ const cellWidth = [1, 2, 4].find(
365
+ (n) => Math.floor(loBit / (n * 8)) === Math.floor((loBit + w - 1) / (n * 8)),
366
+ );
367
+ if (width !== cellWidth) {
368
+ continue;
369
+ }
370
+ // THE ZERO FORM'S EVIDENCE RULE: accept only a keep mask no RAW spelling of the same
371
+ // clear can produce. Three clauses, each measured with the pinned agbcc:
372
+ //
373
+ // · `mask === ~clear` — the 32-bit complement of the window. `~0xF` = -16 for a low
374
+ // nibble, `-0x1000` for `u16 a : 12`: neither is encodable in the byte domain, so a
375
+ // raw spelling narrows to a different object. A pool word like `0xFFFF00F0` clears
376
+ // the same nibble but zeroes bits ABOVE the cell, so it is some other function whose
377
+ // clear is a coincidence.
378
+ // · `(mask & cellMask) !== mask` — a WORD cell is refused outright. `cellMask` has bit
379
+ // 31 clear at widths 1-2, so `mask === ~clear` already forces an outside bit there
380
+ // and this can only bite at width 4. It costs nothing: compiled, `gS.a = 0;` for
381
+ // `u32 a : 20` and `*(s32 *)&gS &= -1048576;` are byte-identical.
382
+ // · `(mask & cellMask) !== 0` — a mask keeping NO bit of the cell is not a
383
+ // read-modify-write. agbcc compiles `g.f8 = 0;`, a field filling its own byte, to
384
+ // `mov #0x0; strb`, so the candidate could not reproduce its own input.
385
+ //
386
+ // The rule refuses a real population as collateral: agbcc narrows the HIGH nibble's
387
+ // clear to the in-cell `mov #0xf`, byte-identical to the raw spelling, so a declared
388
+ // store does NOT always complement in 32 bits. Only the converse is needed.
389
+ //
390
+ // NONE of this may extend to the `or` form, where the insert seated at `lo` is the
391
+ // evidence and agbcc does spell an end-of-cell clear with the narrow in-cell constant.
392
+ // `zeroForm`, the discriminant that built `pairs`, is what keeps the two apart.
393
+ if (zeroForm && (mask !== ~clear || (mask & cellMask) === mask || (mask & cellMask) === 0)) {
394
+ continue;
395
+ }
396
+ // …and the insert must be exactly that value seated at `lo`. The zero form skips it:
397
+ // no insert to seat, and no bit the (absent) `or` writes for C's truncation to disagree
398
+ // with.
399
+ let assigned: BitfieldAssigned = { k: 'zero' };
400
+ if (insV !== null) {
401
+ const shifted = defs.get(insV);
402
+ const inserted =
403
+ lo === 0
404
+ ? insV
405
+ : shifted?.opcode === 'shl' && shifted.operands.length === 1 && shifted.attrs.imm === lo
406
+ ? shifted.operands[0]
407
+ : null;
408
+ if (
409
+ inserted === null ||
410
+ (lo !== 0 && (materialize.has(shifted!) || (useSitesOf.get(insV) ?? []).length !== 1))
411
+ ) {
412
+ continue;
413
+ }
414
+ if (lo + w !== cellBits && provableBits(bits, inserted) > w) {
415
+ continue; // C would truncate bits the asm's `or` writes
416
+ }
417
+ assigned = { k: 'value', v: inserted };
418
+ }
419
+ // …and a DECLARED field must occupy exactly the window.
420
+ const fld = symCtx
421
+ .fieldsOf(cell.name)
422
+ ?.find((f) => f.bitWidth === w && f.offset * 8 + f.bitOffset! === loBit && f.signed !== undefined);
423
+ if (fld && memberQualsAllow(fld, si.const, true)) {
424
+ bitfieldStore.set(op, { global: cell.name, field: fld.name, value: assigned });
425
+ }
426
+ break;
427
+ }
428
+ }
429
+ }
430
+ }
431
+ return { spelling: bitfieldSpelling, stores: bitfieldStore, absorbed: absorbedLoads };
432
+ }