@asmlift/core 0.8.0 → 0.9.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 +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +276 -80
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- package/src/variation-tokens.ts +20 -6
package/src/frontend/ssa.ts
CHANGED
|
@@ -23,19 +23,46 @@ import {
|
|
|
23
23
|
type SlotHomes,
|
|
24
24
|
Value,
|
|
25
25
|
type WriteOrder,
|
|
26
|
+
callArgs,
|
|
26
27
|
defOpMap,
|
|
27
28
|
mkOp,
|
|
28
29
|
mkValue,
|
|
30
|
+
terminator,
|
|
31
|
+
truncateCallArgs,
|
|
29
32
|
} from '../ir/core';
|
|
30
33
|
import { pruneDeadParams, simplifyTrivialPhis } from '../ir/simplify';
|
|
31
|
-
import { T } from '../ir/types';
|
|
32
|
-
import { FrontendUnsupportedError } from './errors';
|
|
34
|
+
import { type IrType, T } from '../ir/types';
|
|
35
|
+
import { type FrontendRefusal, FrontendUnsupportedError } from './errors';
|
|
33
36
|
|
|
34
37
|
export interface SsaBuilder {
|
|
35
38
|
fn: Fn;
|
|
36
39
|
irBlocks: Block[];
|
|
37
40
|
/** Current SSA value of `reg` on entry to block `b` (creating phis/params as needed). */
|
|
38
41
|
readVar(reg: string, b: number): Value;
|
|
42
|
+
/** Read `reg` as an argument register of a call whose arity was GUESSED — otherwise identical to
|
|
43
|
+
* {@link readVar}, and used INSTEAD of it so the read carries no claim.
|
|
44
|
+
*
|
|
45
|
+
* `fallbackArgc` counts argument registers by reaching definition, so a read here is a QUESTION
|
|
46
|
+
* ("did the caller set this up?") rather than an assertion, and {@link trimClobberedCallArgs} is
|
|
47
|
+
* what answers it. That answer already covers a register a call destroyed, and it covers it
|
|
48
|
+
* EXHAUSTIVELY: destroyed on any path means not written-since-the-call on that path, so the
|
|
49
|
+
* must-analysis drops it from the run and the operand goes — bar an argument register the ABI
|
|
50
|
+
* ALIASES onto the return register, which a call that writes its result there does not list as
|
|
51
|
+
* destroyed; one that writes none there does, and the trim reads that too. So none of these
|
|
52
|
+
* reads can leave a destroyed value in the graph, and refusing the function over one would cost
|
|
53
|
+
* a row the trim has already made correct.
|
|
54
|
+
*
|
|
55
|
+
* THAT ALIASING IS A PER-TARGET FACT AND TWO TARGETS HERE DO NOT HAVE IT. ARM and PowerPC both
|
|
56
|
+
* pass argument 0 in the return register (r0/r0, r3/r3), so on them the exemption is the whole
|
|
57
|
+
* of the gap. MIPS returns in `v0` and passes in `a0`, so `clobberedByCall` lists `a0` for both
|
|
58
|
+
* MIPS targets and there is no exemption to reason about — which is the sound direction, not a
|
|
59
|
+
* hole.
|
|
60
|
+
*
|
|
61
|
+
* A DECLARED arity uses `readVar`, and must: there the callee says the argument exists, so
|
|
62
|
+
* reading a destroyed register for it is a wrong value with nothing to retract it.
|
|
63
|
+
*
|
|
64
|
+
* One of the three call verbs `CallDeclarations.lower` owns ({@link noteCall}). */
|
|
65
|
+
readGuessedArg(reg: string, b: number): Value;
|
|
39
66
|
/** Record that `reg` now holds `v` within block `b`. */
|
|
40
67
|
writeVar(reg: string, b: number, v: Value): void;
|
|
41
68
|
/** Mark block `b` fully emitted (terminator pushed); seals any now-ready successors. */
|
|
@@ -63,15 +90,47 @@ export interface SsaBuilder {
|
|
|
63
90
|
* "a def reaches here" and "a value reaches here" are the same question only when every def is
|
|
64
91
|
* a value. */
|
|
65
92
|
hasReachingDef(reg: string, b: number, accept?: (v: Value) => boolean): boolean;
|
|
93
|
+
/** {@link hasReachingDef}, with a path ending where a call destroyed `reg` (`noteCall`): whether a
|
|
94
|
+
* value this function named reaches it here on some path. A path that brings the destroyed bytes
|
|
95
|
+
* and one that brings a value still answer yes, and the read that follows refuses at `finish`. */
|
|
96
|
+
holdsValue(reg: string, b: number, accept?: (v: Value) => boolean): boolean;
|
|
97
|
+
/** The entry parameter `reg` still holds at this point of block `b`, taken WITHOUT a read. A read
|
|
98
|
+
* leaves a definition behind, and `hasReachingDef` counts it as argument setup, so a frontend that
|
|
99
|
+
* only moves the argument somewhere (a frame store) would raise the guessed arity of every later
|
|
100
|
+
* prototype-less call. `undefined` where "still the entry value" is not shown: a definition or a
|
|
101
|
+
* call reaches from some path, or a block on the way is not filled yet. */
|
|
102
|
+
entryValue(reg: string, b: number): Value | undefined;
|
|
103
|
+
/** Whether `reg` still holds its entry value at this point of block `b`, in {@link entryValue}'s
|
|
104
|
+
* sense, answered without the entry parameter that one hands back. A callee-saved register stored
|
|
105
|
+
* to its save slot holds the caller's value, which is no parameter, so asking must not mint one. */
|
|
106
|
+
holdsEntryValue(reg: string, b: number): boolean;
|
|
66
107
|
/** Record that block `b` makes a call HERE: the ABI's caller-saved registers stop being ones the
|
|
67
|
-
* caller set up.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
|
|
108
|
+
* caller set up.
|
|
109
|
+
*
|
|
110
|
+
* THE CALL VERBS ARE `CallDeclarations.lower`'s (frontend/call-plan.ts): a frontend lowers a
|
|
111
|
+
* call through it, and it alone calls this, {@link recordGuessedCall} and {@link readGuessedArg},
|
|
112
|
+
* in one order — `recordGuessedCall`, then the call's own result, then this. The result is the
|
|
113
|
+
* CALLEE's, so it must not count as caller-side argument setup for whatever call comes next.
|
|
114
|
+
*
|
|
115
|
+
* `clobbers` are the registers the callee DESTROYS and leaves holding nothing this function can
|
|
116
|
+
* name — {@link clobberedByCall} spells it, and it is the ABI's caller-saved set minus the
|
|
117
|
+
* return register precisely because of the ordering above: the callee's own result is already
|
|
118
|
+
* written there, so that one register does have a name. A callee that hands a STRUCT back in
|
|
119
|
+
* that register writes no value a caller may read as a word, so none is written and the
|
|
120
|
+
* register is listed here too. Required rather than optional: a call lowered without it would
|
|
121
|
+
* keep resolving a destroyed register to its pre-call value, silently. */
|
|
122
|
+
noteCall(b: number, clobbers: readonly string[]): void;
|
|
71
123
|
/** Register a `call` op whose arity was GUESSED (no prototype), so `finish` can cut it back to the
|
|
72
124
|
* argument registers that were actually set up on every path (see {@link trimClobberedCallArgs}).
|
|
73
|
-
* `abi` is the target's argument-register order and its return register.
|
|
74
|
-
|
|
125
|
+
* `abi` is the target's argument-register order and its return register. It snapshots what the
|
|
126
|
+
* caller wrote, so it comes before the call's result is written ({@link noteCall}).
|
|
127
|
+
* `passesCalleeResult`: see {@link GuessedCallSite}. */
|
|
128
|
+
recordGuessedCall(
|
|
129
|
+
op: Op,
|
|
130
|
+
b: number,
|
|
131
|
+
abi: { argRegs: string[]; returnReg: string },
|
|
132
|
+
passesCalleeResult?: (result: Value) => boolean,
|
|
133
|
+
): void;
|
|
75
134
|
/** Remove trivial phis and enforce the frontend's postconditions; call once every block is
|
|
76
135
|
* filled. Throws FrontendUnsupportedError if a stack slot escaped as an entry parameter. */
|
|
77
136
|
finish(): void;
|
|
@@ -95,7 +154,7 @@ export interface LiveInModel {
|
|
|
95
154
|
*
|
|
96
155
|
* Asserts more than ownership: that this function's own stores are the ONLY writer. An address
|
|
97
156
|
* into the frame that escapes to anything which could write it stops that holding, and the
|
|
98
|
-
* retraction is the frontend's obligation (
|
|
157
|
+
* retraction is the frontend's obligation (the frame-object audit, frontend/frame-objects.ts). */
|
|
99
158
|
ownedLocals?: { from: number; to: number };
|
|
100
159
|
/** Storage this function DECLARES as locals ⇒ a `[sp,#k]` spill here is a DECLARATION RANK
|
|
101
160
|
* (`ir/core.ts` `SlotHomes`, ordered by `l3/slotorder.ts`). `[from, to)`.
|
|
@@ -194,15 +253,56 @@ function checkedLiveInModel(fnName: string, m: LiveInModel): LiveInModel {
|
|
|
194
253
|
return m;
|
|
195
254
|
}
|
|
196
255
|
|
|
256
|
+
/** A read whose register may be holding a callee's leftover — see `refuseStaleCallerSavedReads`. */
|
|
257
|
+
interface StaleRead {
|
|
258
|
+
reg: string;
|
|
259
|
+
value: Value;
|
|
260
|
+
block: number;
|
|
261
|
+
/** the block's op count when the read happened. The uses this read answers for start here: an op
|
|
262
|
+
* before it consumed a value read while the register was still this function's own, and the same
|
|
263
|
+
* register really can be read on both sides of a call without its value changing. */
|
|
264
|
+
fromOp: number;
|
|
265
|
+
/** a call in this very block destroyed it, so the predecessors have nothing to add. */
|
|
266
|
+
local: boolean;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The registers a call leaves holding nothing the CALLER can name — what {@link SsaBuilder.noteCall}
|
|
270
|
+
* wants. The ABI's caller-saved set minus the return register: `CallDeclarations.lower` writes the
|
|
271
|
+
* callee's own result there before recording the clobber, so that one register does have a name
|
|
272
|
+
* and the rest do not.
|
|
273
|
+
*
|
|
274
|
+
* CHECKED rather than trusted, the way `checkedLiveInModel` checks the register partition: a
|
|
275
|
+
* register the caller passes arguments in is by construction one the callee may destroy, so
|
|
276
|
+
* `argRegs` must be a subset of `callerSaved`. A target spelling one of them differently in the two
|
|
277
|
+
* lists would silently stop refusing reads of it. */
|
|
278
|
+
export function clobberedByCall(target: {
|
|
279
|
+
callerSaved: readonly string[];
|
|
280
|
+
argRegs: readonly string[];
|
|
281
|
+
returnReg: string;
|
|
282
|
+
}): readonly string[] {
|
|
283
|
+
const missing = target.argRegs.filter((r) => !target.callerSaved.includes(r));
|
|
284
|
+
if (missing.length > 0) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`this target passes arguments in ${missing.join(', ')} but does not list ${missing.length > 1 ? 'them' : 'it'} ` +
|
|
287
|
+
`as caller-saved, so a read of one past a call cannot be refused`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return target.callerSaved.filter((r) => r !== target.returnReg);
|
|
291
|
+
}
|
|
292
|
+
|
|
197
293
|
export function makeSsaBuilder(
|
|
198
294
|
name: string,
|
|
199
295
|
blockCount: number,
|
|
200
296
|
preds: number[][],
|
|
201
|
-
/**
|
|
202
|
-
* comes from a prologue walk that runs after this call. Evaluated once, on first use. Omitted ⇒
|
|
297
|
+
/** Evaluated once, at its first use, which is where `checkedLiveInModel` checks it. Omitted ⇒
|
|
203
298
|
* no partition is claimed, so every slot refuses and every register is a parameter. */
|
|
204
299
|
liveInOf: () => LiveInModel = () => ({}),
|
|
300
|
+
/** The type of a value that stands for `key` before any instruction defines it — a parameter, a
|
|
301
|
+
* phi. A register FILE, not a register: the FPU's keys are floats wherever they arrive, and every
|
|
302
|
+
* other key is `unknown` for type recovery to settle. */
|
|
303
|
+
keyType: (key: string) => IrType | undefined = () => undefined,
|
|
205
304
|
): SsaBuilder {
|
|
305
|
+
const unset = (key: string): IrType => keyType(key) ?? T.unk(32);
|
|
206
306
|
let modelMemo: LiveInModel | null = null;
|
|
207
307
|
// Checked ONCE, where the model is materialised — every function with a parameter reads a
|
|
208
308
|
// register def-lessly, so this runs on effectively every lift rather than only on the rare
|
|
@@ -213,7 +313,14 @@ export function makeSsaBuilder(
|
|
|
213
313
|
const inRange = (off: number, r?: { from: number; to: number }) => r !== undefined && off >= r.from && off < r.to;
|
|
214
314
|
const irBlocks: Block[] = Array.from({ length: blockCount }, () => ({ params: [] as Value[], ops: [] }));
|
|
215
315
|
// `writeOrder` and `slotHomes` are filled in below, where the builder's counters live.
|
|
216
|
-
const fn: Fn = {
|
|
316
|
+
const fn: Fn = {
|
|
317
|
+
name,
|
|
318
|
+
blocks: irBlocks,
|
|
319
|
+
writeOrder: undefined,
|
|
320
|
+
slotHomes: undefined,
|
|
321
|
+
paramEvidence: undefined,
|
|
322
|
+
localObjects: undefined,
|
|
323
|
+
};
|
|
217
324
|
|
|
218
325
|
const defs: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
219
326
|
const sealed: boolean[] = irBlocks.map(() => false);
|
|
@@ -243,6 +350,25 @@ export function makeSsaBuilder(
|
|
|
243
350
|
// wrapper, and a MISSED write under-counts an arity — which drops a real argument silently.
|
|
244
351
|
const writtenSinceCall: Array<Set<string>> = irBlocks.map(() => new Set());
|
|
245
352
|
const callsIn = new Set<number>();
|
|
353
|
+
// WHAT A CALL DESTROYED, which is a DIFFERENT question from the one above and not its complement.
|
|
354
|
+
// `writtenSinceCall` asks "did the CALLER set this register up" — a MUST question, whose answer
|
|
355
|
+
// for the return register is deliberately no, because `CallDeclarations.lower` writes the
|
|
356
|
+
// callee's result there before recording the clobber. This asks "does this register still hold a
|
|
357
|
+
// value anyone can name" — a MAY question, whose answer for that same register is yes. One
|
|
358
|
+
// analysis serving both would have to be wrong about one of them, so they are two, and each names
|
|
359
|
+
// the other.
|
|
360
|
+
//
|
|
361
|
+
// `clobberedLocal[b]`: destroyed by a call in `b` with nothing written since. `decidedLocal[b]`:
|
|
362
|
+
// registers `b` has settled either way, so a register in neither inherits its fate from the
|
|
363
|
+
// predecessors.
|
|
364
|
+
const clobberedLocal: Array<Set<string>> = irBlocks.map(() => new Set());
|
|
365
|
+
const decidedLocal: Array<Set<string>> = irBlocks.map(() => new Set());
|
|
366
|
+
const staleCandidates: StaleRead[] = [];
|
|
367
|
+
// `readVar` runs once per operand, so a memo per (register, verdict) keeps the record list
|
|
368
|
+
// proportional to what a block READS rather than to how many times it reads it. The value a key
|
|
369
|
+
// holds only changes at a write, and the verdict only flips at a call, so a run of identical
|
|
370
|
+
// reads collapses to the earliest — which is the one whose uses cover the rest.
|
|
371
|
+
const staleSeen: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
246
372
|
const guessedCalls: GuessedCallSite[] = [];
|
|
247
373
|
let abiSeen: { argRegs: string[]; returnReg: string } = { argRegs: [], returnReg: '' };
|
|
248
374
|
|
|
@@ -256,10 +382,11 @@ export function makeSsaBuilder(
|
|
|
256
382
|
fn.writeOrder = writeOrder;
|
|
257
383
|
|
|
258
384
|
// SLOT HOMES (ir/core.ts `SlotHomes`). Measured HERE, in the shared builder, for the same
|
|
259
|
-
// reason the clobber set and the write order are:
|
|
260
|
-
// a write to the key `sp@k` (`stackSlotKey`, below), so the
|
|
261
|
-
// and one rule applies it — a per-frontend stamp would be right
|
|
262
|
-
// route every slot write past a wrapper, and a missed write is a
|
|
385
|
+
// reason the clobber set and the write order are: every slot-modelling frontend (MIPS, Thumb,
|
|
386
|
+
// PowerPC) spells a word spill as a write to the key `sp@k` (`stackSlotKey`, below), so the
|
|
387
|
+
// frontend supplies the coordinate and one rule applies it — a per-frontend stamp would be right
|
|
388
|
+
// only while each remembered to route every slot write past a wrapper, and a missed write is a
|
|
389
|
+
// local with no frame order.
|
|
263
390
|
// Empty rather than absent on a function that spills nothing: this builder measured it.
|
|
264
391
|
const slotHomes: SlotHomes = new Map();
|
|
265
392
|
fn.slotHomes = slotHomes;
|
|
@@ -270,7 +397,7 @@ export function makeSsaBuilder(
|
|
|
270
397
|
}
|
|
271
398
|
// THE KEY SPELLING CANNOT DECIDE THIS, exactly as `readRecursive` says below of a def-less
|
|
272
399
|
// read: `sp@40` is a local on one ABI and the caller's fifth argument on another. So the stamp
|
|
273
|
-
// asks the frontend for a partition and refuses where no answer exists. The
|
|
400
|
+
// asks the frontend for a partition and refuses where no answer exists. The frontends
|
|
274
401
|
// differ here and the refusal is what makes that safe: Thumb declares a range; MIPS declares NO
|
|
275
402
|
// partition (frontend/mips.ts: `addiu sp,sp,±N` is transparent, so its slot keys span O32's
|
|
276
403
|
// caller-owned register-parameter home area `[0,16)` and the incoming stack arguments above
|
|
@@ -306,6 +433,16 @@ export function makeSsaBuilder(
|
|
|
306
433
|
// separately declared SCALARS; intra-aggregate offsets are fixed by the aggregate's layout at
|
|
307
434
|
// expand time.
|
|
308
435
|
//
|
|
436
|
+
// A FORM OF THAT RECOVERY EXISTS, and what keeps the condition unmet is two guards, this
|
|
437
|
+
// function's and one in another file. The frame-object audit declares an untyped frame object
|
|
438
|
+
// as `u8 name[N]` over the DECLARED range (`notTheWholeArea`, frontend/frame-objects.ts) and
|
|
439
|
+
// refuses it where the slot model keys any byte of that range (`overSlot`, refused by
|
|
440
|
+
// `failIfSlotKeysIt`). The slots it does share a frame with lie below `declaredLocals` — the
|
|
441
|
+
// licensed outgoing argument words — and the `declaredLocals` test above stamps none of them.
|
|
442
|
+
// So no slot home sits beside one of those arrays and nothing here is ever asked to order the
|
|
443
|
+
// two. Letting a keyed slot into the declared range beside the array is what meets the
|
|
444
|
+
// condition, and it has to bring `l3/slotorder.ts` with it.
|
|
445
|
+
//
|
|
309
446
|
// UNION, not a choice (ir/core.ts `SlotHomes`): whether the earlier declaration rank is the
|
|
310
447
|
// lower or the higher offset is a per-COMPILER fact, and this builder is handed a name, a
|
|
311
448
|
// block count, a predecessor list and a live-in model — no target. `l3/slotorder.ts` reduces.
|
|
@@ -320,7 +457,7 @@ export function makeSsaBuilder(
|
|
|
320
457
|
};
|
|
321
458
|
// PARAMETER EVIDENCE (ir/core.ts `ParamEvidence`), measured HERE for the same reason the clobber
|
|
322
459
|
// set, the write order and the slot homes are: a slot write is a `writeVar` and a slot read is a
|
|
323
|
-
// `readVar` in
|
|
460
|
+
// `readVar` in every slot-modelling frontend, so one rule covers them and no frontend can forget
|
|
324
461
|
// to route a store past a wrapper. The two directions are NOT symmetric: raise/paramwidth.ts reads
|
|
325
462
|
// an absent observation as proof the declaration was wide, so a missed one costs a narrowing while
|
|
326
463
|
// a spurious one retypes a parameter the machine never declared narrow.
|
|
@@ -369,16 +506,42 @@ export function makeSsaBuilder(
|
|
|
369
506
|
firstEntryWrite.set(reg, v);
|
|
370
507
|
}
|
|
371
508
|
writtenSinceCall[b].add(reg);
|
|
509
|
+
clobberedLocal[b].delete(reg);
|
|
510
|
+
decidedLocal[b].add(reg);
|
|
372
511
|
defs[b].set(reg, v);
|
|
373
512
|
lastWriteAt[b].set(reg, writeCount[b]++);
|
|
374
513
|
};
|
|
375
514
|
const readVar = (reg: string, b: number): Value => {
|
|
515
|
+
const v = readAny(reg, b);
|
|
516
|
+
noteStaleCandidate(reg, b, v);
|
|
517
|
+
return v;
|
|
518
|
+
};
|
|
519
|
+
/** The lookup, with no claim attached. The FRONTEND's read is what asserts "this register holds
|
|
520
|
+
* this value here"; the lookups this construction makes on its own — a single predecessor's
|
|
521
|
+
* definition, a phi's operand at each in-edge — are how that one read is answered, not further
|
|
522
|
+
* reads to be judged. Recording them would blame a use in one block on a read in another. */
|
|
523
|
+
const readAny = (reg: string, b: number): Value => {
|
|
376
524
|
noteSlotTraffic(reg, null);
|
|
377
525
|
return defs[b].get(reg) ?? readRecursive(reg, b);
|
|
378
526
|
};
|
|
527
|
+
/** Record a read whose register MIGHT be holding a callee's leftover, for `finish` to judge. A
|
|
528
|
+
* register this block has already settled answers here and needs no record; one it has not takes
|
|
529
|
+
* its answer from the predecessors, which are not all filled yet. */
|
|
530
|
+
const noteStaleCandidate = (reg: string, b: number, v: Value) => {
|
|
531
|
+
const local = clobberedLocal[b].has(reg);
|
|
532
|
+
if (decidedLocal[b].has(reg) && !local) {
|
|
533
|
+
return; // this function wrote it after the last call in this block: it is its own value
|
|
534
|
+
}
|
|
535
|
+
const key = `${reg}|${local ? 1 : 0}`;
|
|
536
|
+
if (staleSeen[b].get(key) === v) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
staleSeen[b].set(key, v);
|
|
540
|
+
staleCandidates.push({ reg, value: v, block: b, fromOp: irBlocks[b].ops.length, local });
|
|
541
|
+
};
|
|
379
542
|
|
|
380
543
|
const newPhi = (reg: string, b: number): Value => {
|
|
381
|
-
const phi = mkValue(
|
|
544
|
+
const phi = mkValue(unset(reg));
|
|
382
545
|
irBlocks[b].params.push(phi);
|
|
383
546
|
phiBlock.set(phi, b);
|
|
384
547
|
phiKey.set(phi, reg);
|
|
@@ -389,6 +552,9 @@ export function makeSsaBuilder(
|
|
|
389
552
|
defs[b].set(reg, phi); // set before wiring operands to break cycles
|
|
390
553
|
return phi;
|
|
391
554
|
};
|
|
555
|
+
// The single-predecessor blocks the current read is walking up through. A join ends the walk:
|
|
556
|
+
// its phi is in `defs` before its operands are read, so a cycle through it is already cut there.
|
|
557
|
+
let singlePredWalk = new Set<string>();
|
|
392
558
|
const readRecursive = (reg: string, b: number): Value => {
|
|
393
559
|
if (!sealed[b]) {
|
|
394
560
|
// predecessors not all filled yet (e.g. a loop back-edge): defer operand wiring.
|
|
@@ -432,16 +598,38 @@ export function makeSsaBuilder(
|
|
|
432
598
|
defs[b].set(reg, obliged);
|
|
433
599
|
return obliged;
|
|
434
600
|
}
|
|
435
|
-
const p = mkValue(
|
|
601
|
+
const p = mkValue(unset(reg));
|
|
436
602
|
irBlocks[b].params.push(p);
|
|
437
603
|
defs[b].set(reg, p);
|
|
438
604
|
paramReg.set(p, reg);
|
|
439
605
|
return p;
|
|
440
606
|
}
|
|
441
607
|
if (ps.length === 1) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
608
|
+
// A walk up single predecessors that comes back to where it started has gone round a cycle
|
|
609
|
+
// no edge enters from outside, so no path from the entry reaches it — unless the entry is on
|
|
610
|
+
// it. agbcc leaves such a cycle behind a loop it proved never runs. No execution reads the
|
|
611
|
+
// key there, which is the uninitialised read's answer. Through the entry, the value is a
|
|
612
|
+
// live-in joined with the cycle's, which a single-predecessor block has no phi for.
|
|
613
|
+
const at = `${b}|${reg}`;
|
|
614
|
+
if (singlePredWalk.has(at)) {
|
|
615
|
+
if (singlePredWalk.has(`0|${reg}`)) {
|
|
616
|
+
throw new FrontendUnsupportedError(
|
|
617
|
+
`cannot lift '${name}': ${reg} is read in a cycle through the entry block with no join to meet at`,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
const op = mkOp('undef', { results: [mkValue(T.unk(32))], attrs: { key: reg } });
|
|
621
|
+
irBlocks[b].ops.unshift(op);
|
|
622
|
+
defs[b].set(reg, op.results[0]);
|
|
623
|
+
return op.results[0];
|
|
624
|
+
}
|
|
625
|
+
singlePredWalk.add(at);
|
|
626
|
+
try {
|
|
627
|
+
const v = readAny(reg, ps[0]);
|
|
628
|
+
defs[b].set(reg, v);
|
|
629
|
+
return v;
|
|
630
|
+
} finally {
|
|
631
|
+
singlePredWalk.delete(at);
|
|
632
|
+
}
|
|
445
633
|
}
|
|
446
634
|
// sealed join: create the phi and wire every predecessor's terminator arg now.
|
|
447
635
|
const phi = newPhi(reg, b);
|
|
@@ -452,8 +640,17 @@ export function makeSsaBuilder(
|
|
|
452
640
|
// write-order record is keyed by. `phi` is passed rather than looked up: by the time a deferred
|
|
453
641
|
// phi is wired the block may have written its key again, so `defs[b]` no longer names it.
|
|
454
642
|
const addPhiOperands = (reg: string, b: number, phi: Value) => {
|
|
643
|
+
const walk = singlePredWalk;
|
|
644
|
+
singlePredWalk = new Set();
|
|
645
|
+
try {
|
|
646
|
+
addEachPhiOperand(reg, b, phi);
|
|
647
|
+
} finally {
|
|
648
|
+
singlePredWalk = walk;
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
const addEachPhiOperand = (reg: string, b: number, phi: Value) => {
|
|
455
652
|
for (const p of distinctPreds(b)) {
|
|
456
|
-
appendSuccessorArg(p, b,
|
|
653
|
+
appendSuccessorArg(p, b, readAny(reg, p));
|
|
457
654
|
const at = lastWriteAt[p].get(reg);
|
|
458
655
|
if (at !== undefined) {
|
|
459
656
|
const rec = writeOrder.lastWrite.get(irBlocks[p]) ?? new Map<Value, number>();
|
|
@@ -510,14 +707,17 @@ export function makeSsaBuilder(
|
|
|
510
707
|
return; // already a parameter, however it got there
|
|
511
708
|
}
|
|
512
709
|
}
|
|
513
|
-
const p = mkValue(
|
|
710
|
+
const p = mkValue(unset(key));
|
|
514
711
|
irBlocks[b].params.push(p);
|
|
515
712
|
paramReg.set(p, key); // ranked by the ABI sort like any other parameter
|
|
516
713
|
obligedParams[b].set(key, p);
|
|
517
714
|
};
|
|
518
715
|
|
|
519
|
-
const
|
|
716
|
+
const reaches = (reg: string, b: number, accept: (v: Value) => boolean, throughCalls: boolean): boolean => {
|
|
520
717
|
const walk = (at: number, seen: Set<number>): boolean => {
|
|
718
|
+
if (!throughCalls && clobberedLocal[at].has(reg)) {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
521
721
|
const own = defs[at].get(reg);
|
|
522
722
|
// A def `accept` rejects does not fall through to the predecessors: it is still a def, and
|
|
523
723
|
// nothing older than it reaches past it.
|
|
@@ -532,28 +732,153 @@ export function makeSsaBuilder(
|
|
|
532
732
|
};
|
|
533
733
|
return walk(b, new Set<number>());
|
|
534
734
|
};
|
|
735
|
+
const hasReachingDef = (reg: string, b: number, accept: (v: Value) => boolean = () => true): boolean =>
|
|
736
|
+
reaches(reg, b, accept, true);
|
|
737
|
+
const holdsValue = (reg: string, b: number, accept: (v: Value) => boolean = () => true): boolean =>
|
|
738
|
+
reaches(reg, b, accept, false);
|
|
739
|
+
|
|
740
|
+
const holdsEntryValue = (reg: string, b: number): boolean => {
|
|
741
|
+
// A predecessor is known only once filled, and `b` itself only up to here — so a path that comes
|
|
742
|
+
// back round to `b` carries writes this walk has not seen.
|
|
743
|
+
const untouched = (at: number, seen: Set<number>): boolean => {
|
|
744
|
+
if (at === b || !filled[at]) {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
if (seen.has(at)) {
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
750
|
+
seen.add(at);
|
|
751
|
+
return clean(at, seen);
|
|
752
|
+
};
|
|
753
|
+
// `decidedLocal` holds a register a call destroyed as well as one this block wrote.
|
|
754
|
+
const clean = (at: number, seen: Set<number>): boolean => {
|
|
755
|
+
if (defs[at].has(reg) || decidedLocal[at].has(reg)) {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
const ps = distinctPreds(at);
|
|
759
|
+
return ps.length === 0 ? at === 0 : ps.every((p) => untouched(p, seen));
|
|
760
|
+
};
|
|
761
|
+
return clean(b, new Set());
|
|
762
|
+
};
|
|
763
|
+
const entryValue = (reg: string, b: number): Value | undefined => {
|
|
764
|
+
if (!holdsEntryValue(reg, b)) {
|
|
765
|
+
return undefined;
|
|
766
|
+
}
|
|
767
|
+
ensureParam(reg, 0);
|
|
768
|
+
return irBlocks[0].params.find((p) => paramReg.get(p) === reg);
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
/** REFUSE a value the ABI destroyed. The builder is RIGHT that a caller-saved register has a
|
|
772
|
+
* reaching definition after a call; what it cannot see is that the call destroyed the bytes, so
|
|
773
|
+
* that definition names something the callee overwrote. Resolving the read to it is a silently
|
|
774
|
+
* wrong VALUE at exit 0 — `mov r3,#0x2a; bl f; add r4,r3,#0` reading as `return 42` — which is
|
|
775
|
+
* the one failure a frontend may not produce.
|
|
776
|
+
*
|
|
777
|
+
* It fires on a USE rather than on the read, and after two things in `finish` that legitimately
|
|
778
|
+
* retract one: `trimClobberedCallArgs`, which drops a read that fed a guessed call's argument
|
|
779
|
+
* list — the sound answer for those, and not a reason to refuse the function as well — and
|
|
780
|
+
* `pruneDeadParams`, which drops one that fed a join nothing reads. A destroyed value nothing is
|
|
781
|
+
* left holding is a dead register, not a wrong answer.
|
|
782
|
+
*
|
|
783
|
+
* TWO-SIDED, so both sides are stated. TOO STRICT costs a row and nothing else: a register the
|
|
784
|
+
* compiler rematerialised in a way no `writeVar` saw reads as destroyed and the function
|
|
785
|
+
* declines. TOO LOOSE is the wrong value, and the MAY direction below is what rules it out — a
|
|
786
|
+
* register destroyed on ANY path into a block is destroyed there, so a join cannot launder one. */
|
|
787
|
+
/** Per block, the registers a call destroyed on SOME path into it with nothing written since:
|
|
788
|
+
* the UNION, against `trimClobberedCallArgs`'s intersection, and for the opposite reason. That
|
|
789
|
+
* one proves the caller set a register up, which needs every path to agree; this one proves
|
|
790
|
+
* nobody can name it, which one path is enough for. Meaningful once every block is filled. */
|
|
791
|
+
const destroyedOnEntry = (): Array<Set<string>> => {
|
|
792
|
+
const destroyedIn: Array<Set<string>> = irBlocks.map(() => new Set());
|
|
793
|
+
const outOf = (b: number): Set<string> => {
|
|
794
|
+
const out = new Set(clobberedLocal[b]);
|
|
795
|
+
for (const r of destroyedIn[b]) {
|
|
796
|
+
if (!decidedLocal[b].has(r)) {
|
|
797
|
+
out.add(r);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return out;
|
|
801
|
+
};
|
|
802
|
+
for (let changed = true; changed;) {
|
|
803
|
+
changed = false;
|
|
804
|
+
for (let b = 0; b < irBlocks.length; b++) {
|
|
805
|
+
for (const p of distinctPreds(b)) {
|
|
806
|
+
for (const r of outOf(p)) {
|
|
807
|
+
if (!destroyedIn[b].has(r)) {
|
|
808
|
+
destroyedIn[b].add(r);
|
|
809
|
+
changed = true;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return destroyedIn;
|
|
816
|
+
};
|
|
817
|
+
const refuseStaleCallerSavedReads = () => {
|
|
818
|
+
if (staleCandidates.length === 0) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const destroyedIn = destroyedOnEntry();
|
|
822
|
+
for (const c of staleCandidates) {
|
|
823
|
+
if (!c.local && !destroyedIn[c.block].has(c.reg)) {
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
const ops = irBlocks[c.block].ops;
|
|
827
|
+
if (ops.slice(c.fromOp).some((op) => op.operands.includes(c.value))) {
|
|
828
|
+
throw new FrontendUnsupportedError(
|
|
829
|
+
`cannot lift '${name}': ${c.reg} is read on a path where a call has destroyed it, so what ` +
|
|
830
|
+
`it holds is the callee's and not a value this function named — not modelled`,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
const dropPhiRecord = (p: Value) => {
|
|
837
|
+
phiBlock.delete(p);
|
|
838
|
+
phiKey.delete(p);
|
|
839
|
+
forgetOrder(p);
|
|
840
|
+
};
|
|
535
841
|
|
|
536
842
|
return {
|
|
537
843
|
fn,
|
|
538
844
|
irBlocks,
|
|
539
845
|
readVar,
|
|
846
|
+
readGuessedArg: readAny, // see the interface: the trim is this read's answer, not a refusal
|
|
540
847
|
writeVar,
|
|
541
848
|
paramReg,
|
|
542
849
|
ensureParam,
|
|
543
850
|
hasReachingDef,
|
|
544
|
-
|
|
851
|
+
holdsValue,
|
|
852
|
+
entryValue,
|
|
853
|
+
holdsEntryValue,
|
|
854
|
+
noteCall: (b: number, clobbers: readonly string[]) => {
|
|
545
855
|
callsIn.add(b);
|
|
546
856
|
// the callee clobbers the caller-saved registers, its own result register included — see
|
|
547
857
|
// the ordering contract on the interface
|
|
548
858
|
writtenSinceCall[b] = new Set();
|
|
859
|
+
for (const r of clobbers) {
|
|
860
|
+
clobberedLocal[b].add(r);
|
|
861
|
+
decidedLocal[b].add(r);
|
|
862
|
+
}
|
|
549
863
|
},
|
|
550
|
-
recordGuessedCall: (
|
|
864
|
+
recordGuessedCall: (
|
|
865
|
+
op: Op,
|
|
866
|
+
b: number,
|
|
867
|
+
abi: { argRegs: string[]; returnReg: string },
|
|
868
|
+
passesCalleeResult?: (result: Value) => boolean,
|
|
869
|
+
) => {
|
|
551
870
|
abiSeen = abi;
|
|
552
871
|
guessedCalls.push({
|
|
553
872
|
block: b,
|
|
554
873
|
op,
|
|
874
|
+
passesCalleeResult,
|
|
555
875
|
freshBefore: new Set(writtenSinceCall[b]),
|
|
556
876
|
afterCallInBlock: callsIn.has(b), // `noteCall` runs after this, so this means an EARLIER call
|
|
877
|
+
returnRegBefore: clobberedLocal[b].has(abi.returnReg)
|
|
878
|
+
? 'destroyed'
|
|
879
|
+
: decidedLocal[b].has(abi.returnReg)
|
|
880
|
+
? 'written'
|
|
881
|
+
: 'inherited',
|
|
557
882
|
});
|
|
558
883
|
},
|
|
559
884
|
markFilled: (b: number) => {
|
|
@@ -575,31 +900,35 @@ export function makeSsaBuilder(
|
|
|
575
900
|
}
|
|
576
901
|
}
|
|
577
902
|
trimClobberedCallArgs({
|
|
903
|
+
name,
|
|
578
904
|
argRegs: abiSeen.argRegs,
|
|
579
905
|
returnReg: abiSeen.returnReg,
|
|
580
906
|
calleeResults,
|
|
907
|
+
destroyedIn: destroyedOnEntry(),
|
|
581
908
|
preds,
|
|
909
|
+
blocks: irBlocks,
|
|
582
910
|
freshAtEnd: writtenSinceCall,
|
|
583
911
|
callsIn,
|
|
584
912
|
sites: guessedCalls,
|
|
585
913
|
});
|
|
586
914
|
}
|
|
587
915
|
irBlocks.forEach((blk, i) => writeOrder.writes.set(blk, writeCount[i]));
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
// phi
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
916
|
+
// The phis nothing reads at all — a register two paths leave holding different junk (a loop
|
|
917
|
+
// counter after its last use, a scratch the epilogue overwrites) still joins as a phi, and a
|
|
918
|
+
// dead phi is not junk downstream: its edge args become post-loop copies in the emitted C and
|
|
919
|
+
// block gates keyed on "this exit carries nothing".
|
|
920
|
+
//
|
|
921
|
+
// DEAD-PHI PRUNING RUNS FIRST, and `refuseStaleCallerSavedReads` is why. That check asks
|
|
922
|
+
// whether a destroyed value still reaches a use, and a dead phi's edge arg is not one. The
|
|
923
|
+
// trivial-phi
|
|
924
|
+
// pass must not run before it for the opposite reason: that one REPLACES the phi's uses with
|
|
925
|
+
// the arg and splices the arg away, which takes the last visible use of a destroyed value out
|
|
926
|
+
// of sight. Running it twice is what keeps both true — trivial-phi removal can orphan a phi's
|
|
927
|
+
// last reader, never the reverse, so the second sweep is the one that pays for the first.
|
|
928
|
+
pruneDeadParams(fn, dropPhiRecord);
|
|
929
|
+
refuseStaleCallerSavedReads();
|
|
930
|
+
simplifyTrivialPhis(fn, dropPhiRecord);
|
|
931
|
+
pruneDeadParams(fn, dropPhiRecord);
|
|
603
932
|
// SEAL THE PARAMETER EVIDENCE (ir/core.ts `ParamEvidence`). Here and not at the store or the
|
|
604
933
|
// write, because `pruneDeadParams` above is the last thing that can retire an entry
|
|
605
934
|
// parameter, and an observation about a value no longer in the signature is one the reader
|
|
@@ -667,21 +996,58 @@ export function makeSsaBuilder(
|
|
|
667
996
|
};
|
|
668
997
|
}
|
|
669
998
|
|
|
670
|
-
// ──
|
|
999
|
+
// ── a guessed call arity: counted by `CallDeclarations.lower`, trimmed by `finish` ──
|
|
671
1000
|
|
|
672
1001
|
/** Best-effort call arity when a callee has no prototype: the count of contiguous argument
|
|
673
1002
|
* registers with a value reaching the call's block. Correct when the arguments are set up in
|
|
674
1003
|
* the calling block; it can under-count pass-through parameters — which is why a prototype's
|
|
675
|
-
* declared `params` is authoritative when available.
|
|
1004
|
+
* declared `params` is authoritative when available.
|
|
1005
|
+
*
|
|
1006
|
+
* `accept` says which definitions are values (`SsaBuilder.hasReachingDef`). A pending high half
|
|
1007
|
+
* is a def but not a value, and counted it raises the arity: a `lis` hoisted into the prologue
|
|
1008
|
+
* leaves its half in r4 across an intervening call and makes `strlen(s)` into
|
|
1009
|
+
* `strlen(s, <half>)`, which then refuses at the read.
|
|
1010
|
+
*
|
|
1011
|
+
* `gap` MAKES A GAP REFUSE. The count is contiguous, so an argument register with nothing reaching
|
|
1012
|
+
* it ends it — and one is empty for two opposite reasons. Either the call really takes that few
|
|
1013
|
+
* arguments, or the register still holds this function's own untouched incoming argument, a value
|
|
1014
|
+
* the machine passes on that SSA has no definition for because nothing ever wrote it. When a
|
|
1015
|
+
* LATER argument register does hold a value the second reading is the only one left, and taking
|
|
1016
|
+
* the first drops that argument and every one after it: `ac-decomp:evw_anime_colreg_manual` sets
|
|
1017
|
+
* up seven registers for `evw_color_set` and, with r4 at its incoming value, lifts to
|
|
1018
|
+
* `evw_color_set(a0);` — its divide, its multiply and five arguments gone. Which reading it is
|
|
1019
|
+
* cannot be decided here — the function's own arity is exactly what is missing — so this refuses
|
|
1020
|
+
* and names the gap rather than guessing. A declaration answers it, and this scan is never
|
|
1021
|
+
* weighed against one: it is a guess, and a guess that cannot fail cannot witness a width a
|
|
1022
|
+
* declaration left open. */
|
|
676
1023
|
export function fallbackArgc(
|
|
677
|
-
ssa: { hasReachingDef(reg: string, b: number): boolean },
|
|
678
|
-
argRegs: string[],
|
|
1024
|
+
ssa: { hasReachingDef(reg: string, b: number, accept?: (v: Value) => boolean): boolean },
|
|
1025
|
+
argRegs: readonly string[],
|
|
679
1026
|
bi: number,
|
|
1027
|
+
opts: {
|
|
1028
|
+
accept?: (v: Value) => boolean;
|
|
1029
|
+
/** the call's function and address, and the frontend's own refusal */
|
|
1030
|
+
gap?: { name: string; at: number; fail: FrontendRefusal };
|
|
1031
|
+
} = {},
|
|
680
1032
|
): number {
|
|
1033
|
+
// whether the caller set argument register k up: a definition reaches it, through a call too
|
|
1034
|
+
const setUp = (k: number) => ssa.hasReachingDef(argRegs[k], bi, opts.accept);
|
|
681
1035
|
let n = 0;
|
|
682
|
-
while (n < argRegs.length &&
|
|
1036
|
+
while (n < argRegs.length && setUp(n)) {
|
|
683
1037
|
n++;
|
|
684
1038
|
}
|
|
1039
|
+
if (opts.gap !== undefined) {
|
|
1040
|
+
const { name, at, fail } = opts.gap;
|
|
1041
|
+
for (let k = n + 1; k < argRegs.length; k++) {
|
|
1042
|
+
if (setUp(k)) {
|
|
1043
|
+
fail(
|
|
1044
|
+
`cannot lift '${name}': the call at 0x${at.toString(16)} has no prototype, ${argRegs[k]} holds a ` +
|
|
1045
|
+
`value and ${argRegs[n]} holds none — an argument register left at its incoming value and one ` +
|
|
1046
|
+
`the call does not pass look the same here, so the argument count is not decidable`,
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
685
1051
|
return n;
|
|
686
1052
|
}
|
|
687
1053
|
|
|
@@ -689,15 +1055,24 @@ export function fallbackArgc(
|
|
|
689
1055
|
* of its own block up to that instruction. */
|
|
690
1056
|
export interface GuessedCallSite {
|
|
691
1057
|
block: number;
|
|
692
|
-
/** the `call` op — its
|
|
1058
|
+
/** the `call` op — its arguments (`callArgs`) are the guessed ones, in argument-register order */
|
|
693
1059
|
op: Op;
|
|
694
1060
|
/** argument registers written between the last call in this block (or the block's start) and here */
|
|
695
1061
|
freshBefore: Set<string>;
|
|
696
1062
|
/** did this block already make a call before this one? */
|
|
697
1063
|
afterCallInBlock: boolean;
|
|
1064
|
+
/** what this block did to the return register before this call: a call here left it holding
|
|
1065
|
+
* nothing (`destroyed`), the block wrote it since (`written`), or neither, so the predecessors
|
|
1066
|
+
* decide (`inherited`) */
|
|
1067
|
+
returnRegBefore: 'destroyed' | 'written' | 'inherited';
|
|
1068
|
+
/** which earlier callee's result in the return register is an argument here, not a leftover:
|
|
1069
|
+
* the site's lowering says so ({@link SsaBuilder.recordGuessedCall}). Absent where none is. */
|
|
1070
|
+
passesCalleeResult?: (result: Value) => boolean;
|
|
698
1071
|
}
|
|
699
1072
|
|
|
700
1073
|
export interface CallArgTrim {
|
|
1074
|
+
/** the function being lifted, for a refusal */
|
|
1075
|
+
name: string;
|
|
701
1076
|
argRegs: string[];
|
|
702
1077
|
/** the ABI return register. Load-bearing only where it IS `argRegs[0]` (ARM r0, PPC r3) — that
|
|
703
1078
|
* aliasing is what makes a callee's result indistinguishable from caller-side argument setup. */
|
|
@@ -705,8 +1080,14 @@ export interface CallArgTrim {
|
|
|
705
1080
|
/** every value a `call` op produced. Tells a callee's own return apart from a join that merely
|
|
706
1081
|
* PASSES THROUGH one, which the register file cannot: both leave argument 0 unfresh. */
|
|
707
1082
|
calleeResults: ReadonlySet<Value>;
|
|
1083
|
+
/** per block, the registers a call destroyed on SOME path into it with nothing written since. A
|
|
1084
|
+
* call lists the return register there only when it leaves it holding nothing the caller can
|
|
1085
|
+
* name — a struct handed back in it ({@link SsaBuilder.noteCall}). */
|
|
1086
|
+
destroyedIn: ReadonlyArray<ReadonlySet<string>>;
|
|
708
1087
|
/** one entry per CFG edge, as passed to {@link makeSsaBuilder} */
|
|
709
1088
|
preds: number[][];
|
|
1089
|
+
/** the blocks `preds` indexes, terminators included — the edges a guard proves a value on */
|
|
1090
|
+
blocks: readonly Block[];
|
|
710
1091
|
/** per block: the keys written since its LAST call (since its start if it makes none). Indexed by
|
|
711
1092
|
* block, and it holds every key the builder saw, not only argument registers. */
|
|
712
1093
|
freshAtEnd: Array<Set<string>>;
|
|
@@ -736,12 +1117,13 @@ export interface CallArgTrim {
|
|
|
736
1117
|
* real argument CAN go with them: a fresh register above a hole stops the run (a 64-bit return
|
|
737
1118
|
* occupies two registers and the frontend cannot express one, so the caller's r2 goes with the
|
|
738
1119
|
* unfillable r1), and a callee's return read as the callee's own drops an argument a `g(f())`
|
|
739
|
-
* source did pass. A declared prototype is what closes either
|
|
1120
|
+
* source did pass. A declared prototype is what closes either for a named callee; a call through a
|
|
1121
|
+
* register has none, so it keeps that return instead (`GuessedCallSite.passesCalleeResult`).
|
|
740
1122
|
*
|
|
741
1123
|
* Frontend-agnostic: the caller supplies what its own lifting scan observed, so nothing here
|
|
742
1124
|
* re-derives which instruction writes which register. */
|
|
743
1125
|
export function trimClobberedCallArgs(inp: CallArgTrim): void {
|
|
744
|
-
const { argRegs, returnReg, calleeResults, preds, freshAtEnd, callsIn, sites } = inp;
|
|
1126
|
+
const { argRegs, returnReg, calleeResults, destroyedIn, preds, blocks, freshAtEnd, callsIn, sites } = inp;
|
|
745
1127
|
const blockCount = freshAtEnd.length;
|
|
746
1128
|
const all = () => new Set(argRegs);
|
|
747
1129
|
const localEnd = (b: number) => freshAtEnd[b] ?? new Set<string>();
|
|
@@ -792,7 +1174,7 @@ export function trimClobberedCallArgs(inp: CallArgTrim): void {
|
|
|
792
1174
|
// (`bl __mulsf3; add r1,r4,#0; bl __addsf3` is `__addsf3(__mulsf3(a, b), c)`).
|
|
793
1175
|
const setsUpLater = (fresh: Set<string>): boolean => argRegs.some((r, i) => i > 0 && fresh.has(r));
|
|
794
1176
|
// THE RETURN REGISTER IS NOT ARGUMENT SETUP. Where the ABI aliases it onto argument 0, the
|
|
795
|
-
//
|
|
1177
|
+
// call lowering records a call's clobber AFTER its own result, so the result leaves the register
|
|
796
1178
|
// UNfresh here. That disproves caller setup only where the callee's return is BOTH what the
|
|
797
1179
|
// register still holds and all the site has to go on: with a later register set up (above), or
|
|
798
1180
|
// with a value no call produced — a join of one path's return with another path's caller-computed
|
|
@@ -804,20 +1186,136 @@ export function trimClobberedCallArgs(inp: CallArgTrim): void {
|
|
|
804
1186
|
// `f` to return a value and `g` to accept one — a spelling the project's own header rejects
|
|
805
1187
|
// outright when it does not. A declared prototype never reaches here, and stays the way `g(f())`
|
|
806
1188
|
// is recovered.
|
|
807
|
-
|
|
808
|
-
|
|
1189
|
+
//
|
|
1190
|
+
// A SITE THAT PASSES THE RESULT ON keeps it (`GuessedCallSite.passesCalleeResult`).
|
|
1191
|
+
//
|
|
1192
|
+
// THE EXEMPTION NEEDS A RESULT IN THE REGISTER. A call that hands a struct back in it (`struct W1
|
|
1193
|
+
// mkw(s32)`) writes no value there — its bytes are a struct, not a word — so the register still
|
|
1194
|
+
// names what it held BEFORE that call, and neither clause above can tell: that value is no callee
|
|
1195
|
+
// result, which is the join clause's sign of a real argument. Destroyed on any path is no setup on
|
|
1196
|
+
// that path, so the register is an ordinary unfresh one there, and the run stops below it —
|
|
1197
|
+
// unless a later register is set up, which puts argument 0 below a proven one: that argument is
|
|
1198
|
+
// then the struct itself (`other(mv(a), b)`), a value no register here names, and it declines.
|
|
1199
|
+
//
|
|
1200
|
+
// WHETHER AN EDGE PROVES `v` CONSTANT EVERYWHERE `b` IS REACHED: up the chain of blocks with one
|
|
1201
|
+
// predecessor, a conditional branch on `v == K` arriving by its true edge, or on `v != K` by its
|
|
1202
|
+
// false one. K is any value arithmetic builds out of literals: agbcc compares with 1000 as
|
|
1203
|
+
// `mov r1,#250; lsl r1,#2; cmp r0,r1`. `v` holds every name the value goes by: compared past a join
|
|
1204
|
+
// the trivial-phi pass has yet to fold, a callee's result is that join's parameter.
|
|
1205
|
+
let defs: Map<Value, Op> | undefined;
|
|
1206
|
+
const defOf = (v: Value): Op | undefined => {
|
|
1207
|
+
defs ??= new Map(blocks.flatMap((blk) => blk.ops.flatMap((op) => op.results.map((r) => [r, op] as const))));
|
|
1208
|
+
return defs.get(v);
|
|
1209
|
+
};
|
|
1210
|
+
const isLiteral = (v: Value): boolean => {
|
|
1211
|
+
const d = defOf(v);
|
|
1212
|
+
return (
|
|
1213
|
+
d?.opcode === 'const' ||
|
|
1214
|
+
(d !== undefined && LITERAL_ARITHMETIC.has(d.opcode) && d.operands.length > 0 && d.operands.every(isLiteral))
|
|
1215
|
+
);
|
|
1216
|
+
};
|
|
1217
|
+
const provenConstant = (v: ReadonlySet<Value>, b: number): boolean => {
|
|
1218
|
+
for (let at = b, seen = new Set<number>(); !seen.has(at);) {
|
|
1219
|
+
seen.add(at);
|
|
1220
|
+
const ps = [...new Set(preds[at] ?? [])];
|
|
1221
|
+
if (ps.length !== 1) {
|
|
1222
|
+
return false;
|
|
1223
|
+
}
|
|
1224
|
+
const term = terminator(blocks[ps[0]]);
|
|
1225
|
+
const arrives = term?.successors.map((e) => e.block === blocks[at]) ?? [];
|
|
1226
|
+
const cmp = term?.opcode === 'cond_br' && arrives[0] !== arrives[1] ? defOf(term.operands[0]) : undefined;
|
|
1227
|
+
const equalEdge = cmp?.opcode === 'icmp_eq' ? 0 : cmp?.opcode === 'icmp_ne' ? 1 : -1;
|
|
1228
|
+
if (
|
|
1229
|
+
equalEdge >= 0 &&
|
|
1230
|
+
arrives[equalEdge] &&
|
|
1231
|
+
cmp!.operands.some((o) => v.has(o)) &&
|
|
1232
|
+
cmp!.operands.some((o) => !v.has(o) && isLiteral(o))
|
|
1233
|
+
) {
|
|
1234
|
+
return true;
|
|
1235
|
+
}
|
|
1236
|
+
at = ps[0];
|
|
1237
|
+
}
|
|
1238
|
+
return false;
|
|
1239
|
+
};
|
|
1240
|
+
// THE RESULT A SITE PASSES ON is looked for through the joins the trivial-phi pass has yet to
|
|
1241
|
+
// fold: this trim runs before it, so a result r0 carries untouched across a loop reaches the call
|
|
1242
|
+
// as the loop header's parameter, not as the callee's own value. The site passes one where every
|
|
1243
|
+
// path brings it an earlier callee's result it passes (`GuessedCallSite.passesCalleeResult`) —
|
|
1244
|
+
// that value, or the join itself where the paths bring different ones (`v = x ? g() : h(); p(v);`).
|
|
1245
|
+
let joinArgs: Map<Value, Value[]> | undefined;
|
|
1246
|
+
const joinedInto = (v: Value): Value[] | undefined => {
|
|
1247
|
+
if (joinArgs === undefined) {
|
|
1248
|
+
joinArgs = new Map();
|
|
1249
|
+
for (const blk of blocks) {
|
|
1250
|
+
for (const op of blk.ops) {
|
|
1251
|
+
for (const e of op.successors) {
|
|
1252
|
+
// the entry block's parameters are the function's own, whatever a back edge brings
|
|
1253
|
+
if (e.block === blocks[0]) {
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
e.block.params.forEach((param, i) => {
|
|
1257
|
+
const into = joinArgs!.get(param);
|
|
1258
|
+
if (into) {
|
|
1259
|
+
into.push(e.args[i]);
|
|
1260
|
+
} else {
|
|
1261
|
+
joinArgs!.set(param, [e.args[i]]);
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return joinArgs.get(v);
|
|
1269
|
+
};
|
|
1270
|
+
const passedResult = (s: GuessedCallSite): Value | undefined => {
|
|
1271
|
+
const arg = callArgs(s.op)[0];
|
|
1272
|
+
const passes = s.passesCalleeResult;
|
|
1273
|
+
if (arg === undefined || passes === undefined) {
|
|
1274
|
+
return undefined;
|
|
1275
|
+
}
|
|
1276
|
+
const results = new Set<Value>();
|
|
1277
|
+
const seen = new Set<Value>();
|
|
1278
|
+
for (const work = [arg]; work.length > 0;) {
|
|
1279
|
+
const v = work.pop()!;
|
|
1280
|
+
if (seen.has(v)) {
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
seen.add(v);
|
|
1284
|
+
const into = joinedInto(v);
|
|
1285
|
+
if (into !== undefined) {
|
|
1286
|
+
work.push(...into);
|
|
1287
|
+
} else if (calleeResults.has(v) && passes(v)) {
|
|
1288
|
+
results.add(v);
|
|
1289
|
+
} else {
|
|
1290
|
+
return undefined;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return results.size === 0 ? undefined : results.size === 1 ? [...results][0] : arg;
|
|
1294
|
+
};
|
|
1295
|
+
const argcAt = (s: GuessedCallSite, fresh: Set<string>): number => {
|
|
1296
|
+
const destroyed =
|
|
1297
|
+
s.returnRegBefore === 'destroyed' || (s.returnRegBefore === 'inherited' && destroyedIn[s.block].has(returnReg));
|
|
1298
|
+
if (argRegs[0] === returnReg && !fresh.has(argRegs[0]) && destroyed && setsUpLater(fresh)) {
|
|
1299
|
+
const callee = s.op.attrs.target;
|
|
1300
|
+
throw new FrontendUnsupportedError(
|
|
1301
|
+
`cannot lift '${inp.name}': argument 1 of the call to ${typeof callee === 'string' ? `'${callee}'` : 'a callee'} ` +
|
|
1302
|
+
`is a struct an earlier call handed back in ${returnReg}, and nothing here passes one on as an argument — not modelled`,
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
if (argRegs[0] !== returnReg || fresh.has(argRegs[0]) || destroyed) {
|
|
809
1306
|
return runOfFresh(fresh, 0);
|
|
810
1307
|
}
|
|
811
|
-
|
|
1308
|
+
const op = s.op;
|
|
1309
|
+
if (setsUpLater(fresh) || !calleeResults.has(callArgs(op)[0]) || passedResult(s) !== undefined) {
|
|
812
1310
|
return runOfFresh(new Set([argRegs[0], ...fresh]), 0);
|
|
813
1311
|
}
|
|
814
1312
|
return 0;
|
|
815
1313
|
};
|
|
816
1314
|
for (const s of sites) {
|
|
817
1315
|
const fresh = s.afterCallInBlock ? s.freshBefore : new Set([...freshIn[s.block], ...s.freshBefore]);
|
|
818
|
-
const n = argcAt(
|
|
819
|
-
if (n < s.op.
|
|
820
|
-
s.op
|
|
1316
|
+
const n = argcAt(s, fresh);
|
|
1317
|
+
if (n < callArgs(s.op).length) {
|
|
1318
|
+
truncateCallArgs(s.op, n);
|
|
821
1319
|
}
|
|
822
1320
|
// The SHORTER arity the same evidence also allows, recorded for {@link narrowToSetupArgs}: the
|
|
823
1321
|
// run over what THIS BLOCK wrote, dropping the registers that are fresh only because no call
|
|
@@ -825,20 +1323,60 @@ export function trimClobberedCallArgs(inp: CallArgTrim): void {
|
|
|
825
1323
|
// is recorded rather than applied. A survivor is what it drops, so the join clause above has no
|
|
826
1324
|
// place here — but `setsUpLater` still does: a register this block set up two instructions
|
|
827
1325
|
// before the call is not something the narrower reading may call dead.
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
if (
|
|
1326
|
+
//
|
|
1327
|
+
// A SITE THAT PASSES AN EARLIER CALLEE'S RESULT ON keeps it in this reading too, except where an
|
|
1328
|
+
// edge proves that result equal to a constant on the way here (`if (g() == 0) p();`): a source
|
|
1329
|
+
// that passed it there makes the compiler load the constant it proved (`mov r0,#0`), so an asm
|
|
1330
|
+
// with no such load passed nothing. Per site, because this reading cuts every site at once, and a
|
|
1331
|
+
// drop that one site needs would otherwise drop a value another site passes.
|
|
1332
|
+
const result = n > 0 ? passedResult(s) : undefined;
|
|
1333
|
+
const passed = result !== undefined;
|
|
1334
|
+
const dropsPassed = passed && provenConstant(new Set([result, callArgs(s.op)[0]]), s.block);
|
|
1335
|
+
const localFresh =
|
|
1336
|
+
setsUpLater(s.freshBefore) || (passed && !dropsPassed) ? new Set([argRegs[0], ...s.freshBefore]) : s.freshBefore;
|
|
1337
|
+
const local = Math.min(runOfFresh(localFresh, 0), callArgs(s.op).length);
|
|
1338
|
+
if (local < callArgs(s.op).length) {
|
|
831
1339
|
setupArgc.set(s.op, local);
|
|
1340
|
+
if (local === 0 && dropsPassed) {
|
|
1341
|
+
discardsPassedResult.add(s.op);
|
|
1342
|
+
}
|
|
832
1343
|
}
|
|
833
1344
|
}
|
|
834
1345
|
}
|
|
835
1346
|
|
|
1347
|
+
/** The opcodes whose result is a literal when every operand is one ({@link trimClobberedCallArgs}). */
|
|
1348
|
+
const LITERAL_ARITHMETIC: ReadonlySet<string> = new Set([
|
|
1349
|
+
'add',
|
|
1350
|
+
'sub',
|
|
1351
|
+
'mul',
|
|
1352
|
+
'and',
|
|
1353
|
+
'or',
|
|
1354
|
+
'xor',
|
|
1355
|
+
'shl',
|
|
1356
|
+
'shr_u',
|
|
1357
|
+
'shr_s',
|
|
1358
|
+
'not',
|
|
1359
|
+
'neg',
|
|
1360
|
+
]);
|
|
1361
|
+
|
|
836
1362
|
/** The narrower arity {@link narrowToSetupArgs} would cut each guessed call to. A SIDE table and
|
|
837
1363
|
* not an attr: this is a fact about one LIFT, not part of the IR the rest of the pipeline compares
|
|
838
1364
|
* and prints — `structure/hazards.ts` decides two ops equal by comparing their attrs verbatim, so
|
|
839
1365
|
* an attr only one of an otherwise-matching pair carries would cost a recovery. */
|
|
840
1366
|
const setupArgc = new WeakMap<Op, number>();
|
|
841
1367
|
|
|
1368
|
+
/** The calls whose narrower arity drops an earlier callee's result that the site passes on
|
|
1369
|
+
* (`GuessedCallSite.passesCalleeResult`): only where an edge proves that result constant on the way
|
|
1370
|
+
* to the call ({@link trimClobberedCallArgs}). Read by {@link setupArgsDiscardsPassedResult}. */
|
|
1371
|
+
const discardsPassedResult = new WeakSet<Op>();
|
|
1372
|
+
|
|
1373
|
+
/** Whether {@link narrowToSetupArgs} drops, somewhere in `fn`, an earlier callee's result that a
|
|
1374
|
+
* call through a register is passed — the fact that makes the narrowed lift's candidates lose a
|
|
1375
|
+
* score tie (`Candidate.discardsPassedResult`). */
|
|
1376
|
+
export function setupArgsDiscardsPassedResult(fn: Fn): boolean {
|
|
1377
|
+
return fn.blocks.some((b) => b.ops.some((op) => discardsPassedResult.has(op)));
|
|
1378
|
+
}
|
|
1379
|
+
|
|
842
1380
|
/** Whether anything in `fn` HAS the narrower reading — the variation's gate, so the ~99% of functions
|
|
843
1381
|
* with no narrowable call cost no re-lift. Read it off the lift itself: a later pipeline stage may
|
|
844
1382
|
* replace a `call` op (softdiv rewrites one to a division), and the table is keyed by op. */
|
|
@@ -864,8 +1402,8 @@ export function narrowToSetupArgs(fn: Fn): boolean {
|
|
|
864
1402
|
for (const b of fn.blocks) {
|
|
865
1403
|
for (const op of b.ops) {
|
|
866
1404
|
const setup = setupArgc.get(op);
|
|
867
|
-
if (setup !== undefined && setup < op.
|
|
868
|
-
op
|
|
1405
|
+
if (setup !== undefined && setup < callArgs(op).length) {
|
|
1406
|
+
truncateCallArgs(op, setup);
|
|
869
1407
|
changed = true;
|
|
870
1408
|
}
|
|
871
1409
|
}
|
|
@@ -879,9 +1417,11 @@ export function narrowToSetupArgs(fn: Fn): boolean {
|
|
|
879
1417
|
return changed;
|
|
880
1418
|
}
|
|
881
1419
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1420
|
+
// ── shared frontend tail helpers ──
|
|
1421
|
+
|
|
1422
|
+
/** The stack-slot key the MIPS, Thumb and PowerPC frontends use for a word-sized local in the
|
|
1423
|
+
* function's own frame. Shared so they spell it identically and the frame-partition rule can
|
|
1424
|
+
* recognise any frontend's slots. See the virtual-key note in the module header. */
|
|
885
1425
|
const SLOT_PREFIX = 'sp@';
|
|
886
1426
|
export const stackSlotKey = (off: number): string => `${SLOT_PREFIX}${off}`;
|
|
887
1427
|
/** The byte offset a slot key names, or null if `key` is not a slot key at all (an ordinary
|
|
@@ -890,20 +1430,73 @@ export const stackSlotKey = (off: number): string => `${SLOT_PREFIX}${off}`;
|
|
|
890
1430
|
export const slotKeyOffset = (key: string): number | null =>
|
|
891
1431
|
key.startsWith(SLOT_PREFIX) ? Number(key.slice(SLOT_PREFIX.length)) : null;
|
|
892
1432
|
|
|
893
|
-
/**
|
|
894
|
-
* (
|
|
895
|
-
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
1433
|
+
/** Where a live-in of the entry block sits in the calling convention: `slotOf` gives its ABI
|
|
1434
|
+
* argument rank (a register or an incoming stack word), or null for a live-in no argument arrives
|
|
1435
|
+
* in — an uninitialised register the body reads; `holes` gives the keys of the argument slots the
|
|
1436
|
+
* convention proves precede the ones read (`readKeys`), whether or not the body reads them. One per
|
|
1437
|
+
* frontend, read by both halves of the entry-parameter rule: {@link mintArgSlotHoles} and
|
|
1438
|
+
* {@link abiSortEntryParams}.
|
|
1439
|
+
*
|
|
1440
|
+
* `holes` is a function of everything read, not a slot-to-key table, because where two register
|
|
1441
|
+
* files share one slot sequence (MIPS o32's floats) the file a hole comes from depends on what the
|
|
1442
|
+
* function reads — `frontend/fpu.ts` `fpuArgSlots`.
|
|
1443
|
+
*
|
|
1444
|
+
* Both halves need an entry block with NO predecessors, whose parameters are the function's own.
|
|
1445
|
+
* A loop header's are phis, index-aligned with its predecessors' edge arguments, and neither
|
|
1446
|
+
* minting one nor reordering them is legal there — so a frontend whose first block is a branch
|
|
1447
|
+
* target inserts an empty preheader ahead of it (each frontend's `preheader` note). */
|
|
1448
|
+
export interface ArgSlots {
|
|
1449
|
+
slotOf(key: string): number | null;
|
|
1450
|
+
holes(readKeys: readonly string[]): string[];
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/** The slots of a convention that passes arguments in `argRegs` only: every register below the
|
|
1454
|
+
* highest one read. */
|
|
1455
|
+
export const registerArgSlots = (argRegs: readonly string[]): ArgSlots => ({
|
|
1456
|
+
slotOf: (key) => (argRegs.includes(key) ? argRegs.indexOf(key) : null),
|
|
1457
|
+
holes: (readKeys) => argRegs.slice(0, Math.max(0, ...readKeys.map((k) => argRegs.indexOf(k)))),
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
/** A rank past every argument slot, for a live-in that is not one. */
|
|
1461
|
+
const NOT_AN_ARGUMENT = Number.MAX_SAFE_INTEGER;
|
|
1462
|
+
|
|
1463
|
+
/** Give the entry block a parameter for every argument slot below the highest one it takes. Naming
|
|
1464
|
+
* is POSITIONAL (`a0`, `a1`, … in {@link abiSortEntryParams} order), so a slot the body never reads
|
|
1465
|
+
* drops out of the signature and binds every later argument one slot low, silently: `int f(int a,
|
|
1466
|
+
* int b, int c) { return a + c; }` reads r3 and r5, and lifted as `f(a0, a1)` with r5 in r4's slot.
|
|
1467
|
+
* Arguments take their slots in order, registers first and then the stack words, so reading slot
|
|
1468
|
+
* k proves slots 0..k-1 precede it — an obligation on the signature, which is what `ensureParam`
|
|
1469
|
+
* is for. A live-in that is no slot proves nothing about that sequence. Call it before `finish()`,
|
|
1470
|
+
* which records evidence for every entry parameter. */
|
|
1471
|
+
export function mintArgSlotHoles(
|
|
1472
|
+
ssa: Pick<SsaBuilder, 'irBlocks' | 'paramReg' | 'ensureParam'>,
|
|
1473
|
+
entryHasPreds: boolean,
|
|
1474
|
+
slots: ArgSlots,
|
|
1475
|
+
): void {
|
|
1476
|
+
assertTrueEntry(entryHasPreds);
|
|
1477
|
+
const read = ssa.irBlocks[0].params.map((p) => ssa.paramReg.get(p)).filter((k): k is string => k !== undefined);
|
|
1478
|
+
for (const k of slots.holes(read)) {
|
|
1479
|
+
ssa.ensureParam(k, 0);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/** Order the entry block's parameters by ABI argument slot, so downstream naming (`a0`, `a1`, …)
|
|
1484
|
+
* matches the calling convention, not first-read order (a callee-saved copy can read a later
|
|
1485
|
+
* argument register first). A live-in that is no slot goes after every one that is: ranked
|
|
1486
|
+
* first, it takes `a0` and binds every real argument one slot high. */
|
|
900
1487
|
export function abiSortEntryParams(
|
|
901
1488
|
entry: { params: Value[] },
|
|
902
1489
|
entryHasPreds: boolean,
|
|
903
|
-
|
|
1490
|
+
paramReg: ReadonlyMap<Value, string>,
|
|
1491
|
+
slots: ArgSlots,
|
|
904
1492
|
): void {
|
|
1493
|
+
assertTrueEntry(entryHasPreds);
|
|
1494
|
+
const rank = (v: Value): number => slots.slotOf(paramReg.get(v) ?? '') ?? NOT_AN_ARGUMENT;
|
|
1495
|
+
entry.params.sort((x, y) => rank(x) - rank(y));
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function assertTrueEntry(entryHasPreds: boolean): void {
|
|
905
1499
|
if (entryHasPreds) {
|
|
906
|
-
|
|
1500
|
+
throw new Error('internal: the entry-parameter rule needs an entry block with no predecessors');
|
|
907
1501
|
}
|
|
908
|
-
entry.params.sort((x, y) => rank(x) - rank(y));
|
|
909
1502
|
}
|