@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
@@ -0,0 +1,332 @@
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
+ export interface BitfieldSpellings {
77
+ /** extract op → the `gSym.field` read it spells */
78
+ spelling: Map<Op, { global: string; field: string }>;
79
+ /** store op → the `gSym.field = value` write it spells */
80
+ stores: Map<Op, { global: string; field: string; value: Value }>;
81
+ /** loads whose EVERY use is a spelled extract: the fold emits no temp for these */
82
+ absorbed: Set<Op>;
83
+ }
84
+
85
+ export function makeBitfieldSpelling(deps: BitfieldDeps): BitfieldSpellings {
86
+ const {
87
+ fn,
88
+ defs,
89
+ materialize,
90
+ useSitesOf,
91
+ opBlock,
92
+ opIndex,
93
+ emitPos,
94
+ memWriteBetween,
95
+ sym: symCtx,
96
+ littleEndian,
97
+ enabled: spellBitfieldMembers,
98
+ memberQualsAllow,
99
+ } = deps;
100
+ // the READ side: an extract op → the `gSym.field` it spells. Every rule and every refusal behind
101
+ // it is in this module's header.
102
+ const bitfieldSpelling = new Map<Op, { global: string; field: string }>();
103
+ // …and the WRITE side: a store the mask-and-insert idiom recognized (see the block below), with
104
+ // the value the source assigned. THE SECOND inhabitant of "a precomputed member spelling", which
105
+ // is what makes the shape shared rather than anticipated.
106
+ const bitfieldStore = new Map<Op, { global: string; field: string; value: Value }>();
107
+ const absorbedLoads = new Set<Op>();
108
+ if (symCtx && littleEndian && spellBitfieldMembers) {
109
+ // the (name, byte) of a load's address when it resolves through defs alone — `gaddr` or
110
+ // `add(gaddr, const)`; anything else (a materialized base, a variable index) declines. THE
111
+ // shared L2 disjointness query (ir/alias.ts), which the materialization model consults with
112
+ // the same rule, so the fold and the model cannot disagree about what a store can reach.
113
+ const loadTargets = new Map<Op, GlobalCell>();
114
+ const addrOf = (v: Value, off: number): GlobalCell | null => globalCellOf(defs, v, off);
115
+ // A write for the fold's purposes: calls and opaques always; a store/astore unless its base
116
+ // resolves to a global PROVABLY different from the folded one.
117
+ const mayWrite = (sym: string) => mayWriteGlobal(defs, sym);
118
+ for (const blk of fn.blocks) {
119
+ for (const op of blk.ops) {
120
+ if ((op.opcode !== 'shr_u' && op.opcode !== 'shr_s') || op.operands.length !== 1) {
121
+ continue;
122
+ }
123
+ const b = op.attrs.imm as number | undefined;
124
+ const inner = defs.get(op.operands[0]);
125
+ if (typeof b !== 'number' || b <= 0 || b >= 32 || inner?.opcode !== 'shl' || inner.operands.length !== 1) {
126
+ continue;
127
+ }
128
+ const a = inner.attrs.imm as number | undefined;
129
+ if (typeof a !== 'number' || a < 0 || b < a) {
130
+ continue;
131
+ }
132
+ const w = 32 - b; // extract width
133
+ const lo = b - a; // low bit within the loaded value
134
+ const load = defs.get(inner.operands[0]);
135
+ if (load?.opcode !== 'load' || lo + w > (load.attrs.width as number) * 8) {
136
+ continue;
137
+ }
138
+ // a materialized shl would still emit its `v = x << a` temp reading the load — the fold
139
+ // would then ADD member reads on top of it; rare, refuse
140
+ if (materialize.has(inner)) {
141
+ continue;
142
+ }
143
+ const gb = addrOf(load.operands[0], load.attrs.off as number);
144
+ const si = gb ? symCtx.info(gb.name) : undefined;
145
+ if (!gb || si?.shape !== 'struct' || si.volatile) {
146
+ continue;
147
+ }
148
+ // where does the member read RENDER? at the extract's own position when materialized,
149
+ // else wherever each of its consumers ultimately renders (emitPos, transitively —
150
+ // unresolvable refuses); every load→render path must be write-free
151
+ const renders = materialize.has(op)
152
+ ? [{ blk: opBlock.get(op)!, idx: opIndex.get(op)! }]
153
+ : [...new Set((useSitesOf.get(op.results[0]) ?? []).map((s) => s.op))].map((c) => emitPos(c));
154
+ const writes = mayWrite(gb.name);
155
+ if (renders.some((r) => r === null) || renders.some((r) => memWriteBetween(load, r!, writes))) {
156
+ continue;
157
+ }
158
+ const signedRead = op.opcode === 'shr_s';
159
+ const fld = declaredFields(si.layout)?.find(
160
+ (f) => f.bitWidth === w && f.offset * 8 + f.bitOffset! === gb.byte * 8 + lo && f.signed === signedRead,
161
+ );
162
+ if (fld && memberQualsAllow(fld, si.const, false)) {
163
+ bitfieldSpelling.set(op, { global: gb.name, field: fld.name });
164
+ loadTargets.set(load, gb);
165
+ }
166
+ }
167
+ }
168
+ // a load is ABSORBED when every use is an shl whose every use is a spelled extract
169
+ for (const load of loadTargets.keys()) {
170
+ const shls = useSitesOf.get(load.results[0]) ?? [];
171
+ const absorbed =
172
+ shls.length > 0 &&
173
+ shls.every(
174
+ (u) =>
175
+ u.op.opcode === 'shl' && (useSitesOf.get(u.op.results[0]) ?? []).every((v) => bitfieldSpelling.has(v.op)),
176
+ );
177
+ if (absorbed) {
178
+ absorbedLoads.add(load);
179
+ }
180
+ }
181
+
182
+ // ── BITFIELD member WRITES: the mask-and-insert idiom ───────────────────────────────────
183
+ // `store(A, or(and(load(A), ~W), v << lo))` over a struct global's cell IS an assignment to
184
+ // the declared bitfield at bits W — `gSym.field = v;`, one statement where the recovered
185
+ // spelling is a read, a mask, a shift, an or and a store.
186
+ //
187
+ // EXACT, never approximate. The cleared bits must be exactly one declared field's window; the
188
+ // load must address the SAME cell at the same width; the insert must be that value shifted to
189
+ // the window's own position; and the load, the mask, the `and` and the `or` must each be
190
+ // single-use and unmaterialized, because the fold DELETES all of them — a second reader would
191
+ // keep the temp and the emitted C would do the work twice.
192
+ //
193
+ // TRUNCATION is what makes an UNMASKED insert legal, and only sometimes: C truncates the
194
+ // assigned value to the field width, while the asm's `or` writes every bit of `v << lo` that
195
+ // the STORE keeps. The two agree when the field ends the stored cell — bits above it are
196
+ // dropped by the store either way — or when `v` provably has no more bits than the field.
197
+ // Anything else keeps the honest mask spelling.
198
+ //
199
+ // ORDERING is NOT this fold's to police, and the difference from the read fold above is the
200
+ // reason. That fold MOVES a read: its extract renders at the consumer, so a write in between
201
+ // changes what the extract sees. This one moves nothing — the spelling it replaces is a single
202
+ // statement AT THE STORE (`*(u8 *)&gS = v | *(u8 *)&gS & ~W;`), which reads the cell in exactly
203
+ // the position `gS.field = v` does. What keeps that read honest is the MATERIALIZATION model,
204
+ // and it is byte-granular where a symbol-wide alias query is not: a call, or a store this load
205
+ // may alias, forces the load to its own temp at its own position, and `!materialize.has(load)`
206
+ // below then refuses. A store to a DISJOINT byte of the same cell's symbol materializes
207
+ // nothing, and refusing there bought no ordering — it only spelled the same read as arithmetic.
208
+
209
+ // THE KNOWN-BITS QUESTION IS L2 AND LIVES THERE (ir/bits.ts) — this fold only supplies the
210
+ // one fact that layer cannot see: a bitfield READ this pass has already recognized, whose
211
+ // bound comes from the DECLARATION. And it supplies it signedness-first, because a signed
212
+ // field's read is sign-extended and carries all 32 bits however few bits the declaration
213
+ // allots it — bounding one by its own `bitWidth` folds `gS.dest = gS.delta` over a value whose
214
+ // high bits the asm's `or` writes and C's truncation does not.
215
+ const bits: BitsCtx = {
216
+ defs,
217
+ materialize,
218
+ bound: (d) => {
219
+ const bf = bitfieldSpelling.get(d);
220
+ if (!bf) {
221
+ return null;
222
+ }
223
+ const f = symCtx.fieldsOf(bf.global)?.find((x) => x.name === bf.field);
224
+ return f?.signed === false ? (f.bitWidth ?? 32) : 32;
225
+ },
226
+ };
227
+ const maskConst = (v: Value): number | null => constMask(bits, v);
228
+ /** The other operand of a 2-operand commutative op, or null when there is none — a
229
+ * 1-operand op carries its constant as `attrs.imm`, which is not a Value the caller can
230
+ * read a mask off, so the caller falls through to `attrs.imm` itself. */
231
+ const otherOperand = (d: Op, keep: Value): Value | null =>
232
+ d.operands.length === 2 ? (d.operands[0] === keep ? d.operands[1] : d.operands[0]) : null;
233
+
234
+ for (const blk of fn.blocks) {
235
+ for (const op of blk.ops) {
236
+ if (op.opcode !== 'store') {
237
+ continue;
238
+ }
239
+ const width = op.attrs.width as number;
240
+ const cell = globalCellOf(defs, op.operands[0], op.attrs.off as number);
241
+ const si = cell ? symCtx.info(cell.name) : undefined;
242
+ const orOp = defs.get(op.operands[1]);
243
+ if (
244
+ !cell ||
245
+ si?.shape !== 'struct' ||
246
+ si.volatile ||
247
+ orOp?.opcode !== 'or' ||
248
+ orOp.operands.length !== 2 ||
249
+ materialize.has(orOp) ||
250
+ (useSitesOf.get(orOp.results[0]) ?? []).length !== 1
251
+ ) {
252
+ continue;
253
+ }
254
+ const cellBits = width * 8;
255
+ const cellMask = width >= 4 ? -1 : (1 << cellBits) - 1;
256
+ for (const [keepV, insV] of [
257
+ [orOp.operands[0], orOp.operands[1]],
258
+ [orOp.operands[1], orOp.operands[0]],
259
+ ] as const) {
260
+ const andOp = defs.get(keepV);
261
+ if (
262
+ andOp?.opcode !== 'and' ||
263
+ materialize.has(andOp) ||
264
+ (useSitesOf.get(andOp.results[0]) ?? []).length !== 1
265
+ ) {
266
+ continue;
267
+ }
268
+ // `and` is commutative and may carry its constant as an immediate: find the operand that
269
+ // is the SAME cell's load, and read the mask off whatever is left.
270
+ const loadV = andOp.operands.find((o) => {
271
+ const l = defs.get(o);
272
+ const c = l?.opcode === 'load' ? globalCellOf(defs, l.operands[0], l.attrs.off as number) : null;
273
+ return c !== null && c.name === cell.name && c.byte === cell.byte && l!.attrs.width === width;
274
+ });
275
+ const load = loadV === undefined ? undefined : defs.get(loadV)!;
276
+ const maskV = loadV === undefined ? null : otherOperand(andOp, loadV);
277
+ const mask =
278
+ maskV !== null
279
+ ? maskConst(maskV)
280
+ : typeof andOp.attrs.imm === 'number'
281
+ ? (andOp.attrs.imm as number) | 0
282
+ : null;
283
+ if (
284
+ load === undefined ||
285
+ mask === null ||
286
+ materialize.has(load) ||
287
+ (useSitesOf.get(load.results[0]) ?? []).length !== 1
288
+ ) {
289
+ continue;
290
+ }
291
+ // The cleared bits must be ONE contiguous window inside the stored cell.
292
+ const clear = ~mask & cellMask;
293
+ if (clear === 0) {
294
+ continue;
295
+ }
296
+ const lo = 31 - Math.clz32(clear & -clear);
297
+ const w = 32 - Math.clz32(clear >>> lo);
298
+ if ((((w >= 32 ? -1 : (1 << w) - 1) << lo) & cellMask) !== clear) {
299
+ continue;
300
+ }
301
+ // …and the insert must be exactly that value seated at `lo`.
302
+ const shifted = defs.get(insV);
303
+ const value =
304
+ lo === 0
305
+ ? insV
306
+ : shifted?.opcode === 'shl' && shifted.operands.length === 1 && shifted.attrs.imm === lo
307
+ ? shifted.operands[0]
308
+ : null;
309
+ if (
310
+ value === null ||
311
+ (lo !== 0 && (materialize.has(shifted!) || (useSitesOf.get(insV) ?? []).length !== 1))
312
+ ) {
313
+ continue;
314
+ }
315
+ if (lo + w !== cellBits && provableBits(bits, value) > w) {
316
+ continue; // C would truncate bits the asm's `or` writes
317
+ }
318
+ const fld = symCtx
319
+ .fieldsOf(cell.name)
320
+ ?.find(
321
+ (f) => f.bitWidth === w && f.offset * 8 + f.bitOffset! === cell.byte * 8 + lo && f.signed !== undefined,
322
+ );
323
+ if (fld && memberQualsAllow(fld, si.const, true)) {
324
+ bitfieldStore.set(op, { global: cell.name, field: fld.name, value });
325
+ }
326
+ break;
327
+ }
328
+ }
329
+ }
330
+ }
331
+ return { spelling: bitfieldSpelling, stores: bitfieldStore, absorbed: absorbedLoads };
332
+ }
@@ -0,0 +1,274 @@
1
+ // THE GLOBAL-ACCESS ADDRESS DECOMPOSITION — the pure half of the symbol-map access spellings.
2
+ //
3
+ // Everything here is a function of an `Expr`, a `SymbolInfo` and a width: no structurer state, no
4
+ // naming walk, no loop context. It answers three questions the access spellings in structure.ts
5
+ // ask over and over — is this address `&gSym` plus something; does that something divide into
6
+ // whole elements; and do the terms of it name the DECLARED subscripts of a multidimensional array
7
+ // — and it answers them the same way for every caller, which is the point of the split: the
8
+ // rank-pinning fallback and the declared-subscript recovery share `bareArrayElement`, so the two
9
+ // cannot disagree about what a bare element spelling is.
10
+ import type { Expr } from '../l3/ast';
11
+ import { type SymbolInfo, arrayInnerExtents } from '../symbols';
12
+
13
+ // `&gSym`, possibly wearing the value-context integer cast the additive lowering adds
14
+ // (`(u32)&gSym` — see `intifyAddr` in structure.ts's lowerDef): both spell the same link-time
15
+ // constant, so the fold rules match through the cast and every access that CAN spell a named
16
+ // element still does.
17
+ // WIDTH 32 ONLY — a NARROWING cast (`(u8)&gSym`, from a zext/sext lowering) is a different
18
+ // VALUE (`addr & 0xFF`), and folding through it would read the named global at a wrong address
19
+ // (the adversarial round's probe: `*(u8*)(u8)&gSym` must keep its truncation, never become
20
+ // `*(u8*)&gSym` — let alone a confidently-named `gSym.field`).
21
+ export function addrIn(e: Expr): Extract<Expr, { k: 'addr' }> | null {
22
+ if (e.k === 'addr') {
23
+ return e;
24
+ }
25
+ if (e.k === 'cast' && e.to.kind === 'int' && e.to.width === 32 && e.e.k === 'addr') {
26
+ return e.e;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ // If `e` is a global address `&gSym` (optionally `+ index`), return the global name and the
32
+ // element index (byte residual divided by the access width). `&gSym` alone → idx const 0;
33
+ // `&gSym + i` → idx `i / width` (exact division only — a non-multiple residual is a mid-element
34
+ // access this whole-global spelling can't express, so it declines to null and the caller casts).
35
+ export function globalOf(e: Expr, width: number): { name: string; idx: Expr } | null {
36
+ const gb = globalByteBase(e);
37
+ if (gb === null) {
38
+ return null;
39
+ }
40
+ const idx = elementIndex(gb.residual, width);
41
+ return idx ? { name: gb.name, idx } : null;
42
+ }
43
+
44
+ /** The same split ONE step earlier: `&gSym` and the raw BYTE residual added to it, before the
45
+ * division into elements throws the individual terms away. The multidimensional recovery needs
46
+ * the terms — a row index is a term at the row's byte stride. */
47
+ export function globalByteBase(e: Expr): { name: string; residual: Expr } | null {
48
+ const top = addrIn(e);
49
+ if (top) {
50
+ return { name: top.name, residual: { k: 'const', value: 0 } };
51
+ }
52
+ if (e.k === 'bin' && e.op === '+') {
53
+ for (const [side, other] of [
54
+ [e.l, e.r],
55
+ [e.r, e.l],
56
+ ] as const) {
57
+ const addrSide = addrIn(side);
58
+ if (addrSide) {
59
+ return { name: addrSide.name, residual: other };
60
+ }
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+
66
+ // THE one gate on the BARE-NAME array-global spelling (`gSym[i]` rather than `((T *)&gSym)[i]`),
67
+ // shared by the constant-offset and variable-index access paths so the two cannot disagree.
68
+ // Returns the `index` node's `lead` fragment when the bare form is spellable, or null to fall
69
+ // through to the always-valid `&gSym` cast form.
70
+ //
71
+ // Two facts are required, not one. The element WIDTH must match, as it always has. And the RANK
72
+ // must be SPELLABLE, because one subscript reaches an element only on a rank-1 array: on `u16
73
+ // g[4][0x400]`, `g[i]` is a ROW. Against the project's own header that is usually a type error,
74
+ // but where the row address flows into an integer context it is merely a warning and the emitted C
75
+ // then addresses a different object than the asm did — silently.
76
+ //
77
+ // A rank > 1 pins the leading dimensions at 0 and puts the whole flat element index in the last
78
+ // subscript (`g[0][i]`) — the same address arithmetic, and the idiom decomp sources themselves use
79
+ // when the split is not observable in the asm either (`gBgTilemapBufs[0][…]` in kleod,
80
+ // `gNatureStatTable[nature][…]` in pokeemerald). A rank the map states but cannot spell (an unknown
81
+ // inner extent) gets no bare form at all; `((T *)&gSym)[i]` is byte-identical and valid under ANY
82
+ // declaration, which is why it is the safe fallback. See symbols.ts arrayInnerExtents for why an
83
+ // ABSENT rank is read as 1 rather than as unknown.
84
+ export function bareArrayLead(si: SymbolInfo, width: number, signed: boolean): { lead?: Expr[] } | null {
85
+ if (!bareArrayElement(si, width, signed)) {
86
+ return null;
87
+ }
88
+ const inner = arrayInnerExtents(si);
89
+ return inner === null ? null : inner.length === 0 ? {} : { lead: inner.map((): Expr => ({ k: 'const', value: 0 })) };
90
+ }
91
+
92
+ /** The element half of the bare-name rule: the global is an ARRAY whose declared element the
93
+ * access reads WHOLE and with the declared extension. Shared by the rank-pinning fallback above
94
+ * and the declared-subscript recovery below, so the two cannot disagree about what they spell. */
95
+ export function bareArrayElement(si: SymbolInfo, width: number, signed: boolean): boolean {
96
+ if (si.shape !== 'array' || si.elemSize !== width) {
97
+ return false;
98
+ }
99
+ // …and the element must EXTEND the way the access does, for the same reason the width must
100
+ // match: the bare spelling carries no cast, so the declared element type is the only thing in
101
+ // the emitted C that says whether a sub-word read sign- or zero-fills. Against the DECLARED
102
+ // signedness, defaulted exactly as the element type registered for the env is (noteGlobal, just
103
+ // below) — a disagreement there makes the deref legalization wrap the base, and a leading
104
+ // subscript has no room for that wrapping.
105
+ //
106
+ // The caller then falls through to `((T *)&gSym)[i]`, byte-identical under any declaration.
107
+ return !(width < 4 && (si.elemSigned ?? false) !== signed);
108
+ }
109
+
110
+ // ── the DECLARED subscripts, recovered from the address arithmetic ───────────────────────────
111
+ //
112
+ // A rank-2 access the source wrote as `g[r][i]` reaches the element through `&g + r*rowBytes +
113
+ // i*elemSize`, and agbcc leaves those two terms SEPARATE (`lsl #0xb` and `lsl #0x1`, added). The
114
+ // flat spelling `((u16 *)&g)[r*1024 + i]` does not: it scales once (`lsl #0xa` then `lsl #0x1`),
115
+ // so the two are distinguishable in the asm and the term at the declared ROW stride is the
116
+ // evidence that the source named the row. bareArrayLead cannot use it — one subscript is all it
117
+ // spells, so a residual with a row term falls through to `*(T *)(… + (u32)&g)` — and that cast
118
+ // form is what this recovers the subscripts out of.
119
+ //
120
+ // THE EVIDENCE IS THE BYTE RESIDUAL, AND ONLY THE BYTE RESIDUAL — the refusal that keeps the
121
+ // paragraph above from being read backwards. An index ALREADY DIVIDED into elements (what
122
+ // arrayAccess holds, when the asm scaled the whole sum once at the end) is what BOTH spellings
123
+ // reduce to: `(r<<11) + (i<<1)` and `((r<<10) + i) << 1` differ only in where the element scale
124
+ // sits, which is exactly what the division removes. So this runs on the byte residual and
125
+ // arrayAccess does not call it — see the note at that site for what the two spellings measure.
126
+ // `packages/cli/test/matching/array-rank-axis.test.ts` compiles both halves of that.
127
+ //
128
+ // The recovered address is the SAME address either way (C scales `[r]` by the declared row size,
129
+ // which is the constant the arithmetic multiplied by), so this is a spelling, not a re-addressing.
130
+ //
131
+ // AND IT IS AN AXIS, NOT A DEFAULT — the evidence above says the residual carries a ROW, and it
132
+ // does not say which of the two spellings that reach it wrote one. The cast form this replaces
133
+ // (`*(T *)((r<<11) + (i<<1) + (u32)&g)`) compiles to the SAME shift structure and differs only in
134
+ // scheduling on agbcc, kmc and mwcc, and is byte-identical on IDO — where the flat sum is
135
+ // distributed into those same separate scales, so the premise above is a per-compiler fact and not
136
+ // a universal one. `spellDeclaredSubscripts` is the switch and `/flat-rank` the arm; see that
137
+ // option for the compiled table.
138
+
139
+ /** `residual` as a list of additively combined terms with their sign — `a + (b - c)` is
140
+ * `[+a, +b, -c]`. Only `+`/`-` are opened; anything else is one opaque term. */
141
+ function addTerms(e: Expr, sign: 1 | -1, into: { e: Expr; sign: 1 | -1 }[]): void {
142
+ if (e.k === 'bin' && (e.op === '+' || e.op === '-')) {
143
+ addTerms(e.l, sign, into);
144
+ addTerms(e.r, e.op === '+' ? sign : (-sign as 1 | -1), into);
145
+ return;
146
+ }
147
+ into.push({ e, sign });
148
+ }
149
+
150
+ /** `x` when `t` is the NON-CONSTANT value `x` scaled by exactly `stride` (`x * stride` or
151
+ * `x << log2(stride)`), else null. A constant term is never a recovered subscript: both
152
+ * spellings of a constant row index compile identically, so nothing referees the choice. */
153
+ function scaledBy(t: Expr, stride: number): Expr | null {
154
+ if (t.k !== 'bin') {
155
+ return null;
156
+ }
157
+ if (t.op === '<<' && t.r.k === 'const' && t.r.value < 31 && 1 << t.r.value === stride && t.l.k !== 'const') {
158
+ return t.l;
159
+ }
160
+ if (t.op === '*' && t.r.k === 'const' && t.r.value === stride && t.l.k !== 'const') {
161
+ return t.l;
162
+ }
163
+ return null;
164
+ }
165
+
166
+ /** The leading subscripts of `si` recovered from the BYTE `residual`, plus what is left for the
167
+ * last one. The parameter is a byte residual by contract, not by convention: a residual already
168
+ * divided into elements carries no evidence about the row (see the header), so there is no unit
169
+ * to pass and no caller that could pass one.
170
+ *
171
+ * REFUSES (falling back to the caller's existing spelling, which is byte-identical) when: the
172
+ * symbol is not an array of exactly this element; the declared rank is 1 or unspellable; any
173
+ * leading stride is not strictly larger than the one below it (an extent of 1 makes two
174
+ * positions indistinguishable, so the split would be a guess); NO term is a non-constant
175
+ * multiple of a leading stride (there is nothing to recover and today's answer already spells
176
+ * the same address); or what remains does not divide into whole elements.
177
+ *
178
+ * It does NOT refuse an unknown OUTERMOST extent (`dims: [null, 0x400]` — 8 symbols across the
179
+ * six vendored maps), and that is deliberate rather than an omission: only the INNER extents
180
+ * enter a stride, and declare.ts leaves the outermost dimension unsized in the emitted
181
+ * declaration either way (`extern u16 gRows[][1024];` for `[4,1024]` and for `[null,1024]`
182
+ * alike), so the recovered subscripts and the declaration they are read against agree and stride
183
+ * identically. There is therefore no guard on the outermost extent anywhere below, and its
184
+ * absence is the rule rather than a gap in it. */
185
+ export function declaredSubscripts(
186
+ si: SymbolInfo,
187
+ residual: Expr,
188
+ width: number,
189
+ signed: boolean,
190
+ ): { lead: Expr[]; idx: Expr } | null {
191
+ if (!bareArrayElement(si, width, signed)) {
192
+ return null;
193
+ }
194
+ const inner = arrayInnerExtents(si);
195
+ if (inner === null || inner.length === 0) {
196
+ return null;
197
+ }
198
+ const strides: number[] = [];
199
+ for (let p = 0; p < inner.length; p++) {
200
+ strides.push(inner.slice(p).reduce((a, b) => a * b, width));
201
+ }
202
+ for (let p = 0; p < strides.length; p++) {
203
+ if (!Number.isSafeInteger(strides[p]) || strides[p] <= (p + 1 < strides.length ? strides[p + 1] : width)) {
204
+ return null;
205
+ }
206
+ }
207
+ const terms: { e: Expr; sign: 1 | -1 }[] = [];
208
+ addTerms(residual, 1, terms);
209
+ const taken = new Set<number>();
210
+ const lead = strides.map((stride): Expr => {
211
+ for (let i = 0; i < terms.length; i++) {
212
+ const x = taken.has(i) || terms[i].sign !== 1 ? null : scaledBy(terms[i].e, stride);
213
+ if (x) {
214
+ taken.add(i);
215
+ return x;
216
+ }
217
+ }
218
+ return { k: 'const', value: 0 };
219
+ });
220
+ if (taken.size === 0) {
221
+ return null;
222
+ }
223
+ let sum: Expr = { k: 'const', value: 0 };
224
+ for (const [i, t] of terms.entries()) {
225
+ sum = taken.has(i)
226
+ ? sum
227
+ : sum.k === 'const' && sum.value === 0 && t.sign === 1
228
+ ? t.e
229
+ : { k: 'bin', op: t.sign === 1 ? '+' : '-', l: sum, r: t.e };
230
+ }
231
+ const idx = elementIndex(sum, width);
232
+ return idx === null ? null : { lead, idx };
233
+ }
234
+
235
+ // A BYTE residual read as an ELEMENT index of `elemSize`-wide elements, or null when it is not one
236
+ // — the residual then addresses mid-element and no whole-element spelling can express it, so the
237
+ // caller falls through to the honest cast forms. THE one copy of the rule, indexing the
238
+ // `&gSym`-based array spelling: width 1 → the byte residual IS the index; wider → a constant
239
+ // residual must divide exactly, and a non-constant one must already be element-scaled
240
+ // (`i * elemSize` / `i << log2(elemSize)`), which is exactly what the asm's own index scaling
241
+ // produced.
242
+ export function elementIndex(residual: Expr, elemSize: number): Expr | null {
243
+ if (elemSize === 1) {
244
+ return residual;
245
+ }
246
+ if (residual.k === 'const') {
247
+ return residual.value % elemSize === 0 ? { k: 'const', value: residual.value / elemSize } : null;
248
+ }
249
+ if (residual.k === 'bin' && (residual.op === '*' || residual.op === '<<')) {
250
+ const factor =
251
+ residual.op === '<<'
252
+ ? residual.r.k === 'const'
253
+ ? 1 << residual.r.value
254
+ : 0
255
+ : residual.r.k === 'const'
256
+ ? residual.r.value
257
+ : 0;
258
+ if (factor === elemSize) {
259
+ return residual.l;
260
+ }
261
+ }
262
+ return null;
263
+ }
264
+
265
+ /** `idx + n` with a constant fold, `idx` itself for n 0 — the one place an access's memory-operand
266
+ * displacement joins the subscript it was always part of. */
267
+ export function addOffset(idx: Expr, n: number): Expr {
268
+ if (n === 0) {
269
+ return idx;
270
+ }
271
+ return idx.k === 'const'
272
+ ? { k: 'const', value: idx.value + n }
273
+ : { k: 'bin', op: '+', l: idx, r: { k: 'const', value: n } };
274
+ }