@asmlift/core 0.4.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 (87) 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 -164
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +341 -22
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +24 -6
  10. package/src/frontend/opaque.ts +31 -18
  11. package/src/frontend/ppc.ts +54 -7
  12. package/src/frontend/ssa.ts +632 -13
  13. package/src/frontend/thumb.ts +2786 -286
  14. package/src/ir/alias.ts +129 -0
  15. package/src/ir/bits.ts +75 -0
  16. package/src/ir/core.ts +337 -2
  17. package/src/ir/opcodes.ts +156 -27
  18. package/src/ir/parse.ts +19 -2
  19. package/src/ir/print.ts +27 -2
  20. package/src/ir/simplify.ts +190 -3
  21. package/src/ir/struct-names.ts +42 -0
  22. package/src/ir/verify.ts +43 -49
  23. package/src/l3/address.ts +62 -0
  24. package/src/l3/argbase.ts +8 -2
  25. package/src/l3/ast.ts +464 -49
  26. package/src/l3/basecse.ts +709 -88
  27. package/src/l3/coalesce.ts +521 -66
  28. package/src/l3/dce.ts +54 -19
  29. package/src/l3/gates.ts +88 -0
  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 +113 -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 +110 -85
  42. package/src/l3/reindex.ts +715 -78
  43. package/src/l3/scopebase.ts +649 -219
  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 +23 -4
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +263 -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 +236 -13
  57. package/src/pipeline.ts +206 -49
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/divpow2.ts +4 -3
  61. package/src/raise/globalshape.ts +1038 -0
  62. package/src/raise/gvn.ts +44 -19
  63. package/src/raise/latch.ts +126 -0
  64. package/src/raise/memberarrays.ts +594 -0
  65. package/src/raise/narrow.ts +124 -0
  66. package/src/raise/narrowlocal.ts +556 -0
  67. package/src/raise/paramwidth.ts +179 -0
  68. package/src/raise/pre-recovery.ts +101 -16
  69. package/src/raise/recover.ts +56 -23
  70. package/src/raise/retsink.ts +215 -14
  71. package/src/raise/shortcircuit.ts +477 -79
  72. package/src/raise/struct-arrays.ts +21 -3
  73. package/src/raise/structs.ts +61 -3
  74. package/src/rank-axes.ts +630 -0
  75. package/src/rank-declare.ts +256 -0
  76. package/src/rank.ts +1726 -251
  77. package/src/structure/analysis.ts +1516 -220
  78. package/src/structure/bitfields.ts +332 -0
  79. package/src/structure/globalaccess.ts +274 -0
  80. package/src/structure/hazards.ts +411 -20
  81. package/src/structure/loops.ts +2 -49
  82. package/src/structure/namecoalesce.ts +435 -0
  83. package/src/structure/structure.ts +2850 -533
  84. package/src/structure/switch-recover.ts +688 -147
  85. package/src/symbols.ts +62 -1
  86. package/src/target.ts +367 -24
  87. package/src/trace.ts +111 -32
