@asmlift/core 0.4.0 → 0.5.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/package.json +1 -1
- package/src/backend/cfamily.ts +5 -2
- package/src/contracts.ts +166 -2
- package/src/frontend/mips.ts +13 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +18 -7
- package/src/frontend/ssa.ts +249 -5
- package/src/frontend/thumb.ts +1082 -72
- package/src/ir/alias.ts +75 -0
- package/src/ir/opcodes.ts +24 -14
- package/src/l3/argbase.ts +6 -1
- package/src/l3/ast.ts +9 -1
- package/src/l3/basecse.ts +57 -24
- package/src/l3/coalesce.ts +107 -38
- package/src/l3/dce.ts +31 -18
- package/src/l3/gates.ts +67 -0
- package/src/l3/scopebase.ts +11 -7
- package/src/l3/tailmerge.ts +8 -4
- package/src/pipeline.ts +60 -4
- package/src/raise/divpow2.ts +2 -1
- package/src/raise/gvn.ts +16 -6
- package/src/raise/pre-recovery.ts +4 -2
- package/src/raise/retsink.ts +5 -4
- package/src/raise/shortcircuit.ts +3 -5
- package/src/raise/struct-arrays.ts +2 -1
- package/src/raise/structs.ts +29 -1
- package/src/rank.ts +26 -2
- package/src/structure/analysis.ts +168 -123
- package/src/structure/structure.ts +228 -63
- package/src/structure/switch-recover.ts +96 -27
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';
|
|
18
|
+
import { Block, Fn, Op, Value, mkValue } from '../ir/core';
|
|
19
19
|
import { 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,15 +29,44 @@ 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 before writing
|
|
52
|
+
* the call's own result. */
|
|
53
|
+
noteCall(b: number): void;
|
|
54
|
+
/** Register a `call` op whose arity was GUESSED (no prototype), so `finish` can cut it back to the
|
|
55
|
+
* argument registers that were actually set up on every path (see {@link trimClobberedCallArgs}).
|
|
56
|
+
* `argRegs` is the target's argument-register order. */
|
|
57
|
+
recordGuessedCall(op: Op, b: number, argRegs: string[]): void;
|
|
58
|
+
/** Remove trivial phis and enforce the frontend's postconditions; call once every block is
|
|
59
|
+
* filled. Throws FrontendUnsupportedError if a stack slot escaped as an entry parameter. */
|
|
36
60
|
finish(): void;
|
|
37
61
|
}
|
|
38
62
|
|
|
39
63
|
/** `preds` is per-EDGE (see the module header): one entry per CFG edge into each block. */
|
|
64
|
+
// VARIABLE NAMES ARE NOT ALWAYS MACHINE REGISTERS. `readVar`/`writeVar` key on an arbitrary string,
|
|
65
|
+
// and frontends mint VIRTUAL keys for storage the ISA has no register for — MIPS `sp@<off>` for a
|
|
66
|
+
// stack slot (frontend/mips.ts), Thumb `@sarg<k>` for an incoming stack argument (frontend/thumb.ts).
|
|
67
|
+
// A virtual key must be outside its ISA's register grammar so it cannot collide with a real one, and
|
|
68
|
+
// a key read with no reaching def becomes a function PARAMETER by the live-in path below — which is
|
|
69
|
+
// how both of those capabilities get their parameters without a new opcode or pass.
|
|
40
70
|
export function makeSsaBuilder(name: string, blockCount: number, preds: number[][]): SsaBuilder {
|
|
41
71
|
const irBlocks: Block[] = Array.from({ length: blockCount }, () => ({ params: [] as Value[], ops: [] }));
|
|
42
72
|
const fn: Fn = { name, blocks: irBlocks };
|
|
@@ -46,18 +76,43 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
46
76
|
const filled: boolean[] = irBlocks.map(() => false);
|
|
47
77
|
const incompletePhis: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
48
78
|
const phiBlock = new Map<Value, number>();
|
|
79
|
+
// The key each phi stands for. `paramReg` covers live-ins only, so without this a slot that
|
|
80
|
+
// arrives as a PHI — which is what happens when the entry block is itself a loop header — is
|
|
81
|
+
// invisible to the escape check below. Braun's construction gives no other way to tell.
|
|
82
|
+
const phiKey = new Map<Value, string>();
|
|
49
83
|
const paramReg = new Map<Value, string>();
|
|
84
|
+
// Parameters created by ensureParam that nothing has read yet. They are deliberately NOT in
|
|
85
|
+
// `defs`: a parameter asserted because a calling convention proves it exists is not evidence that
|
|
86
|
+
// a VALUE reaches anything, and writing one into `defs` would say it does. That distinction is
|
|
87
|
+
// load-bearing — `hasReachingDef` feeds `fallbackArgc`, so a def here silently raises the guessed
|
|
88
|
+
// arity of every prototype-less call in the function, making it pass registers the calling block
|
|
89
|
+
// never set up (`unknown(1)` became `unknown(1, a1, a2, a3)`). The first read adopts the value
|
|
90
|
+
// from here instead of minting a second parameter for the same key.
|
|
91
|
+
const obligedParams: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
50
92
|
|
|
51
93
|
// `preds` lists an entry per CFG EDGE; these are the distinct predecessor BLOCKS.
|
|
52
94
|
const distinctPreds = (b: number): number[] => [...new Set(preds[b])];
|
|
53
95
|
|
|
54
|
-
|
|
96
|
+
// CALLER-SAVED CLOBBER, for guessed call arities (see trimClobberedCallArgs). Tracked HERE
|
|
97
|
+
// because every register write in every frontend already goes through `writeVar`: a frontend
|
|
98
|
+
// that gathered this itself would be sound only while it remembered to route each write past a
|
|
99
|
+
// wrapper, and a MISSED write under-counts an arity — which drops a real argument silently.
|
|
100
|
+
const writtenSinceCall: Array<Set<string>> = irBlocks.map(() => new Set());
|
|
101
|
+
const callsIn = new Set<number>();
|
|
102
|
+
const guessedCalls: GuessedCallSite[] = [];
|
|
103
|
+
let argRegsSeen: string[] = [];
|
|
104
|
+
|
|
105
|
+
const writeVar = (reg: string, b: number, v: Value) => {
|
|
106
|
+
writtenSinceCall[b].add(reg);
|
|
107
|
+
defs[b].set(reg, v);
|
|
108
|
+
};
|
|
55
109
|
const readVar = (reg: string, b: number): Value => defs[b].get(reg) ?? readRecursive(reg, b);
|
|
56
110
|
|
|
57
111
|
const newPhi = (reg: string, b: number): Value => {
|
|
58
112
|
const phi = mkValue(T.unk(32));
|
|
59
113
|
irBlocks[b].params.push(phi);
|
|
60
114
|
phiBlock.set(phi, b);
|
|
115
|
+
phiKey.set(phi, reg);
|
|
61
116
|
defs[b].set(reg, phi); // set before wiring operands to break cycles
|
|
62
117
|
return phi;
|
|
63
118
|
};
|
|
@@ -74,6 +129,14 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
74
129
|
const ps = distinctPreds(b);
|
|
75
130
|
if (ps.length === 0) {
|
|
76
131
|
// live-in with no predecessor: an incoming argument register → function parameter.
|
|
132
|
+
// If one was already asserted for this key (ensureParam), adopt it — minting a second
|
|
133
|
+
// parameter for the same key would put the key in the signature twice.
|
|
134
|
+
const obliged = obligedParams[b].get(reg);
|
|
135
|
+
if (obliged !== undefined) {
|
|
136
|
+
obligedParams[b].delete(reg);
|
|
137
|
+
defs[b].set(reg, obliged);
|
|
138
|
+
return obliged;
|
|
139
|
+
}
|
|
77
140
|
const p = mkValue(T.unk(32));
|
|
78
141
|
irBlocks[b].params.push(p);
|
|
79
142
|
defs[b].set(reg, p);
|
|
@@ -130,6 +193,25 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
130
193
|
};
|
|
131
194
|
sealReadyBlocks(); // seals the entry (no predecessors) up front
|
|
132
195
|
|
|
196
|
+
// See the interface docs. Two cases, and the split is the whole point: when nothing defines the
|
|
197
|
+
// key, the ordinary live-in path already does exactly the right thing; when something does, a
|
|
198
|
+
// parameter still has to exist for the signature, and it must be added WITHOUT redirecting the
|
|
199
|
+
// dataflow to it.
|
|
200
|
+
const ensureParam = (key: string, b: number): void => {
|
|
201
|
+
if (preds[b].length > 0) {
|
|
202
|
+
return; // a parameter here is a phi; see the precondition on the interface
|
|
203
|
+
}
|
|
204
|
+
for (const p of irBlocks[b].params) {
|
|
205
|
+
if (paramReg.get(p) === key) {
|
|
206
|
+
return; // already a parameter, however it got there
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const p = mkValue(T.unk(32));
|
|
210
|
+
irBlocks[b].params.push(p);
|
|
211
|
+
paramReg.set(p, key); // ranked by the ABI sort like any other parameter
|
|
212
|
+
obligedParams[b].set(key, p);
|
|
213
|
+
};
|
|
214
|
+
|
|
133
215
|
const hasReachingDef = (reg: string, b: number, seen = new Set<number>()): boolean => {
|
|
134
216
|
if (defs[b].has(reg)) {
|
|
135
217
|
return true;
|
|
@@ -147,12 +229,71 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
147
229
|
readVar,
|
|
148
230
|
writeVar,
|
|
149
231
|
paramReg,
|
|
232
|
+
ensureParam,
|
|
150
233
|
hasReachingDef,
|
|
234
|
+
noteCall: (b: number) => {
|
|
235
|
+
callsIn.add(b);
|
|
236
|
+
writtenSinceCall[b] = new Set(); // the callee clobbers the caller-saved registers
|
|
237
|
+
},
|
|
238
|
+
recordGuessedCall: (op: Op, b: number, argRegs: string[]) => {
|
|
239
|
+
argRegsSeen = argRegs;
|
|
240
|
+
guessedCalls.push({
|
|
241
|
+
block: b,
|
|
242
|
+
op,
|
|
243
|
+
freshBefore: new Set(writtenSinceCall[b]),
|
|
244
|
+
afterCallInBlock: callsIn.has(b), // `noteCall` runs after this, so this means an EARLIER call
|
|
245
|
+
});
|
|
246
|
+
},
|
|
151
247
|
markFilled: (b: number) => {
|
|
152
248
|
filled[b] = true;
|
|
153
249
|
sealReadyBlocks();
|
|
154
250
|
},
|
|
155
|
-
finish: () =>
|
|
251
|
+
finish: () => {
|
|
252
|
+
// Guessed arities counted argument registers by reaching definition alone; now that every
|
|
253
|
+
// block's calls are known, drop the ones an intervening call had already clobbered.
|
|
254
|
+
if (guessedCalls.length) {
|
|
255
|
+
trimClobberedCallArgs({
|
|
256
|
+
argRegs: argRegsSeen,
|
|
257
|
+
preds,
|
|
258
|
+
freshAtEnd: writtenSinceCall,
|
|
259
|
+
callsIn,
|
|
260
|
+
sites: guessedCalls,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
simplifyTrivialPhis(fn, (p) => {
|
|
264
|
+
phiBlock.delete(p);
|
|
265
|
+
phiKey.delete(p);
|
|
266
|
+
});
|
|
267
|
+
// A STACK SLOT MAY NEVER LEAVE AS AN ENTRY PARAMETER. A slot is memory the function itself
|
|
268
|
+
// allocated, so its value can only come from a store the function made; arriving as a live-in
|
|
269
|
+
// instead means it was read on a path that never stored it, and the signature has grown an
|
|
270
|
+
// argument the function does not take, standing in for uninitialised stack.
|
|
271
|
+
//
|
|
272
|
+
// Checked here, of the FINISHED function, rather than as a precondition at each read. The
|
|
273
|
+
// per-read test available during construction (`hasReachingDef`) asks whether a store reaches
|
|
274
|
+
// on SOME path, which a diamond defeats; strengthening it to "every path" is not answerable
|
|
275
|
+
// mid-fill, because a loop's back-edge predecessor is not filled yet and the query would
|
|
276
|
+
// report "unassigned" for a slot initialised before the loop — the commonest real shape.
|
|
277
|
+
// Asking about the symptom instead costs one pass and cannot be defeated by fill order.
|
|
278
|
+
//
|
|
279
|
+
// It is total because in Braun's construction a value undefined on some path can surface only
|
|
280
|
+
// as a live-in of a block with no predecessors — and BOTH spellings of that are checked:
|
|
281
|
+
// `paramReg` for the live-in path, `phiKey` for the case where the entry block is itself a
|
|
282
|
+
// loop header and the fabricated value arrives as a phi instead. Missing the second is what
|
|
283
|
+
// let this survive on MIPS.
|
|
284
|
+
//
|
|
285
|
+
// In `finish()` and not a helper each frontend remembers to call: this is the frontend's only
|
|
286
|
+
// semantic postcondition, and a postcondition enforced by convention is not enforced.
|
|
287
|
+
for (const p of irBlocks[0].params) {
|
|
288
|
+
const key = paramReg.get(p) ?? phiKey.get(p);
|
|
289
|
+
if (key?.startsWith(SLOT_PREFIX)) {
|
|
290
|
+
throw new FrontendUnsupportedError(
|
|
291
|
+
`cannot lift '${name}': stack slot ${key} is read on a path that never stores it ` +
|
|
292
|
+
`(partially-initialised local) — not modelled`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
},
|
|
156
297
|
};
|
|
157
298
|
}
|
|
158
299
|
|
|
@@ -174,6 +315,109 @@ export function fallbackArgc(
|
|
|
174
315
|
return n;
|
|
175
316
|
}
|
|
176
317
|
|
|
318
|
+
/** One call site whose arity was GUESSED by {@link fallbackArgc}, with what the lifting scan saw
|
|
319
|
+
* of its own block up to that instruction. */
|
|
320
|
+
export interface GuessedCallSite {
|
|
321
|
+
block: number;
|
|
322
|
+
/** the `call` op — its operands are the guessed arguments, in argument-register order */
|
|
323
|
+
op: Op;
|
|
324
|
+
/** argument registers written between the last call in this block (or the block's start) and here */
|
|
325
|
+
freshBefore: Set<string>;
|
|
326
|
+
/** did this block already make a call before this one? */
|
|
327
|
+
afterCallInBlock: boolean;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export interface CallArgTrim {
|
|
331
|
+
argRegs: string[];
|
|
332
|
+
/** one entry per CFG edge, as passed to {@link makeSsaBuilder} */
|
|
333
|
+
preds: number[][];
|
|
334
|
+
/** per block: the keys written since its LAST call (since its start if it makes none). Indexed by
|
|
335
|
+
* block, and it holds every key the builder saw, not only argument registers. */
|
|
336
|
+
freshAtEnd: Array<Set<string>>;
|
|
337
|
+
/** blocks that make at least one call */
|
|
338
|
+
callsIn: Set<number>;
|
|
339
|
+
sites: GuessedCallSite[];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Cut a GUESSED call arity down by the ABI's caller-saved clobber.
|
|
343
|
+
*
|
|
344
|
+
* `fallbackArgc` counts argument registers that merely have a reaching definition. A call clobbers
|
|
345
|
+
* r0..r3, so a definition the call sits between cannot be an argument the caller set up — correct
|
|
346
|
+
* compiled code would have re-materialized it. Counting it anyway INVENTS arguments
|
|
347
|
+
* (`m4aSongNumStart(0x89, 30, x, &g)` for a one-argument callee) — a hard compile error where the
|
|
348
|
+
* project's own header is in scope, and silently wrong code where C89's implicit declaration
|
|
349
|
+
* covers for it.
|
|
350
|
+
*
|
|
351
|
+
* SCOPE: this closes the arguments an intervening CALL disproves, which is the common case in real
|
|
352
|
+
* code. It does not close the rest — a dead value the compiler happened to leave in the next
|
|
353
|
+
* argument register with no call in between still reads as an argument, and nothing about the
|
|
354
|
+
* register file can say otherwise. Only a declared prototype closes those.
|
|
355
|
+
*
|
|
356
|
+
* A must-analysis: a register is FRESH at a point iff on EVERY path reaching it, it was written
|
|
357
|
+
* 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 — a register the analysis cannot prove clobbered stays an
|
|
359
|
+
* argument — so no real argument can be dropped by it.
|
|
360
|
+
*
|
|
361
|
+
* Frontend-agnostic: the caller supplies what its own lifting scan observed, so nothing here
|
|
362
|
+
* re-derives which instruction writes which register. */
|
|
363
|
+
export function trimClobberedCallArgs(inp: CallArgTrim): void {
|
|
364
|
+
const { argRegs, preds, freshAtEnd, callsIn, sites } = inp;
|
|
365
|
+
const blockCount = freshAtEnd.length;
|
|
366
|
+
const all = () => new Set(argRegs);
|
|
367
|
+
const localEnd = (b: number) => freshAtEnd[b] ?? new Set<string>();
|
|
368
|
+
// freshOut[b]: registers fresh where b ends. A block that calls forgets everything before its
|
|
369
|
+
// last call; one that does not passes its input through, plus what it wrote.
|
|
370
|
+
const freshOut: Set<string>[] = Array.from({ length: blockCount }, () => all());
|
|
371
|
+
const freshIn: Set<string>[] = Array.from({ length: blockCount }, () => all());
|
|
372
|
+
const inOf = (b: number): Set<string> => {
|
|
373
|
+
// A block with NO predecessors is the function entry (or unreachable): its argument registers
|
|
374
|
+
// are the ones the caller set up. An entry that DOES have predecessors — an entry that is also
|
|
375
|
+
// a loop header — gets the ordinary intersection instead, because on the back edge the caller's
|
|
376
|
+
// setup is long gone and an intervening call may have clobbered it.
|
|
377
|
+
const ps = [...new Set(preds[b] ?? [])];
|
|
378
|
+
if (ps.length === 0) {
|
|
379
|
+
return all();
|
|
380
|
+
}
|
|
381
|
+
const acc = new Set(freshOut[ps[0]]);
|
|
382
|
+
for (const p of ps.slice(1)) {
|
|
383
|
+
for (const r of [...acc]) {
|
|
384
|
+
if (!freshOut[p].has(r)) {
|
|
385
|
+
acc.delete(r);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return acc;
|
|
390
|
+
};
|
|
391
|
+
for (let changed = true; changed;) {
|
|
392
|
+
changed = false;
|
|
393
|
+
for (let b = 0; b < blockCount; b++) {
|
|
394
|
+
const fin = inOf(b);
|
|
395
|
+
const fout = callsIn.has(b) ? localEnd(b) : new Set([...fin, ...localEnd(b)]);
|
|
396
|
+
if (fout.size !== freshOut[b].size || [...fout].some((r) => !freshOut[b].has(r))) {
|
|
397
|
+
changed = true;
|
|
398
|
+
}
|
|
399
|
+
freshIn[b] = fin;
|
|
400
|
+
freshOut[b] = fout;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
for (const s of sites) {
|
|
404
|
+
const fresh = s.afterCallInBlock ? s.freshBefore : new Set([...freshIn[s.block], ...s.freshBefore]);
|
|
405
|
+
let n = 0;
|
|
406
|
+
while (n < argRegs.length && fresh.has(argRegs[n])) {
|
|
407
|
+
n++;
|
|
408
|
+
}
|
|
409
|
+
if (n < s.op.operands.length) {
|
|
410
|
+
s.op.operands.length = n;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** 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 `assertNoSlotEscaped` can
|
|
417
|
+
* recognise either frontend's slots. See the virtual-key note in the module header. */
|
|
418
|
+
const SLOT_PREFIX = 'sp@';
|
|
419
|
+
export const stackSlotKey = (off: number): string => `${SLOT_PREFIX}${off}`;
|
|
420
|
+
|
|
177
421
|
/** Order the TRUE entry block's parameters by ABI argument register, so downstream naming
|
|
178
422
|
* (`a0`, `a1`, …) matches the calling convention, not first-read order (a callee-saved copy can
|
|
179
423
|
* read a later argument register first). No-op when the entry has predecessors — a loop
|