@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.
- package/README.md +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +270 -171
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +243 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +78 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +345 -2
- package/src/ir/opcodes.ts +176 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -5
- package/src/l3/ast.ts +510 -59
- package/src/l3/basecse.ts +686 -78
- package/src/l3/coalesce.ts +432 -46
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +96 -1
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +176 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +114 -89
- package/src/l3/reindex.ts +722 -80
- package/src/l3/scopebase.ts +649 -220
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +16 -1
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +687 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +239 -16
- package/src/pipeline.ts +173 -60
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +4 -4
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +1058 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +572 -0
- package/src/raise/paramwidth.ts +201 -0
- package/src/raise/pre-recovery.ts +169 -21
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +585 -19
- package/src/raise/shortcircuit.ts +1050 -89
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +34 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank-variations.ts +760 -0
- package/src/rank.ts +2122 -326
- package/src/structure/analysis.ts +1398 -150
- package/src/structure/bitfields.ts +432 -0
- package/src/structure/globalaccess.ts +300 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +454 -0
- package/src/structure/structure.ts +3979 -612
- package/src/structure/switch-recover.ts +710 -145
- package/src/symbols.ts +188 -6
- package/src/target.ts +495 -32
- package/src/trace.ts +112 -33
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// L3 respell variation: 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 variation, `/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 variation
|
|
36
|
+
// family owns that question; `0` is NULL, never an address, which is also the sibling qualifier
|
|
37
|
+
// variation'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 RESPELL VARIATION RATHER THAN A STRUCTURE VARIATION, which is a cost decision and not the
|
|
45
|
+
// underdetermination one (docs/level-tower.md, "a third fork sits inside the ranked variations").
|
|
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
|
+
// STRUCTURE_VARIATIONS entry. A structure variation 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 a structure variation.
|
|
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: 'only a local holding an address is substituted; a local holding a scalar value is a different question',
|
|
75
|
+
sound: false,
|
|
76
|
+
rejects: (c) => !c.isPointer,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'object-volatile',
|
|
80
|
+
why: 'the substitution keeps only the `volatile` of what the pointer points to, so a pointer that is itself volatile 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 stack slot the assembly used is a fact about the function, 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: 'a base of `0` is a null pointer, never an address, and `volatile` 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 variation 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 variation 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 variation would delete — rank.ts narrows `/volatile` to exactly these before
|
|
168
|
+
* pairing, so the qualified output never qualifies a pointer the variation 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,176 @@
|
|
|
1
|
+
// What one L3 tree does to each of its locals, counted once.
|
|
2
|
+
//
|
|
3
|
+
// Respell variations 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, walkExprs } 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-hoisting variation 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 variation
|
|
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 pass 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
|
+
}
|
|
114
|
+
|
|
115
|
+
/** THE ONE WALK behind `mentionsAnyLocal` and `mentionedLocals` below: which of `names` anything
|
|
116
|
+
* under `stmts` still NAMES — as an assignment TARGET (which carries no expression, so no walk
|
|
117
|
+
* over values can see it), as a read, or as an address. `first` returns at the first hit it
|
|
118
|
+
* reaches, which is all the boolean caller needs.
|
|
119
|
+
*
|
|
120
|
+
* The question a pass that DELETES a declaration has to answer, and it lives here rather than in
|
|
121
|
+
* the deleting pass for the reason this file's header states about its own walk: a second walk
|
|
122
|
+
* over the node vocabulary is how a new node kind becomes a silent undercount, and beside
|
|
123
|
+
* `localMentions` a divergence is at least visible. This one is answered over a SUBTREE, so it
|
|
124
|
+
* cannot be derived from the counts above — `localMentions` is keyed to `sfn.locals` across the
|
|
125
|
+
* whole body, and l3/unmerge.ts's whole point is that those counts are sampled before any
|
|
126
|
+
* rewriting and go stale.
|
|
127
|
+
*
|
|
128
|
+
* TOTAL over the vocabulary by construction: `assign` is the only `Stmt` carrying a bare name and
|
|
129
|
+
* `var`/`addr` the only `Expr`s, and both walks are derived from `stmtChildren`/`stmtExprs` — so a
|
|
130
|
+
* `for`'s init and inc, a `switch`'s scrutinee, its cases and its default are all covered. */
|
|
131
|
+
function scanMentions(stmts: readonly Stmt[], names: ReadonlySet<string>, first: boolean): Set<string> {
|
|
132
|
+
// TWO FLAT SWEEPS, not one expression walk per nesting level. `walkExprs` already descends
|
|
133
|
+
// `stmtChildren` (ast.ts), so calling it per statement from inside a recursion that ALSO
|
|
134
|
+
// descends re-walks every nested expression once per enclosing level — quadratic in the nesting
|
|
135
|
+
// depth, 301 `has` calls at depth 24 where one pass needs 25. Small at today's call sites (one
|
|
136
|
+
// rewritten subtree per un-merge site, one dropped-locals set per respelled tree), but this is a
|
|
137
|
+
// SHARED helper and its cost belongs in its contract.
|
|
138
|
+
const found = new Set<string>();
|
|
139
|
+
const body = [...stmts] as Stmt[];
|
|
140
|
+
const stack: Stmt[] = [...body];
|
|
141
|
+
while (stack.length > 0) {
|
|
142
|
+
const s = stack.pop()!;
|
|
143
|
+
if (s.k === 'assign' && names.has(s.name)) {
|
|
144
|
+
found.add(s.name);
|
|
145
|
+
if (first) {
|
|
146
|
+
return found;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
stack.push(...stmtChildren(s));
|
|
150
|
+
}
|
|
151
|
+
for (const e of walkExprs(body)) {
|
|
152
|
+
if ((e.k === 'var' || e.k === 'addr') && names.has(e.name)) {
|
|
153
|
+
found.add(e.name);
|
|
154
|
+
if (first) {
|
|
155
|
+
return found;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return found;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** true when anything under `stmts` still names one of `names`. See `scanMentions` above. */
|
|
163
|
+
export function mentionsAnyLocal(stmts: readonly Stmt[], names: ReadonlySet<string>): boolean {
|
|
164
|
+
return scanMentions(stmts, names, true).size > 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** WHICH of `names` the tree still mentions — the same walk `mentionsAnyLocal` answers "any" over,
|
|
168
|
+
* told to keep going, for the caller that has to NAME the survivors.
|
|
169
|
+
*
|
|
170
|
+
* It exists so that contracts.ts's `assertNoOrphanedLocals` — the loud backstop for exactly the
|
|
171
|
+
* mistake this predicate guards — does not carry a THIRD hand-rolled copy of the node vocabulary.
|
|
172
|
+
* The boolean cannot serve it (the diagnostic needs the set) and one call per dropped name would
|
|
173
|
+
* be a walk per name. */
|
|
174
|
+
export function mentionedLocals(stmts: readonly Stmt[], names: ReadonlySet<string>): Set<string> {
|
|
175
|
+
return scanMentions(stmts, names, false);
|
|
176
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// L3 respell variation: 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 variation
|
|
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 variation 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 respell variation: 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 variation
|
|
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 variation (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 variation. */
|
|
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
|
+
}
|