@asmlift/core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +238 -164
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +341 -22
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +24 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +54 -7
- package/src/frontend/ssa.ts +632 -13
- package/src/frontend/thumb.ts +2786 -286
- package/src/ir/alias.ts +129 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +156 -27
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/argbase.ts +8 -2
- package/src/l3/ast.ts +464 -49
- package/src/l3/basecse.ts +709 -88
- package/src/l3/coalesce.ts +521 -66
- package/src/l3/dce.ts +54 -19
- package/src/l3/gates.ts +88 -0
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +113 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +649 -219
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +23 -4
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +236 -13
- package/src/pipeline.ts +206 -49
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +4 -3
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +44 -19
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +101 -16
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +215 -14
- package/src/raise/shortcircuit.ts +477 -79
- package/src/raise/struct-arrays.ts +21 -3
- package/src/raise/structs.ts +61 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1726 -251
- package/src/structure/analysis.ts +1516 -220
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2850 -533
- package/src/structure/switch-recover.ts +688 -147
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
package/src/frontend/ssa.ts
CHANGED
|
@@ -15,9 +15,10 @@
|
|
|
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 { Block, Fn, Value, mkValue } from '../ir/core';
|
|
19
|
-
import { simplifyTrivialPhis } from '../ir/simplify';
|
|
18
|
+
import { Block, Fn, Op, type SlotHomes, Value, type WriteOrder, mkOp, mkValue } from '../ir/core';
|
|
19
|
+
import { pruneDeadParams, simplifyTrivialPhis } from '../ir/simplify';
|
|
20
20
|
import { T } from '../ir/types';
|
|
21
|
+
import { FrontendUnsupportedError } from './errors';
|
|
21
22
|
|
|
22
23
|
export interface SsaBuilder {
|
|
23
24
|
fn: Fn;
|
|
@@ -28,36 +29,301 @@ export interface SsaBuilder {
|
|
|
28
29
|
writeVar(reg: string, b: number, v: Value): void;
|
|
29
30
|
/** Mark block `b` fully emitted (terminator pushed); seals any now-ready successors. */
|
|
30
31
|
markFilled(b: number): void;
|
|
31
|
-
/** Live-in parameter value → the
|
|
32
|
+
/** Live-in parameter value → the key it arrived on (for calling-convention order). Usually an
|
|
33
|
+
* ABI register name, but a frontend's virtual key (see the module header) ranks here too. */
|
|
32
34
|
paramReg: Map<Value, string>;
|
|
35
|
+
/** Assert that block `b` takes a parameter for `key`, whether or not anything reads it.
|
|
36
|
+
*
|
|
37
|
+
* `readVar` cannot express this. It asks "what value does `key` hold here?", so a key the block
|
|
38
|
+
* DEFINES before any read answers with that local definition and no parameter is created — to it
|
|
39
|
+
* "never read" and "written before first read" are the same thing. When a calling convention
|
|
40
|
+
* proves an argument exists, that is an obligation on the SIGNATURE, independent of whether the
|
|
41
|
+
* body happens to use it, so it needs its own verb.
|
|
42
|
+
*
|
|
43
|
+
* Never touches the block's definitions: the parameter is added and left unused, so any local
|
|
44
|
+
* value already flowing keeps flowing. Only meaningful on a block with no predecessors —
|
|
45
|
+
* elsewhere a parameter is a phi whose position is aligned with its predecessors' terminator
|
|
46
|
+
* args, and appending an unpaired one would corrupt that. */
|
|
47
|
+
ensureParam(key: string, b: number): void;
|
|
33
48
|
/** Whether `reg` has a definition reaching block `b` (best-effort call-arity heuristic). */
|
|
34
49
|
hasReachingDef(reg: string, b: number, seen?: Set<number>): boolean;
|
|
35
|
-
/**
|
|
50
|
+
/** Record that block `b` makes a call HERE: the ABI's caller-saved registers stop being ones the
|
|
51
|
+
* caller set up. Call it AFTER `recordGuessedCall` for the same instruction, and after writing
|
|
52
|
+
* the call's own result — the result is the CALLEE's, so it must not count as caller-side
|
|
53
|
+
* argument setup for whatever call comes next. */
|
|
54
|
+
noteCall(b: number): void;
|
|
55
|
+
/** Register a `call` op whose arity was GUESSED (no prototype), so `finish` can cut it back to the
|
|
56
|
+
* argument registers that were actually set up on every path (see {@link trimClobberedCallArgs}).
|
|
57
|
+
* `abi` is the target's argument-register order and its return register. */
|
|
58
|
+
recordGuessedCall(op: Op, b: number, abi: { argRegs: string[]; returnReg: string }): void;
|
|
59
|
+
/** Remove trivial phis and enforce the frontend's postconditions; call once every block is
|
|
60
|
+
* filled. Throws FrontendUnsupportedError if a stack slot escaped as an entry parameter. */
|
|
36
61
|
finish(): void;
|
|
37
62
|
}
|
|
38
63
|
|
|
39
64
|
/** `preds` is per-EDGE (see the module header): one entry per CFG edge into each block. */
|
|
40
|
-
|
|
65
|
+
// VARIABLE NAMES ARE NOT ALWAYS MACHINE REGISTERS. `readVar`/`writeVar` key on an arbitrary string,
|
|
66
|
+
// and frontends mint VIRTUAL keys for storage the ISA has no register for — MIPS `sp@<off>` for a
|
|
67
|
+
// stack slot (frontend/mips.ts), Thumb `@sarg<k>` for an incoming stack argument (frontend/thumb.ts).
|
|
68
|
+
// A virtual key must be outside its ISA's register grammar so it cannot collide with a real one, and
|
|
69
|
+
// a key read with no reaching def becomes a function PARAMETER by the live-in path below — which is
|
|
70
|
+
// how both of those capabilities get their parameters without a new opcode or pass.
|
|
71
|
+
/** What a def-less live-in MEANS here, in two coordinate systems — the frame in slot-key offsets,
|
|
72
|
+
* the register file by key. RANGES and LISTS rather than a verdict, so the classification below is
|
|
73
|
+
* checkable here: a frontend that is wrong about its own frame gets refused instead of believed,
|
|
74
|
+
* and a range that collapses to empty (an unmeasurable frame) stops claiming anything on its own.
|
|
75
|
+
* Ghidra carries the same partition as compiler-spec data (`<localrange>`, stack `<pentry>`) read
|
|
76
|
+
* by architecture-neutral code. */
|
|
77
|
+
export interface LiveInModel {
|
|
78
|
+
/** Storage this function owns as LOCALS ⇒ a def-less read is an uninitialised local. `[from, to)`.
|
|
79
|
+
*
|
|
80
|
+
* Asserts more than ownership: that this function's own stores are the ONLY writer. An address
|
|
81
|
+
* into the frame that escapes to anything which could write it stops that holding, and the
|
|
82
|
+
* retraction is the frontend's obligation (frontend/thumb.ts, after the frame-object audit). */
|
|
83
|
+
ownedLocals?: { from: number; to: number };
|
|
84
|
+
/** Storage this function DECLARES as locals ⇒ a `[sp,#k]` spill here is a DECLARATION RANK
|
|
85
|
+
* (`ir/core.ts` `SlotHomes`, ordered by `l3/slotorder.ts`). `[from, to)`.
|
|
86
|
+
*
|
|
87
|
+
* WHY THIS IS NOT `ownedLocals`, and the distinction is the whole point of the field. Owning
|
|
88
|
+
* storage and declaring it are different claims, and agbcc's frame contains storage it owns
|
|
89
|
+
* and does not declare: `ACCUMULATE_OUTGOING_ARGS` puts the OUTGOING STACK-ARGUMENT area at
|
|
90
|
+
* the BOTTOM of `localArea` (frontend/thumb.ts says so at its own decline), so `[0, localArea)`
|
|
91
|
+
* admits argument slots. A def-less read of one is still an uninitialised local — `ownedLocals`
|
|
92
|
+
* is right for that question — but its offset is an ABI position, not an `expand_decl` rank,
|
|
93
|
+
* and ranking a declaration list by it would be wrong with no diagnostic.
|
|
94
|
+
*
|
|
95
|
+
* TODAY THE TWO RANGES COINCIDE UNDER THUMB, AND THAT IS DELEGATED, NOT PROVED. What keeps
|
|
96
|
+
* argument slots out of `SlotHomes` is `prefixStored` (frontend/thumb.ts): a function whose
|
|
97
|
+
* frame has an outgoing area DECLINES before it reaches here, so every function that does
|
|
98
|
+
* reach here has none. That guard's own comment says "Neither is sound alone and the pair is
|
|
99
|
+
* not either", and lifting it is a named next step — so this field exists to make the
|
|
100
|
+
* dependency TYPED and LOCAL rather than implicit and cross-module. Whoever lifts that decline
|
|
101
|
+
* must narrow this range above the argument block; leaving it equal to `ownedLocals` would
|
|
102
|
+
* start minting declaration ranks out of argument slots silently.
|
|
103
|
+
*
|
|
104
|
+
* The class is populated, not hypothetical. Over a sweep of every sa3 and klonoa listing, of
|
|
105
|
+
* 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 `prefixStored` encodes as where an argument
|
|
107
|
+
* block starts (`PackSaveSector` homes [0,4,…,72], `modf` [0,4,…,36], `RenderDialogSprites`
|
|
108
|
+
* [0,4,…,36], and eight more). None reaches the ordering today, for an unrelated reason
|
|
109
|
+
* (`l3/slotorder.ts`'s REACH note), so nothing downstream is guarding this.
|
|
110
|
+
*
|
|
111
|
+
* ABSENT ⇒ NO STAMP. MIPS and PPC declare no frame partition at all, so they stamp nothing,
|
|
112
|
+
* which is the refusing direction. */
|
|
113
|
+
declaredLocals?: { from: number; to: number };
|
|
114
|
+
/** Storage the CALLER wrote — incoming stack arguments ⇒ a def-less read is a parameter.
|
|
115
|
+
* `[from, to)`. O32's register-parameter home area belongs to NEITHER range: caller-owned, but
|
|
116
|
+
* not an argument. */
|
|
117
|
+
callerParams?: { from: number; to: number };
|
|
118
|
+
/** Registers a def-less read of which is an uninitialised local the compiler put in a register.
|
|
119
|
+
* TWO facts, and the frontend owes both: the ABI passes no argument there (`target.nonArgRegs`,
|
|
120
|
+
* so no caller could have handed a value over, however early the read happens) AND this function
|
|
121
|
+
* saved the register (so it is one the compiler was free to home a local in). The ABI half alone
|
|
122
|
+
* describes the CALLER, and asm that follows no ABI — hand-written, or a mid-function fragment,
|
|
123
|
+
* which klonoa's `bl`-as-a-long-branch splits produce for real — is genuinely handed live values
|
|
124
|
+
* in registers it never saved. Passing the ABI list unfiltered cost the MP2K engine's
|
|
125
|
+
* `ChnVolSetAsm` its two-pointer signature and left it storing through `uninit_r4`, silently.
|
|
126
|
+
*
|
|
127
|
+
* The save is a MEASUREMENT, like the frame's, and belongs to whoever can make it — Thumb reads
|
|
128
|
+
* the leading push run (`savedRegs`); a frontend that cannot measure it passes nothing here and
|
|
129
|
+
* keeps the parameter it would have got anyway.
|
|
130
|
+
*
|
|
131
|
+
* The frame's sole-writer obligation has no counterpart here and needs none: a register has no
|
|
132
|
+
* address, so nothing outside this function can name it and there is no escape to retract.
|
|
133
|
+
*
|
|
134
|
+
* LISTED, not derived as "everything outside argRegs", because the complement contains the
|
|
135
|
+
* VIRTUAL keys too (`@sarg<k>` — an incoming stack argument, which really is a parameter), and a
|
|
136
|
+
* rule that had to exclude them would be reading a grammar this module does not own. A register
|
|
137
|
+
* spelling nobody listed keeps its existing treatment, so the list is safe to grow.
|
|
138
|
+
*
|
|
139
|
+
* Declaring this obliges `argRegs` below, and the two must be DISJOINT. */
|
|
140
|
+
uninitRegs?: readonly string[];
|
|
141
|
+
/** Registers the ABI DOES pass arguments in — the other side of the register partition, and the
|
|
142
|
+
* only reason the side above is checkable rather than believed. The frame coordinate declares
|
|
143
|
+
* both of its sides and refuses an offset in neither; this one declares both and refuses a key
|
|
144
|
+
* in BOTH, which is the same move.
|
|
145
|
+
*
|
|
146
|
+
* Without it the whole contract rests on one hand-written list in target.ts being right, in a
|
|
147
|
+
* file whose idiom is "a compiler fact is one field": spelling `r1` where `r11` was meant
|
|
148
|
+
* deletes a parameter and emits `s32 uninit_r1;` in its place, with no diagnostic anywhere.
|
|
149
|
+
* `readRecursive` cannot catch that on its own — it never sees the argument registers, because
|
|
150
|
+
* a read of one takes the parameter path by falling through every other case. */
|
|
151
|
+
argRegs?: readonly string[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The register partition's postcondition, checked at the point of USE. `uninitRegs` asserts "no
|
|
155
|
+
* caller could have handed a value over in these"; `argRegs` is the set of registers a caller
|
|
156
|
+
* hands values over in. A key in both is a target that contradicts itself, and a `uninitRegs` with
|
|
157
|
+
* no `argRegs` beside it is one whose assertion nothing can check — both refuse rather than
|
|
158
|
+
* silently reclassify an argument as an uninitialised local. */
|
|
159
|
+
function checkedLiveInModel(fnName: string, m: LiveInModel): LiveInModel {
|
|
160
|
+
if (m.uninitRegs === undefined) {
|
|
161
|
+
return m;
|
|
162
|
+
}
|
|
163
|
+
const args = m.argRegs;
|
|
164
|
+
if (args === undefined) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`lifting '${fnName}': the live-in model lists registers the ABI does not pass arguments in ` +
|
|
167
|
+
`but not the ones it does, so nothing can check the two agree`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const both = m.uninitRegs.filter((r) => args.includes(r));
|
|
171
|
+
if (both.length > 0) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`lifting '${fnName}': the live-in model lists ${both.join(', ')} as BOTH an argument register ` +
|
|
174
|
+
`and one the ABI does not pass arguments in`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return m;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function makeSsaBuilder(
|
|
181
|
+
name: string,
|
|
182
|
+
blockCount: number,
|
|
183
|
+
preds: number[][],
|
|
184
|
+
/** A supplier because half the partition is MEASURED rather than declared: Thumb's local area
|
|
185
|
+
* comes from a prologue walk that runs after this call. Evaluated once, on first use. Omitted ⇒
|
|
186
|
+
* no partition is claimed, so every slot refuses and every register is a parameter. */
|
|
187
|
+
liveInOf: () => LiveInModel = () => ({}),
|
|
188
|
+
): SsaBuilder {
|
|
189
|
+
let modelMemo: LiveInModel | null = null;
|
|
190
|
+
// Checked ONCE, where the model is materialised — every function with a parameter reads a
|
|
191
|
+
// register def-lessly, so this runs on effectively every lift rather than only on the rare
|
|
192
|
+
// function that reads the mis-listed register. A contradictory or half-declared partition is a
|
|
193
|
+
// bug in the TARGET, not an unliftable function, so it throws a plain Error: a decline would
|
|
194
|
+
// report the target's typo as a property of the input, once per function, forever.
|
|
195
|
+
const model = (): LiveInModel => (modelMemo ??= checkedLiveInModel(name, liveInOf()));
|
|
196
|
+
const inRange = (off: number, r?: { from: number; to: number }) => r !== undefined && off >= r.from && off < r.to;
|
|
41
197
|
const irBlocks: Block[] = Array.from({ length: blockCount }, () => ({ params: [] as Value[], ops: [] }));
|
|
42
|
-
|
|
198
|
+
// `writeOrder` and `slotHomes` are filled in below, where the builder's counters live.
|
|
199
|
+
const fn: Fn = { name, blocks: irBlocks, writeOrder: undefined, slotHomes: undefined };
|
|
43
200
|
|
|
44
201
|
const defs: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
45
202
|
const sealed: boolean[] = irBlocks.map(() => false);
|
|
46
203
|
const filled: boolean[] = irBlocks.map(() => false);
|
|
47
204
|
const incompletePhis: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
48
205
|
const phiBlock = new Map<Value, number>();
|
|
206
|
+
// The key each phi stands for. `paramReg` covers live-ins only, so without this a slot that
|
|
207
|
+
// arrives as a PHI — which is what happens when the entry block is itself a loop header — is
|
|
208
|
+
// invisible to the escape check below. Braun's construction gives no other way to tell.
|
|
209
|
+
const phiKey = new Map<Value, string>();
|
|
49
210
|
const paramReg = new Map<Value, string>();
|
|
211
|
+
// Parameters created by ensureParam that nothing has read yet. They are deliberately NOT in
|
|
212
|
+
// `defs`: a parameter asserted because a calling convention proves it exists is not evidence that
|
|
213
|
+
// a VALUE reaches anything, and writing one into `defs` would say it does. That distinction is
|
|
214
|
+
// load-bearing — `hasReachingDef` feeds `fallbackArgc`, so a def here silently raises the guessed
|
|
215
|
+
// arity of every prototype-less call in the function, making it pass registers the calling block
|
|
216
|
+
// never set up (`unknown(1)` became `unknown(1, a1, a2, a3)`). The first read adopts the value
|
|
217
|
+
// from here instead of minting a second parameter for the same key.
|
|
218
|
+
const obligedParams: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
50
219
|
|
|
51
220
|
// `preds` lists an entry per CFG EDGE; these are the distinct predecessor BLOCKS.
|
|
52
221
|
const distinctPreds = (b: number): number[] => [...new Set(preds[b])];
|
|
53
222
|
|
|
54
|
-
|
|
223
|
+
// CALLER-SAVED CLOBBER, for guessed call arities (see trimClobberedCallArgs). Tracked HERE
|
|
224
|
+
// because every register write in every frontend already goes through `writeVar`: a frontend
|
|
225
|
+
// that gathered this itself would be sound only while it remembered to route each write past a
|
|
226
|
+
// wrapper, and a MISSED write under-counts an arity — which drops a real argument silently.
|
|
227
|
+
const writtenSinceCall: Array<Set<string>> = irBlocks.map(() => new Set());
|
|
228
|
+
const callsIn = new Set<number>();
|
|
229
|
+
const guessedCalls: GuessedCallSite[] = [];
|
|
230
|
+
let abiSeen: { argRegs: string[]; returnReg: string } = { argRegs: [], returnReg: '' };
|
|
231
|
+
|
|
232
|
+
// WRITE ORDER (ir/core.ts `WriteOrder`). Measured here for the same reason the clobber set is:
|
|
233
|
+
// every register write in every frontend already goes through `writeVar`, and a delay-slot write
|
|
234
|
+
// is decoded before its branch is emitted, so the ordinal is the machine's own program order on
|
|
235
|
+
// every ISA with no frontend code. Per block, because the consumer asks a per-EDGE question.
|
|
236
|
+
const writeCount: number[] = irBlocks.map(() => 0);
|
|
237
|
+
const lastWriteAt: Array<Map<string, number>> = irBlocks.map(() => new Map());
|
|
238
|
+
const writeOrder: WriteOrder = { lastWrite: new Map(), writes: new Map() };
|
|
239
|
+
fn.writeOrder = writeOrder;
|
|
240
|
+
|
|
241
|
+
// SLOT HOMES (ir/core.ts `SlotHomes`). Measured HERE, in the shared builder, for the same
|
|
242
|
+
// reason the clobber set and the write order are: BOTH frontends already spell a word spill as
|
|
243
|
+
// a write to the key `sp@k` (`stackSlotKey`, below), so the frontend supplies the coordinate
|
|
244
|
+
// and one rule applies it — a per-frontend stamp would be right only while each remembered to
|
|
245
|
+
// route every slot write past a wrapper, and a missed write is a local with no frame order.
|
|
246
|
+
// Empty rather than absent on a function that spills nothing: this builder measured it.
|
|
247
|
+
const slotHomes: SlotHomes = new Map();
|
|
248
|
+
fn.slotHomes = slotHomes;
|
|
249
|
+
const noteSlotHome = (key: string, v: Value) => {
|
|
250
|
+
const off = slotKeyOffset(key);
|
|
251
|
+
if (off === null) {
|
|
252
|
+
return; // an ordinary register: no frame coordinate exists
|
|
253
|
+
}
|
|
254
|
+
// THE KEY SPELLING CANNOT DECIDE THIS, exactly as `readRecursive` says below of a def-less
|
|
255
|
+
// read: `sp@40` is a local on one ABI and the caller's fifth argument on another. So the stamp
|
|
256
|
+
// asks the frontend for a partition and refuses where no answer exists. The two frontends
|
|
257
|
+
// differ here and the refusal is what makes that safe: Thumb declares a range; MIPS declares NO
|
|
258
|
+
// partition (frontend/mips.ts: `addiu sp,sp,±N` is transparent, so its slot keys span O32's
|
|
259
|
+
// caller-owned register-parameter home area `[0,16)` and the incoming stack arguments above
|
|
260
|
+
// it), and PPC declares none either — so both stamp nothing rather than reporting the caller's
|
|
261
|
+
// frame as this function's declaration ranks.
|
|
262
|
+
//
|
|
263
|
+
// AND IT ASKS `declaredLocals`, NOT `ownedLocals`, which is a different question with a
|
|
264
|
+
// different answer under agbcc — the outgoing stack-argument area is storage the function owns
|
|
265
|
+
// and does not declare. The two ranges are equal under Thumb today only because `prefixStored`
|
|
266
|
+
// declines every function with an outgoing area; see `declaredLocals`' own doc for the
|
|
267
|
+
// measurement and for what lifting that decline obliges.
|
|
268
|
+
if (!inRange(off, model().declaredLocals)) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// ONE CLASS INSIDE THE PARTITION IS STILL NOT A DECLARATION RANK, and NOTHING HERE REFUSES IT.
|
|
272
|
+
// A stack AGGREGATE the frontend decomposed into per-word keys yields several `sp@k`s that are
|
|
273
|
+
// fields of ONE declared object, not several declared scalars — and it reaches the stamp
|
|
274
|
+
// because a non-address-taken array never mints an `laddr`, which is the only aggregate the
|
|
275
|
+
// structurer's `frame` refusal catches. It is the only class the ordering meets in the wild:
|
|
276
|
+
// over 2,463 real agbcc functions (158 sa3 + klonoa listings) exactly one carries two
|
|
277
|
+
// slot-carrying locals, sa3 `sub_80617E0`, whose [sp,#0]..[sp,#0xc] are the four words of
|
|
278
|
+
// `Vec2_32 sp00[2]` (its own preprocessed source declares it), with the only genuine reload
|
|
279
|
+
// spill at [sp,#0x10].
|
|
280
|
+
//
|
|
281
|
+
// That one is declined downstream — its words land under two names at offset 12, and
|
|
282
|
+
// `l3/slotorder.ts`'s injectivity refusal reads a duplicate as evidence reload did not produce
|
|
283
|
+
// — but the class is NOT covered by that refusal: an aggregate whose words reach L3 under
|
|
284
|
+
// distinct names at distinct offsets is injective and would be ordered. FLIP CONDITION: once
|
|
285
|
+
// stack-array recovery declares `sp00[2]`, ordering an aggregate against a reload spill by the
|
|
286
|
+
// minimum of its element offsets is WRONG — `assign_stack_local` runs before reload and puts
|
|
287
|
+
// every array below every spill slot regardless of declaration rank. The licence this
|
|
288
|
+
// capability rests on (reload hands a spilled pseudo its slot by `expand_decl` rank) is about
|
|
289
|
+
// separately declared SCALARS; intra-aggregate offsets are fixed by the aggregate's layout at
|
|
290
|
+
// expand time.
|
|
291
|
+
//
|
|
292
|
+
// UNION, not a choice (ir/core.ts `SlotHomes`): whether the earlier declaration rank is the
|
|
293
|
+
// lower or the higher offset is a per-COMPILER fact, and this builder is handed a name, a
|
|
294
|
+
// block count, a predecessor list and a live-in model — no target. `l3/slotorder.ts` reduces.
|
|
295
|
+
// A GUARD WITH NO CORPUS INHABITANT: over both benchmark tiers no value is ever written to two
|
|
296
|
+
// DIFFERENT slots, so the `else` below has never produced a set of size two on a real input.
|
|
297
|
+
const prev = slotHomes.get(v);
|
|
298
|
+
if (prev === undefined) {
|
|
299
|
+
slotHomes.set(v, new Set([off]));
|
|
300
|
+
} else {
|
|
301
|
+
prev.add(off);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const forgetOrder = (p: Value) => {
|
|
305
|
+
for (const m of writeOrder.lastWrite.values()) {
|
|
306
|
+
m.delete(p);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const writeVar = (reg: string, b: number, v: Value) => {
|
|
311
|
+
noteSlotHome(reg, v);
|
|
312
|
+
writtenSinceCall[b].add(reg);
|
|
313
|
+
defs[b].set(reg, v);
|
|
314
|
+
lastWriteAt[b].set(reg, writeCount[b]++);
|
|
315
|
+
};
|
|
55
316
|
const readVar = (reg: string, b: number): Value => defs[b].get(reg) ?? readRecursive(reg, b);
|
|
56
317
|
|
|
57
318
|
const newPhi = (reg: string, b: number): Value => {
|
|
58
319
|
const phi = mkValue(T.unk(32));
|
|
59
320
|
irBlocks[b].params.push(phi);
|
|
60
321
|
phiBlock.set(phi, b);
|
|
322
|
+
phiKey.set(phi, reg);
|
|
323
|
+
// A slot that arrives as a PHI — a loop header reading back what an earlier iteration spilled
|
|
324
|
+
// — is the same frame coordinate under a block param, and the structurer names it like any
|
|
325
|
+
// other value, so it carries the home too.
|
|
326
|
+
noteSlotHome(reg, phi);
|
|
61
327
|
defs[b].set(reg, phi); // set before wiring operands to break cycles
|
|
62
328
|
return phi;
|
|
63
329
|
};
|
|
@@ -73,7 +339,37 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
73
339
|
// manufacture a join (and a phi) where there is none.
|
|
74
340
|
const ps = distinctPreds(b);
|
|
75
341
|
if (ps.length === 0) {
|
|
76
|
-
// live-in with no predecessor: an incoming argument
|
|
342
|
+
// A live-in with no predecessor is a value this function never produced: an incoming argument,
|
|
343
|
+
// or storage it allocated and never wrote. WHICH ONE is the partition's answer, in whichever
|
|
344
|
+
// coordinate the key names. The key spelling cannot decide a slot on its own — `sp@40` is a
|
|
345
|
+
// local on one ABI and the caller's fifth argument on another — so a slot in neither range is
|
|
346
|
+
// refused rather than guessed. A register is decided by the calling convention instead of by
|
|
347
|
+
// a measurement: a caller cannot pass a value in a register the ABI does not pass arguments
|
|
348
|
+
// in, so a read of one before any write is an uninitialised local.
|
|
349
|
+
const off = slotKeyOffset(reg);
|
|
350
|
+
if (off !== null && !inRange(off, model().ownedLocals) && !inRange(off, model().callerParams)) {
|
|
351
|
+
throw new FrontendUnsupportedError(
|
|
352
|
+
`cannot lift '${name}': ${reg} is read on a path that never stores it, and lies outside ` +
|
|
353
|
+
`this function's frame partition (uninitialised local, or storage it does not own) — not modelled`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
const uninitialised =
|
|
357
|
+
off !== null ? inRange(off, model().ownedLocals) : (model().uninitRegs?.includes(reg) ?? false);
|
|
358
|
+
if (uninitialised) {
|
|
359
|
+
const op = mkOp('undef', { results: [mkValue(T.unk(32))], attrs: { key: reg } });
|
|
360
|
+
irBlocks[b].ops.unshift(op); // ahead of everything in a block that nothing precedes
|
|
361
|
+
defs[b].set(reg, op.results[0]);
|
|
362
|
+
return op.results[0];
|
|
363
|
+
}
|
|
364
|
+
// an incoming argument register → function parameter.
|
|
365
|
+
// If one was already asserted for this key (ensureParam), adopt it — minting a second
|
|
366
|
+
// parameter for the same key would put the key in the signature twice.
|
|
367
|
+
const obliged = obligedParams[b].get(reg);
|
|
368
|
+
if (obliged !== undefined) {
|
|
369
|
+
obligedParams[b].delete(reg);
|
|
370
|
+
defs[b].set(reg, obliged);
|
|
371
|
+
return obliged;
|
|
372
|
+
}
|
|
77
373
|
const p = mkValue(T.unk(32));
|
|
78
374
|
irBlocks[b].params.push(p);
|
|
79
375
|
defs[b].set(reg, p);
|
|
@@ -87,12 +383,21 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
87
383
|
}
|
|
88
384
|
// sealed join: create the phi and wire every predecessor's terminator arg now.
|
|
89
385
|
const phi = newPhi(reg, b);
|
|
90
|
-
addPhiOperands(reg, b);
|
|
386
|
+
addPhiOperands(reg, b, phi);
|
|
91
387
|
return phi;
|
|
92
388
|
};
|
|
93
|
-
|
|
389
|
+
// The ONE point that knows the phi, its key and each predecessor together, which is what the
|
|
390
|
+
// write-order record is keyed by. `phi` is passed rather than looked up: by the time a deferred
|
|
391
|
+
// phi is wired the block may have written its key again, so `defs[b]` no longer names it.
|
|
392
|
+
const addPhiOperands = (reg: string, b: number, phi: Value) => {
|
|
94
393
|
for (const p of distinctPreds(b)) {
|
|
95
394
|
appendSuccessorArg(p, b, readVar(reg, p));
|
|
395
|
+
const at = lastWriteAt[p].get(reg);
|
|
396
|
+
if (at !== undefined) {
|
|
397
|
+
const rec = writeOrder.lastWrite.get(irBlocks[p]) ?? new Map<Value, number>();
|
|
398
|
+
rec.set(phi, at);
|
|
399
|
+
writeOrder.lastWrite.set(irBlocks[p], rec);
|
|
400
|
+
}
|
|
96
401
|
}
|
|
97
402
|
};
|
|
98
403
|
// Append `arg` to EVERY successor edge of predecessor p that targets block b.
|
|
@@ -116,8 +421,8 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
116
421
|
return;
|
|
117
422
|
}
|
|
118
423
|
sealed[b] = true; // set first: addPhiOperands may recurse back here
|
|
119
|
-
for (const reg of incompletePhis[b]
|
|
120
|
-
addPhiOperands(reg, b);
|
|
424
|
+
for (const [reg, phi] of incompletePhis[b]) {
|
|
425
|
+
addPhiOperands(reg, b, phi);
|
|
121
426
|
}
|
|
122
427
|
incompletePhis[b].clear();
|
|
123
428
|
};
|
|
@@ -130,6 +435,25 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
130
435
|
};
|
|
131
436
|
sealReadyBlocks(); // seals the entry (no predecessors) up front
|
|
132
437
|
|
|
438
|
+
// See the interface docs. Two cases, and the split is the whole point: when nothing defines the
|
|
439
|
+
// key, the ordinary live-in path already does exactly the right thing; when something does, a
|
|
440
|
+
// parameter still has to exist for the signature, and it must be added WITHOUT redirecting the
|
|
441
|
+
// dataflow to it.
|
|
442
|
+
const ensureParam = (key: string, b: number): void => {
|
|
443
|
+
if (preds[b].length > 0) {
|
|
444
|
+
return; // a parameter here is a phi; see the precondition on the interface
|
|
445
|
+
}
|
|
446
|
+
for (const p of irBlocks[b].params) {
|
|
447
|
+
if (paramReg.get(p) === key) {
|
|
448
|
+
return; // already a parameter, however it got there
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const p = mkValue(T.unk(32));
|
|
452
|
+
irBlocks[b].params.push(p);
|
|
453
|
+
paramReg.set(p, key); // ranked by the ABI sort like any other parameter
|
|
454
|
+
obligedParams[b].set(key, p);
|
|
455
|
+
};
|
|
456
|
+
|
|
133
457
|
const hasReachingDef = (reg: string, b: number, seen = new Set<number>()): boolean => {
|
|
134
458
|
if (defs[b].has(reg)) {
|
|
135
459
|
return true;
|
|
@@ -147,12 +471,102 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
147
471
|
readVar,
|
|
148
472
|
writeVar,
|
|
149
473
|
paramReg,
|
|
474
|
+
ensureParam,
|
|
150
475
|
hasReachingDef,
|
|
476
|
+
noteCall: (b: number) => {
|
|
477
|
+
callsIn.add(b);
|
|
478
|
+
// the callee clobbers the caller-saved registers, its own result register included — see
|
|
479
|
+
// the ordering contract on the interface
|
|
480
|
+
writtenSinceCall[b] = new Set();
|
|
481
|
+
},
|
|
482
|
+
recordGuessedCall: (op: Op, b: number, abi: { argRegs: string[]; returnReg: string }) => {
|
|
483
|
+
abiSeen = abi;
|
|
484
|
+
guessedCalls.push({
|
|
485
|
+
block: b,
|
|
486
|
+
op,
|
|
487
|
+
freshBefore: new Set(writtenSinceCall[b]),
|
|
488
|
+
afterCallInBlock: callsIn.has(b), // `noteCall` runs after this, so this means an EARLIER call
|
|
489
|
+
});
|
|
490
|
+
},
|
|
151
491
|
markFilled: (b: number) => {
|
|
152
492
|
filled[b] = true;
|
|
153
493
|
sealReadyBlocks();
|
|
154
494
|
},
|
|
155
|
-
finish: () =>
|
|
495
|
+
finish: () => {
|
|
496
|
+
// Guessed arities counted argument registers by reaching definition alone; now that every
|
|
497
|
+
// block's calls are known, drop the ones an intervening call had already clobbered.
|
|
498
|
+
if (guessedCalls.length) {
|
|
499
|
+
const calleeResults = new Set<Value>();
|
|
500
|
+
for (const b of irBlocks) {
|
|
501
|
+
for (const op of b.ops) {
|
|
502
|
+
if (op.opcode === 'call') {
|
|
503
|
+
for (const r of op.results) {
|
|
504
|
+
calleeResults.add(r);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
trimClobberedCallArgs({
|
|
510
|
+
argRegs: abiSeen.argRegs,
|
|
511
|
+
returnReg: abiSeen.returnReg,
|
|
512
|
+
calleeResults,
|
|
513
|
+
preds,
|
|
514
|
+
freshAtEnd: writtenSinceCall,
|
|
515
|
+
callsIn,
|
|
516
|
+
sites: guessedCalls,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
irBlocks.forEach((blk, i) => writeOrder.writes.set(blk, writeCount[i]));
|
|
520
|
+
simplifyTrivialPhis(fn, (p) => {
|
|
521
|
+
phiBlock.delete(p);
|
|
522
|
+
phiKey.delete(p);
|
|
523
|
+
forgetOrder(p);
|
|
524
|
+
});
|
|
525
|
+
// Then the phis nothing reads at all — a register two paths leave holding different junk
|
|
526
|
+
// (a loop counter after its last use, a scratch the epilogue overwrites) still joins as a
|
|
527
|
+
// phi, and a dead phi is not junk downstream: its edge args become post-loop copies in the
|
|
528
|
+
// emitted C and block gates keyed on "this exit carries nothing". Order matters only for
|
|
529
|
+
// economy: trivial-phi removal can orphan a phi's last reader, never the reverse.
|
|
530
|
+
pruneDeadParams(fn, (p) => {
|
|
531
|
+
phiBlock.delete(p);
|
|
532
|
+
phiKey.delete(p);
|
|
533
|
+
forgetOrder(p);
|
|
534
|
+
});
|
|
535
|
+
// A STACK SLOT MAY NEVER LEAVE AS AN ENTRY PARAMETER. A slot is memory the function itself
|
|
536
|
+
// allocated, so its value can only come from a store the function made; arriving as a live-in
|
|
537
|
+
// instead means it was read on a path that never stored it, and the signature has grown an
|
|
538
|
+
// argument the function does not take, standing in for uninitialised stack.
|
|
539
|
+
//
|
|
540
|
+
// Checked here, of the FINISHED function, rather than as a precondition at each read. The
|
|
541
|
+
// per-read test available during construction (`hasReachingDef`) asks whether a store reaches
|
|
542
|
+
// on SOME path, which a diamond defeats; strengthening it to "every path" is not answerable
|
|
543
|
+
// mid-fill, because a loop's back-edge predecessor is not filled yet and the query would
|
|
544
|
+
// report "unassigned" for a slot initialised before the loop — the commonest real shape.
|
|
545
|
+
// Asking about the symptom instead costs one pass and cannot be defeated by fill order.
|
|
546
|
+
//
|
|
547
|
+
// It is total because in Braun's construction a value undefined on some path can surface only
|
|
548
|
+
// as a live-in of a block with no predecessors — and BOTH spellings of that are checked:
|
|
549
|
+
// `paramReg` for the live-in path, `phiKey` for the case where the entry block is itself a
|
|
550
|
+
// loop header and the fabricated value arrives as a phi instead. Missing the second is what
|
|
551
|
+
// let this survive on MIPS.
|
|
552
|
+
//
|
|
553
|
+
// In `finish()` and not a helper each frontend remembers to call: this is the frontend's only
|
|
554
|
+
// semantic postcondition, and a postcondition enforced by convention is not enforced.
|
|
555
|
+
for (const p of irBlocks[0].params) {
|
|
556
|
+
const key = paramReg.get(p) ?? phiKey.get(p);
|
|
557
|
+
// The SAME rule the mint site used, over the same ranges, so the two cannot disagree. A
|
|
558
|
+
// slot that reached the signature is either owned storage (which should have become an
|
|
559
|
+
// `undef`) or unclassified — both are bugs, and this is where a per-read test cannot be
|
|
560
|
+
// total, so it is asserted over the finished function.
|
|
561
|
+
const koff = key === undefined ? null : slotKeyOffset(key);
|
|
562
|
+
if (koff !== null && !inRange(koff, model().callerParams)) {
|
|
563
|
+
throw new FrontendUnsupportedError(
|
|
564
|
+
`cannot lift '${name}': ${key} is read on a path that never stores it ` +
|
|
565
|
+
`(partially-initialised local, or storage this function does not own) — not modelled`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
},
|
|
156
570
|
};
|
|
157
571
|
}
|
|
158
572
|
|
|
@@ -174,6 +588,211 @@ export function fallbackArgc(
|
|
|
174
588
|
return n;
|
|
175
589
|
}
|
|
176
590
|
|
|
591
|
+
/** One call site whose arity was GUESSED by {@link fallbackArgc}, with what the lifting scan saw
|
|
592
|
+
* of its own block up to that instruction. */
|
|
593
|
+
export interface GuessedCallSite {
|
|
594
|
+
block: number;
|
|
595
|
+
/** the `call` op — its operands are the guessed arguments, in argument-register order */
|
|
596
|
+
op: Op;
|
|
597
|
+
/** argument registers written between the last call in this block (or the block's start) and here */
|
|
598
|
+
freshBefore: Set<string>;
|
|
599
|
+
/** did this block already make a call before this one? */
|
|
600
|
+
afterCallInBlock: boolean;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export interface CallArgTrim {
|
|
604
|
+
argRegs: string[];
|
|
605
|
+
/** the ABI return register. Load-bearing only where it IS `argRegs[0]` (ARM r0, PPC r3) — that
|
|
606
|
+
* aliasing is what makes a callee's result indistinguishable from caller-side argument setup. */
|
|
607
|
+
returnReg: string;
|
|
608
|
+
/** every value a `call` op produced. Tells a callee's own return apart from a join that merely
|
|
609
|
+
* PASSES THROUGH one, which the register file cannot: both leave argument 0 unfresh. */
|
|
610
|
+
calleeResults: ReadonlySet<Value>;
|
|
611
|
+
/** one entry per CFG edge, as passed to {@link makeSsaBuilder} */
|
|
612
|
+
preds: number[][];
|
|
613
|
+
/** per block: the keys written since its LAST call (since its start if it makes none). Indexed by
|
|
614
|
+
* block, and it holds every key the builder saw, not only argument registers. */
|
|
615
|
+
freshAtEnd: Array<Set<string>>;
|
|
616
|
+
/** blocks that make at least one call */
|
|
617
|
+
callsIn: Set<number>;
|
|
618
|
+
sites: GuessedCallSite[];
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** Cut a GUESSED call arity down by the ABI's caller-saved clobber.
|
|
622
|
+
*
|
|
623
|
+
* `fallbackArgc` counts argument registers that merely have a reaching definition. A call clobbers
|
|
624
|
+
* r0..r3, so a definition the call sits between cannot be an argument the caller set up — correct
|
|
625
|
+
* compiled code would have re-materialized it. Counting it anyway INVENTS arguments
|
|
626
|
+
* (`m4aSongNumStart(0x89, 30, x, &g)` for a one-argument callee) — a hard compile error where the
|
|
627
|
+
* project's own header is in scope, and silently wrong code where C89's implicit declaration
|
|
628
|
+
* covers for it.
|
|
629
|
+
*
|
|
630
|
+
* SCOPE: this closes the arguments an intervening CALL disproves, which is the common case in real
|
|
631
|
+
* code. It does not close the rest — a dead value the compiler happened to leave in the next
|
|
632
|
+
* argument register with no call in between still reads as an argument, and nothing about the
|
|
633
|
+
* register file can say otherwise. A declared prototype closes those outright; short of one, the
|
|
634
|
+
* narrower reading is recorded here and offered as a ranked candidate ({@link narrowToSetupArgs}).
|
|
635
|
+
*
|
|
636
|
+
* A must-analysis: a register is FRESH at a point iff on EVERY path reaching it, it was written
|
|
637
|
+
* after the last call. The entry block starts all-fresh (those are the caller's own arguments).
|
|
638
|
+
* The result only ever SHRINKS an arity, but two of the shrinks are REFUSALS and not proofs, so a
|
|
639
|
+
* real argument CAN go with them: a fresh register above a hole stops the run (a 64-bit return
|
|
640
|
+
* occupies two registers and the frontend cannot express one, so the caller's r2 goes with the
|
|
641
|
+
* unfillable r1), and a callee's return read as the callee's own drops an argument a `g(f())`
|
|
642
|
+
* source did pass. A declared prototype is what closes either.
|
|
643
|
+
*
|
|
644
|
+
* Frontend-agnostic: the caller supplies what its own lifting scan observed, so nothing here
|
|
645
|
+
* re-derives which instruction writes which register. */
|
|
646
|
+
export function trimClobberedCallArgs(inp: CallArgTrim): void {
|
|
647
|
+
const { argRegs, returnReg, calleeResults, preds, freshAtEnd, callsIn, sites } = inp;
|
|
648
|
+
const blockCount = freshAtEnd.length;
|
|
649
|
+
const all = () => new Set(argRegs);
|
|
650
|
+
const localEnd = (b: number) => freshAtEnd[b] ?? new Set<string>();
|
|
651
|
+
// freshOut[b]: registers fresh where b ends. A block that calls forgets everything before its
|
|
652
|
+
// last call; one that does not passes its input through, plus what it wrote.
|
|
653
|
+
const freshOut: Set<string>[] = Array.from({ length: blockCount }, () => all());
|
|
654
|
+
const freshIn: Set<string>[] = Array.from({ length: blockCount }, () => all());
|
|
655
|
+
const inOf = (b: number): Set<string> => {
|
|
656
|
+
// A block with NO predecessors is the function entry (or unreachable): its argument registers
|
|
657
|
+
// are the ones the caller set up. An entry that DOES have predecessors — an entry that is also
|
|
658
|
+
// a loop header — gets the ordinary intersection instead, because on the back edge the caller's
|
|
659
|
+
// setup is long gone and an intervening call may have clobbered it.
|
|
660
|
+
const ps = [...new Set(preds[b] ?? [])];
|
|
661
|
+
if (ps.length === 0) {
|
|
662
|
+
return all();
|
|
663
|
+
}
|
|
664
|
+
const acc = new Set(freshOut[ps[0]]);
|
|
665
|
+
for (const p of ps.slice(1)) {
|
|
666
|
+
for (const r of [...acc]) {
|
|
667
|
+
if (!freshOut[p].has(r)) {
|
|
668
|
+
acc.delete(r);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return acc;
|
|
673
|
+
};
|
|
674
|
+
for (let changed = true; changed;) {
|
|
675
|
+
changed = false;
|
|
676
|
+
for (let b = 0; b < blockCount; b++) {
|
|
677
|
+
const fin = inOf(b);
|
|
678
|
+
const fout = callsIn.has(b) ? localEnd(b) : new Set([...fin, ...localEnd(b)]);
|
|
679
|
+
if (fout.size !== freshOut[b].size || [...fout].some((r) => !freshOut[b].has(r))) {
|
|
680
|
+
changed = true;
|
|
681
|
+
}
|
|
682
|
+
freshIn[b] = fin;
|
|
683
|
+
freshOut[b] = fout;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const runOfFresh = (fresh: Set<string>, from: number): number => {
|
|
687
|
+
let n = from;
|
|
688
|
+
while (n < argRegs.length && fresh.has(argRegs[n])) {
|
|
689
|
+
n++;
|
|
690
|
+
}
|
|
691
|
+
return n;
|
|
692
|
+
};
|
|
693
|
+
// A LATER argument register this caller set up proves the call takes arguments at all, and
|
|
694
|
+
// argument 0 sits below one that is proven — so it is being passed too, whatever put it there
|
|
695
|
+
// (`bl __mulsf3; add r1,r4,#0; bl __addsf3` is `__addsf3(__mulsf3(a, b), c)`).
|
|
696
|
+
const setsUpLater = (fresh: Set<string>): boolean => argRegs.some((r, i) => i > 0 && fresh.has(r));
|
|
697
|
+
// THE RETURN REGISTER IS NOT ARGUMENT SETUP. Where the ABI aliases it onto argument 0, the
|
|
698
|
+
// frontends record a call's clobber AFTER its own result, so the result leaves the register
|
|
699
|
+
// UNfresh here. That disproves caller setup only where the callee's return is BOTH what the
|
|
700
|
+
// register still holds and all the site has to go on: with a later register set up (above), or
|
|
701
|
+
// with a value no call produced — a join of one path's return with another path's caller-computed
|
|
702
|
+
// value — argument 0 is a real argument, and dropping the second kind would delete the
|
|
703
|
+
// instructions that computed it.
|
|
704
|
+
//
|
|
705
|
+
// With neither, the site carries no argument evidence at all: `bl f; bl g` is `f(); g();` as
|
|
706
|
+
// readily as `g(f())`, the two spell the same bytes on this ABI, and only the nested one needs
|
|
707
|
+
// `f` to return a value and `g` to accept one — a spelling the project's own header rejects
|
|
708
|
+
// outright when it does not. A declared prototype never reaches here, and stays the way `g(f())`
|
|
709
|
+
// is recovered.
|
|
710
|
+
const argcAt = (fresh: Set<string>, op: Op): number => {
|
|
711
|
+
if (argRegs[0] !== returnReg || fresh.has(argRegs[0])) {
|
|
712
|
+
return runOfFresh(fresh, 0);
|
|
713
|
+
}
|
|
714
|
+
if (setsUpLater(fresh) || !calleeResults.has(op.operands[0])) {
|
|
715
|
+
return runOfFresh(new Set([argRegs[0], ...fresh]), 0);
|
|
716
|
+
}
|
|
717
|
+
return 0;
|
|
718
|
+
};
|
|
719
|
+
for (const s of sites) {
|
|
720
|
+
const fresh = s.afterCallInBlock ? s.freshBefore : new Set([...freshIn[s.block], ...s.freshBefore]);
|
|
721
|
+
const n = argcAt(fresh, s.op);
|
|
722
|
+
if (n < s.op.operands.length) {
|
|
723
|
+
s.op.operands.length = n;
|
|
724
|
+
}
|
|
725
|
+
// The SHORTER arity the same evidence also allows, recorded for {@link narrowToSetupArgs}: the
|
|
726
|
+
// run over what THIS BLOCK wrote, dropping the registers that are fresh only because no call
|
|
727
|
+
// stands between here and wherever they were last written. Both readings stay live, so this one
|
|
728
|
+
// is recorded rather than applied. A survivor is what it drops, so the join clause above has no
|
|
729
|
+
// place here — but `setsUpLater` still does: a register this block set up two instructions
|
|
730
|
+
// before the call is not something the narrower reading may call dead.
|
|
731
|
+
const localFresh = setsUpLater(s.freshBefore) ? new Set([argRegs[0], ...s.freshBefore]) : s.freshBefore;
|
|
732
|
+
const local = Math.min(runOfFresh(localFresh, 0), s.op.operands.length);
|
|
733
|
+
if (local < s.op.operands.length) {
|
|
734
|
+
setupArgc.set(s.op, local);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** The narrower arity {@link narrowToSetupArgs} would cut each guessed call to. A SIDE table and
|
|
740
|
+
* not an attr: this is a fact about one LIFT, not part of the IR the rest of the pipeline compares
|
|
741
|
+
* and prints — `structure/hazards.ts` decides two ops equal by comparing their attrs verbatim, so
|
|
742
|
+
* an attr only one of an otherwise-matching pair carries would cost a recovery. */
|
|
743
|
+
const setupArgc = new WeakMap<Op, number>();
|
|
744
|
+
|
|
745
|
+
/** Whether anything in `fn` HAS the narrower reading — the lever's gate, so the ~99% of functions
|
|
746
|
+
* with no narrowable call cost no re-lift. Read it off the lift itself: a later pipeline stage may
|
|
747
|
+
* replace a `call` op (softdiv rewrites one to a division), and the table is keyed by op. */
|
|
748
|
+
export function hasSetupArgsNarrowing(fn: Fn): boolean {
|
|
749
|
+
return fn.blocks.some((b) => b.ops.some((op) => setupArgc.has(op)));
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** Cut every guessed call to the arity its OWN BLOCK set up, and report whether anything moved.
|
|
753
|
+
*
|
|
754
|
+
* `trimClobberedCallArgs` keeps an argument register whose value merely survives from an earlier
|
|
755
|
+
* block, because compiled code really does pass one that way: agbcc leaves a value already in r0
|
|
756
|
+
* where it is and branches to the call (`if (x) f(x);` is `cmp r0,#0; beq; bl f`, no setup at
|
|
757
|
+
* all). Those are also the bytes `if (x) f();` compiles to, so usually neither reading is
|
|
758
|
+
* refutable — but where the guard is an EQUALITY the compiler proves the argument constant and
|
|
759
|
+
* has to materialize it (`if (x == 0) f(x);` opens the arm with `mov r0,#0`), and the absence of
|
|
760
|
+
* that instruction rules the wider reading out. Which case a function is in is not knowable from
|
|
761
|
+
* the register file, and is exactly what a differ decides. Hence a ranked candidate rather than a
|
|
762
|
+
* default: the arm that passes only what the calling block itself put there.
|
|
763
|
+
*
|
|
764
|
+
* Applies only to arities that were GUESSED — a declared prototype never recorded the fact. */
|
|
765
|
+
export function narrowToSetupArgs(fn: Fn): boolean {
|
|
766
|
+
let changed = false;
|
|
767
|
+
for (const b of fn.blocks) {
|
|
768
|
+
for (const op of b.ops) {
|
|
769
|
+
const setup = setupArgc.get(op);
|
|
770
|
+
if (setup !== undefined && setup < op.operands.length) {
|
|
771
|
+
op.operands.length = setup;
|
|
772
|
+
changed = true;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (changed) {
|
|
777
|
+
// A dropped argument can be a join's last reader, and `finish()` pruned the dead phis before
|
|
778
|
+
// this reading existed. Left in, the phi's edge args render as assignments to a local nothing
|
|
779
|
+
// reads (`v0 = UpdateWorldMapCursor();`) — see the note on the prune in `finish`.
|
|
780
|
+
pruneDeadParams(fn);
|
|
781
|
+
}
|
|
782
|
+
return changed;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** The stack-slot key both the MIPS and Thumb frontends use for a word-sized local in the
|
|
786
|
+
* function's own frame. Shared so the two spell it identically and the frame-partition rule can
|
|
787
|
+
* recognise either frontend's slots. See the virtual-key note in the module header. */
|
|
788
|
+
const SLOT_PREFIX = 'sp@';
|
|
789
|
+
export const stackSlotKey = (off: number): string => `${SLOT_PREFIX}${off}`;
|
|
790
|
+
/** The byte offset a slot key names, or null if `key` is not a slot key at all (an ordinary
|
|
791
|
+
* register). The grammar stays owned by this module — {@link LiveInModel} is expressed in the same
|
|
792
|
+
* coordinate, so the classification rule can be generic. */
|
|
793
|
+
export const slotKeyOffset = (key: string): number | null =>
|
|
794
|
+
key.startsWith(SLOT_PREFIX) ? Number(key.slice(SLOT_PREFIX.length)) : null;
|
|
795
|
+
|
|
177
796
|
/** Order the TRUE entry block's parameters by ABI argument register, so downstream naming
|
|
178
797
|
* (`a0`, `a1`, …) matches the calling convention, not first-read order (a callee-saved copy can
|
|
179
798
|
* read a later argument register first). No-op when the entry has predecessors — a loop
|