@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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// asmlift — the symbol-naming policy for relocations, as data.
|
|
2
|
+
//
|
|
3
|
+
// A relocation hands the frontend a LINKER's name, and a linker's namespace is strictly larger than
|
|
4
|
+
// C's: it holds anonymous constant pools, section-relative labels, C++ vtables and mangled
|
|
5
|
+
// function-scope statics, none of which any C or C++ source can spell. Recovering an address is
|
|
6
|
+
// only half the job — the other half is deciding, per KIND of name, whether the recovered global
|
|
7
|
+
// can be written down at all.
|
|
8
|
+
//
|
|
9
|
+
// The failure this exists to prevent is the one m2c shipped as `unksp0` for years: a gap rendered
|
|
10
|
+
// as an ordinary-looking identifier. `__vt__6System` is the sharpest case — `extern u32
|
|
11
|
+
// __vt__6System;` COMPILES, so nothing downstream would ever complain; the candidate would simply
|
|
12
|
+
// be wrong in a way that reads as right. So the decision is made here, once, by the shape of the
|
|
13
|
+
// name, and every refusing kind gets its own sentence naming what was seen.
|
|
14
|
+
//
|
|
15
|
+
// Consumed by frontend/ppc.ts, which refuses before it recovers. A kind is listed only when it
|
|
16
|
+
// behaves differently: the decomp projects' generated labels (`lbl_1_bss_2464`, `fn_1_458`) are
|
|
17
|
+
// ordinary identifiers that the project's own headers declare and its own sources spell, so they
|
|
18
|
+
// are `plain` and get no entry of their own.
|
|
19
|
+
//
|
|
20
|
+
// THE SCOPE OF THIS POLICY IS THE NAME, AND ONLY ON A DATA RELOCATION.
|
|
21
|
+
// • Not the TYPE. A name this passes is still rendered at the width the asm implies, which is a
|
|
22
|
+
// different seam and fails loud in the compiler (docs/symbol-naming-policy.md).
|
|
23
|
+
// • Not the LINKAGE. A file-scope `static` and an `extern` of the same name compile to the same
|
|
24
|
+
// object under mwcc — same bytes, same named relocation — so the minted `extern` asserts
|
|
25
|
+
// nothing the reference did not already assert (measured; see the doc).
|
|
26
|
+
// • Not a `bl` CALLEE. Those reach the emitter through the call path and are NOT classified here:
|
|
27
|
+
// a C++ row's candidate is compiled inside an `extern \"C\"` block, where a mangled name written
|
|
28
|
+
// verbatim denotes exactly that symbol (apps/benchmark/src/compile/real.ts `candidateLinkage`),
|
|
29
|
+
// so a callee needs no policy the way a global that must be DECLARED does.
|
|
30
|
+
//
|
|
31
|
+
// The evidence, the counts and the corpus behind every rule are in docs/symbol-naming-policy.md.
|
|
32
|
+
|
|
33
|
+
/** What sort of name a relocation carries. Everything but `plain` is unspellable in C. */
|
|
34
|
+
export type RelocSymbolKind =
|
|
35
|
+
'plain' | 'anon-pool' | 'section-local' | 'local-static' | 'cpp-vtable' | 'cpp-mangled' | 'not-an-identifier';
|
|
36
|
+
|
|
37
|
+
/** Classify a relocation's symbol by its spelling. Order matters: the shapes that ARE valid C
|
|
38
|
+
* identifiers (`__vt__…`, a mangled class-scoped name) or contain characters a C identifier may
|
|
39
|
+
* not (`@`, `.`, `$`) are each recognised before the general identifier test, so the catch-all
|
|
40
|
+
* below can be exactly "a name of no kind this policy knows, and not an identifier either". */
|
|
41
|
+
export function classifyRelocSymbol(sym: string): RelocSymbolKind {
|
|
42
|
+
if (sym.startsWith('@')) {
|
|
43
|
+
return 'anon-pool'; // `@193` — mwcc's anonymous string/constant pool entries
|
|
44
|
+
}
|
|
45
|
+
if (sym.startsWith('.')) {
|
|
46
|
+
return 'section-local'; // `...bss.0`, `.rodata` — an offset into a section
|
|
47
|
+
}
|
|
48
|
+
if (sym.includes('$')) {
|
|
49
|
+
return 'local-static'; // `sprHideTbl$797` — a function-scope static plus mwcc's TU-wide counter
|
|
50
|
+
}
|
|
51
|
+
if (sym.startsWith('__vt__')) {
|
|
52
|
+
return 'cpp-vtable'; // `__vt__6System` — a compiler-emitted virtual table
|
|
53
|
+
}
|
|
54
|
+
// mwcc mangles a class-scoped name as `<name>__<length><Class>` (`statbuff__9CmdStream`) or,
|
|
55
|
+
// for a nested scope, `<name>__Q<depth><…>` (`__ct__Q26Action5ChildFv`). The marker is the `__`
|
|
56
|
+
// followed by that LENGTH or `Q<depth>`, never a double underscore on its own: a rule that fired
|
|
57
|
+
// on `__` would refuse ordinary C globals like `g_my__table` and `__initialised`.
|
|
58
|
+
//
|
|
59
|
+
// THE MARKER IS THE SCOPE, NOT THE MANGLING, and the difference is deliberate. A free function's
|
|
60
|
+
// parameter mangle (`makeObjectBoss__Fv`, `ARAMFinish__FUl` — 89 of the 24,236 distinct symbols a
|
|
61
|
+
// data relocation names across the three checkouts) is `plain`: what this kind refuses is the
|
|
62
|
+
// missing DECLARATION, and a free function has no class scope for the symbol map to suppress one
|
|
63
|
+
// through. 0 of the 4,663 lifted functions in the same sweep emit such a name, so the line is
|
|
64
|
+
// documented rather than moved; docs/symbol-naming-policy.md names the two measurements that
|
|
65
|
+
// would have to come first.
|
|
66
|
+
if (/__(?:\d|Q\d)/.test(sym)) {
|
|
67
|
+
return 'cpp-mangled';
|
|
68
|
+
}
|
|
69
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(sym) ? 'plain' : 'not-an-identifier';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Why a symbol of this kind cannot be written into a candidate, as the tail of a refusal sentence
|
|
73
|
+
* — or null for the one kind that can. The caller prefixes the function, mnemonic and address, so
|
|
74
|
+
* a reader of the artifact learns the kind without re-deriving it from the name. */
|
|
75
|
+
export function unspellableReason(sym: string): string | null {
|
|
76
|
+
switch (classifyRelocSymbol(sym)) {
|
|
77
|
+
case 'plain':
|
|
78
|
+
return null;
|
|
79
|
+
case 'anon-pool':
|
|
80
|
+
return (
|
|
81
|
+
`names an anonymous constant pool entry ('${sym}') — the compiler generated that name for a ` +
|
|
82
|
+
`literal it has no declaration for, so no C source can refer to it`
|
|
83
|
+
);
|
|
84
|
+
case 'section-local':
|
|
85
|
+
return (
|
|
86
|
+
`names a section-relative label ('${sym}') — it denotes an offset into a section, not an ` +
|
|
87
|
+
`object, so there is nothing to declare`
|
|
88
|
+
);
|
|
89
|
+
case 'local-static':
|
|
90
|
+
return (
|
|
91
|
+
`names a function-scope static ('${sym}') — the suffix is a translation-unit-wide counter ` +
|
|
92
|
+
`the compiler assigned, which no source can spell`
|
|
93
|
+
);
|
|
94
|
+
case 'cpp-vtable':
|
|
95
|
+
return (
|
|
96
|
+
`names a C++ virtual table ('${sym}') — the compiler emits it from a class definition, so ` +
|
|
97
|
+
`no source spells it (and declaring it anyway would compile, which is why this refuses)`
|
|
98
|
+
);
|
|
99
|
+
case 'cpp-mangled':
|
|
100
|
+
return (
|
|
101
|
+
`names a C++ class-scoped symbol ('${sym}') — a reference spelled this way reaches exactly ` +
|
|
102
|
+
`that symbol, but nothing here can DECLARE it: the row's own unit declares the member under ` +
|
|
103
|
+
`its class scope, which this frontend does not decode, and the candidate would name an ` +
|
|
104
|
+
`identifier no declaration introduces`
|
|
105
|
+
);
|
|
106
|
+
case 'not-an-identifier':
|
|
107
|
+
return `names '${sym}', which is not a C identifier`;
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/frontend/splat.ts
CHANGED
|
@@ -15,11 +15,16 @@
|
|
|
15
15
|
// • constant immediate EXPRESSIONS (`(0x660104 >> 16)`, `(x & 0xFFFF)`) — the assembler's hi/lo
|
|
16
16
|
// split of a 32-bit literal, evaluated here to the plain number the decode switch parses.
|
|
17
17
|
//
|
|
18
|
-
// `%hi`/`%lo` operands (a global's address) are
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
18
|
+
// `%hi`/`%lo` operands (a global's address) are turned into exactly what a relocatable object
|
|
19
|
+
// carries — an `R_MIPS_HI16`/`R_MIPS_LO16` record on the instruction, plus the immediate the
|
|
20
|
+
// instruction really encodes — so both MIPS dialects reach ONE fold (frontend/mips.ts,
|
|
21
|
+
// frontend/high-half.ts) and neither gets a pairing rule of its own. The encoding is the
|
|
22
|
+
// assembler's: `%hi(x)` is `((x + 0x8000) >> 16) & 0xffff`, ADJUSTED so the sign-extended low half
|
|
23
|
+
// cancels the carry, and `%lo(x)` is the sign-extended low 16 bits — which is what lets the fold
|
|
24
|
+
// recover `x` as `(hi << 16) + (s16)lo` for a positive or a negative offset alike.
|
|
25
|
+
// The other GOT/PIC relocations (`%gp_rel`, `%got`, …) are declined LOUD — small-data /
|
|
26
|
+
// position-independent access is not modelled.
|
|
27
|
+
import type { DisasmInstr, DisasmReloc } from './disasm';
|
|
23
28
|
import { FrontendUnsupportedError } from './errors';
|
|
24
29
|
|
|
25
30
|
// One instruction line: `/* ROM VRAM BYTES */ MNEMONIC OPS`. Group 1 is the VRAM address word.
|
|
@@ -29,9 +34,12 @@ const INSN_SIGNAL = /\/\*\s*[0-9A-Fa-f]+\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]+\s*\*\//;
|
|
|
29
34
|
// A local-label DEFINITION on its own line (`.L800011C0_1DC0:`); the colon is required.
|
|
30
35
|
const LABEL_DEF = /^(\.[\w.$]+):$/;
|
|
31
36
|
// A GOT/PIC relocation operand this reader does not support (small-data / position-independent
|
|
32
|
-
// access) — declined loud. `%hi`/`%lo` are NOT here: they name a global's address and
|
|
33
|
-
//
|
|
37
|
+
// access) — declined loud. `%hi`/`%lo` are NOT here: they name a global's address and become
|
|
38
|
+
// relocation records for the MIPS frontend to fold (see normalizeOperand / frontend/mips.ts).
|
|
34
39
|
const RELOC_OP = /%(gp_rel|gprel|got|call16|call_hi|call_lo|higher|highest|neg|tprel|dtprel)\b/i;
|
|
40
|
+
// Any `%hi`/`%lo` spelling at all, so a half `normalizeOperand`'s pattern cannot resolve is caught
|
|
41
|
+
// rather than falling through to the paths that read an operand as arithmetic.
|
|
42
|
+
const HILO_OP = /%(hi|lo)\s*\(/i;
|
|
35
43
|
// Data directives whose bytes could encode an effect: skipping one inside a function slice would
|
|
36
44
|
// silently delete it, so they decline (mirrors the Thumb frontend's in-code-data guard).
|
|
37
45
|
const DATA_DIRECTIVE =
|
|
@@ -120,7 +128,17 @@ export function parseSplatMips(asm: string, name: string): DisasmInstr[] {
|
|
|
120
128
|
`cannot lift '${name}': data directive '${mnemonic}' in the code stream — skipping it would silently delete its effect`,
|
|
121
129
|
);
|
|
122
130
|
}
|
|
123
|
-
const
|
|
131
|
+
const normalized = m[3].trim() ? splitOperands(m[3].trim()).map((o) => normalizeOperand(name, o)) : [];
|
|
132
|
+
const ops = normalized.map((n) => n.op);
|
|
133
|
+
// At most one relocation per instruction — the same invariant disasm.ts enforces on objdump
|
|
134
|
+
// output, and for the same reason: two would leave one symbol standing for the other's operand.
|
|
135
|
+
const relocs = normalized.map((n) => n.reloc).filter((r): r is DisasmReloc => r !== undefined);
|
|
136
|
+
if (relocs.length > 1) {
|
|
137
|
+
throw new FrontendUnsupportedError(
|
|
138
|
+
`cannot lift '${name}': two relocation operands on one instruction ('${mnemonic}' at ` +
|
|
139
|
+
`0x${addr.toString(16)}): '${relocs[0].sym}' and '${relocs[1].sym}'`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
124
142
|
// addi/addiu SIGN-EXTEND their 16-bit immediate; Splat may spell the low half of a materialised
|
|
125
143
|
// constant as an unsigned mask (`(0x8000ABCD & 0xFFFF)` = 0xABCD), so re-sign it here to match
|
|
126
144
|
// the hardware — and the objdump path, which prints the already-signed value. Zero-extending ops
|
|
@@ -132,7 +150,7 @@ export function parseSplatMips(asm: string, name: string): DisasmInstr[] {
|
|
|
132
150
|
labelAddr.set(l, addr);
|
|
133
151
|
}
|
|
134
152
|
pending = [];
|
|
135
|
-
instrs.push({ addr, mnemonic, ops });
|
|
153
|
+
instrs.push({ addr, mnemonic, ops, reloc: relocs[0] });
|
|
136
154
|
}
|
|
137
155
|
|
|
138
156
|
// Resolve every branch/jump's target label to an address. A target that is not a local label of
|
|
@@ -194,31 +212,51 @@ function splitOperands(s: string): string[] {
|
|
|
194
212
|
|
|
195
213
|
// Rewrite one Splat operand into the canonical objdump spelling the frontend consumes: strip the
|
|
196
214
|
// `$` register sigil, fold a memory operand's displacement expression, evaluate a bare constant
|
|
197
|
-
// expression,
|
|
198
|
-
function normalizeOperand(name: string, op: string): string {
|
|
199
|
-
// `%hi(SYM)` / `%lo(SYM + N)` / `%lo(SYM)(base)` — a global's address.
|
|
200
|
-
//
|
|
201
|
-
|
|
215
|
+
// expression, split a `%hi`/`%lo` reference into an immediate plus its record, decline a PIC one.
|
|
216
|
+
function normalizeOperand(name: string, op: string): { op: string; reloc?: DisasmReloc } {
|
|
217
|
+
// `%hi(SYM)` / `%lo(SYM + N)` / `%lo(SYM)(base)` — a global's address. Becomes the relocation
|
|
218
|
+
// record an object file would carry plus the immediate the instruction really encodes, so the
|
|
219
|
+
// frontend folds this dialect through the same path as objdump; NOT declined like the PIC relocs.
|
|
220
|
+
const hilo = op.match(
|
|
221
|
+
/^%(hi|lo)\(\s*([A-Za-z_.$][\w.$]*)\s*(?:([+-])\s*(0x[0-9a-fA-F]+|\d+))?\s*\)(?:\((\$?[A-Za-z]\w*)\))?$/,
|
|
222
|
+
);
|
|
202
223
|
if (hilo) {
|
|
203
|
-
|
|
224
|
+
const addend = hilo[4] ? evalConst(name, hilo[4]) * (hilo[3] === '-' ? -1 : 1) : 0;
|
|
225
|
+
const imm = hilo[1] === 'hi' ? ((addend + 0x8000) >> 16) & 0xffff : (addend << 16) >> 16;
|
|
226
|
+
const reloc: DisasmReloc = { type: hilo[1] === 'hi' ? 'R_MIPS_HI16' : 'R_MIPS_LO16', sym: hilo[2], addend: 0 };
|
|
227
|
+
return { op: hilo[5] ? `${imm}(${hilo[5].replace(/^\$/, '')})` : String(imm), reloc };
|
|
228
|
+
}
|
|
229
|
+
// A `%hi`/`%lo` the pattern above did NOT convert is still a relocation operand, and the paths
|
|
230
|
+
// below it read an operand as arithmetic: `%lo(0x800A1234)($v0)` matches the memory-operand shape
|
|
231
|
+
// and `evalConst` drops the tokens it does not know, so the displacement becomes the bare number
|
|
232
|
+
// and the access lifts as an index into the base register. A bare `%hi(…)` falls through to
|
|
233
|
+
// `plain` and the frontend refuses it one level down as a non-numeric immediate; refusing here
|
|
234
|
+
// says instead that what it saw was a relocation.
|
|
235
|
+
if (HILO_OP.test(op)) {
|
|
236
|
+
throw new FrontendUnsupportedError(
|
|
237
|
+
`cannot lift '${name}': relocation operand '${op}' — this reader resolves a '%hi'/'%lo' half ` +
|
|
238
|
+
`only against a symbol ('SYM' or 'SYM ± <integer>'), and will not treat one it cannot resolve ` +
|
|
239
|
+
`as arithmetic`,
|
|
240
|
+
);
|
|
204
241
|
}
|
|
205
242
|
if (RELOC_OP.test(op)) {
|
|
206
243
|
throw new FrontendUnsupportedError(
|
|
207
244
|
`cannot lift '${name}': relocation operand '${op}' (small-data / PIC data access) — not modelled`,
|
|
208
245
|
);
|
|
209
246
|
}
|
|
247
|
+
const plain = (v: string) => ({ op: v });
|
|
210
248
|
// Memory operand `DISP(base)` — base is a register (letter-first), DISP a constant/expression.
|
|
211
249
|
const mem = op.match(/^(.*)\((\$?[A-Za-z]\w*)\)$/);
|
|
212
250
|
if (mem) {
|
|
213
251
|
const disp = mem[1].trim();
|
|
214
252
|
const off = disp === '' ? '0' : String(evalConst(name, disp));
|
|
215
|
-
return `${off}(${mem[2].replace(/^\$/, '')})
|
|
253
|
+
return plain(`${off}(${mem[2].replace(/^\$/, '')})`);
|
|
216
254
|
}
|
|
217
255
|
// A bare constant expression (`(0x660104 >> 16)`) — the assembler's hi/lo literal split.
|
|
218
256
|
if (op.startsWith('(')) {
|
|
219
|
-
return String(evalConst(name, op));
|
|
257
|
+
return plain(String(evalConst(name, op)));
|
|
220
258
|
}
|
|
221
|
-
return op.replace(/^\$/, '');
|
|
259
|
+
return plain(op.replace(/^\$/, ''));
|
|
222
260
|
}
|
|
223
261
|
|
|
224
262
|
// Evaluate a constant integer expression (the assembler's hi/lo split: hex/dec literals with
|
package/src/frontend/ssa.ts
CHANGED
|
@@ -15,7 +15,18 @@
|
|
|
15
15
|
// computation via read/writeVar, push its terminator op last (successors referencing
|
|
16
16
|
// `irBlocks`, args left empty — phi wiring appends them), then call `markFilled(b)`. When all
|
|
17
17
|
// blocks are filled, call `finish()` to remove trivial phis.
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
Block,
|
|
20
|
+
Fn,
|
|
21
|
+
Op,
|
|
22
|
+
type ParamObservation,
|
|
23
|
+
type SlotHomes,
|
|
24
|
+
Value,
|
|
25
|
+
type WriteOrder,
|
|
26
|
+
defOpMap,
|
|
27
|
+
mkOp,
|
|
28
|
+
mkValue,
|
|
29
|
+
} from '../ir/core';
|
|
19
30
|
import { pruneDeadParams, simplifyTrivialPhis } from '../ir/simplify';
|
|
20
31
|
import { T } from '../ir/types';
|
|
21
32
|
import { FrontendUnsupportedError } from './errors';
|
|
@@ -45,8 +56,13 @@ export interface SsaBuilder {
|
|
|
45
56
|
* elsewhere a parameter is a phi whose position is aligned with its predecessors' terminator
|
|
46
57
|
* args, and appending an unpaired one would corrupt that. */
|
|
47
58
|
ensureParam(key: string, b: number): void;
|
|
48
|
-
/** Whether `reg` has a definition reaching block `b` (best-effort call-arity heuristic).
|
|
49
|
-
|
|
59
|
+
/** Whether `reg` has a definition reaching block `b` (best-effort call-arity heuristic).
|
|
60
|
+
*
|
|
61
|
+
* `accept` says what counts as a definition. A frontend that defines a register with something
|
|
62
|
+
* that is NOT a value — PowerPC's `@ha` high half — passes a predicate rejecting it, because
|
|
63
|
+
* "a def reaches here" and "a value reaches here" are the same question only when every def is
|
|
64
|
+
* a value. */
|
|
65
|
+
hasReachingDef(reg: string, b: number, accept?: (v: Value) => boolean): boolean;
|
|
50
66
|
/** Record that block `b` makes a call HERE: the ABI's caller-saved registers stop being ones the
|
|
51
67
|
* caller set up. Call it AFTER `recordGuessedCall` for the same instruction, and after writing
|
|
52
68
|
* the call's own result — the result is the CALLEE's, so it must not count as caller-side
|
|
@@ -92,21 +108,22 @@ export interface LiveInModel {
|
|
|
92
108
|
* is right for that question — but its offset is an ABI position, not an `expand_decl` rank,
|
|
93
109
|
* and ranking a declaration list by it would be wrong with no diagnostic.
|
|
94
110
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
111
|
+
* THE TWO RANGES DIFFER UNDER THUMB, AND A PROOF IS WHAT SEPARATES THEM. The frontend passes
|
|
112
|
+
* `{ from: area, to: localArea }`, where `area` is the largest outgoing block
|
|
113
|
+
* `analyzeOutgoingArgs` (frontend/stackargs.ts) LICENSED — the extent over which a callee's declared
|
|
114
|
+
* parameter count and this function's own staging stores agree word for word. That licence, not
|
|
115
|
+
* a decline, is what keeps argument slots out of `SlotHomes`: a frame whose outgoing area cannot
|
|
116
|
+
* be licensed still declines in the frontend and never reaches here, and a frame with no call
|
|
117
|
+
* taking stack arguments has `area` 0, so the ranges coincide exactly when there is provably
|
|
118
|
+
* nothing to skip. The dependency is TYPED and LOCAL for that reason — the offsets a frontend
|
|
119
|
+
* must not report as declarations are stated here rather than implied across modules.
|
|
103
120
|
*
|
|
104
121
|
* The class is populated, not hypothetical. Over a sweep of every sa3 and klonoa listing, of
|
|
105
122
|
* 2,001 lifted real agbcc functions 27 carry any L1 slot home, 12 of those also CALL, and 11 of
|
|
106
|
-
* those carry a home at offset 0 — the exact offset
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
123
|
+
* those carry a home at offset 0 — the exact offset an outgoing argument block starts at
|
|
124
|
+
* (`PackSaveSector` homes [0,4,…,72], `modf` [0,4,…,36], `RenderDialogSprites` [0,4,…,36], and
|
|
125
|
+
* eight more). None reaches the ordering today, for an unrelated reason (`l3/slotorder.ts`'s
|
|
126
|
+
* REACH note), so nothing downstream is guarding this.
|
|
110
127
|
*
|
|
111
128
|
* ABSENT ⇒ NO STAMP. MIPS and PPC declare no frame partition at all, so they stamp nothing,
|
|
112
129
|
* which is the refusing direction. */
|
|
@@ -196,7 +213,7 @@ export function makeSsaBuilder(
|
|
|
196
213
|
const inRange = (off: number, r?: { from: number; to: number }) => r !== undefined && off >= r.from && off < r.to;
|
|
197
214
|
const irBlocks: Block[] = Array.from({ length: blockCount }, () => ({ params: [] as Value[], ops: [] }));
|
|
198
215
|
// `writeOrder` and `slotHomes` are filled in below, where the builder's counters live.
|
|
199
|
-
const fn: Fn = { name, blocks: irBlocks, writeOrder: undefined, slotHomes: undefined };
|
|
216
|
+
const fn: Fn = { name, blocks: irBlocks, writeOrder: undefined, slotHomes: undefined, paramEvidence: undefined };
|
|
200
217
|
|
|
201
218
|
const defs: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
202
219
|
const sealed: boolean[] = irBlocks.map(() => false);
|
|
@@ -262,9 +279,9 @@ export function makeSsaBuilder(
|
|
|
262
279
|
//
|
|
263
280
|
// AND IT ASKS `declaredLocals`, NOT `ownedLocals`, which is a different question with a
|
|
264
281
|
// different answer under agbcc — the outgoing stack-argument area is storage the function owns
|
|
265
|
-
// and does not declare.
|
|
266
|
-
//
|
|
267
|
-
//
|
|
282
|
+
// and does not declare. Under Thumb the declared range therefore starts where the largest
|
|
283
|
+
// LICENSED outgoing block ends, and the two coincide only when that block is empty; see
|
|
284
|
+
// `declaredLocals`' own doc for what earns the narrowing and for the frames still refused.
|
|
268
285
|
if (!inRange(off, model().declaredLocals)) {
|
|
269
286
|
return;
|
|
270
287
|
}
|
|
@@ -301,6 +318,44 @@ export function makeSsaBuilder(
|
|
|
301
318
|
prev.add(off);
|
|
302
319
|
}
|
|
303
320
|
};
|
|
321
|
+
// PARAMETER EVIDENCE (ir/core.ts `ParamEvidence`), measured HERE for the same reason the clobber
|
|
322
|
+
// set, the write order and the slot homes are: a slot write is a `writeVar` and a slot read is a
|
|
323
|
+
// `readVar` in BOTH slot-modelling frontends, so one rule covers them and no frontend can forget
|
|
324
|
+
// to route a store past a wrapper. The two directions are NOT symmetric: raise/paramwidth.ts reads
|
|
325
|
+
// an absent observation as proof the declaration was wide, so a missed one costs a narrowing while
|
|
326
|
+
// a spurious one retypes a parameter the machine never declared narrow.
|
|
327
|
+
//
|
|
328
|
+
// RAW, in two ways that matter. Both halves record VALUES rather than verdicts, because the entry
|
|
329
|
+
// parameters are not final until `pruneDeadParams` has run in `finish()`, which is where the map
|
|
330
|
+
// is sealed. And the slot half asks no frame partition, unlike `noteSlotHome` directly above: see
|
|
331
|
+
// `ParamEvidence` for why "stored and never read back" needs none.
|
|
332
|
+
//
|
|
333
|
+
// A READ IS A `readVar`, AND `hasReachingDef` IS NOT ONE. The guard in `frontend/mips.ts`'s
|
|
334
|
+
// `emitLoad` asks whether a slot was ever stored before it reads it; asking is not reading, and
|
|
335
|
+
// the `readVar` on the line after it is.
|
|
336
|
+
const slotWrites = new Map<string, Set<Value>>();
|
|
337
|
+
const slotReads = new Set<string>();
|
|
338
|
+
const noteSlotTraffic = (key: string, v: Value | null) => {
|
|
339
|
+
if (slotKeyOffset(key) === null) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (v === null) {
|
|
343
|
+
slotReads.add(key);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const at = slotWrites.get(key);
|
|
347
|
+
if (at === undefined) {
|
|
348
|
+
slotWrites.set(key, new Set([v]));
|
|
349
|
+
} else {
|
|
350
|
+
at.add(v);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
// THE FIRST entry-block write to each REGISTER, for `selfRedefined`. First and not any: a later
|
|
354
|
+
// write is the allocator reusing a register the argument is done with, which says nothing about
|
|
355
|
+
// the argument. Entry block only, because a widening the machine performs on the argument's own
|
|
356
|
+
// register is prologue work — a write in a later block has body code before it.
|
|
357
|
+
const firstEntryWrite = new Map<string, Value>();
|
|
358
|
+
|
|
304
359
|
const forgetOrder = (p: Value) => {
|
|
305
360
|
for (const m of writeOrder.lastWrite.values()) {
|
|
306
361
|
m.delete(p);
|
|
@@ -309,11 +364,18 @@ export function makeSsaBuilder(
|
|
|
309
364
|
|
|
310
365
|
const writeVar = (reg: string, b: number, v: Value) => {
|
|
311
366
|
noteSlotHome(reg, v);
|
|
367
|
+
noteSlotTraffic(reg, v);
|
|
368
|
+
if (b === 0 && !firstEntryWrite.has(reg)) {
|
|
369
|
+
firstEntryWrite.set(reg, v);
|
|
370
|
+
}
|
|
312
371
|
writtenSinceCall[b].add(reg);
|
|
313
372
|
defs[b].set(reg, v);
|
|
314
373
|
lastWriteAt[b].set(reg, writeCount[b]++);
|
|
315
374
|
};
|
|
316
|
-
const readVar = (reg: string, b: number): Value =>
|
|
375
|
+
const readVar = (reg: string, b: number): Value => {
|
|
376
|
+
noteSlotTraffic(reg, null);
|
|
377
|
+
return defs[b].get(reg) ?? readRecursive(reg, b);
|
|
378
|
+
};
|
|
317
379
|
|
|
318
380
|
const newPhi = (reg: string, b: number): Value => {
|
|
319
381
|
const phi = mkValue(T.unk(32));
|
|
@@ -454,15 +516,21 @@ export function makeSsaBuilder(
|
|
|
454
516
|
obligedParams[b].set(key, p);
|
|
455
517
|
};
|
|
456
518
|
|
|
457
|
-
const hasReachingDef = (reg: string, b: number,
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
519
|
+
const hasReachingDef = (reg: string, b: number, accept: (v: Value) => boolean = () => true): boolean => {
|
|
520
|
+
const walk = (at: number, seen: Set<number>): boolean => {
|
|
521
|
+
const own = defs[at].get(reg);
|
|
522
|
+
// A def `accept` rejects does not fall through to the predecessors: it is still a def, and
|
|
523
|
+
// nothing older than it reaches past it.
|
|
524
|
+
if (own !== undefined) {
|
|
525
|
+
return accept(own);
|
|
526
|
+
}
|
|
527
|
+
if (seen.has(at)) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
seen.add(at);
|
|
531
|
+
return preds[at].length > 0 && preds[at].some((p) => walk(p, seen));
|
|
532
|
+
};
|
|
533
|
+
return walk(b, new Set<number>());
|
|
466
534
|
};
|
|
467
535
|
|
|
468
536
|
return {
|
|
@@ -532,6 +600,35 @@ export function makeSsaBuilder(
|
|
|
532
600
|
phiKey.delete(p);
|
|
533
601
|
forgetOrder(p);
|
|
534
602
|
});
|
|
603
|
+
// SEAL THE PARAMETER EVIDENCE (ir/core.ts `ParamEvidence`). Here and not at the store or the
|
|
604
|
+
// write, because `pruneDeadParams` above is the last thing that can retire an entry
|
|
605
|
+
// parameter, and an observation about a value no longer in the signature is one the reader
|
|
606
|
+
// would never find. Every surviving entry parameter gets an entry — EMPTY-but-present on a
|
|
607
|
+
// function that shows neither, because this builder measured it and found nothing.
|
|
608
|
+
const evidence = new Map<Value, ParamObservation>();
|
|
609
|
+
const deadHomed = new Set<Value>();
|
|
610
|
+
for (const [key, stored] of slotWrites) {
|
|
611
|
+
if (slotReads.has(key)) {
|
|
612
|
+
continue; // the slot is read back: the store is live and says nothing about a declaration
|
|
613
|
+
}
|
|
614
|
+
for (const v of stored) {
|
|
615
|
+
deadHomed.add(v);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const defs0 = defOpMap(fn);
|
|
619
|
+
for (const p of irBlocks[0].params) {
|
|
620
|
+
const reg = paramReg.get(p);
|
|
621
|
+
const first = reg === undefined ? undefined : firstEntryWrite.get(reg);
|
|
622
|
+
// The redefining op must READ the parameter — that is what makes the write the argument's
|
|
623
|
+
// own value moving, rather than an unrelated value landing in a register it had finished
|
|
624
|
+
// with. Direct, not transitive: a chain through body code is body code.
|
|
625
|
+
const redef = first === undefined ? undefined : defs0.get(first);
|
|
626
|
+
evidence.set(p, {
|
|
627
|
+
deadHome: deadHomed.has(p),
|
|
628
|
+
selfRedefined: redef !== undefined && redef.operands.includes(p),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
fn.paramEvidence = evidence;
|
|
535
632
|
// A STACK SLOT MAY NEVER LEAVE AS AN ENTRY PARAMETER. A slot is memory the function itself
|
|
536
633
|
// allocated, so its value can only come from a store the function made; arriving as a live-in
|
|
537
634
|
// instead means it was read on a path that never stored it, and the signature has grown an
|
|
@@ -742,7 +839,7 @@ export function trimClobberedCallArgs(inp: CallArgTrim): void {
|
|
|
742
839
|
* an attr only one of an otherwise-matching pair carries would cost a recovery. */
|
|
743
840
|
const setupArgc = new WeakMap<Op, number>();
|
|
744
841
|
|
|
745
|
-
/** Whether anything in `fn` HAS the narrower reading — the
|
|
842
|
+
/** Whether anything in `fn` HAS the narrower reading — the variation's gate, so the ~99% of functions
|
|
746
843
|
* with no narrowable call cost no re-lift. Read it off the lift itself: a later pipeline stage may
|
|
747
844
|
* replace a `call` op (softdiv rewrites one to a division), and the table is keyed by op. */
|
|
748
845
|
export function hasSetupArgsNarrowing(fn: Fn): boolean {
|