@asmlift/core 0.3.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/README.md +5 -3
- package/package.json +1 -1
- package/src/backend/cfamily.ts +130 -4
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +181 -4
- package/src/declare.ts +35 -9
- package/src/frontend/mips.ts +37 -29
- package/src/frontend/opaque.ts +70 -20
- package/src/frontend/ppc.ts +18 -7
- package/src/frontend/ssa.ts +279 -56
- package/src/frontend/thumb.ts +1372 -87
- package/src/ir/alias.ts +75 -0
- package/src/ir/opcodes.ts +57 -3
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +221 -0
- package/src/l3/ast.ts +127 -5
- package/src/l3/basecse.ts +58 -62
- package/src/l3/coalesce.ts +215 -0
- package/src/l3/dce.ts +33 -41
- package/src/l3/gates.ts +67 -0
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +440 -0
- package/src/l3/tailmerge.ts +124 -0
- package/src/macros.ts +222 -13
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +65 -6
- package/src/raise/divpow2.ts +227 -0
- package/src/raise/gvn.ts +151 -0
- package/src/raise/pre-recovery.ts +39 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +37 -7
- package/src/raise/shortcircuit.ts +262 -22
- package/src/raise/struct-arrays.ts +2 -1
- package/src/raise/structs.ts +41 -3
- package/src/rank.ts +196 -20
- package/src/structure/analysis.ts +175 -89
- package/src/structure/structure.ts +588 -55
- package/src/structure/switch-recover.ts +117 -30
- package/src/symbols.ts +128 -13
- package/src/target.ts +4 -2
- package/src/trace.ts +9 -0
package/src/frontend/ssa.ts
CHANGED
|
@@ -3,12 +3,22 @@
|
|
|
3
3
|
// CFG (predecessors per block) and, per block, emits ops through `readVar`/`writeVar`; this
|
|
4
4
|
// module materialises block-argument phis at joins and back-edges.
|
|
5
5
|
//
|
|
6
|
+
// `preds` is an EDGE list, not a block list: it carries one entry per CFG edge, so a `switch_br`
|
|
7
|
+
// with several case values reaching one block appears there several times. Both readings are
|
|
8
|
+
// needed and they are not interchangeable — phi wiring wants the distinct predecessor BLOCKS (one
|
|
9
|
+
// value each), while the args it appends belong to the EDGES (every one of them). `distinctPreds`
|
|
10
|
+
// names the first; `appendSuccessorArg` walks the second. (ir/core.ts `predecessors` and
|
|
11
|
+
// structure.ts `predecessorBlocks` have the same duality, and structure.ts already dedups ad hoc
|
|
12
|
+
// at its two join sites.)
|
|
13
|
+
//
|
|
6
14
|
// Protocol: create the builder, then fill blocks in index order. For each block, emit its
|
|
7
15
|
// computation via read/writeVar, push its terminator op last (successors referencing
|
|
8
16
|
// `irBlocks`, args left empty — phi wiring appends them), then call `markFilled(b)`. When all
|
|
9
17
|
// blocks are filled, call `finish()` to remove trivial phis.
|
|
10
|
-
import { Block, Fn,
|
|
18
|
+
import { Block, Fn, Op, Value, mkValue } from '../ir/core';
|
|
19
|
+
import { simplifyTrivialPhis } from '../ir/simplify';
|
|
11
20
|
import { T } from '../ir/types';
|
|
21
|
+
import { FrontendUnsupportedError } from './errors';
|
|
12
22
|
|
|
13
23
|
export interface SsaBuilder {
|
|
14
24
|
fn: Fn;
|
|
@@ -19,14 +29,44 @@ export interface SsaBuilder {
|
|
|
19
29
|
writeVar(reg: string, b: number, v: Value): void;
|
|
20
30
|
/** Mark block `b` fully emitted (terminator pushed); seals any now-ready successors. */
|
|
21
31
|
markFilled(b: number): void;
|
|
22
|
-
/** 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. */
|
|
23
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;
|
|
24
48
|
/** Whether `reg` has a definition reaching block `b` (best-effort call-arity heuristic). */
|
|
25
49
|
hasReachingDef(reg: string, b: number, seen?: Set<number>): boolean;
|
|
26
|
-
/**
|
|
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. */
|
|
27
60
|
finish(): void;
|
|
28
61
|
}
|
|
29
62
|
|
|
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.
|
|
30
70
|
export function makeSsaBuilder(name: string, blockCount: number, preds: number[][]): SsaBuilder {
|
|
31
71
|
const irBlocks: Block[] = Array.from({ length: blockCount }, () => ({ params: [] as Value[], ops: [] }));
|
|
32
72
|
const fn: Fn = { name, blocks: irBlocks };
|
|
@@ -36,15 +76,43 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
36
76
|
const filled: boolean[] = irBlocks.map(() => false);
|
|
37
77
|
const incompletePhis: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
38
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>();
|
|
39
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());
|
|
92
|
+
|
|
93
|
+
// `preds` lists an entry per CFG EDGE; these are the distinct predecessor BLOCKS.
|
|
94
|
+
const distinctPreds = (b: number): number[] => [...new Set(preds[b])];
|
|
40
95
|
|
|
41
|
-
|
|
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
|
+
};
|
|
42
109
|
const readVar = (reg: string, b: number): Value => defs[b].get(reg) ?? readRecursive(reg, b);
|
|
43
110
|
|
|
44
111
|
const newPhi = (reg: string, b: number): Value => {
|
|
45
112
|
const phi = mkValue(T.unk(32));
|
|
46
113
|
irBlocks[b].params.push(phi);
|
|
47
114
|
phiBlock.set(phi, b);
|
|
115
|
+
phiKey.set(phi, reg);
|
|
48
116
|
defs[b].set(reg, phi); // set before wiring operands to break cycles
|
|
49
117
|
return phi;
|
|
50
118
|
};
|
|
@@ -55,9 +123,20 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
55
123
|
incompletePhis[b].set(reg, phi);
|
|
56
124
|
return phi;
|
|
57
125
|
}
|
|
58
|
-
|
|
126
|
+
// DISTINCT predecessor blocks: a switch_br reaching this block on several case values is one
|
|
127
|
+
// predecessor with several edges, and it supplies ONE value — counting the edges instead would
|
|
128
|
+
// manufacture a join (and a phi) where there is none.
|
|
129
|
+
const ps = distinctPreds(b);
|
|
59
130
|
if (ps.length === 0) {
|
|
60
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
|
+
}
|
|
61
140
|
const p = mkValue(T.unk(32));
|
|
62
141
|
irBlocks[b].params.push(p);
|
|
63
142
|
defs[b].set(reg, p);
|
|
@@ -75,16 +154,24 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
75
154
|
return phi;
|
|
76
155
|
};
|
|
77
156
|
const addPhiOperands = (reg: string, b: number) => {
|
|
78
|
-
for (const p of
|
|
157
|
+
for (const p of distinctPreds(b)) {
|
|
79
158
|
appendSuccessorArg(p, b, readVar(reg, p));
|
|
80
159
|
}
|
|
81
160
|
};
|
|
82
|
-
// Append `arg` to predecessor p
|
|
161
|
+
// Append `arg` to EVERY successor edge of predecessor p that targets block b.
|
|
162
|
+
//
|
|
163
|
+
// A predecessor normally has one edge to a given successor, but a `switch_br` has as many as it
|
|
164
|
+
// has case values, and two cases sharing a body (`case 1: case 2:`) is ordinary C. Block args
|
|
165
|
+
// belong to the EDGE, so each of those edges needs its own copy: appending to just the first (a
|
|
166
|
+
// `find`) left the others short, while `preds` listing the block once per edge made the loop run
|
|
167
|
+
// k times and pile k copies onto that same first edge. Both halves of that — every edge, once per
|
|
168
|
+
// predecessor BLOCK — have to hold together, which is why they are fixed in one place.
|
|
83
169
|
const appendSuccessorArg = (p: number, b: number, arg: Value) => {
|
|
84
170
|
const term = irBlocks[p].ops[irBlocks[p].ops.length - 1];
|
|
85
|
-
const s
|
|
86
|
-
|
|
87
|
-
|
|
171
|
+
for (const s of term.successors) {
|
|
172
|
+
if (s.block === irBlocks[b]) {
|
|
173
|
+
s.args.push(arg);
|
|
174
|
+
}
|
|
88
175
|
}
|
|
89
176
|
};
|
|
90
177
|
const sealBlock = (b: number) => {
|
|
@@ -106,6 +193,25 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
106
193
|
};
|
|
107
194
|
sealReadyBlocks(); // seals the entry (no predecessors) up front
|
|
108
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
|
+
|
|
109
215
|
const hasReachingDef = (reg: string, b: number, seen = new Set<number>()): boolean => {
|
|
110
216
|
if (defs[b].has(reg)) {
|
|
111
217
|
return true;
|
|
@@ -123,12 +229,71 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
123
229
|
readVar,
|
|
124
230
|
writeVar,
|
|
125
231
|
paramReg,
|
|
232
|
+
ensureParam,
|
|
126
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
|
+
},
|
|
127
247
|
markFilled: (b: number) => {
|
|
128
248
|
filled[b] = true;
|
|
129
249
|
sealReadyBlocks();
|
|
130
250
|
},
|
|
131
|
-
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
|
+
},
|
|
132
297
|
};
|
|
133
298
|
}
|
|
134
299
|
|
|
@@ -150,6 +315,109 @@ export function fallbackArgc(
|
|
|
150
315
|
return n;
|
|
151
316
|
}
|
|
152
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
|
+
|
|
153
421
|
/** Order the TRUE entry block's parameters by ABI argument register, so downstream naming
|
|
154
422
|
* (`a0`, `a1`, …) matches the calling convention, not first-read order (a callee-saved copy can
|
|
155
423
|
* read a later argument register first). No-op when the entry has predecessors — a loop
|
|
@@ -167,48 +435,3 @@ export function abiSortEntryParams(
|
|
|
167
435
|
}
|
|
168
436
|
entry.params.sort((x, y) => rank(x) - rank(y));
|
|
169
437
|
}
|
|
170
|
-
|
|
171
|
-
// Remove block-parameters that are really trivial phis: those whose incoming operands (across
|
|
172
|
-
// every predecessor edge, ignoring self-references from a back-edge) are all the same single
|
|
173
|
-
// value. Such a parameter carries no join information — a loop-invariant register or a value
|
|
174
|
-
// defined before the join — so it is replaced by that value and the corresponding argument
|
|
175
|
-
// dropped from each predecessor's terminator. Iterated to fixpoint because removing one phi
|
|
176
|
-
// can make another trivial.
|
|
177
|
-
function simplifyTrivialPhis(fn: Fn, phiBlock: Map<Value, number>): void {
|
|
178
|
-
const edgesTo = (b: Block): Successor[] => {
|
|
179
|
-
const out: Successor[] = [];
|
|
180
|
-
for (const pb of fn.blocks) {
|
|
181
|
-
for (const op of pb.ops) {
|
|
182
|
-
for (const s of op.successors) {
|
|
183
|
-
if (s.block === b) {
|
|
184
|
-
out.push(s);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return out;
|
|
190
|
-
};
|
|
191
|
-
let changed = true;
|
|
192
|
-
while (changed) {
|
|
193
|
-
changed = false;
|
|
194
|
-
for (const b of fn.blocks) {
|
|
195
|
-
const incoming = edgesTo(b);
|
|
196
|
-
for (let i = b.params.length - 1; i >= 0; i--) {
|
|
197
|
-
const param = b.params[i];
|
|
198
|
-
const operands = incoming.map((s) => s.args[i]);
|
|
199
|
-
const distinct = [...new Set(operands.filter((v) => v !== param))];
|
|
200
|
-
if (distinct.length !== 1) {
|
|
201
|
-
continue;
|
|
202
|
-
} // a genuine join (or unreachable) — keep it
|
|
203
|
-
const v = distinct[0];
|
|
204
|
-
replaceAllUsesWith(fn, param, v);
|
|
205
|
-
b.params.splice(i, 1);
|
|
206
|
-
for (const s of incoming) {
|
|
207
|
-
s.args.splice(i, 1);
|
|
208
|
-
}
|
|
209
|
-
phiBlock.delete(param);
|
|
210
|
-
changed = true;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|