@@ -0,0 +1,193 @@
1
+ // L3 re-spelling lever: DELETE a pointer local holding a CONSTANT address and spell each access
2
+ // through it as the cast constant (`*(u16 *)0x4000208` rather than `p = (u16 *)0x4000208; *p`).
3
+ //
4
+ // The local is structure/analysis.ts's value-home spelling for a `const` with 2+ consumers that
5
+ // is LIVE ACROSS A CALL: a value the compiler needs after a call survives in a callee-saved
6
+ // register, and a named local reproduces that register. The machine fact is real — agbcc does
7
+ // park the address in `r4` across the calls — but it does NOT imply the source named anything:
8
+ // a constant re-spelled at each use is CSEd into the same one register. So the asm
9
+ // underdetermines the spelling, and this is the differ-refereed other side of it.
10
+ //
11
+ // It is codegen-visible, which is why both sides have to be enumerated rather than one picked:
12
+ // the extra `p = …;` statement is scheduled ahead of the rest of the entry block, so the pool
13
+ // load moves in front of the frame-address materialization the target emits first
14
+ // (pokeemerald:EReader_Reset, agbcc 2.9-arm-000512 — 1 insert + 1 delete, the whole residual).
15
+ //
16
+ // SEMANTICS ARE PRESERVED BY CONSTRUCTION: this is constant propagation of a local that is
17
+ // assigned once, from a compile-time constant, before anything mentions it, and whose address is
18
+ // never taken — so every use reads that constant on every path, and the substituted expression
19
+ // carries the local's own declared type AND its pointee volatility.
20
+ //
21
+ // THE QUALIFIER TRAVELS WITH THE ADDRESS. The deleted local is the only place a `volatile`
22
+ // pointee could be written, and a raw address is precisely the case with no declaration
23
+ // anywhere else to carry it — so dropping it here spells an MMIO access non-volatile in the one
24
+ // place the differ sometimes cannot referee. On pokeemerald:EReader_Reset the two spellings
25
+ // separate at 11 against 12 on their own, and are BYTE-IDENTICAL once the slot qualifier is
26
+ // there too — the shape that matches (agbcc 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit
27
+ // -fhex-asm -fprologue-bugfix`; `.s` diff empty, `.o` identical under cmp). So rank.ts emits
28
+ // the qualified spelling as a second OUTPUT of this lever, `/volatile` narrowed to the locals
29
+ // it deletes. Each cast this mints carries the qualifier; a use whose width does not stride the
30
+ // declared pointee renders through the C-family printer's reinterpret cast instead, which
31
+ // carries it too (backend/cfamily.ts) — in C the access takes the OUTER type, so a plain cast
32
+ // there would spell exactly the silent drop this paragraph exists to prevent.
33
+ //
34
+ // GATE (INLINEBASE_GATES) — the local must be all of: pointer-typed; initialized by a bare
35
+ // NONZERO `const` (a `(T *)base` CAST initializer is l3/basecse.ts's reuse hoist, whose own lever
36
+ // family owns that question; `0` is NULL, never an address, which is also the sibling qualifier
37
+ // lever's rule); assigned exactly once, by a statement at the body's TOP LEVEL that no earlier
38
+ // statement's mention precedes; never address-taken; used only as the base of an `index`, at 2+
39
+ // sites (one use is not the reused address this exists for); not object-`volatile` (a
40
+ // `T *volatile p` has no inhabitant, and cfamily.ts prints that flag in the pointee's position);
41
+ // and not `frame` (a slot is an asm fact — see the SFn.locals doc). Anything else, and nothing
42
+ // qualifying at all, DECLINES (null) rather than approximating.
43
+ //
44
+ // A RE-SPELLING RATHER THAN A STRUCTURING AXIS, which is a cost choice and not the
45
+ // underdetermination one (docs/level-tower.md, "a third fork sits inside the ranked population").
46
+ // The question — does a `const` with 2+ consumers live across a call in a named local — is the one
47
+ // `/reread-globals`, `/addr-home`, `/expr-home` and `/derived-home` each answer as a
48
+ // STRUCTURING_AXES entry. An axis here would double the enumeration on every function it admits;
49
+ // substituting on the already-homed tree costs 766 candidates over 47058 (+1.6%) across the 33 of
50
+ // 69 klonoa functions that lift with no symbol map.
51
+ //
52
+ // KNOWN GAP, and it is the price of that choice rather than an oversight: only `index` bases are
53
+ // re-spelled, so the same L2 home passed to a callee or standing as a `field` base is out of
54
+ // reach — `otherUses` refuses it. Reaching those needs the un-homed tree, which is
55
+ // structure/analysis.ts's decision; the day a row demands one, this becomes the axis.
56
+ //
57
+ // The idiom it recovers is every GBA project's register macro: `*(vu16 *)0x4000208` is what
58
+ // `REG_IME` expands to, so the deleted local is not merely an undone home.
59
+ import { type Expr, type SFn, mapExprChildren, mapStmtExprs } from './ast';
60
+ import { type Gate, firstRejection } from './gates';
61
+ import { type Mentions, localMentions } from './mentions';
62
+
63
+ /** One local as the gates read it. */
64
+ interface BaseCtx {
65
+ isPointer: boolean;
66
+ objectVolatile: boolean;
67
+ hasFrame: boolean;
68
+ m: Mentions;
69
+ }
70
+
71
+ export const INLINEBASE_GATES: readonly Gate<BaseCtx>[] = [
72
+ {
73
+ id: 'non-pointer',
74
+ why: 'the lever re-spells an address; a scalar value home is a different question',
75
+ sound: false,
76
+ rejects: (c) => !c.isPointer,
77
+ },
78
+ {
79
+ id: 'object-volatile',
80
+ why: 'the substitution carries the POINTEE flag, so an object-volatile pointer would lose its own',
81
+ sound: true,
82
+ guardedBy: 'inlinebase.test.ts: an object-volatile or frame local declines',
83
+ rejects: (c) => c.objectVolatile,
84
+ },
85
+ {
86
+ id: 'frame',
87
+ why: 'a slot the asm materialized is an asm fact, not a spelling to undo',
88
+ sound: false,
89
+ rejects: (c) => c.hasFrame,
90
+ },
91
+ {
92
+ id: 'multi-assign',
93
+ why: 'a name assigned more than once is not one constant',
94
+ sound: true,
95
+ guardedBy: 'inlinebase.test.ts: a second assignment means the name is not one constant',
96
+ rejects: (c) => c.m.assigns !== 1,
97
+ },
98
+ {
99
+ id: 'const-init',
100
+ why: 'only a bare `const` at the body’s top level is an address available on every path',
101
+ sound: true,
102
+ guardedBy: 'inlinebase.test.ts: an assignment below the top level may not run on every path',
103
+ rejects: (c) => c.m.topAssignAt === null || c.m.constValue === null,
104
+ },
105
+ {
106
+ id: 'null-base',
107
+ why: '`0` is NULL, never an address — the sibling qualifier lever (volatileptr.ts) refuses it too',
108
+ sound: false,
109
+ rejects: (c) => c.m.constValue === 0,
110
+ },
111
+ {
112
+ id: 'use-before-assign',
113
+ why: 'a mention ahead of the assignment reads something the constant does not stand for',
114
+ sound: true,
115
+ guardedBy: 'inlinebase.test.ts: a use in a loop ABOVE the assignment reads the local before it is set',
116
+ rejects: (c) => c.m.firstAt !== c.m.topAssignAt,
117
+ },
118
+ {
119
+ id: 'addr-taken',
120
+ why: 'a deleted local has no address to take',
121
+ sound: true,
122
+ guardedBy: 'inlinebase.test.ts: an address-taken local has an identity the constant cannot stand in for',
123
+ rejects: (c) => c.m.addrTaken !== 0,
124
+ },
125
+ {
126
+ id: 'other-uses',
127
+ why: 'a use the substitution cannot reach would name the deleted local',
128
+ sound: true,
129
+ guardedBy: 'inlinebase.test.ts: a use that is not an `index` base is outside what the lever re-spells',
130
+ rejects: (c) => c.m.otherUses !== 0,
131
+ },
132
+ {
133
+ id: 'single-use',
134
+ why: 'one use is not the reused address this lever exists for',
135
+ sound: false,
136
+ rejects: (c) => c.m.baseUses < 2,
137
+ },
138
+ ];
139
+
140
+ /** The locals INLINEBASE_GATES admits, each with the cast its uses become. */
141
+ function plan(sfn: SFn): Map<string, Extract<Expr, { k: 'cast' }>> {
142
+ const t = localMentions(sfn);
143
+ const out = new Map<string, Extract<Expr, { k: 'cast' }>>();
144
+ for (const l of sfn.locals) {
145
+ const m = t.get(l.name);
146
+ if (
147
+ m === undefined ||
148
+ firstRejection(INLINEBASE_GATES, {
149
+ isPointer: l.type.kind === 'ptr',
150
+ objectVolatile: l.volatile !== undefined,
151
+ hasFrame: l.frame !== undefined,
152
+ m,
153
+ }) !== null
154
+ ) {
155
+ continue;
156
+ }
157
+ out.set(l.name, {
158
+ k: 'cast',
159
+ to: l.type,
160
+ ...(l.pointeeVolatile ? { volatile: true as const } : {}),
161
+ e: { k: 'const', value: m.constValue! },
162
+ });
163
+ }
164
+ return out;
165
+ }
166
+
167
+ /** Which locals this lever would delete — rank.ts narrows `/volatile` to exactly these before
168
+ * pairing, so the qualified output never qualifies a pointer the lever leaves standing. */
169
+ export function inlinableConstBases(sfn: SFn): string[] {
170
+ return [...plan(sfn).keys()];
171
+ }
172
+
173
+ /** The `/inlinebase` candidate, or null when no local qualifies. Read-only: returns a fresh SFn
174
+ * whose body is rebuilt, leaving the input untouched. */
175
+ export function inlineConstBases(sfn: SFn): SFn | null {
176
+ const inline = plan(sfn);
177
+ if (inline.size === 0) {
178
+ return null;
179
+ }
180
+ // A FRESH node per use — never one shared tree — because identity-keyed rules downstream
181
+ // (contracts.ts's dot-base exemption) read node identity.
182
+ const sub = (e: Expr): Expr => {
183
+ if (e.k === 'var') {
184
+ const at = inline.get(e.name);
185
+ if (at !== undefined) {
186
+ return { ...at, e: { ...at.e } };
187
+ }
188
+ }
189
+ return mapExprChildren(e, sub);
190
+ };
191
+ const body = sfn.body.filter((s) => !(s.k === 'assign' && inline.has(s.name))).map((s) => mapStmtExprs(s, sub));
192
+ return { ...sfn, locals: sfn.locals.filter((l) => !inline.has(l.name)), body };
193
+ }
@@ -0,0 +1,113 @@
1
+ // What one L3 tree does to each of its locals, counted once.
2
+ //
3
+ // Levers ask overlapping versions of the question and would disagree if each walked the tree
4
+ // its own way: l3/inlinebase.ts needs the SHAPE of every use (only an `index` base is re-spellable,
5
+ // and the single assignment must be a top-level `const` nothing mentions earlier), while
6
+ // l3/volatileval.ts needs the COUNTS, to check the tree still performs every access the machine
7
+ // did before it declares them all observable. One walk answers both, and it has to: on the counting
8
+ // consumer a miscount is a wrong `volatile` claim, not a missed candidate.
9
+ //
10
+ // Derived from the ONE traversal vocabulary (exprChildren/stmtExprs/stmtChildren) for every node
11
+ // kind, so a new one is a compile error there rather than a silent undercount here — with `index`
12
+ // as the ONE hand-rolled case, because the callback needs to know which child stands as the base
13
+ // and `exprChildren` flattens that away. That hand-rolling is a standing hazard rather than an
14
+ // oversight: a POSITION added to `index` reaches the generic vocabulary for free and this walk not
15
+ // at all. Every position is enumerated below and pinned by
16
+ // test/array-rank-guards.test.ts, beside the generic helpers it cannot speak for.
17
+ import { type Expr, type SFn, type Stmt, exprChildren, stmtChildren, stmtExprs } from './ast';
18
+
19
+ export interface Mentions {
20
+ /** assignments to the name, at any nesting */
21
+ assigns: number;
22
+ /** body-top-level index of its single top-level assignment, or null */
23
+ topAssignAt: number | null;
24
+ /** the bare-`const` value that assignment stores, or null if it stores anything else */
25
+ constValue: number | null;
26
+ addrTaken: number;
27
+ /** uses as the `base` of an `index` node — the only use shape a base lever can re-spell */
28
+ baseUses: number;
29
+ /** every other read */
30
+ otherUses: number;
31
+ /** body-top-level index of the first statement mentioning the name at all */
32
+ firstAt: number | null;
33
+ }
34
+
35
+ /** reads of the name, however spelled */
36
+ export function readsOf(m: Mentions): number {
37
+ return m.baseUses + m.otherUses;
38
+ }
39
+
40
+ const blank = (): Mentions => ({
41
+ assigns: 0,
42
+ topAssignAt: null,
43
+ constValue: null,
44
+ addrTaken: 0,
45
+ baseUses: 0,
46
+ otherUses: 0,
47
+ firstAt: null,
48
+ });
49
+
50
+ /** Visit every node, telling the callback whether it stands as an `index`'s base. */
51
+ function walkExpr(e: Expr, visit: (x: Expr, isIndexBase: boolean) => void, isIndexBase = false): void {
52
+ visit(e, isIndexBase);
53
+ if (e.k === 'index') {
54
+ walkExpr(e.base, visit, true);
55
+ // `lead` — a multidimensional global's LEADING subscripts — is an ordinary value position, so
56
+ // a name mentioned there is a real read. Missing it does not cost a candidate: it lets a lever
57
+ // DELETE a local the body still names.
58
+ for (const l of e.lead ?? []) {
59
+ walkExpr(l, visit, false);
60
+ }
61
+ walkExpr(e.idx, visit, false);
62
+ return;
63
+ }
64
+ for (const c of exprChildren(e)) {
65
+ walkExpr(c, visit, false);
66
+ }
67
+ }
68
+
69
+ /** Every mention of every local, keyed by name. Locals only — a param or a global name is not in
70
+ * the map, and a lever asking about one gets `undefined` rather than a zeroed record. */
71
+ export function localMentions(sfn: SFn): Map<string, Mentions> {
72
+ const t = new Map<string, Mentions>(sfn.locals.map((l) => [l.name, blank()]));
73
+ const seen = (name: string, at: number): Mentions | undefined => {
74
+ const m = t.get(name);
75
+ if (m && m.firstAt === null) {
76
+ m.firstAt = at;
77
+ }
78
+ return m;
79
+ };
80
+ const stmt = (s: Stmt, at: number, top: boolean): void => {
81
+ if (s.k === 'assign') {
82
+ const m = seen(s.name, at);
83
+ if (m) {
84
+ m.assigns++;
85
+ if (top) {
86
+ m.topAssignAt = at;
87
+ m.constValue = s.value.k === 'const' ? s.value.value : null;
88
+ }
89
+ }
90
+ }
91
+ for (const e of stmtExprs(s)) {
92
+ walkExpr(e, (x, isIndexBase) => {
93
+ if (x.k === 'var' || x.k === 'addr') {
94
+ const m = seen(x.name, at);
95
+ if (m) {
96
+ if (x.k === 'addr') {
97
+ m.addrTaken++;
98
+ } else if (isIndexBase) {
99
+ m.baseUses++;
100
+ } else {
101
+ m.otherUses++;
102
+ }
103
+ }
104
+ }
105
+ });
106
+ }
107
+ for (const c of stmtChildren(s)) {
108
+ stmt(c, at, false);
109
+ }
110
+ };
111
+ sfn.body.forEach((s, i) => stmt(s, i, true));
112
+ return t;
113
+ }
@@ -0,0 +1,42 @@
1
+ // L3 re-spelling lever: put the PRODUCT operand first in a commutative `+`.
2
+ //
3
+ // structure.ts's def-order rule spells commutative operands in EVALUATION order, which recovers
4
+ // gcc's left-to-right source order. IDO and mwcc break the correspondence for exactly one shape:
5
+ // in `a*b + c` they SCHEDULE the independent load of `c` above the product's `mflo`/`mullw`, so
6
+ // the machine add reads (c, product) and def order re-spells the source's product-first sum as
7
+ // c-first. Which order the source used is not recoverable from positions there — so this lever
8
+ // emits the product-first sibling and the differ referees (verified byte-identical against IDO
9
+ // on the bg_area row; the def-order spelling stays in the list for sources that really were
10
+ // c-first).
11
+ //
12
+ // SCOPE (decline over approximate): a `+` is flipped only when exactly ONE side is a product
13
+ // (`bin('*')` at the root, casts looked through) — two products or none leave nothing to anchor
14
+ // the flip on. A side carrying an effect (a call, a marker) never moves — evaluation order of
15
+ // the operands is what the lever edits. Declines (null) when no `+` changes, so no duplicate
16
+ // candidate.
17
+ import type { Expr, SFn } from './ast';
18
+ import { exprHasEffect, mapExprChildren, mapStmtExprs } from './ast';
19
+
20
+ const isProduct = (e: Expr): boolean =>
21
+ e.k === 'bin' && e.op === '*' ? true : e.k === 'cast' ? isProduct(e.e) : false;
22
+
23
+ export function mulFirstSums(sfn: SFn): SFn | null {
24
+ let changed = false;
25
+ const rewrite = (e: Expr): Expr => {
26
+ const m = mapExprChildren(e, rewrite);
27
+ if (
28
+ m.k === 'bin' &&
29
+ m.op === '+' &&
30
+ isProduct(m.r) &&
31
+ !isProduct(m.l) &&
32
+ !exprHasEffect(m.l) &&
33
+ !exprHasEffect(m.r)
34
+ ) {
35
+ changed = true;
36
+ return { ...m, l: m.r, r: m.l };
37
+ }
38
+ return m;
39
+ };
40
+ const body = sfn.body.map((s) => mapStmtExprs(s, rewrite));
41
+ return changed ? { ...sfn, body } : null;
42
+ }
@@ -0,0 +1,152 @@
1
+ // L3 re-spelling lever: NEIGHBOR absolute addresses derive from one shared base local.
2
+ //
3
+ // A cluster of raw-address accesses a few bytes apart is one object's cells: the compiler holds
4
+ // the object's base in a register and derives each cell (`add #72` / `add #74` off one pool
5
+ // word, a halfword offset beyond the load range forcing the add, the in-range word staying
6
+ // `[rN, #112]`), where a per-cell spelling anchors one pool constant per address. This lever
7
+ // re-spells every deref base in a cluster as an offset from a `u8 *` base local holding the
8
+ // cluster's lowest address, and the differ referees:
9
+ //
10
+ // *(u16 *)0x0300104A → u8 *b = (u8 *)0x03001048; *(u16 *)(b + 2)
11
+ //
12
+ // SCOPE (decline over approximate): cluster MEMBERSHIP comes from CONST deref bases only (a
13
+ // struct-pointer cast base and everything inside a dot-form field subtree keep their spelling —
14
+ // their stride is the struct's, not a byte's); a cluster needs at least two DISTINCT addresses
15
+ // within the target's declared derivation reach of its lowest (TargetDescription nearBaseSpan —
16
+ // beyond it the derive costs more than the pool word it saves); every access the walk visits
17
+ // rewrites, so a cluster splits only across the field-subtree and struct-pointer-cast
18
+ // boundaries. A member basecse
19
+ // already hoisted arrives as a `var` base and is invisible here — the reused-base and
20
+ // neighbor-base spellings stay separate candidates. Once a cluster HAS formed, a bare const
21
+ // VALUE inside its window re-spells too, as `(s32)(b + off)` — the address of a cell handed to
22
+ // something (a DMA source register) is the same derived add in the original, and the two
23
+ // spellings are value-equal by construction, so the differ referees — including an integer that
24
+ // only coincidentally lands in the window, which is the stated cost of the lever (the `s32` cast
25
+ // assumes addresses below 2^31, true of every target that declares nearBaseSpan today). Declines
26
+ // (null) when no cluster forms.
27
+ import { baseConst } from './address';
28
+ import type { Expr, SFn } from './ast';
29
+ import { mapExprChildren, mapStmtExprs } from './ast';
30
+ import { type BaseInit, nameAllocator, placeBaseLocals } from './hoist';
31
+
32
+ /** `span` is the target's single-add-immediate derivation reach
33
+ * (TargetDescription.compilerBehaviors.nearBaseSpan) — a target that declares none never runs
34
+ * this lever. */
35
+ export function nearBaseClusters(sfn: SFn, span: number): SFn | null {
36
+ if (!Number.isFinite(span) || span < 0) {
37
+ return null; // a hostile span stalls the cluster window instead of shrinking it
38
+ }
39
+ // collect every DISTINCT const deref-base address
40
+ const addrs = new Set<number>();
41
+ const collect = (e: Expr): Expr => {
42
+ if (e.k === 'field') {
43
+ return e; // a dot-form subtree keeps its struct base — never collected, never rewritten
44
+ }
45
+ if (e.k === 'cast' && e.to.kind === 'ptr' && e.to.to.kind === 'struct') {
46
+ return e; // rewrite refuses these subtrees, so collecting under them would seed a cluster
47
+ }
48
+ const m = mapExprChildren(e, collect);
49
+ if (m.k === 'index') {
50
+ const c = baseConst(m.base);
51
+ if (c !== null) {
52
+ addrs.add(c);
53
+ }
54
+ }
55
+ return m;
56
+ };
57
+ for (const s of sfn.body) {
58
+ mapStmtExprs(s, collect);
59
+ }
60
+ // greedy clusters over the sorted addresses; only multi-member clusters rewrite
61
+ const sorted = [...addrs].sort((a, b) => a - b);
62
+ const baseOf = new Map<number, number>();
63
+ for (let i = 0; i < sorted.length;) {
64
+ const lo = sorted[i];
65
+ let j = i;
66
+ while (j < sorted.length && sorted[j] - lo <= span) {
67
+ j++;
68
+ }
69
+ if (j - i >= 2) {
70
+ for (let k = i; k < j; k++) {
71
+ baseOf.set(sorted[k], lo);
72
+ }
73
+ }
74
+ i = j;
75
+ }
76
+ if (baseOf.size === 0) {
77
+ return null;
78
+ }
79
+ const baseName = new Map<number, string>();
80
+ const fresh = nameAllocator(sfn); // the shared minting mechanism — collides with nothing in sfn
81
+ for (const lo of new Set(baseOf.values())) {
82
+ baseName.set(lo, fresh());
83
+ }
84
+ const derived = (lo: number, off: number): Expr =>
85
+ off === 0
86
+ ? { k: 'var', name: baseName.get(lo)! }
87
+ : { k: 'bin', op: '+', l: { k: 'var', name: baseName.get(lo)! }, r: { k: 'const', value: off } };
88
+ // the cluster (if any) whose window covers a bare const value
89
+ const windows = [...new Set(baseOf.values())];
90
+ const coveringLo = (v: number): number | undefined => windows.find((lo) => v >= lo && v - lo <= span);
91
+ const rewrite = (e: Expr): Expr => {
92
+ if (e.k === 'field') {
93
+ return e;
94
+ }
95
+ if (e.k === 'index') {
96
+ const c = baseConst(e.base);
97
+ const lo = c !== null ? baseOf.get(c) : undefined;
98
+ if (c !== null && lo !== undefined) {
99
+ // the base is replaced wholesale — its inner const must not reach the value path below
100
+ return { ...e, base: derived(lo, c - lo), idx: rewrite(e.idx) };
101
+ }
102
+ return { ...e, base: rewrite(e.base), idx: rewrite(e.idx) };
103
+ }
104
+ if (e.k === 'cast' && e.to.kind === 'ptr' && e.to.to.kind === 'struct') {
105
+ return e; // the struct-arrays base keeps its spelling — same refusal as baseConst's
106
+ }
107
+ if (e.k === 'const') {
108
+ const lo = coveringLo(e.value);
109
+ if (lo !== undefined) {
110
+ return {
111
+ k: 'cast',
112
+ to: { kind: 'int', width: 32, signed: true },
113
+ e: derived(lo, e.value - lo),
114
+ };
115
+ }
116
+ return e;
117
+ }
118
+ return mapExprChildren(e, rewrite);
119
+ };
120
+ const inits: BaseInit[] = [...baseName.entries()].map(([lo, name]) => ({
121
+ k: 'assign',
122
+ name,
123
+ value: {
124
+ k: 'cast',
125
+ to: { kind: 'ptr', to: { kind: 'int', width: 8, signed: false } },
126
+ e: { k: 'const', value: lo },
127
+ },
128
+ }));
129
+ const locals = [
130
+ ...sfn.locals,
131
+ ...[...baseName.values()].map((name) => ({
132
+ name,
133
+ type: { kind: 'ptr', to: { kind: 'int', width: 8, signed: false } } as SFn['locals'][number]['type'],
134
+ })),
135
+ ];
136
+ // The body rebuild is `l3/hoist.ts`'s, shared with the two other passes that place into the
137
+ // leading base-init run. The ORDERING is not: `prepend` returns before the first-use query, so
138
+ // this pass takes the rebuild and abstains from the policy (see `BaseInitPlacement`).
139
+ //
140
+ // The DEFAULT is `prepend` — the cluster bases go ABOVE a run already there rather than being
141
+ // merged into it in first-use order — and it rests on a row, not on a compiler fact. Placing
142
+ // them in first-use order instead turns `synthetic:dmafield` (won by
143
+ // `signed/livebase/volatile/nearbase/initfirst`) from a MATCH into diff:5, measured 2026-08-26.
144
+ // The reading that goes with it — a cluster base is reached at 2+ addresses by construction, so
145
+ // its pool word is not "first touched late" — explains why first-use order is not obviously
146
+ // right, not why prepending is; which order the source wrote is per-function knowledge the asm
147
+ // does not carry. So it is a DEFAULT and not a decision: `rank.ts` offers the sunk ordering
148
+ // beside it as `/nearbase/sinkinit`, and the differ settles which one a function wanted.
149
+ const rewritten = sfn.body.map((s) => mapStmtExprs(s, rewrite));
150
+ const { body } = placeBaseLocals({ ...sfn, locals, body: rewritten }, inits, 'prepend');
151
+ return { ...sfn, locals, body };
152
+ }