@asmlift/core 0.5.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 -167
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +194 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +54 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +140 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/argbase.ts +2 -1
- package/src/l3/ast.ts +464 -57
- package/src/l3/basecse.ts +664 -76
- package/src/l3/coalesce.ts +429 -43
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +21 -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 +644 -218
- 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 +15 -0
- 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 +157 -56
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +33 -18
- 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 +97 -14
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +210 -10
- package/src/raise/shortcircuit.ts +474 -74
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +33 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1723 -272
- package/src/structure/analysis.ts +1392 -141
- 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 +2678 -526
- package/src/structure/switch-recover.ts +616 -144
- 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,8 +15,8 @@
|
|
|
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, Op, 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
21
|
import { FrontendUnsupportedError } from './errors';
|
|
22
22
|
|
|
@@ -48,13 +48,14 @@ export interface SsaBuilder {
|
|
|
48
48
|
/** Whether `reg` has a definition reaching block `b` (best-effort call-arity heuristic). */
|
|
49
49
|
hasReachingDef(reg: string, b: number, seen?: Set<number>): boolean;
|
|
50
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
|
|
52
|
-
* the call's own result
|
|
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. */
|
|
53
54
|
noteCall(b: number): void;
|
|
54
55
|
/** Register a `call` op whose arity was GUESSED (no prototype), so `finish` can cut it back to the
|
|
55
56
|
* argument registers that were actually set up on every path (see {@link trimClobberedCallArgs}).
|
|
56
|
-
* `
|
|
57
|
-
recordGuessedCall(op: Op, b: number, argRegs: string[]): void;
|
|
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;
|
|
58
59
|
/** Remove trivial phis and enforce the frontend's postconditions; call once every block is
|
|
59
60
|
* filled. Throws FrontendUnsupportedError if a stack slot escaped as an entry parameter. */
|
|
60
61
|
finish(): void;
|
|
@@ -67,9 +68,135 @@ export interface SsaBuilder {
|
|
|
67
68
|
// A virtual key must be outside its ISA's register grammar so it cannot collide with a real one, and
|
|
68
69
|
// a key read with no reaching def becomes a function PARAMETER by the live-in path below — which is
|
|
69
70
|
// how both of those capabilities get their parameters without a new opcode or pass.
|
|
70
|
-
|
|
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;
|
|
71
197
|
const irBlocks: Block[] = Array.from({ length: blockCount }, () => ({ params: [] as Value[], ops: [] }));
|
|
72
|
-
|
|
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 };
|
|
73
200
|
|
|
74
201
|
const defs: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
75
202
|
const sealed: boolean[] = irBlocks.map(() => false);
|
|
@@ -100,11 +227,91 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
100
227
|
const writtenSinceCall: Array<Set<string>> = irBlocks.map(() => new Set());
|
|
101
228
|
const callsIn = new Set<number>();
|
|
102
229
|
const guessedCalls: GuessedCallSite[] = [];
|
|
103
|
-
let
|
|
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
|
+
};
|
|
104
309
|
|
|
105
310
|
const writeVar = (reg: string, b: number, v: Value) => {
|
|
311
|
+
noteSlotHome(reg, v);
|
|
106
312
|
writtenSinceCall[b].add(reg);
|
|
107
313
|
defs[b].set(reg, v);
|
|
314
|
+
lastWriteAt[b].set(reg, writeCount[b]++);
|
|
108
315
|
};
|
|
109
316
|
const readVar = (reg: string, b: number): Value => defs[b].get(reg) ?? readRecursive(reg, b);
|
|
110
317
|
|
|
@@ -113,6 +320,10 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
113
320
|
irBlocks[b].params.push(phi);
|
|
114
321
|
phiBlock.set(phi, b);
|
|
115
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);
|
|
116
327
|
defs[b].set(reg, phi); // set before wiring operands to break cycles
|
|
117
328
|
return phi;
|
|
118
329
|
};
|
|
@@ -128,7 +339,29 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
128
339
|
// manufacture a join (and a phi) where there is none.
|
|
129
340
|
const ps = distinctPreds(b);
|
|
130
341
|
if (ps.length === 0) {
|
|
131
|
-
// 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.
|
|
132
365
|
// If one was already asserted for this key (ensureParam), adopt it — minting a second
|
|
133
366
|
// parameter for the same key would put the key in the signature twice.
|
|
134
367
|
const obliged = obligedParams[b].get(reg);
|
|
@@ -150,12 +383,21 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
150
383
|
}
|
|
151
384
|
// sealed join: create the phi and wire every predecessor's terminator arg now.
|
|
152
385
|
const phi = newPhi(reg, b);
|
|
153
|
-
addPhiOperands(reg, b);
|
|
386
|
+
addPhiOperands(reg, b, phi);
|
|
154
387
|
return phi;
|
|
155
388
|
};
|
|
156
|
-
|
|
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) => {
|
|
157
393
|
for (const p of distinctPreds(b)) {
|
|
158
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
|
+
}
|
|
159
401
|
}
|
|
160
402
|
};
|
|
161
403
|
// Append `arg` to EVERY successor edge of predecessor p that targets block b.
|
|
@@ -179,8 +421,8 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
179
421
|
return;
|
|
180
422
|
}
|
|
181
423
|
sealed[b] = true; // set first: addPhiOperands may recurse back here
|
|
182
|
-
for (const reg of incompletePhis[b]
|
|
183
|
-
addPhiOperands(reg, b);
|
|
424
|
+
for (const [reg, phi] of incompletePhis[b]) {
|
|
425
|
+
addPhiOperands(reg, b, phi);
|
|
184
426
|
}
|
|
185
427
|
incompletePhis[b].clear();
|
|
186
428
|
};
|
|
@@ -233,10 +475,12 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
233
475
|
hasReachingDef,
|
|
234
476
|
noteCall: (b: number) => {
|
|
235
477
|
callsIn.add(b);
|
|
236
|
-
|
|
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();
|
|
237
481
|
},
|
|
238
|
-
recordGuessedCall: (op: Op, b: number, argRegs: string[]) => {
|
|
239
|
-
|
|
482
|
+
recordGuessedCall: (op: Op, b: number, abi: { argRegs: string[]; returnReg: string }) => {
|
|
483
|
+
abiSeen = abi;
|
|
240
484
|
guessedCalls.push({
|
|
241
485
|
block: b,
|
|
242
486
|
op,
|
|
@@ -252,17 +496,41 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
252
496
|
// Guessed arities counted argument registers by reaching definition alone; now that every
|
|
253
497
|
// block's calls are known, drop the ones an intervening call had already clobbered.
|
|
254
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
|
+
}
|
|
255
509
|
trimClobberedCallArgs({
|
|
256
|
-
argRegs:
|
|
510
|
+
argRegs: abiSeen.argRegs,
|
|
511
|
+
returnReg: abiSeen.returnReg,
|
|
512
|
+
calleeResults,
|
|
257
513
|
preds,
|
|
258
514
|
freshAtEnd: writtenSinceCall,
|
|
259
515
|
callsIn,
|
|
260
516
|
sites: guessedCalls,
|
|
261
517
|
});
|
|
262
518
|
}
|
|
519
|
+
irBlocks.forEach((blk, i) => writeOrder.writes.set(blk, writeCount[i]));
|
|
263
520
|
simplifyTrivialPhis(fn, (p) => {
|
|
264
521
|
phiBlock.delete(p);
|
|
265
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);
|
|
266
534
|
});
|
|
267
535
|
// A STACK SLOT MAY NEVER LEAVE AS AN ENTRY PARAMETER. A slot is memory the function itself
|
|
268
536
|
// allocated, so its value can only come from a store the function made; arriving as a live-in
|
|
@@ -286,10 +554,15 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
286
554
|
// semantic postcondition, and a postcondition enforced by convention is not enforced.
|
|
287
555
|
for (const p of irBlocks[0].params) {
|
|
288
556
|
const key = paramReg.get(p) ?? phiKey.get(p);
|
|
289
|
-
|
|
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)) {
|
|
290
563
|
throw new FrontendUnsupportedError(
|
|
291
|
-
`cannot lift '${name}':
|
|
292
|
-
`(partially-initialised local) — not modelled`,
|
|
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`,
|
|
293
566
|
);
|
|
294
567
|
}
|
|
295
568
|
}
|
|
@@ -329,6 +602,12 @@ export interface GuessedCallSite {
|
|
|
329
602
|
|
|
330
603
|
export interface CallArgTrim {
|
|
331
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>;
|
|
332
611
|
/** one entry per CFG edge, as passed to {@link makeSsaBuilder} */
|
|
333
612
|
preds: number[][];
|
|
334
613
|
/** per block: the keys written since its LAST call (since its start if it makes none). Indexed by
|
|
@@ -351,17 +630,21 @@ export interface CallArgTrim {
|
|
|
351
630
|
* SCOPE: this closes the arguments an intervening CALL disproves, which is the common case in real
|
|
352
631
|
* code. It does not close the rest — a dead value the compiler happened to leave in the next
|
|
353
632
|
* argument register with no call in between still reads as an argument, and nothing about the
|
|
354
|
-
* register file can say otherwise.
|
|
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}).
|
|
355
635
|
*
|
|
356
636
|
* A must-analysis: a register is FRESH at a point iff on EVERY path reaching it, it was written
|
|
357
637
|
* after the last call. The entry block starts all-fresh (those are the caller's own arguments).
|
|
358
|
-
* The result only ever SHRINKS an arity
|
|
359
|
-
* argument
|
|
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.
|
|
360
643
|
*
|
|
361
644
|
* Frontend-agnostic: the caller supplies what its own lifting scan observed, so nothing here
|
|
362
645
|
* re-derives which instruction writes which register. */
|
|
363
646
|
export function trimClobberedCallArgs(inp: CallArgTrim): void {
|
|
364
|
-
const { argRegs, preds, freshAtEnd, callsIn, sites } = inp;
|
|
647
|
+
const { argRegs, returnReg, calleeResults, preds, freshAtEnd, callsIn, sites } = inp;
|
|
365
648
|
const blockCount = freshAtEnd.length;
|
|
366
649
|
const all = () => new Set(argRegs);
|
|
367
650
|
const localEnd = (b: number) => freshAtEnd[b] ?? new Set<string>();
|
|
@@ -400,23 +683,115 @@ export function trimClobberedCallArgs(inp: CallArgTrim): void {
|
|
|
400
683
|
freshOut[b] = fout;
|
|
401
684
|
}
|
|
402
685
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
let n = 0;
|
|
686
|
+
const runOfFresh = (fresh: Set<string>, from: number): number => {
|
|
687
|
+
let n = from;
|
|
406
688
|
while (n < argRegs.length && fresh.has(argRegs[n])) {
|
|
407
689
|
n++;
|
|
408
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);
|
|
409
722
|
if (n < s.op.operands.length) {
|
|
410
723
|
s.op.operands.length = n;
|
|
411
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);
|
|
412
781
|
}
|
|
782
|
+
return changed;
|
|
413
783
|
}
|
|
414
784
|
|
|
415
785
|
/** The stack-slot key both the MIPS and Thumb frontends use for a word-sized local in the
|
|
416
|
-
* function's own frame. Shared so the two spell it identically and
|
|
786
|
+
* function's own frame. Shared so the two spell it identically and the frame-partition rule can
|
|
417
787
|
* recognise either frontend's slots. See the virtual-key note in the module header. */
|
|
418
788
|
const SLOT_PREFIX = 'sp@';
|
|
419
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;
|
|
420
795
|
|
|
421
796
|
/** Order the TRUE entry block's parameters by ABI argument register, so downstream naming
|
|
422
797
|
* (`a0`, `a1`, …) matches the calling convention, not first-read order (a callee-saved copy can
|