@asmlift/core 0.3.0 → 0.4.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 +5 -3
- package/package.json +1 -1
- package/src/backend/cfamily.ts +125 -2
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +15 -2
- package/src/declare.ts +35 -9
- package/src/frontend/mips.ts +24 -23
- package/src/frontend/opaque.ts +39 -2
- package/src/frontend/ssa.ts +32 -53
- package/src/frontend/thumb.ts +301 -26
- package/src/ir/opcodes.ts +44 -0
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +216 -0
- package/src/l3/ast.ts +118 -4
- package/src/l3/basecse.ts +3 -40
- package/src/l3/coalesce.ts +146 -0
- package/src/l3/dce.ts +2 -23
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +436 -0
- package/src/l3/tailmerge.ts +120 -0
- package/src/macros.ts +222 -13
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +5 -2
- package/src/raise/divpow2.ts +226 -0
- package/src/raise/gvn.ts +141 -0
- package/src/raise/pre-recovery.ts +37 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +36 -7
- package/src/raise/shortcircuit.ts +264 -22
- package/src/raise/structs.ts +12 -2
- package/src/rank.ts +172 -20
- package/src/structure/analysis.ts +42 -1
- package/src/structure/structure.ts +399 -31
- package/src/structure/switch-recover.ts +21 -3
- package/src/symbols.ts +128 -13
- package/src/target.ts +4 -2
- package/src/trace.ts +9 -0
package/src/l3/dce.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// unresolved `?` value trips the contract first and never reaches DCE; `mustKeep` treating `?` as
|
|
23
23
|
// keep is defense-in-depth for any future caller that skips that check.
|
|
24
24
|
import type { Expr, SFn, Stmt } from './ast';
|
|
25
|
-
import { exprChildren, stmtChildren, stmtExprs } from './ast';
|
|
25
|
+
import { exprChildren, negateCond, stmtChildren, stmtExprs } from './ast';
|
|
26
26
|
|
|
27
27
|
/** Accumulate every LOCAL-eligible `var` name read anywhere in `e` (recurses all sub-exprs). An
|
|
28
28
|
* `addr` node names a global, not a local, so it is not a local read. */
|
|
@@ -70,27 +70,6 @@ function allReadsInto(stmts: Stmt[], out: Set<string>): void {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/** Negate a condition, flipping a relational operator directly (`!= → ==`, `< → >=`, …) so an
|
|
74
|
-
* empty-then flip reads cleanly; anything else wraps in `!( … )`. Both forms are semantically
|
|
75
|
-
* exact over C's total integer order. */
|
|
76
|
-
function negate(cond: Expr): Expr {
|
|
77
|
-
if (cond.k === 'bin') {
|
|
78
|
-
const table: Record<string, '==' | '!=' | '<' | '<=' | '>' | '>='> = {
|
|
79
|
-
'==': '!=',
|
|
80
|
-
'!=': '==',
|
|
81
|
-
'<': '>=',
|
|
82
|
-
'>=': '<',
|
|
83
|
-
'>': '<=',
|
|
84
|
-
'<=': '>',
|
|
85
|
-
};
|
|
86
|
-
const f = table[cond.op];
|
|
87
|
-
if (f) {
|
|
88
|
-
return { k: 'bin', op: f, l: cond.l, r: cond.r };
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return { k: 'un', op: '!', e: cond };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
73
|
/** Backward live-variable walk over one block. `liveOut` is the set of locals live on exit;
|
|
95
74
|
* returns the rewritten block and the set live on entry. */
|
|
96
75
|
function dceBlock(
|
|
@@ -171,7 +150,7 @@ function dceBlock(
|
|
|
171
150
|
rev.push({ k: 'exprstmt', value: s.cond });
|
|
172
151
|
}
|
|
173
152
|
} else if (t.out.length === 0) {
|
|
174
|
-
rev.push({ k: 'if', cond:
|
|
153
|
+
rev.push({ k: 'if', cond: negateCond(s.cond), then: e.out, else: [] });
|
|
175
154
|
} else {
|
|
176
155
|
rev.push({ k: 'if', cond: s.cond, then: t.out, else: e.out });
|
|
177
156
|
}
|
package/src/l3/hoist.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// L3 — the naming MECHANISM shared by every pass that hoists a value into a fresh local.
|
|
2
|
+
//
|
|
3
|
+
// Two passes name bases today (`basecse.ts` hoists a REUSED base; `argbase.ts` names a call's
|
|
4
|
+
// argument bases), and they differ in POLICY — which bases are eligible, and when it is worth
|
|
5
|
+
// doing — but not in how a name is chosen. That half was copied, and the copy silently lost a
|
|
6
|
+
// safety guard: basecse added the callee-name exclusion in its own audit precisely so a hoist
|
|
7
|
+
// local could not shadow a called function, and the second implementation did not have it. A third
|
|
8
|
+
// hoisting pass would lose it again, so the mechanism lives here and the policy stays with each
|
|
9
|
+
// caller.
|
|
10
|
+
import type { Expr, SFn, Stmt } from './ast';
|
|
11
|
+
import { mapExprChildren, stmtChildren, stmtExprs } from './ast';
|
|
12
|
+
|
|
13
|
+
/** Every identifier a hoist name must not collide with, anywhere in `sfn`.
|
|
14
|
+
*
|
|
15
|
+
* Wider than "the declared locals" on purpose, and each addition is a real collision:
|
|
16
|
+
* - params and locals, obviously;
|
|
17
|
+
* - every `var`/`addr` mentioned — a GLOBAL is referenced by bare name, so a local shadowing one
|
|
18
|
+
* silently redirects every later mention of it;
|
|
19
|
+
* - every CALL TARGET — a local named like a callee shadows the function;
|
|
20
|
+
* - every assignment target, which includes names no declaration list carries. */
|
|
21
|
+
function takenNames(sfn: SFn): Set<string> {
|
|
22
|
+
const taken = new Set<string>([...sfn.params.map((p) => p.name), ...sfn.locals.map((l) => l.name)]);
|
|
23
|
+
const visit = (e: Expr): void => {
|
|
24
|
+
if (e.k === 'var' || e.k === 'addr') {
|
|
25
|
+
taken.add(e.name);
|
|
26
|
+
}
|
|
27
|
+
if (e.k === 'call') {
|
|
28
|
+
taken.add(e.fn);
|
|
29
|
+
}
|
|
30
|
+
mapExprChildren(e, (c) => {
|
|
31
|
+
visit(c);
|
|
32
|
+
return c;
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
const walk = (stmts: Stmt[]): void => {
|
|
36
|
+
for (const s of stmts) {
|
|
37
|
+
if (s.k === 'assign') {
|
|
38
|
+
taken.add(s.name);
|
|
39
|
+
}
|
|
40
|
+
stmtExprs(s).forEach(visit);
|
|
41
|
+
walk(stmtChildren(s));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
walk(sfn.body);
|
|
45
|
+
return taken;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A generator of fresh `p<n>` hoist names for `sfn`, colliding with nothing already in it.
|
|
50
|
+
*
|
|
51
|
+
* Returned as a closure over one `taken` set so successive calls cannot collide with each OTHER
|
|
52
|
+
* either — the failure a caller re-deriving the set per name would hit.
|
|
53
|
+
*/
|
|
54
|
+
export function nameAllocator(sfn: SFn): () => string {
|
|
55
|
+
const taken = takenNames(sfn);
|
|
56
|
+
return () => {
|
|
57
|
+
let n = 0;
|
|
58
|
+
while (taken.has(`p${n}`)) {
|
|
59
|
+
n++;
|
|
60
|
+
}
|
|
61
|
+
const nm = `p${n}`;
|
|
62
|
+
taken.add(nm);
|
|
63
|
+
return nm;
|
|
64
|
+
};
|
|
65
|
+
}
|
package/src/l3/reindex.ts
CHANGED
|
@@ -163,10 +163,17 @@ function reindexExpr(e: Expr, walk: WalkLoop, iv: string): Expr | null {
|
|
|
163
163
|
if (mentionsVar(e.idx, walk.p)) {
|
|
164
164
|
return null; // a p-dependent element offset — beyond the v1 shape
|
|
165
165
|
}
|
|
166
|
+
if (e.lead && e.lead.length > 0) {
|
|
167
|
+
return null; // leading subscripts (a multidim array global) — the rebuild below would drop
|
|
168
|
+
// them, turning an element access into a row's. Decline rather than reindex.
|
|
169
|
+
}
|
|
166
170
|
const idx: Expr =
|
|
167
171
|
e.idx.k === 'const' && e.idx.value === 0
|
|
168
172
|
? { k: 'var', name: iv }
|
|
169
173
|
: { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: e.idx };
|
|
174
|
+
// NOTE: this rebuilds the node from parts, so any field not named here is DROPPED. `lead` is
|
|
175
|
+
// declined above (the deref side); it cannot arrive on the base side either, since `walk.base`
|
|
176
|
+
// is a local pointer and structuring only ever puts `lead` on an array GLOBAL's own name.
|
|
170
177
|
return { k: 'index', base: { k: 'var', name: walk.base }, idx, width: e.width, signed: e.signed };
|
|
171
178
|
}
|
|
172
179
|
let failed = false;
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// L3 re-spelling lever: hoist a reused global base into a pointer local at the INNERMOST scope
|
|
2
|
+
// that contains all of its uses.
|
|
3
|
+
//
|
|
4
|
+
// `l3/basecse.ts` already hoists a reused leaf base — but always to the FUNCTION TOP, and only for
|
|
5
|
+
// an `addr`/`const` base. Both limits are load-bearing here, and each costs a real row:
|
|
6
|
+
//
|
|
7
|
+
// PLACEMENT. A base used only inside one `if` arm, hoisted to the function top, is live across
|
|
8
|
+
// everything before that arm — a live range the original never had, which is the register-pressure
|
|
9
|
+
// failure basecse's own loop gate exists for. Measured on kleod:UpdateHUDCounterDisplay by
|
|
10
|
+
// hand-editing the REFERENCE source: naming the `gBgTilemapBufs` store base inside the arm that
|
|
11
|
+
// uses it is byte-exact, and moving that same declaration to the function top costs 24. That is
|
|
12
|
+
// the reason the lever is scope-aware; it is NOT a claim about what the lever achieves. On that
|
|
13
|
+
// row it now declines outright (a later pass retired the phi it keyed on, so the base's uses span
|
|
14
|
+
// the function body), and the cluster fallback below is what recovers it.
|
|
15
|
+
// basecse's header already names the gap — "a loop-body base is left
|
|
16
|
+
// inline for a future scope-aware hoist" — and this is that hoist.
|
|
17
|
+
//
|
|
18
|
+
// ELIGIBILITY. With a symbol map that states an array's RANK, the access renders as the bare
|
|
19
|
+
// `gSym[0][i]`, whose base node is a `var` naming the global, not an `addr`. basecse's
|
|
20
|
+
// `isHoistableBase` takes only `addr`/`const`, so the rank-aware spelling — the one a project with
|
|
21
|
+
// real headers actually gets — is invisible to it.
|
|
22
|
+
//
|
|
23
|
+
// WHY IT MATCHES, and it is not a readability preference: a store whose destination address the
|
|
24
|
+
// compiler materialized into a register before computing the source reads back as exactly this
|
|
25
|
+
// shape. The decomp author's alternative is a no-op read-modify-write (`g[0][K] += 0;`) purely to
|
|
26
|
+
// force that materialization; naming the base is the same codegen without the quirk.
|
|
27
|
+
//
|
|
28
|
+
// A LEVER, not a rewrite: emitted as an ADDITIONAL candidate (rank.ts `/scopebase`) with the
|
|
29
|
+
// differ refereeing, so the un-hoisted spelling is always still in the list and this can never cost
|
|
30
|
+
// a match.
|
|
31
|
+
//
|
|
32
|
+
// SEMANTICS ARE PRESERVED BY CONSTRUCTION. The hoisted value is a pure ADDRESS of a global — no
|
|
33
|
+
// load, nothing observable, nothing that can fault — so evaluating it earlier in a scope that
|
|
34
|
+
// DOMINATES every use is invisible. The rewritten accesses keep their own width/signedness, so
|
|
35
|
+
// every stride is unchanged. Domination is the load-bearing half: `collect` and `rewriteStmt` must
|
|
36
|
+
// walk the SAME tree, or an access the planner never placed gets repointed at a local whose
|
|
37
|
+
// assignment does not reach it — compiling C that reads an uninitialized pointer, which neither
|
|
38
|
+
// boundary contract catches (they check resolution and deref typing, not definite assignment).
|
|
39
|
+
//
|
|
40
|
+
// ORDERING: `hoistReusedGlobalBases` (basecse) runs unconditionally in `structureChecked`, BEFORE
|
|
41
|
+
// rank's levers see the tree. So this pass's `addr`/`const` input is only what basecse REFUSED —
|
|
42
|
+
// loop uses and repeated-constant-offset uses — which is why it carries basecse's const-offset gate
|
|
43
|
+
// rather than assuming those bases never arrive.
|
|
44
|
+
import { type IrType, T, scalarTypeForAccess } from '../ir/types';
|
|
45
|
+
import type { Expr, SFn, Stmt } from './ast';
|
|
46
|
+
import { mapExprChildren, stmtExprs } from './ast';
|
|
47
|
+
import { nameAllocator } from './hoist';
|
|
48
|
+
|
|
49
|
+
/** A base this lever may name: a leaf whose value is a fixed address.
|
|
50
|
+
*
|
|
51
|
+
* `var` is included ONLY for a name in `SFn.globals`. That list is populated by `noteGlobal` alone
|
|
52
|
+
* (two call sites in structure.ts, both on the `bareArrayLead` path, which requires
|
|
53
|
+
* `shape === 'array'`) — so a `var` base here is always an ARRAY-declared global and `(T *)&gSym`
|
|
54
|
+
* is its start address under any declaration. The invariant is worth stating because it is what
|
|
55
|
+
* keeps a POINTER-shaped global out: for one of those, `(T *)&gPtr` names the pointer CELL rather
|
|
56
|
+
* than the object it points at, which would be silently the wrong address. A local `var` is
|
|
57
|
+
* excluded for the ordinary reason: it can be assigned between the hoist point and a use. */
|
|
58
|
+
type LeafBase = Extract<Expr, { k: 'addr' } | { k: 'const' } | { k: 'var' }>;
|
|
59
|
+
const isLeaf = (e: Expr, globals: ReadonlySet<string>): e is LeafBase =>
|
|
60
|
+
e.k === 'addr' || e.k === 'const' || (e.k === 'var' && globals.has(e.name));
|
|
61
|
+
|
|
62
|
+
/** THE identity of a base — what makes two accesses "the same address".
|
|
63
|
+
*
|
|
64
|
+
* A global reaches L3 under two spellings, `addr g` and the bare `var g`, and they denote the same
|
|
65
|
+
* cell; keying on the NAME alone means a function that mixes them still sees one base. */
|
|
66
|
+
const baseId = (b: LeafBase): string => (b.k === 'const' ? `c:${b.value}` : `n:${b.name}`);
|
|
67
|
+
|
|
68
|
+
/** An access this lever may re-point, or null.
|
|
69
|
+
*
|
|
70
|
+
* REFUSES a non-zero `lead`. `lead` pins the leading subscripts of a multidimensional array, so
|
|
71
|
+
* `g[1][i]` is a whole ROW past `g[0][i]`. The hoisted local points at the START of the object, and
|
|
72
|
+
* the rewrite DROPS the lead — sound only when every leading subscript is 0. A non-zero lead would
|
|
73
|
+
* silently address the wrong row, which no contract checks: the tree stays well-typed and
|
|
74
|
+
* spellable, it just names different bytes. (Today `bareArrayLead` only ever emits zeros; this
|
|
75
|
+
* guard is what keeps that an implementation detail rather than a correctness dependency.) */
|
|
76
|
+
function eligible(e: Expr, globals: ReadonlySet<string>): Extract<Expr, { k: 'index' }> | null {
|
|
77
|
+
if (e.k !== 'index' || !isLeaf(e.base, globals)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return (e.lead ?? []).every((n) => n === 0) ? e : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The (base, access-shape) key an access shares with its reuse siblings. Width and signedness are
|
|
84
|
+
* part of it because the hoisted local carries the access's pointer type — two widths through one
|
|
85
|
+
* base are two different locals, exactly as in basecse. */
|
|
86
|
+
const keyOf = (n: Extract<Expr, { k: 'index' }>): string => `${baseId(n.base as LeafBase)} ${n.width} ${n.signed}`;
|
|
87
|
+
|
|
88
|
+
/** One use, located by its chain of enclosing statement LISTS (outermost first).
|
|
89
|
+
*
|
|
90
|
+
* `loop[i]` says whether `path[i]` is a LOOP BODY. Recorded here, at the only point the tree walk
|
|
91
|
+
* actually knows it, so the loop question below is a lookup rather than a second traversal that
|
|
92
|
+
* could disagree with this one. */
|
|
93
|
+
interface Site {
|
|
94
|
+
path: Stmt[][];
|
|
95
|
+
loop: boolean[];
|
|
96
|
+
/** `idx[i]` is the index, within `path[i]`, of the statement this use sits under. Used to place
|
|
97
|
+
* the hoist immediately before the FIRST statement that needs it rather than at the list head:
|
|
98
|
+
* a call between the assignment and the first use is exactly what forces the pointer into a
|
|
99
|
+
* CALLEE-SAVED register and adds the prologue push/pop the original avoided — the same failure,
|
|
100
|
+
* one level smaller, that this module exists to fix. argbase.ts places by the same rule. */
|
|
101
|
+
idx: number[];
|
|
102
|
+
/** the use runs EVERY ITERATION of a loop whose body is not on `path` — a loop's own condition,
|
|
103
|
+
* or a `for`'s increment. No scope reachable from `path` runs at that cadence, so a key with any
|
|
104
|
+
* such use is refused outright rather than hoisted to a point that runs once. */
|
|
105
|
+
perIteration: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Set when the tree holds a shape `collect` and `rewriteStmt` would disagree about — see the
|
|
109
|
+
* `for`-part note below. The pass then declines outright. */
|
|
110
|
+
let compound = false;
|
|
111
|
+
|
|
112
|
+
/** Walk every expression in the tree, recording each eligible access's key and its scope path. */
|
|
113
|
+
function collect(
|
|
114
|
+
body: Stmt[],
|
|
115
|
+
globals: ReadonlySet<string>,
|
|
116
|
+
out: Map<string, { uses: Site[]; sample: Extract<Expr, { k: 'index' }>; constOff: Map<number, number> }>,
|
|
117
|
+
path: Stmt[][],
|
|
118
|
+
loop: boolean[],
|
|
119
|
+
idxPath: number[],
|
|
120
|
+
): void {
|
|
121
|
+
let at = 0;
|
|
122
|
+
const visit = (e: Expr, perIteration: boolean): void => {
|
|
123
|
+
const ix = eligible(e, globals);
|
|
124
|
+
if (ix) {
|
|
125
|
+
const k = keyOf(ix);
|
|
126
|
+
const rec = out.get(k) ?? { uses: [], sample: ix, constOff: new Map<number, number>() };
|
|
127
|
+
rec.uses.push({ path, loop, perIteration, idx: [...idxPath, at] });
|
|
128
|
+
if (ix.idx.k === 'const') {
|
|
129
|
+
rec.constOff.set(ix.idx.value, (rec.constOff.get(ix.idx.value) ?? 0) + 1);
|
|
130
|
+
}
|
|
131
|
+
out.set(k, rec);
|
|
132
|
+
}
|
|
133
|
+
mapExprChildren(e, (c) => {
|
|
134
|
+
visit(c, perIteration);
|
|
135
|
+
return c;
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
for (const [i, s] of body.entries()) {
|
|
139
|
+
at = i;
|
|
140
|
+
const isLoop = s.k === 'while' || s.k === 'dowhile' || s.k === 'for';
|
|
141
|
+
// A loop's OWN condition runs every iteration — a base there is loop-invariant exactly as a
|
|
142
|
+
// body use is, and it lives at THIS list, which does not. basecse.ts and argbase.ts treat the
|
|
143
|
+
// CONDITION the same way. They do NOT agree about a `for`'s `init`: basecse counts it in-loop
|
|
144
|
+
// (its `stmtChildren('for')` is `[init, inc, …body]`, recursed with `nested`), this pass counts
|
|
145
|
+
// it at the enclosing cadence, which is the truthful reading — it runs once. Recorded because
|
|
146
|
+
// the divergence is real and an extraction has to pick one.
|
|
147
|
+
stmtExprs(s).forEach((e) => visit(e, isLoop));
|
|
148
|
+
if (s.k === 'for') {
|
|
149
|
+
// `init`/`inc` are typed as the full Stmt union, so a COMPOUND one is type-legal. `stmtExprs`
|
|
150
|
+
// reaches only its own expressions while `rewriteStmt` descends into any nested list — the
|
|
151
|
+
// round-1 walker asymmetry, one node kind deeper, and the fuzz reproduces it (a use inside
|
|
152
|
+
// `for (if (1) i = g[3]; …)` gets repointed at a local the `if` arm may never have assigned).
|
|
153
|
+
// No producer emits a compound part today (structure.ts and reindex.ts both emit `assign`), so
|
|
154
|
+
// rather than grow a second recursion this REFUSES the whole function — loud decline over a
|
|
155
|
+
// silently unreachable definition. Delete this when `stmtLists` makes collect/rewrite share
|
|
156
|
+
// one traversal.
|
|
157
|
+
if (childLists(s.init).length > 0 || childLists(s.inc).length > 0) {
|
|
158
|
+
compound = true;
|
|
159
|
+
}
|
|
160
|
+
// `init` and `inc` are STATEMENTS, so their expressions are reached by neither `stmtExprs`
|
|
161
|
+
// nor `childLists` — yet `rewriteStmt` rewrites them. Collect and rewrite MUST see the same
|
|
162
|
+
// tree: an access the planner never counted would still be repointed, at a local whose
|
|
163
|
+
// assignment need not dominate it (`for (i = p0[3]; …)` after an `if` arm that defines p0).
|
|
164
|
+
// `init` runs once, at this list's cadence; `inc` runs every iteration, like the condition.
|
|
165
|
+
stmtExprs(s.init).forEach((e) => visit(e, false));
|
|
166
|
+
stmtExprs(s.inc).forEach((e) => visit(e, true));
|
|
167
|
+
}
|
|
168
|
+
for (const child of childLists(s)) {
|
|
169
|
+
collect(child, globals, out, [...path, child], [...loop, isLoop], [...idxPath, i]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The nested statement LISTS of a statement — the scopes a hoist could land in.
|
|
175
|
+
*
|
|
176
|
+
* Deliberately not `stmtChildren`, which flattens a `for`'s `init`/`inc` in with its body: those
|
|
177
|
+
* are single statements, not lists, and a hoist has nowhere legal to go in either (before the loop
|
|
178
|
+
* changes when it runs, inside the body repeats it). A `for`'s body IS a list and is included. */
|
|
179
|
+
function childLists(s: Stmt): Stmt[][] {
|
|
180
|
+
switch (s.k) {
|
|
181
|
+
case 'if':
|
|
182
|
+
return [s.then, s.else];
|
|
183
|
+
case 'while':
|
|
184
|
+
case 'dowhile':
|
|
185
|
+
case 'for':
|
|
186
|
+
return [s.body];
|
|
187
|
+
case 'switch':
|
|
188
|
+
return [...s.cases.map((c) => c.body), ...(s.default ? [s.default] : [])];
|
|
189
|
+
// Exhaustive on purpose — no `default`. A future Stmt kind carrying a nested list must be a
|
|
190
|
+
// COMPILE error here, exactly as it is in `stmtChildren`: a silent `[]` would collect that
|
|
191
|
+
// kind's uses at the wrong scope while `rewriteStmt`, which IS exhaustive, still rewrote them.
|
|
192
|
+
case 'assign':
|
|
193
|
+
case 'store':
|
|
194
|
+
case 'exprstmt':
|
|
195
|
+
case 'return':
|
|
196
|
+
case 'break':
|
|
197
|
+
case 'continue':
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The innermost statement list common to every use, or null when they span the function body.
|
|
203
|
+
*
|
|
204
|
+
* Null is NOT a decline any more: the caller falls through to `deepestCluster`. Kept as a distinct
|
|
205
|
+
* answer because "one scope holds everything" is the better shape when it exists — every use is
|
|
206
|
+
* named, not just a cluster. The consolidation this file still owes would make both of these one
|
|
207
|
+
* selector parameter over a single collected index. */
|
|
208
|
+
function commonScope(uses: Site[]): { scope: Stmt[]; depth: number } | null {
|
|
209
|
+
const first = uses[0].path;
|
|
210
|
+
let depth = 0;
|
|
211
|
+
while (depth < first.length && uses.every((u) => u.path[depth] === first[depth])) {
|
|
212
|
+
depth++;
|
|
213
|
+
}
|
|
214
|
+
return depth === 0 ? null : { scope: first[depth - 1], depth };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The DEEPEST statement list holding 2+ uses, with just those uses — the fallback when no single
|
|
218
|
+
* scope holds them all.
|
|
219
|
+
*
|
|
220
|
+
* Ties are broken by first appearance, so emission stays deterministic. Returning a SUBSET is the
|
|
221
|
+
* whole point: the uses outside the cluster keep their original spelling, which is exactly the
|
|
222
|
+
* mixed form the compiler produces when it materializes an address in one arm and re-derives it
|
|
223
|
+
* elsewhere. */
|
|
224
|
+
function deepestCluster(all: Site[]): { scope: Stmt[]; depth: number; uses: Site[] } | null {
|
|
225
|
+
const byList = new Map<Stmt[], { depth: number; uses: Site[] }>();
|
|
226
|
+
for (const u of all) {
|
|
227
|
+
u.path.forEach((list, i) => {
|
|
228
|
+
const e = byList.get(list) ?? { depth: i + 1, uses: [] };
|
|
229
|
+
e.uses.push(u);
|
|
230
|
+
byList.set(list, e);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
let best: { scope: Stmt[]; depth: number; uses: Site[] } | null = null;
|
|
234
|
+
for (const [scope, e] of byList) {
|
|
235
|
+
if (e.uses.length >= 2 && (best === null || e.depth > best.depth)) {
|
|
236
|
+
best = { scope, depth: e.depth, uses: e.uses };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return best;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Does any use sit inside a LOOP nested below the chosen scope?
|
|
243
|
+
*
|
|
244
|
+
* OVER-REFUSES in two shapes, deliberately: a `do { … } while (g[1]) ;` body head and a
|
|
245
|
+
* `for (…; …; i = g[5])` body head both DO run at the flagged cadence, so a hoist there would be
|
|
246
|
+
* legal. Refusing them costs a missed spelling and nothing else (bench: 0 lost, 0 gained), and the
|
|
247
|
+
* precise rule needs the loop-DEPTH model an extraction would bring. Otherwise:
|
|
248
|
+
* the hoist would be loop-invariant code motion to a point the original never had — the
|
|
249
|
+
* register-pressure failure `basecse.ts`'s own `inLoop` gate refuses, and the reason that gate
|
|
250
|
+
* exists. When EVERY use is inside the loop, the common scope IS the loop body: the assignment
|
|
251
|
+
* then runs per iteration exactly as the inline spelling did, and there is nothing to refuse. */
|
|
252
|
+
function underNestedLoop(uses: Site[], depth: number): boolean {
|
|
253
|
+
return uses.some((u) => u.perIteration || u.loop.slice(depth).some(Boolean));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The `/scopebase` re-spelling, or null when nothing qualifies (the caller then adds no candidate
|
|
258
|
+
* rather than a duplicate of the primary).
|
|
259
|
+
*/
|
|
260
|
+
export function hoistScopedBases(sfn: SFn): SFn | null {
|
|
261
|
+
compound = false;
|
|
262
|
+
// A name that is BOTH a declared global and a local/param is not safely a global here: `&g` would
|
|
263
|
+
// take the address of the LOCAL, silently a different object. Excluded rather than assumed apart.
|
|
264
|
+
const shadowed = new Set([...sfn.locals.map((l) => l.name), ...sfn.params.map((p) => p.name)]);
|
|
265
|
+
const globals = new Set((sfn.globals ?? []).map((g) => g.name).filter((n) => !shadowed.has(n)));
|
|
266
|
+
const found = new Map<
|
|
267
|
+
string,
|
|
268
|
+
{ uses: Site[]; sample: Extract<Expr, { k: 'index' }>; constOff: Map<number, number> }
|
|
269
|
+
>();
|
|
270
|
+
collect(sfn.body, globals, found, [], [], []);
|
|
271
|
+
if (compound) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const fresh = nameAllocator(sfn);
|
|
276
|
+
// key → (scope list identity, local name)
|
|
277
|
+
const plan: { scope: Stmt[]; key: string; name: string; type: IrType; base: LeafBase; before: number }[] = [];
|
|
278
|
+
for (const [key, rec] of found) {
|
|
279
|
+
if (rec.uses.length < 2) {
|
|
280
|
+
continue; // one access re-materializes as cheaply as a named local
|
|
281
|
+
}
|
|
282
|
+
// A constant offset touched 2+ times is a SCALAR re-access at one fixed location (an MMIO
|
|
283
|
+
// read-modify-write, a repeated `*p`), which the compiler re-materializes rather than
|
|
284
|
+
// register-holds. basecse.ts learned this by LOSING the ProcessHBlankWait match to it. Inherited
|
|
285
|
+
// here rather than re-lost — but honestly: the evidence is a `const` MMIO address, and it
|
|
286
|
+
// applies cleanly only to the `addr`/`const` half of this pass's input, which is exactly what
|
|
287
|
+
// basecse refused and left behind. For the `var` (array-global) half basecse never ran, so this
|
|
288
|
+
// is an EXTRAPOLATION, not an inheritance. Conservative direction, so the cost is a missed
|
|
289
|
+
// hoist rather than a wrong one. It also SLIPS on a fixed offset not spelled as a literal —
|
|
290
|
+
// two identical `g[i]` accesses are not tallied — which basecse acknowledges in its own comment
|
|
291
|
+
// and which this pass is MORE exposed to, since it deliberately admits loop-body uses, exactly
|
|
292
|
+
// the input basecse's `inLoop` gate kept away from that hole.
|
|
293
|
+
if ([...rec.constOff.values()].some((n) => n >= 2)) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
let at = commonScope(rec.uses);
|
|
297
|
+
let uses = rec.uses;
|
|
298
|
+
if (!at) {
|
|
299
|
+
// The uses span the FUNCTION BODY, so no single scope holds them. Rather than decline, take a
|
|
300
|
+
// scope that holds two or more and name the base for THOSE only, leaving the rest as they
|
|
301
|
+
// were.
|
|
302
|
+
//
|
|
303
|
+
// The selection rule is DEEPEST, with no size term, and that is a real limitation rather than
|
|
304
|
+
// a model of the compiler: a scope with four uses enclosing a nested scope with two will name
|
|
305
|
+
// the TWO and leave the four re-deriving the address. Only ONE cluster is ever served, and
|
|
306
|
+
// when two siblings tie on depth the first-appearing wins — arbitrary, not principled.
|
|
307
|
+
// Largest-cluster-with-deepest-as-tie-break is the better rule; it is a behaviour change and
|
|
308
|
+
// belongs with the placement-selector consolidation, not bolted on here.
|
|
309
|
+
//
|
|
310
|
+
// NOTE this fires for an `addr`/`const` base too — nothing here tests the base kind. That is
|
|
311
|
+
// not a duplicate of basecse's hoist: basecse runs FIRST (see the ordering note in the file
|
|
312
|
+
// header), so any `addr`/`const` base reaching this pass is one basecse already REFUSED.
|
|
313
|
+
const cluster = deepestCluster(rec.uses);
|
|
314
|
+
if (!cluster) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
at = { scope: cluster.scope, depth: cluster.depth };
|
|
318
|
+
uses = cluster.uses;
|
|
319
|
+
}
|
|
320
|
+
if (underNestedLoop(uses, at.depth)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const type = T.ptr(scalarTypeForAccess(rec.sample.width, rec.sample.signed));
|
|
324
|
+
// the earliest statement of the scope list that (transitively) holds a use
|
|
325
|
+
// `path` starts EMPTY, so `idx` carries one entry more than `path`: idx[j+1] is the index
|
|
326
|
+
// within path[j]. The scope is path[depth-1], so its index is idx[depth].
|
|
327
|
+
const before = Math.min(...uses.map((u) => u.idx[at.depth]));
|
|
328
|
+
plan.push({ scope: at.scope, key, name: fresh(), type, base: rec.sample.base as LeafBase, before });
|
|
329
|
+
}
|
|
330
|
+
if (plan.length === 0) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// A plan entry may own only a SUBSET of its key's uses (see deepestCluster), so repointing is
|
|
335
|
+
// scoped: a key becomes active when the rewrite enters its scope and inactive on the way out.
|
|
336
|
+
// Repointing by key alone would rewrite uses the hoist does not dominate.
|
|
337
|
+
// SAFE ONLY because `plan` holds at most one entry per key, so `delete` on the way out cannot
|
|
338
|
+
// discard an outer binding. Serving a second cluster for one key — the obvious next step — makes
|
|
339
|
+
// that false, and an inner delete would silently unbind the outer one for the rest of its scope:
|
|
340
|
+
// a use of an unassigned pointer, the defect class this module has already shipped twice. Switch
|
|
341
|
+
// to save/restore (or pass the bindings as an argument) before serving more than one cluster.
|
|
342
|
+
const active = new Map<string, string>();
|
|
343
|
+
const point = (e: Expr): Expr => {
|
|
344
|
+
const ix = eligible(e, globals);
|
|
345
|
+
if (ix) {
|
|
346
|
+
const nm = active.get(keyOf(ix));
|
|
347
|
+
if (nm) {
|
|
348
|
+
// `lead` is DROPPED — the local already points at the object start, and `eligible` has
|
|
349
|
+
// established every leading subscript is 0.
|
|
350
|
+
const { lead: _drop, ...rest } = ix;
|
|
351
|
+
return { ...rest, base: { k: 'var', name: nm }, idx: point(ix.idx) };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return mapExprChildren(e, point);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// Rebuild the tree, inserting each hoist at the head of its own scope list. Statement lists are
|
|
358
|
+
// matched by IDENTITY against the ORIGINAL tree, so the rewrite walks the original and emits a
|
|
359
|
+
// fresh tree in one pass — a two-pass version would compare rebuilt lists that no longer match.
|
|
360
|
+
const rewriteList = (list: Stmt[]): Stmt[] => {
|
|
361
|
+
const here = plan.filter((p) => p.scope === list);
|
|
362
|
+
// SAVE/RESTORE, not set/delete. A plain delete on the way out is correct only while `plan`
|
|
363
|
+
// holds one entry per key; the moment a second cluster for one key is served, an inner exit
|
|
364
|
+
// would unbind an OUTER hoist for the rest of its scope — under-repointing silently. Restoring
|
|
365
|
+
// makes the nesting correct by construction instead of by an unguarded invariant.
|
|
366
|
+
const saved = here.map((p) => [p.key, active.get(p.key)] as const);
|
|
367
|
+
for (const p of here) {
|
|
368
|
+
active.set(p.key, p.name);
|
|
369
|
+
}
|
|
370
|
+
const rewritten = list.map(rewriteStmt);
|
|
371
|
+
for (const [key, prev] of saved) {
|
|
372
|
+
if (prev === undefined) {
|
|
373
|
+
active.delete(key);
|
|
374
|
+
} else {
|
|
375
|
+
active.set(key, prev);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Insert each hoist immediately before the first statement that uses it. Descending by index so
|
|
379
|
+
// earlier insertions do not shift the positions later ones were computed against. NOTE that two
|
|
380
|
+
// hoists sharing a `before` come out REVERSED relative to `plan` order — the sort is stable and
|
|
381
|
+
// descending, so both splice at the same index and the later one ends up first. Deterministic
|
|
382
|
+
// and semantically irrelevant, but it is not first-appearance order, which this comment used to
|
|
383
|
+
// claim.
|
|
384
|
+
for (const p of [...here].sort((a, b) => b.before - a.before)) {
|
|
385
|
+
rewritten.splice(p.before, 0, {
|
|
386
|
+
k: 'assign',
|
|
387
|
+
name: p.name,
|
|
388
|
+
// The always-valid form: `(T *)&gSym` is byte-identical under ANY declaration of gSym, which
|
|
389
|
+
// is why it is also what `bareArrayLead` falls back to. A `const` base keeps its literal.
|
|
390
|
+
value: { k: 'cast', to: p.type, e: p.base.k === 'const' ? p.base : { k: 'addr', name: p.base.name } },
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
return rewritten;
|
|
394
|
+
};
|
|
395
|
+
const rewriteStmt = (s: Stmt): Stmt => {
|
|
396
|
+
switch (s.k) {
|
|
397
|
+
case 'assign':
|
|
398
|
+
return { ...s, value: point(s.value) };
|
|
399
|
+
case 'store':
|
|
400
|
+
return { ...s, lval: point(s.lval), value: point(s.value) };
|
|
401
|
+
case 'exprstmt':
|
|
402
|
+
return { ...s, value: point(s.value) };
|
|
403
|
+
case 'return':
|
|
404
|
+
return s.value === undefined ? s : { ...s, value: point(s.value) };
|
|
405
|
+
case 'if':
|
|
406
|
+
return { ...s, cond: point(s.cond), then: rewriteList(s.then), else: rewriteList(s.else) };
|
|
407
|
+
case 'while':
|
|
408
|
+
case 'dowhile':
|
|
409
|
+
return { ...s, cond: point(s.cond), body: rewriteList(s.body) };
|
|
410
|
+
case 'for':
|
|
411
|
+
return {
|
|
412
|
+
...s,
|
|
413
|
+
init: rewriteStmt(s.init),
|
|
414
|
+
cond: point(s.cond),
|
|
415
|
+
inc: rewriteStmt(s.inc),
|
|
416
|
+
body: rewriteList(s.body),
|
|
417
|
+
};
|
|
418
|
+
case 'switch':
|
|
419
|
+
return {
|
|
420
|
+
...s,
|
|
421
|
+
scrutinee: point(s.scrutinee),
|
|
422
|
+
cases: s.cases.map((c) => ({ ...c, body: rewriteList(c.body) })),
|
|
423
|
+
...(s.default ? { default: rewriteList(s.default) } : {}),
|
|
424
|
+
};
|
|
425
|
+
case 'break':
|
|
426
|
+
case 'continue':
|
|
427
|
+
return s;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const body = rewriteList(sfn.body);
|
|
432
|
+
// Declared from `plan`, one per hoist — NOT accumulated inside `rewriteList`, which would emit a
|
|
433
|
+
// duplicate declaration (non-compiling C) if a `Stmt[]` were ever structurally shared by two tree
|
|
434
|
+
// positions.
|
|
435
|
+
return { ...sfn, body, locals: [...sfn.locals, ...plan.map((p) => ({ name: p.name, type: p.type }))] };
|
|
436
|
+
}
|