@asmlift/core 0.6.0 → 0.8.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 +48 -24
- package/package.json +1 -1
- package/src/backend/cfamily.ts +39 -11
- package/src/backend/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- package/src/contracts.ts +60 -11
- package/src/frontend/disasm.ts +141 -11
- package/src/frontend/high-half.ts +149 -0
- package/src/frontend/mips.ts +458 -209
- package/src/frontend/ppc.ts +332 -67
- package/src/frontend/reloc-symbol.ts +109 -0
- package/src/frontend/splat.ts +56 -18
- package/src/frontend/ssa.ts +127 -30
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +209 -232
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +70 -3
- package/src/ir/opcodes.ts +52 -7
- package/src/ir/parse.ts +7 -1
- package/src/ir/simplify.ts +1 -1
- package/src/l3/address.ts +2 -2
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -6
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +110 -22
- package/src/l3/basecse.ts +50 -30
- package/src/l3/coalesce.ts +118 -61
- package/src/l3/gates.ts +75 -1
- package/src/l3/hoist.ts +1 -1
- package/src/l3/homesplit.ts +13 -13
- package/src/l3/initfirst.ts +3 -3
- package/src/l3/inlinebase.ts +16 -16
- package/src/l3/mentions.ts +68 -5
- package/src/l3/mulfirst.ts +3 -3
- package/src/l3/nearbase.ts +4 -4
- package/src/l3/offmember.ts +5 -5
- package/src/l3/parkfirst.ts +6 -6
- package/src/l3/pollguard.ts +3 -3
- package/src/l3/ptrfield.ts +4 -4
- package/src/l3/regspell.ts +8 -8
- package/src/l3/reindex.ts +22 -17
- package/src/l3/scopebase.ts +32 -29
- package/src/l3/sinkinit.ts +7 -7
- package/src/l3/slotorder.ts +3 -3
- package/src/l3/storage.ts +1 -1
- package/src/l3/tailmerge.ts +2 -2
- package/src/l3/tailret.ts +70 -0
- package/src/l3/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +15 -14
- package/src/l3/volatileptr.ts +11 -11
- package/src/l3/volatileval.ts +11 -11
- package/src/l3/volstore.ts +16 -16
- package/src/l3/zerosub.ts +6 -6
- package/src/mangle.ts +49 -0
- package/src/pattern/engine.ts +132 -17
- package/src/pipeline.ts +39 -16
- package/src/proto.ts +2 -2
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/extscale.ts +345 -0
- package/src/raise/globalshape.ts +32 -12
- package/src/raise/gvn.ts +2 -2
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +4 -4
- package/src/raise/narrowlocal.ts +18 -2
- package/src/raise/paramwidth.ts +133 -3
- package/src/raise/pre-recovery.ts +100 -25
- package/src/raise/retsink.ts +389 -19
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +141 -0
- package/src/rank-declare.ts +21 -13
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1176 -805
- package/src/structure/analysis.ts +87 -90
- package/src/structure/bitfields.ts +130 -30
- package/src/structure/globalaccess.ts +30 -4
- package/src/structure/namecoalesce.ts +32 -13
- package/src/structure/retspell.ts +95 -0
- package/src/structure/structure.ts +1425 -201
- package/src/structure/switch-recover.ts +101 -8
- package/src/symbols.ts +127 -6
- package/src/target.ts +374 -44
- package/src/trace.ts +28 -19
- package/src/variation-definitions.ts +1590 -0
- package/src/variation-gates.ts +92 -0
- package/src/variation-tokens.ts +356 -0
package/src/l3/argbase.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: materialize the deref BASES of a call's arguments into locals, before the
|
|
2
2
|
// call.
|
|
3
3
|
//
|
|
4
4
|
// When a call's arguments are each a deref through a different fixed address, the compiler loads
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
// order names the bases first (`vu8 *p = ®_VCOUNT_L; u8 *e = gEntityArray; f(*p, e[8])`), which
|
|
17
17
|
// is what a decomp author writes and what this pass reproduces.
|
|
18
18
|
//
|
|
19
|
-
// A
|
|
19
|
+
// A VARIATION, not a committed rewrite: it is emitted as an ADDITIONAL candidate (rank.ts `/argbase`) and the
|
|
20
20
|
// differ referees, so the inline spelling is always still there to win. That is what bounds the
|
|
21
|
-
// risk — a
|
|
21
|
+
// risk — a rewrite that replaced the default could lose a match, this one cannot.
|
|
22
22
|
//
|
|
23
23
|
// SEMANTICS ARE PRESERVED BY CONSTRUCTION, which matters because on a NONMATCH row the
|
|
24
24
|
// best-scoring candidate is what the user is shown. Only a PURE leaf base is eligible — a global's
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// KNOWN LIMITATION: the hoisted local is a plain `T *` — `IrType` models no cv-qualifier at all,
|
|
30
30
|
// so naming a VOLATILE cell through it drops the qualifier that macros.ts goes out of its way to
|
|
31
31
|
// carry. Pre-existing and not introduced here (every pointer local in the tower has it), but the
|
|
32
|
-
// two features meet on exactly the MMIO shape this
|
|
32
|
+
// two features meet on exactly the MMIO shape this variation targets, so it is written down rather
|
|
33
33
|
// than left to be rediscovered.
|
|
34
34
|
//
|
|
35
35
|
// GATE: at least TWO arguments of the same call must qualify, with DISTINCT bases. The reordering
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
// leaves the diff at 2; both together take it to 0.)
|
|
40
40
|
import { type IrType, T, scalarTypeForAccess } from '../ir/types';
|
|
41
41
|
import type { Expr, SFn, Stmt } from './ast';
|
|
42
|
-
import { mapExprChildren, stmtExprs } from './ast';
|
|
42
|
+
import { isLoop, mapExprChildren, stmtExprs } from './ast';
|
|
43
43
|
import { nameAllocator } from './hoist';
|
|
44
44
|
import { declaredGlobals } from './storage';
|
|
45
45
|
|
|
@@ -133,7 +133,7 @@ export function materializeArgBases(sfn: SFn): SFn | null {
|
|
|
133
133
|
// had. That is the register-pressure failure basecse.ts's `inLoop` gate exists to refuse, and
|
|
134
134
|
// it would contradict this pass's own placement rule two comments down. So a loop's condition
|
|
135
135
|
// is left alone; only its body (via the recursion) is eligible.
|
|
136
|
-
const ownExprs = s
|
|
136
|
+
const ownExprs = isLoop(s) ? [] : stmtExprs(s);
|
|
137
137
|
for (const e of ownExprs) {
|
|
138
138
|
const scan = (x: Expr): void => {
|
|
139
139
|
if (x.k === 'call') {
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// L3 respell variation: copy a pointer PARAMETER into a local for ONE REGION.
|
|
2
|
+
//
|
|
3
|
+
// A pointer parameter that a whole function reads pins its incoming register for the whole body.
|
|
4
|
+
// A source that instead copies it into a local — `u8 *b = a0;` at the head of the block that uses
|
|
5
|
+
// it — hands the allocator a SECOND name for the same address, which it may home in a different
|
|
6
|
+
// register, freeing the parameter's for something else. On
|
|
7
|
+
// pokeemerald:SetMauvilleOldManLanguage:agbcc that something else is the arm's loop counter: the
|
|
8
|
+
// target copies the base with `adds r6, r5, #0` and then counts in r5.
|
|
9
|
+
//
|
|
10
|
+
// WHY THIS IS NOT ANY OF ITS NEIGHBOURS. `l3/scopebase.ts` hoists one base per region too, but the
|
|
11
|
+
// value it hoists is the ADDRESS OF A GLOBAL (`(T *)&gSym`) and its `shadowed-or-nonarray-base`
|
|
12
|
+
// rule refuses a `var` base precisely because `&local` names a different object; a parameter
|
|
13
|
+
// already HOLDS the pointer, so nothing is addressed and that rule's argument does not reach here.
|
|
14
|
+
// `l3/argbase.ts` names a call argument's fixed addresses, `l3/inlinebase.ts` DELETES a
|
|
15
|
+
// constant-address local, and `l3/parkfirst.ts` only reorders a copy the tree already has. None of
|
|
16
|
+
// them mints `b = a0`.
|
|
17
|
+
//
|
|
18
|
+
// THE COPY IS A PLAIN LOCAL, NOT A SCOPED DECLARATION. Both spellings — a braced region
|
|
19
|
+
// declaration, and a function-top local assigned at the head of the region — were taken through
|
|
20
|
+
// this row's own agbcc and produce the same bytes, so this pass emits the one L3 already spells
|
|
21
|
+
// and adds no block-scope representation to carry a distinction no object shows.
|
|
22
|
+
//
|
|
23
|
+
// WHAT IT GIVES UP, NAMED. A pointer parameter read only inside a loop that sits at FUNCTION TOP
|
|
24
|
+
// LEVEL gets no candidate at all: `regions()` does not offer the function's own list, so the only
|
|
25
|
+
// region over those reads is the loop body, and `loop-region` refuses it. That population — a base
|
|
26
|
+
// live across a top-level loop — is `/livebase` and `/hipress`'s, and the decline is deliberate.
|
|
27
|
+
// The spelling that shape wants is a copy placed BEFORE the loop with only the loop's reads
|
|
28
|
+
// repointed: a copy SITE separate from its region, which nothing here represents. Relaxing
|
|
29
|
+
// `loop-region` does not reach it — it mints the per-iteration copy that rule exists to refuse.
|
|
30
|
+
//
|
|
31
|
+
// WHAT IS ENUMERATED, AND WHY. Which region the source copied in is not derivable from the tree,
|
|
32
|
+
// so every legal region is offered as its own candidate and the differ referees — the `/regcopy`
|
|
33
|
+
// idiom this file shares with `l3/coalesce.ts`. Uses OUTSIDE the chosen region keep naming the
|
|
34
|
+
// parameter, which is the point: the copy is what makes the two live ranges separable.
|
|
35
|
+
//
|
|
36
|
+
// IT KEEPS ITS OWN REGION WALK, AND THAT IS A SECOND REGION MODEL. `l3/scopebase.ts` ships one
|
|
37
|
+
// already, and this is a narrower duplicate; merging them is the right end state. What blocks that
|
|
38
|
+
// is the SAFETY OBLIGATION, not the region question. scopebase hoists a pure ADDRESS and
|
|
39
|
+
// discharges its obligation by PLACEMENT — `assertHoistsDominate` re-walks the emitted tree to
|
|
40
|
+
// check the hoist dominates every use it repointed. This pass hoists a VALUE the caller passed:
|
|
41
|
+
// its obligation is a whole-function invariance fact (`assigned`, `addressed`) that no placement
|
|
42
|
+
// check can see, and it needs no dominance check at all, because uses outside the region are not
|
|
43
|
+
// repointed. One table over both would need scopebase's `collect()` to admit a plain `var` read
|
|
44
|
+
// as a site and `AccessCtx`/`keyOf` to key a parameter — a widening of a file four shipped
|
|
45
|
+
// variations rest on. Until then every walk HERE goes through `ast.ts`'s shared ones, so the two
|
|
46
|
+
// models at least cannot disagree about what a statement contains.
|
|
47
|
+
import type { Expr, SFn, Stmt } from './ast';
|
|
48
|
+
import { isLoop, mapExprChildren, mapStmtExprs, mapStmtLists, stmtChildren, stmtLists, walkExprs } from './ast';
|
|
49
|
+
import { type Gate, firstRejection } from './gates';
|
|
50
|
+
import { nameAllocator } from './hoist';
|
|
51
|
+
|
|
52
|
+
/** One candidate copy, as the gates see it. */
|
|
53
|
+
export interface ArgCopyCtx {
|
|
54
|
+
/** the parameter being copied */
|
|
55
|
+
readonly param: string;
|
|
56
|
+
/** the parameter's declared type is a pointer — the register this variation is about is a base */
|
|
57
|
+
readonly isPointer: boolean;
|
|
58
|
+
/** the function assigns the parameter somewhere, so a copy taken earlier can go stale */
|
|
59
|
+
readonly assigned: boolean;
|
|
60
|
+
/** `¶m` occurs, so the copy is a DIFFERENT object and any write through the address misses it */
|
|
61
|
+
readonly addressed: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The admission rules. The two SOUND ones are the whole soundness argument: with the parameter
|
|
65
|
+
* never assigned and never addressed, the copy holds the parameter's value at every point the
|
|
66
|
+
* region can reach, so repointing the region's reads at it renames a value rather than changing
|
|
67
|
+
* one. Drop either and the rewrite names different memory — C that compiles and scores.
|
|
68
|
+
*
|
|
69
|
+
* Both are decided over the WHOLE tree — see `countReads` on why every walk in this file goes
|
|
70
|
+
* through `stmtChildren`/`walkExprs`. A gate that called itself sound while judging a subset of the
|
|
71
|
+
* statements `repoint` rewrites would be sound about a function nobody compiles. */
|
|
72
|
+
export const ARGCOPY_GATES: readonly Gate<ArgCopyCtx>[] = [
|
|
73
|
+
{
|
|
74
|
+
id: 'non-pointer',
|
|
75
|
+
why: 'the freed register is a base register, and a scalar parameter does not hold one',
|
|
76
|
+
sound: false,
|
|
77
|
+
rejects: (c) => !c.isPointer,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'assigned',
|
|
81
|
+
why: 'the function assigns the parameter, so the copy would hold a value the parameter no longer has',
|
|
82
|
+
sound: true,
|
|
83
|
+
guardedBy: 'argcopy.test.ts: a parameter a `for` header ADVANCES is never copied',
|
|
84
|
+
rejects: (c) => c.assigned,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'addressed',
|
|
88
|
+
why: 'taking the parameter’s address names its own cell, and the copy is a different cell',
|
|
89
|
+
sound: true,
|
|
90
|
+
guardedBy: 'argcopy.test.ts: a parameter whose ADDRESS is taken in a `for` header is never copied',
|
|
91
|
+
rejects: (c) => c.addressed,
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
/** One candidate REGION, as the region rules see it. Separate from {@link ARGCOPY_GATES} for the
|
|
96
|
+
* reason `scopebase` keeps two tables: those rules judge the PARAMETER once, these judge each
|
|
97
|
+
* place it could be copied, and a refusal tally that mixed the two would count a parameter's
|
|
98
|
+
* single verdict once per region. */
|
|
99
|
+
export interface ArgCopyRegionCtx {
|
|
100
|
+
/** reads of the parameter inside this region */
|
|
101
|
+
readonly reads: number;
|
|
102
|
+
/** a loop encloses the region — its own body, or any list nested below one */
|
|
103
|
+
readonly underLoop: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Which regions are worth offering. NEITHER is sound — each refuses a spelling that is correct
|
|
107
|
+
* but models nothing, and what they buy is the candidate count: every one of these is a COMPILE,
|
|
108
|
+
* and this variation multiplies with `/coalesce`. */
|
|
109
|
+
export const ARGCOPY_REGION_GATES: readonly Gate<ArgCopyRegionCtx>[] = [
|
|
110
|
+
{
|
|
111
|
+
id: 'single-read',
|
|
112
|
+
why: 'a region reading the parameter once gives the allocator no live range to shorten, so the copy buys nothing',
|
|
113
|
+
sound: false,
|
|
114
|
+
rejects: (c) => c.reads < 2,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'loop-region',
|
|
118
|
+
why: 'a copy anywhere inside a loop re-runs every iteration, so it belongs to a region that holds the loop instead — and no region does when the loop is at function top level, where this variation offers nothing at all',
|
|
119
|
+
sound: false,
|
|
120
|
+
// the second clause has its own test, which `guardedBy` cannot also name (one title per gate):
|
|
121
|
+
// `argcopy.test.ts: a TOP-LEVEL loop is offered NOTHING`
|
|
122
|
+
guardedBy: 'argcopy.test.ts: an arm NESTED inside a loop body is refused too',
|
|
123
|
+
rejects: (c) => c.underLoop,
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
/** How many `var n` leaves the region holds, nested statements included — the region rules'
|
|
128
|
+
* yardstick for whether a copy has a range to shorten.
|
|
129
|
+
*
|
|
130
|
+
* `walkExprs` (ast.ts) is the whole-tree walk, which descends through `stmtChildren` and so sees a
|
|
131
|
+
* `for`'s `init` and `inc`. That is the walk this file must use everywhere: `repoint` rewrites
|
|
132
|
+
* those two statements (`mapStmtExprs` recurses into them), so a count taken with `stmtLists` —
|
|
133
|
+
* which deliberately omits them, being a walk over the SCOPES a statement opens — would report
|
|
134
|
+
* fewer reads than the rewrite touches. */
|
|
135
|
+
function countReads(list: Stmt[], n: string): number {
|
|
136
|
+
let reads = 0;
|
|
137
|
+
for (const e of walkExprs(list)) {
|
|
138
|
+
if (e.k === 'var' && e.name === n) {
|
|
139
|
+
reads++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return reads;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Every nested statement list in `body`, each with the path that reaches it — a REGION is any
|
|
146
|
+
* list a statement contains, which is what a `case` body, an `if` arm and a loop body all are.
|
|
147
|
+
* The function's own top-level list is NOT among them: repointing it renames the parameter over
|
|
148
|
+
* the whole function, which is a PARK — the shape `l3/parkfirst.ts` reorders — and not the two
|
|
149
|
+
* separable live ranges this variation is about. */
|
|
150
|
+
function regions(body: Stmt[]): { at: number[]; list: Stmt[]; underLoop: boolean }[] {
|
|
151
|
+
const out: { at: number[]; list: Stmt[]; underLoop: boolean }[] = [];
|
|
152
|
+
// carried DOWN rather than read off the immediate parent: an `if` arm two levels inside a loop
|
|
153
|
+
// body re-runs every iteration exactly as the body does, which is what `loop-region` refuses
|
|
154
|
+
const walk = (list: Stmt[], path: number[], underLoop: boolean): void => {
|
|
155
|
+
list.forEach((st, i) => {
|
|
156
|
+
const inside = underLoop || isLoop(st);
|
|
157
|
+
stmtLists(st).forEach((inner, j) => {
|
|
158
|
+
const here = [...path, i, j];
|
|
159
|
+
out.push({ at: here, list: inner, underLoop: inside });
|
|
160
|
+
walk(inner, here, inside);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
walk(body, [], false);
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** `list` with every `var n` leaf rewritten to `var to`, nested statements included. Both kind
|
|
169
|
+
* switches are ast.ts's, so a new statement or expression kind is that file's problem, not this
|
|
170
|
+
* one's.
|
|
171
|
+
*
|
|
172
|
+
* ONE walk: `mapStmtExprs` already recurses into every nested statement — a `for`'s `init`/`inc`
|
|
173
|
+
* included, which is what makes this symmetric with `countReads` — so a single `map` over the list
|
|
174
|
+
* reaches the whole subtree. Driving it through `mapStmtLists` as well would visit a statement at
|
|
175
|
+
* depth d once per level above it: 40 expression visits against 13 on a five-level tree (switch >
|
|
176
|
+
* for > do-while > if > store). This runs once per region per parameter per candidate, and each
|
|
177
|
+
* candidate is a compile. */
|
|
178
|
+
function repoint(list: Stmt[], n: string, to: string): Stmt[] {
|
|
179
|
+
const inExpr = (e: Expr): Expr => (e.k === 'var' && e.name === n ? { ...e, name: to } : mapExprChildren(e, inExpr));
|
|
180
|
+
return list.map((s) => mapStmtExprs(s, inExpr));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Replace the region at `at` with `next`. */
|
|
184
|
+
function replaceRegion(body: Stmt[], at: number[], next: Stmt[]): Stmt[] {
|
|
185
|
+
const [i, j, ...rest] = at;
|
|
186
|
+
return body.map((st, k) => {
|
|
187
|
+
if (k !== i) {
|
|
188
|
+
return st;
|
|
189
|
+
}
|
|
190
|
+
let seen = -1;
|
|
191
|
+
return mapStmtLists(st, (inner) => {
|
|
192
|
+
seen++;
|
|
193
|
+
if (seen !== j) {
|
|
194
|
+
return inner;
|
|
195
|
+
}
|
|
196
|
+
return rest.length ? replaceRegion(inner, rest, next) : next;
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** `argCopyCandidates` with the gate table supplied plus which gate refused each parameter — the
|
|
202
|
+
* same ablation-as-a-value seam the coalescer provides. */
|
|
203
|
+
export function argCopyUnder(
|
|
204
|
+
gates: readonly Gate<ArgCopyCtx>[],
|
|
205
|
+
sfn: SFn,
|
|
206
|
+
regionGates: readonly Gate<ArgCopyRegionCtx>[] = ARGCOPY_REGION_GATES,
|
|
207
|
+
): { candidates: { merged: string; sfn: SFn }[]; refusals: Map<string, number> } {
|
|
208
|
+
const refusals = new Map<string, number>();
|
|
209
|
+
const out: { merged: string; sfn: SFn }[] = [];
|
|
210
|
+
/** `&n` anywhere in the function — every expression node, `for` header included. */
|
|
211
|
+
const addressed = (n: string): boolean => {
|
|
212
|
+
for (const e of walkExprs(sfn.body)) {
|
|
213
|
+
if (e.k === 'addr' && e.name === n) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
};
|
|
219
|
+
/** The function assigns `n` anywhere. `stmtChildren`, never `stmtLists`: a `for`'s `init` and
|
|
220
|
+
* `inc` are STATEMENTS rather than lists, and an induction step that advances the parameter is
|
|
221
|
+
* exactly one of them — the shape `structure/structure.ts`'s `recognizeForLoops` mints. */
|
|
222
|
+
const assignsTo = (n: string, list: Stmt[] = sfn.body): boolean =>
|
|
223
|
+
list.some((st) => (st.k === 'assign' && st.name === n) || assignsTo(n, stmtChildren(st)));
|
|
224
|
+
for (const p of sfn.params) {
|
|
225
|
+
const refused = firstRejection(gates, {
|
|
226
|
+
param: p.name,
|
|
227
|
+
isPointer: p.type.kind === 'ptr',
|
|
228
|
+
assigned: assignsTo(p.name),
|
|
229
|
+
addressed: addressed(p.name),
|
|
230
|
+
});
|
|
231
|
+
if (refused !== null) {
|
|
232
|
+
refusals.set(refused, (refusals.get(refused) ?? 0) + 1);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
for (const r of regions(sfn.body)) {
|
|
236
|
+
// not a gate: a region with no read of the parameter has nothing to repoint
|
|
237
|
+
const reads = countReads(r.list, p.name);
|
|
238
|
+
if (reads === 0) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const regionRefused = firstRejection(regionGates, { reads, underLoop: r.underLoop });
|
|
242
|
+
if (regionRefused !== null) {
|
|
243
|
+
refusals.set(regionRefused, (refusals.get(regionRefused) ?? 0) + 1);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
// allocated against the ORIGINAL function, so every candidate names its copy identically —
|
|
247
|
+
// each is a separate tree and none of them ever meets another
|
|
248
|
+
const name = nameAllocator(sfn)();
|
|
249
|
+
const copied = [
|
|
250
|
+
{ k: 'assign', name, value: { k: 'var', name: p.name } } as Stmt,
|
|
251
|
+
...repoint(r.list, p.name, name),
|
|
252
|
+
];
|
|
253
|
+
out.push({
|
|
254
|
+
merged: `${p.name}@${r.at.join('.')}`,
|
|
255
|
+
sfn: {
|
|
256
|
+
...sfn,
|
|
257
|
+
body: replaceRegion(sfn.body, r.at, copied),
|
|
258
|
+
locals: [...sfn.locals, { name, type: p.type }],
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { candidates: out, refusals };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Every legal region copy, each as its own tree. */
|
|
267
|
+
export function argCopyCandidates(sfn: SFn): { merged: string; sfn: SFn }[] {
|
|
268
|
+
return argCopyUnder(ARGCOPY_GATES, sfn).candidates;
|
|
269
|
+
}
|
package/src/l3/ast.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// asmlift L3 — the language-NEUTRAL structured AST. A LanguageBackend lowers this to a
|
|
2
2
|
// concrete language (C / Pascal / C++) and prints it. "Return a value" and binary ops
|
|
3
3
|
// are neutral nodes here; each backend owns its own spelling.
|
|
4
|
-
import type
|
|
4
|
+
import { type IrType, typeEquals } from '../ir/types';
|
|
5
5
|
|
|
6
6
|
export type Expr =
|
|
7
7
|
| { k: 'var'; name: string }
|
|
@@ -74,7 +74,7 @@ export type Expr =
|
|
|
74
74
|
// • a local (`var`) — nothing to reassociate into; already right, and no reader fires.
|
|
75
75
|
// • a plus tree — the fold pulls the displacement into the tree, costing an `add` and a
|
|
76
76
|
// register. The repair is to home the base in a local so it becomes a
|
|
77
|
-
// `var`, which is the ADDRESS-HOME
|
|
77
|
+
// `var`, which is the ADDRESS-HOME variation and lives one level down at L2
|
|
78
78
|
// (`structure/analysis.ts`'s `sharedBaseClasses`) because materializing
|
|
79
79
|
// a value is a structuring decision, not a spelling.
|
|
80
80
|
// • a leaf const/addr — the fold bakes the displacement into the literal, changing the `.word`.
|
|
@@ -96,6 +96,16 @@ export type Expr =
|
|
|
96
96
|
// reason — both spellings denote the same cell — and it is per SYMBOL, so every access of one
|
|
97
97
|
// name carries the same answer, which is right for agbcc because one CSEd pool load serves
|
|
98
98
|
// them all.
|
|
99
|
+
//
|
|
100
|
+
// `baseAdvanced` is the THIRD, and it answers about the ADDRESS COMPUTATION: this access's
|
|
101
|
+
// address was reached by ADDING this many bytes to a register that already held — and had just
|
|
102
|
+
// been used as — another address (`adds r3, #2` between two stores), rather than by a second
|
|
103
|
+
// pool word or a memory-operand displacement. `raise/const.ts` records it on the literal its
|
|
104
|
+
// fold produces, because the fold is what makes the three shapes indistinguishable; the
|
|
105
|
+
// structure seam copies it here. `l3/advance.ts` is what reads it, to offer a pointer local
|
|
106
|
+
// advanced in place. `exprEquals` ignores it for `operandOff`'s reason. Absence is never proof:
|
|
107
|
+
// a target whose frontend does not lift the advance stamps nothing, and the value is a byte
|
|
108
|
+
// count that can be NEGATIVE, so readers test `!== undefined`.
|
|
99
109
|
| {
|
|
100
110
|
k: 'index';
|
|
101
111
|
base: Expr;
|
|
@@ -103,8 +113,35 @@ export type Expr =
|
|
|
103
113
|
width: number;
|
|
104
114
|
signed: boolean;
|
|
105
115
|
lead?: Expr[];
|
|
116
|
+
/** The ELEMENT type the base's own DECLARATION gives it, for the one base whose stride the
|
|
117
|
+
* C type walk cannot reconstruct: a map-declared array MEMBER (`gPtr->arr`), which
|
|
118
|
+
* `exprCType` types `undefined` because the `field` node hangs off an untyped `var`. Without
|
|
119
|
+
* it the C backend legalizes the base through a reinterpret cast (`((u8 *)gPtr->arr)[i]`),
|
|
120
|
+
* which is the CAST form's object again and defeats the whole point of naming the member.
|
|
121
|
+
*
|
|
122
|
+
* IT IS A PRODUCER INVARIANT: the backend cannot vet a stated type. `derefStrideOk` tests
|
|
123
|
+
* the STATED type against the access width, never against the base, so it is true by
|
|
124
|
+
* construction for anything a producer could state AND for a statement gone stale. What
|
|
125
|
+
* the consumer does guard is PRECEDENCE — this field is consulted only where `exprCType`
|
|
126
|
+
* answers nothing (cfamily.ts `legalizedIndexBase`), so wherever the walk can read the base
|
|
127
|
+
* it corrects a stale statement instead of being overridden by it. The one producer,
|
|
128
|
+
* structure/structure.ts `pointeeElement`, sets it from the same `elemSize`/`elemSigned` it
|
|
129
|
+
* has just passed `spellsAccessType` on — and that predicate IS
|
|
130
|
+
* `typeEquals(T.int(width*8, elemSigned), scalarTypeForAccess(width, signed))`, so
|
|
131
|
+
* `derefStrideOk` over the stated pointer is true by construction at every width (4 by
|
|
132
|
+
* `width === 4`, 1 and 2 by `to.signed === signed`). The obligation is on the PRODUCER:
|
|
133
|
+
* state the type the access's own width and signedness agree with.
|
|
134
|
+
*
|
|
135
|
+
* THE ALTERNATIVE REJECTED: teach `exprCType` the pointee layout, the way
|
|
136
|
+
* `sym.noteGlobal` types the bare-array spelling —
|
|
137
|
+
* the printer already renders `u8 grid[6][8]` for this member from this same layout. It was
|
|
138
|
+
* rejected because `SFn.globals` is also the ADDRESSABLE-BASE list, so typing the symbol
|
|
139
|
+
* there admits it as a `/livebase` base (measured on the probe: 8 extra base locals). Typing
|
|
140
|
+
* the one node costs no candidates. */
|
|
141
|
+
baseElem?: IrType;
|
|
106
142
|
operandOff?: number;
|
|
107
143
|
baseOrdered?: true;
|
|
144
|
+
baseAdvanced?: number;
|
|
108
145
|
}
|
|
109
146
|
// A named struct-field access `base->name` (raise/structs.ts recovered `base` as a struct
|
|
110
147
|
// pointer, so the byte offset resolves to a named field instead of a scaled array index).
|
|
@@ -145,7 +182,7 @@ export type Expr =
|
|
|
145
182
|
//
|
|
146
183
|
// The COMPARISONS stay collapsed, and that asymmetry is the rule applying rather than an omission:
|
|
147
184
|
// which side a compare was spelled from genuinely underdetermines — a signed spelling that
|
|
148
|
-
// byte-matched was proved non-negative by the compiler — so it is refereed as
|
|
185
|
+
// byte-matched was proved non-negative by the compiler — so it is refereed as a variation, while a
|
|
149
186
|
// division helper is a pure function of the expression's C type with no such proof available.
|
|
150
187
|
export type BinOp =
|
|
151
188
|
| '+'
|
|
@@ -229,7 +266,13 @@ export type Stmt =
|
|
|
229
266
|
// by instrumenting the printer. Both regimes produce them: the jump table spells `case 4:` of
|
|
230
267
|
// `kleod:UpdateWorldMapNodeAnim`, the comparison tree `synthetic:sw_fallmem:agbcc`.
|
|
231
268
|
| { k: 'switch'; scrutinee: Expr; cases: SwitchCase[]; default?: Stmt[]; defaultAt?: number }
|
|
232
|
-
|
|
269
|
+
// `unspelled` — the assembly shows no `return` STATEMENT behind this node: the machine reached
|
|
270
|
+
// the epilogue here by falling into it, or on a conditional branch's own edge, never by the
|
|
271
|
+
// `b <epilogue>` a source `return;` compiles to (structure.ts sets it; the reading is stated
|
|
272
|
+
// there). A void return is then a no-op wherever nothing follows it, and `l3/tailret.ts` drops
|
|
273
|
+
// it — which is not cosmetic: unoptimised code gives every source `return;` a branch of its own,
|
|
274
|
+
// so the redundant spelling costs bytes. Advice about SPELLING; nothing may read it as semantics.
|
|
275
|
+
| { k: 'return'; value?: Expr; unspelled?: true };
|
|
233
276
|
|
|
234
277
|
/** One arm of a `switch`. `values` stacks multiple `case K:` labels onto one body (`case 1: case 2:`).
|
|
235
278
|
* `fallsThrough` true ⇒ the body flows into the NEXT arm (no `break;`); see the non-neutrality note. */
|
|
@@ -245,7 +288,7 @@ export interface SFn {
|
|
|
245
288
|
/** Recovered locals, declared at function top. Two INDEPENDENT volatility facts, mirroring
|
|
246
289
|
* symbols.ts's cell-vs-pointee split: `volatile` = the local OBJECT is volatile (the
|
|
247
290
|
* address-escaped frame scratch; dce.ts treats reads of it as observable), `pointeeVolatile`
|
|
248
|
-
* = the local is a pointer TO volatile data (the l3/volatileptr.ts
|
|
291
|
+
* = the local is a pointer TO volatile data (the l3/volatileptr.ts variation; a declaration
|
|
249
292
|
* spelling only — nothing about the local itself is observable).
|
|
250
293
|
*
|
|
251
294
|
* `frame` is present on a local the structurer recovered from an `laddr` — the asm
|
|
@@ -254,7 +297,7 @@ export interface SFn {
|
|
|
254
297
|
* SUB-WORD frame object: `strh/ldrh/strb/ldrb` have no `[sp,#imm]` form, so a compiler must
|
|
255
298
|
* copy `sp` first, while a word spill goes straight to `[sp,#imm]` and is recovered as an
|
|
256
299
|
* SSA value with no local of its own. So `frame` is NOT the set of every value the machine
|
|
257
|
-
* slotted. `loads`/`stores` are the yardstick a qualifier
|
|
300
|
+
* slotted. `loads`/`stores` are the yardstick a qualifier variation must match before it may
|
|
258
301
|
* declare every access to the object observable: the readability passes between here and L3
|
|
259
302
|
* may drop a store or render one machine load as two reads, and `volatile` over an access
|
|
260
303
|
* set asmlift did not preserve is a source that contradicts itself. ABSENT where the counts
|
|
@@ -312,7 +355,7 @@ export interface SFn {
|
|
|
312
355
|
* exists to keep it out of. So the tree is neutral in its NODES — every node still spells the
|
|
313
356
|
* same thing in C, C++ and Pascal — and not in its emission policy, which this field is.
|
|
314
357
|
* `ascending` = the earlier-declared spilled local takes the LOWER `[sp,#k]`. Set by the
|
|
315
|
-
* structurer from `StructureOptions.spillSlotOrder`, itself a
|
|
358
|
+
* structurer from `StructureOptions.spillSlotOrder`, itself a compiler behavior declared in
|
|
316
359
|
* `TargetDescription.compilerBehaviors`. ABSENT means the direction is unknown for this target
|
|
317
360
|
* and `l3/slotorder.ts` is the identity — never "ascending by default". */
|
|
318
361
|
slotOrder?: 'ascending' | 'descending';
|
|
@@ -396,7 +439,7 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
396
439
|
const bb = b as typeof a;
|
|
397
440
|
// `volatile` is part of the SPELLING, compared for the same reason `lead` and `dot` are: a
|
|
398
441
|
// CSE or dedup that treats these as equal keeps one node and drops the other, silently
|
|
399
|
-
//
|
|
442
|
+
// rewriting a volatile access as a plain one.
|
|
400
443
|
return (
|
|
401
444
|
JSON.stringify(a.to) === JSON.stringify(bb.to) &&
|
|
402
445
|
(a.volatile ?? false) === (bb.volatile ?? false) &&
|
|
@@ -411,9 +454,9 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
411
454
|
const bb = b as typeof a;
|
|
412
455
|
// `lead` is part of the ADDRESS (`g[0][i]` and `g[1][i]` are different elements), so it
|
|
413
456
|
// must be compared — an omission here would let CSE/dedup collapse two distinct accesses.
|
|
414
|
-
// `operandOff` deliberately
|
|
415
|
-
//
|
|
416
|
-
// collapses them respells nothing.
|
|
457
|
+
// The EVIDENCE fields (`operandOff`, `baseOrdered`, `baseAdvanced`) deliberately are NOT:
|
|
458
|
+
// two accesses agreeing on everything else denote the same cell and print the same subscript
|
|
459
|
+
// however the machine spelled the offset, so a CSE that collapses them respells nothing.
|
|
417
460
|
const lead = a.lead ?? [];
|
|
418
461
|
const bLead = bb.lead ?? [];
|
|
419
462
|
return (
|
|
@@ -421,6 +464,13 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
421
464
|
a.signed === bb.signed &&
|
|
422
465
|
lead.length === bLead.length &&
|
|
423
466
|
lead.every((v, i) => exprEquals(v, bLead[i])) &&
|
|
467
|
+
// `baseElem` is part of the SPELLING for the same reason `lead` is: it decides whether the
|
|
468
|
+
// base takes the reinterpret cast, so two otherwise-equal nodes disagreeing about it print
|
|
469
|
+
// two different expressions. (Its one producer derives it from the base and the width, so
|
|
470
|
+
// nodes that agree on those agree here too — this cannot reject a real CSE.)
|
|
471
|
+
(a.baseElem === undefined
|
|
472
|
+
? bb.baseElem === undefined
|
|
473
|
+
: bb.baseElem !== undefined && typeEquals(a.baseElem, bb.baseElem)) &&
|
|
424
474
|
exprEquals(a.base, bb.base) &&
|
|
425
475
|
exprEquals(a.idx, bb.idx)
|
|
426
476
|
);
|
|
@@ -428,7 +478,7 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
428
478
|
case 'field': {
|
|
429
479
|
const bb = b as typeof a;
|
|
430
480
|
// `dot` is part of the SPELLING, and for the same reason `lead` is compared above: a CSE or
|
|
431
|
-
// dedup that treats these as equal keeps one node and discards the other, silently
|
|
481
|
+
// dedup that treats these as equal keeps one node and discards the other, silently rewriting
|
|
432
482
|
// `p->field_4` as `p.field_4` (or the reverse). Both compile only for the base type each
|
|
433
483
|
// belongs to, so collapsing them is how a valid access becomes an invalid one — or worse, a
|
|
434
484
|
// valid one against a different object.
|
|
@@ -533,7 +583,7 @@ export function stmtExprs(s: Stmt): Expr[] {
|
|
|
533
583
|
* heads included, nested statements recursively. The rewrite dual of stmtExprs/stmtChildren, for
|
|
534
584
|
* the PURE 1:1 case: one expression in, one expression out, every statement kept.
|
|
535
585
|
*
|
|
536
|
-
* A
|
|
586
|
+
* A PASS THAT HAND-ROLLS ITS OWN MAPPER IS NOT A MISSED MIGRATION. Several do, because their
|
|
537
587
|
* contract is not this one — a rewrite that may DECLINE, one that INSERTS statements, one that
|
|
538
588
|
* rewrites assign TARGETS as well as expressions, one that turns a statement into a list. Check
|
|
539
589
|
* the contract before pointing one of them here. */
|
|
@@ -583,13 +633,13 @@ export function mapStmtExprs(s: Stmt, f: (e: Expr) => Expr): Stmt {
|
|
|
583
633
|
* one admits and the other refuses can be re-indexed but never qualified, so the paired
|
|
584
634
|
* `/indexed/volatile` spelling is unreachable at exactly the hardware addresses it is for.
|
|
585
635
|
*
|
|
586
|
-
* Two
|
|
636
|
+
* Two respell variations reading the same initializers are deliberately NOT here: l3/inlinebase.ts
|
|
587
637
|
* substitutes the address at each use, l3/nearbase.ts clusters neighbours by distance, and both
|
|
588
638
|
* need the VALUE, which is the evaluator above. Declining a shift-encoded base there costs a
|
|
589
|
-
*
|
|
639
|
+
* variation that does not fire, and the population is small: over klonoa's 531 lifting functions,
|
|
590
640
|
* one inlinebase-shaped local with no symbol map and none with it; a folded nearbase would form
|
|
591
641
|
* a new cluster in 4 functions mapless and 1 with the map. Both counts were zero over the agbcc
|
|
592
|
-
* benchmark rows that lifted when the
|
|
642
|
+
* benchmark rows that lifted when the variations landed — a MEASUREMENT over a corpus that grows, so
|
|
593
643
|
* re-take it rather than quoting it: a new row falsifies the number, not the argument. */
|
|
594
644
|
export function rematerializableAddress(e: Expr): boolean {
|
|
595
645
|
let nonZero = false;
|
|
@@ -626,8 +676,8 @@ export function exprHasEffect(e: Expr): boolean {
|
|
|
626
676
|
* about a QUALIFIER, and a pass that asks the first where it means the second reorders device
|
|
627
677
|
* accesses while its gate reports clean.
|
|
628
678
|
*
|
|
629
|
-
* Three spellings assert one thing, and all three are here because a
|
|
630
|
-
* would miss the two a later
|
|
679
|
+
* Three spellings assert one thing, and all three are here because a pass that knew only the cast
|
|
680
|
+
* would miss the two a later variation writes: a `volatile` cast (where a raw address carries it), a
|
|
631
681
|
* read through a pointer local declared to point at volatile data (l3/volatileptr.ts), and a read
|
|
632
682
|
* of a `volatile` local object (l3/volatileval.ts). A bare cast counts even with no deref under it
|
|
633
683
|
* — the qualifier is on the ACCESS the cast exists to spell, and every caller so far is asking
|
|
@@ -681,6 +731,44 @@ export function stmtChildren(s: Stmt): Stmt[] {
|
|
|
681
731
|
}
|
|
682
732
|
}
|
|
683
733
|
|
|
734
|
+
/** A statement that REPEATS what it contains — what {@link isLoop} narrows to.
|
|
735
|
+
*
|
|
736
|
+
* Derived STRUCTURALLY, not from a second list of kinds: a repeating statement is exactly one that
|
|
737
|
+
* carries both a `cond` and a `body`, and no other `Stmt` carries both (`if` has `cond` without
|
|
738
|
+
* `body`, `switch` has neither). A new repeating kind therefore joins this type by construction,
|
|
739
|
+
* where a hand-written kind list falls out of step SILENTLY — TypeScript does not check a
|
|
740
|
+
* predicate's body against the type it asserts. */
|
|
741
|
+
export type Loop = Extract<Stmt, { cond: Expr; body: Stmt[] }>;
|
|
742
|
+
|
|
743
|
+
/** Does this statement REPEAT what it contains? Its body and its own condition run once per
|
|
744
|
+
* iteration, which is the fact half of `l3/` asks about: a hoist out of one is loop-invariant
|
|
745
|
+
* code motion, a copy inside one re-runs, and a live range that crosses one is under pressure.
|
|
746
|
+
*
|
|
747
|
+
* It lives here, exhaustive and with no `default`, for the reason {@link stmtLists} does: a new
|
|
748
|
+
* `Stmt` kind that repeats is then a compile error at ONE site rather than a silent `false` in
|
|
749
|
+
* each caller's own hand-spelled kind test. It NARROWS to {@link Loop} rather than returning
|
|
750
|
+
* `boolean`, because a caller that reaches `body` afterwards cannot use a `boolean` one and spells
|
|
751
|
+
* the kind test out again instead. `backend/pascal.ts` is where that costs correctness: its
|
|
752
|
+
* `hasReturn` walk decides a LOUD failure — Pascal's `case-of` cannot spell an early `return` — so
|
|
753
|
+
* a repeating kind read as a non-loop turns the refusal into an emitted miscompile. */
|
|
754
|
+
export function isLoop(s: Stmt): s is Loop {
|
|
755
|
+
switch (s.k) {
|
|
756
|
+
case 'while':
|
|
757
|
+
case 'dowhile':
|
|
758
|
+
case 'for':
|
|
759
|
+
return true;
|
|
760
|
+
case 'if':
|
|
761
|
+
case 'switch':
|
|
762
|
+
case 'assign':
|
|
763
|
+
case 'store':
|
|
764
|
+
case 'exprstmt':
|
|
765
|
+
case 'return':
|
|
766
|
+
case 'break':
|
|
767
|
+
case 'continue':
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
684
772
|
/** The nested statement LISTS of a statement — the SCOPES it opens.
|
|
685
773
|
*
|
|
686
774
|
* Deliberately not `stmtChildren`, which flattens a `for`'s `init`/`inc` in with its body: those
|
|
@@ -801,14 +889,14 @@ export function* walkExprs(body: Stmt[]): Generator<Expr> {
|
|
|
801
889
|
// so this must never be used to negate a general integer expression — only an `if`/loop test or an
|
|
802
890
|
// operand of one of the connectives above.
|
|
803
891
|
//
|
|
804
|
-
// SCOPE of rule 2: it fires wherever a branch-sense
|
|
892
|
+
// SCOPE of rule 2: it fires wherever a branch-sense variation negates the condition, which is both `if`
|
|
805
893
|
// classes — `preserveDivergentBranchSense` on divergent ifs, `negateJoinedBranchSense` on
|
|
806
894
|
// reconverging ones — and on the joined class it is a DEFAULT emission, not a differ-only
|
|
807
|
-
// alternative, so a source `&&` can come out as its `||` dual with no
|
|
895
|
+
// alternative, so a source `&&` can come out as its `||` dual with no variation asked for
|
|
808
896
|
// (`synthetic:ifand_far`, where the branch range put the fold on the other arm). Neither
|
|
809
|
-
//
|
|
897
|
+
// variation reaches a LOOP test, so a connective that ended up as one has no dual candidate at all —
|
|
810
898
|
// the differ never sees the other form, and on such a row this rule changes how the code READS and
|
|
811
|
-
// nothing else. Widening a branch-sense
|
|
899
|
+
// nothing else. Widening a branch-sense variation to loop tests is what would make it a matching variation
|
|
812
900
|
// there, and that is a separate change.
|
|
813
901
|
export const NEGATE_REL: Partial<Record<BinOp, BinOp>> = {
|
|
814
902
|
'<': '>=',
|