@asmlift/core 0.9.0 → 0.11.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 +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
package/src/frontend/ssa.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// `irBlocks`, args left empty — phi wiring appends them), then call `markFilled(b)`. When all
|
|
17
17
|
// blocks are filled, call `finish()` to remove trivial phis.
|
|
18
18
|
import {
|
|
19
|
+
type AsmLayout,
|
|
19
20
|
Block,
|
|
20
21
|
Fn,
|
|
21
22
|
Op,
|
|
@@ -27,11 +28,13 @@ import {
|
|
|
27
28
|
defOpMap,
|
|
28
29
|
mkOp,
|
|
29
30
|
mkValue,
|
|
31
|
+
replaceAllUsesWith,
|
|
30
32
|
terminator,
|
|
31
33
|
truncateCallArgs,
|
|
32
34
|
} from '../ir/core';
|
|
33
35
|
import { pruneDeadParams, simplifyTrivialPhis } from '../ir/simplify';
|
|
34
36
|
import { type IrType, T } from '../ir/types';
|
|
37
|
+
import { globalRegisterSymbol } from '../target';
|
|
35
38
|
import { type FrontendRefusal, FrontendUnsupportedError } from './errors';
|
|
36
39
|
|
|
37
40
|
export interface SsaBuilder {
|
|
@@ -63,8 +66,21 @@ export interface SsaBuilder {
|
|
|
63
66
|
*
|
|
64
67
|
* One of the three call verbs `CallDeclarations.lower` owns ({@link noteCall}). */
|
|
65
68
|
readGuessedArg(reg: string, b: number): Value;
|
|
69
|
+
/** Record that the call being lowered in block `b` reads `v` from argument register `reg` it
|
|
70
|
+
* guessed it takes, for {@link readAfterEveryReachingWrite} alone: the call is an instruction
|
|
71
|
+
* reading what the register's last write left there, as a sibling path's call may be, while
|
|
72
|
+
* {@link readGuessedArg} makes the read carry no claim for `finish` to judge. */
|
|
73
|
+
noteGuessedArgRead(reg: string, b: number, v: Value): void;
|
|
74
|
+
/** The value `reg` holds at the END of block `b` once `b` is filled, as the edges out of it carry
|
|
75
|
+
* it; `null` before. A lookup, not a read: it records no read for `finish` to judge. */
|
|
76
|
+
readAtEnd(reg: string, b: number): Value | null;
|
|
66
77
|
/** Record that `reg` now holds `v` within block `b`. */
|
|
67
78
|
writeVar(reg: string, b: number, v: Value): void;
|
|
79
|
+
/** Every value written to a frame-slot key ({@link stackSlotKey}) or an incoming stack-argument key
|
|
80
|
+
* ({@link stackArgKey}): a word the machine STORED to its stack. The slot is a variable here, so a
|
|
81
|
+
* store nothing reloads leaves no op behind, and the value it stored looks unused — the dead-read
|
|
82
|
+
* pin asks this (frontend/dead-reads.ts `spilled`). */
|
|
83
|
+
readonly frameWrites: ReadonlySet<Value>;
|
|
68
84
|
/** Mark block `b` fully emitted (terminator pushed); seals any now-ready successors. */
|
|
69
85
|
markFilled(b: number): void;
|
|
70
86
|
/** Live-in parameter value → the key it arrived on (for calling-convention order). Usually an
|
|
@@ -81,7 +97,9 @@ export interface SsaBuilder {
|
|
|
81
97
|
* Never touches the block's definitions: the parameter is added and left unused, so any local
|
|
82
98
|
* value already flowing keeps flowing. Only meaningful on a block with no predecessors —
|
|
83
99
|
* elsewhere a parameter is a phi whose position is aligned with its predecessors' terminator
|
|
84
|
-
* args, and appending an unpaired one would corrupt that.
|
|
100
|
+
* args, and appending an unpaired one would corrupt that. Nor on a register the live-in model
|
|
101
|
+
* calls an uninitialised local's home (`LiveInModel.uninitRegs`): no caller fills it, so no
|
|
102
|
+
* convention proves an argument there, and {@link entryValue} answers `undefined` for it. */
|
|
85
103
|
ensureParam(key: string, b: number): void;
|
|
86
104
|
/** Whether `reg` has a definition reaching block `b` (best-effort call-arity heuristic).
|
|
87
105
|
*
|
|
@@ -94,6 +112,12 @@ export interface SsaBuilder {
|
|
|
94
112
|
* value this function named reaches it here on some path. A path that brings the destroyed bytes
|
|
95
113
|
* and one that brings a value still answer yes, and the read that follows refuses at `finish`. */
|
|
96
114
|
holdsValue(reg: string, b: number, accept?: (v: Value) => boolean): boolean;
|
|
115
|
+
/** The MUST question beside {@link hasReachingDef}'s MAY one: whether EVERY path into the end of
|
|
116
|
+
* block `b` brings a definition of `reg` that `accept` takes, with no call destroying it since
|
|
117
|
+
* (`noteCall`). A path from the entry that defines nothing answers no — the register holds the
|
|
118
|
+
* caller's value there. Asked once every block is filled: before that, a back edge from a block
|
|
119
|
+
* not yet filled carries definitions this walk cannot see, so it refuses to answer. */
|
|
120
|
+
everyReachingDef(reg: string, b: number, accept: (v: Value) => boolean): boolean;
|
|
97
121
|
/** The entry parameter `reg` still holds at this point of block `b`, taken WITHOUT a read. A read
|
|
98
122
|
* leaves a definition behind, and `hasReachingDef` counts it as argument setup, so a frontend that
|
|
99
123
|
* only moves the argument somewhere (a frame store) would raise the guessed arity of every later
|
|
@@ -104,6 +128,61 @@ export interface SsaBuilder {
|
|
|
104
128
|
* sense, answered without the entry parameter that one hands back. A callee-saved register stored
|
|
105
129
|
* to its save slot holds the caller's value, which is no parameter, so asking must not mint one. */
|
|
106
130
|
holdsEntryValue(reg: string, b: number): boolean;
|
|
131
|
+
/** Whether block `b` WROTE `reg` since its last call so far (since its start if it made none).
|
|
132
|
+
* Only a write counts: a read leaves a definition behind but is not one (`writeVar`). */
|
|
133
|
+
writtenSinceCall(reg: string, b: number): boolean;
|
|
134
|
+
/** How many ops block `b` held when it last wrote `reg` so far, or `undefined` where `b` has not
|
|
135
|
+
* written it: every op at or past that index came after the write. A copy (`add r1, r5, #0`) names
|
|
136
|
+
* no new value, so this is how a question about one WRITE of a register is asked, where its value
|
|
137
|
+
* was read before the write too (frontend/call-plan.ts `leftover`). */
|
|
138
|
+
lastWriteOp(reg: string, b: number): number | undefined;
|
|
139
|
+
/** Whether an instruction READ `reg` after EACH write of it that reaches block `b` and whose value
|
|
140
|
+
* `passes`, on ANY path out of that write but the ones into `b`, the first read on that path being
|
|
141
|
+
* one whose value `accept` takes. A write no read follows on any path was
|
|
142
|
+
* made for `b`'s use, so one is enough to answer no. A call destroying `reg` is no write of this
|
|
143
|
+
* function's, and neither is the entry's value where a path from the entry writes neither, so they
|
|
144
|
+
* do not answer no; the entry's value read after (as `accept` takes it), or read by a lowered op
|
|
145
|
+
* at all (as `unlowered` takes it: no write placed that read), counts as a write read. Where a
|
|
146
|
+
* call destroyed `reg` on EVERY path in, it holds what a callee left there and the answer is yes.
|
|
147
|
+
* The question is about the write, not its value: a copy (`add r4, r0, #0`) reads `r0` and lowers
|
|
148
|
+
* to no op, so no op-level question sees that read, and it counts here; a copy made in an earlier
|
|
149
|
+
* block names the value it copies, read before that write too, and is no read of it. Any path out
|
|
150
|
+
* of the write, because a MIPS compiler hoists one call's argument into the delay slot of the
|
|
151
|
+
* branch that picks it (`bnez v0, L; move a0, zero`), so the write reaches the other call unread
|
|
152
|
+
* on the path between. `b`'s own reads are not asked: what a read there that lowers to no op says
|
|
153
|
+
* differs by compiler (frontend/call-plan.ts `leftover`). Only the frontend's own reads count
|
|
154
|
+
* ({@link readVar}), and a guessed call's argument read the planner records for it
|
|
155
|
+
* ({@link noteGuessedArgRead}: a sibling path's call reads the write); a lookup this construction
|
|
156
|
+
* makes, or the planner's own probe ({@link readGuessedArg} alone), is none. A block not
|
|
157
|
+
* filled yet is passed through looking for the write, as {@link holdsValue} does; a path out of
|
|
158
|
+
* a write that reaches one has reads nobody has seen yet, and the write's value answers for it
|
|
159
|
+
* as `unlowered` takes it (IDO reloads `lw a0, 24(sp)` in a branch's delay slot for the arm laid
|
|
160
|
+
* out after the call). A write of a value `answersIfRead` takes (an earlier call's result, copied
|
|
161
|
+
* or reloaded) answers yes where it was read after, and is otherwise none: such a value is the
|
|
162
|
+
* callee's, saved across a call as often as handed to one. `undefined` where no write of this
|
|
163
|
+
* function's is left to judge and no entry value read: the question has no answer here. */
|
|
164
|
+
readAfterEveryReachingWrite(
|
|
165
|
+
reg: string,
|
|
166
|
+
b: number,
|
|
167
|
+
ask: {
|
|
168
|
+
passes: (v: Value) => boolean;
|
|
169
|
+
accept: (v: Value) => boolean;
|
|
170
|
+
unlowered: (v: Value) => boolean;
|
|
171
|
+
answersIfRead: (v: Value) => boolean;
|
|
172
|
+
},
|
|
173
|
+
): boolean | undefined;
|
|
174
|
+
/** Whether EVERY path into this point of block `b` wrote `reg` (or had a call destroy it) after
|
|
175
|
+
* leaving the last block `stop` names: {@link writtenSinceCall} asked of `b` so far and, where `b`
|
|
176
|
+
* did not write it, of each predecessor in turn. A path reaching a `stop` block, the entry, or a
|
|
177
|
+
* block not filled yet answers no; a cycle adds no path the first visit did not judge. `visit`
|
|
178
|
+
* is told each block the walk passes through after `b`. */
|
|
179
|
+
writtenOnEveryPath(reg: string, b: number, stop: (at: number) => boolean, visit?: (at: number) => void): boolean;
|
|
180
|
+
/** Record that the frontend CONSUMED `v`, which it read from a register in block `b`, without
|
|
181
|
+
* placing it in any op: PowerPC's register save routine takes its frame address in r11 and is
|
|
182
|
+
* erased, so the address appears in no operand. The refusal of a read a call destroyed
|
|
183
|
+
* (`refuseStaleCallerSavedReads`) judges a read by its uses, and an op-less use is one too — left
|
|
184
|
+
* unrecorded, a frame address a call destroyed would still be consumed, silently. */
|
|
185
|
+
useOutsideOps(v: Value, b: number): void;
|
|
107
186
|
/** Record that block `b` makes a call HERE: the ABI's caller-saved registers stop being ones the
|
|
108
187
|
* caller set up.
|
|
109
188
|
*
|
|
@@ -117,20 +196,57 @@ export interface SsaBuilder {
|
|
|
117
196
|
* return register precisely because of the ordering above: the callee's own result is already
|
|
118
197
|
* written there, so that one register does have a name. A callee that hands a STRUCT back in
|
|
119
198
|
* that register writes no value a caller may read as a word, so none is written and the
|
|
120
|
-
* register is listed here too
|
|
121
|
-
*
|
|
199
|
+
* register is listed here too — save one whose bytes a local of the struct is read back from,
|
|
200
|
+
* which is written as the call's value and noted as an image ({@link noteStructImage}).
|
|
201
|
+
* Required rather than optional: a call lowered without it would keep resolving a destroyed
|
|
202
|
+
* register to its pre-call value, silently. */
|
|
122
203
|
noteCall(b: number, clobbers: readonly string[]): void;
|
|
204
|
+
/** The call {@link noteCall} just recorded in `b` left `reg` holding `image`, the bytes of a
|
|
205
|
+
* struct its callee is declared to return there (frontend/call-plan.ts `registerStructLocal`).
|
|
206
|
+
* A read takes it as the call's value, and the lift re-reads it off the struct's local; a GUESSED
|
|
207
|
+
* arity does not count it, as it counts no register a call destroyed: a call nothing declares
|
|
208
|
+
* may be handed the struct by value or a member of it, in the same register, and the bytes do
|
|
209
|
+
* not say which. So the trim treats `reg` as destroyed, and a guess that still passes the image
|
|
210
|
+
* refuses in `finish`. */
|
|
211
|
+
noteStructImage(b: number, reg: string, image: Value): void;
|
|
123
212
|
/** Register a `call` op whose arity was GUESSED (no prototype), so `finish` can cut it back to the
|
|
124
213
|
* argument registers that were actually set up on every path (see {@link trimClobberedCallArgs}).
|
|
125
214
|
* `abi` is the target's argument-register order and its return register. It snapshots what the
|
|
126
215
|
* caller wrote, so it comes before the call's result is written ({@link noteCall}).
|
|
127
|
-
* `passesCalleeResult
|
|
216
|
+
* `passesCalleeResult`, `trimmed`, `withholdsSetupArgs`, `asksEveryCut` and `floor`: see
|
|
217
|
+
* {@link GuessedCallSite}. */
|
|
128
218
|
recordGuessedCall(
|
|
129
219
|
op: Op,
|
|
130
220
|
b: number,
|
|
131
221
|
abi: { argRegs: string[]; returnReg: string },
|
|
132
222
|
passesCalleeResult?: (result: Value) => boolean,
|
|
223
|
+
trimmed?: (cut: readonly string[], kept: readonly string[]) => void,
|
|
224
|
+
withholdsSetupArgs?: () => boolean,
|
|
225
|
+
asksEveryCut?: () => boolean,
|
|
226
|
+
floor?: number,
|
|
133
227
|
): void;
|
|
228
|
+
/** Require that nothing but `op` reads `v` once the function is lifted — no other op, no join that
|
|
229
|
+
* survives, no frame store (`frameWrites`), no op-less use (`useOutsideOps`) — or call `refuse`.
|
|
230
|
+
* A read the lowering has yet to see may come after `op` through a copy in a register the call
|
|
231
|
+
* leaves alone, so the question is asked in `finish`, of the finished function. Its asker is the
|
|
232
|
+
* guessed-arity gap (frontend/call-plan.ts `decideGap`). */
|
|
233
|
+
requireSoleReader(v: Value, op: Op, refuse: () => void): void;
|
|
234
|
+
/** Require that something other than a `ret` reads `v` once the function is lifted — an op, a
|
|
235
|
+
* frame store or an op-less use, of `v` or of a join it reaches — or call `refuse`. Asked in
|
|
236
|
+
* `finish`, as `requireSoleReader` is, and by the same asker: a decided gap at the first argument
|
|
237
|
+
* register, where the call's result being read is what says the callee returns a word
|
|
238
|
+
* (frontend/call-plan.ts `decideGap`). A `ret` does not count, since a frontend returns the
|
|
239
|
+
* return register at every `ret` whatever the declared type. */
|
|
240
|
+
requireReader(v: Value, refuse: () => void): void;
|
|
241
|
+
/** Record that `v` is a value an instruction wrote as a SIDE EFFECT of its own job, which the
|
|
242
|
+
* compiler may leave in a register nobody reads: the base writeback of a Thumb `ldmia`/`stmia`,
|
|
243
|
+
* which has no form without one, and the base a PowerPC load or store with update advances. Such
|
|
244
|
+
* a value is no evidence that anything wanted it, so the guessed-arity gap (frontend/call-plan.ts
|
|
245
|
+
* `decideGap`) never takes one as an argument. agbcc copies a struct of 3+ words with
|
|
246
|
+
* `ldmia r3!, {r4, r5, r6}; stmia r2!, {r4, r5, r6}` and leaves both writebacks dead in r2/r3. */
|
|
247
|
+
noteIncidentalWrite(v: Value): void;
|
|
248
|
+
/** Whether {@link noteIncidentalWrite} recorded `v`. */
|
|
249
|
+
isIncidentalWrite(v: Value): boolean;
|
|
134
250
|
/** Remove trivial phis and enforce the frontend's postconditions; call once every block is
|
|
135
251
|
* filled. Throws FrontendUnsupportedError if a stack slot escaped as an entry parameter. */
|
|
136
252
|
finish(): void;
|
|
@@ -139,7 +255,7 @@ export interface SsaBuilder {
|
|
|
139
255
|
/** `preds` is per-EDGE (see the module header): one entry per CFG edge into each block. */
|
|
140
256
|
// VARIABLE NAMES ARE NOT ALWAYS MACHINE REGISTERS. `readVar`/`writeVar` key on an arbitrary string,
|
|
141
257
|
// and frontends mint VIRTUAL keys for storage the ISA has no register for — MIPS `sp@<off>` for a
|
|
142
|
-
// stack slot (frontend/mips.ts),
|
|
258
|
+
// stack slot (frontend/mips.ts), `@sarg<k>` for an incoming stack argument (`stackArgKey`).
|
|
143
259
|
// A virtual key must be outside its ISA's register grammar so it cannot collide with a real one, and
|
|
144
260
|
// a key read with no reaching def becomes a function PARAMETER by the live-in path below — which is
|
|
145
261
|
// how both of those capabilities get their parameters without a new opcode or pass.
|
|
@@ -184,8 +300,8 @@ export interface LiveInModel {
|
|
|
184
300
|
* eight more). None reaches the ordering today, for an unrelated reason (`l3/slotorder.ts`'s
|
|
185
301
|
* REACH note), so nothing downstream is guarding this.
|
|
186
302
|
*
|
|
187
|
-
* ABSENT ⇒ NO STAMP. MIPS and PPC declare
|
|
188
|
-
* which is the refusing direction. */
|
|
303
|
+
* ABSENT ⇒ NO STAMP. MIPS and PPC declare none (MIPS claims `ownedLocals` alone), so they stamp
|
|
304
|
+
* nothing, which is the refusing direction. */
|
|
189
305
|
declaredLocals?: { from: number; to: number };
|
|
190
306
|
/** Storage the CALLER wrote — incoming stack arguments ⇒ a def-less read is a parameter.
|
|
191
307
|
* `[from, to)`. O32's register-parameter home area belongs to NEITHER range: caller-owned, but
|
|
@@ -197,13 +313,23 @@ export interface LiveInModel {
|
|
|
197
313
|
* saved the register (so it is one the compiler was free to home a local in). The ABI half alone
|
|
198
314
|
* describes the CALLER, and asm that follows no ABI — hand-written, or a mid-function fragment,
|
|
199
315
|
* which klonoa's `bl`-as-a-long-branch splits produce for real — is genuinely handed live values
|
|
200
|
-
* in registers it never saved. Passing the ABI list unfiltered
|
|
201
|
-
* `ChnVolSetAsm`
|
|
316
|
+
* in registers it never saved. Passing the ABI list unfiltered left the MP2K engine's
|
|
317
|
+
* `ChnVolSetAsm` storing through `uninit_r4`, silently; such a read is
|
|
318
|
+
* `unsavedCalleeSavedRegs`' instead.
|
|
202
319
|
*
|
|
203
|
-
* The save is a MEASUREMENT, like the frame's, and belongs to whoever can make it
|
|
204
|
-
*
|
|
320
|
+
* The save is a MEASUREMENT, like the frame's, and belongs to whoever can make it. Thumb reads
|
|
321
|
+
* its `push`es (`savedRegs`; past the prologue, `bodySavedRegs`). PPC reads every save in the
|
|
322
|
+
* entry block of a register nothing before it named (`prologueSavedRegs`), because mwcc
|
|
323
|
+
* interleaves its saves with the body (`stw r31; mr r31,r3; stw r30`); "nothing named it first"
|
|
324
|
+
* is the condition Thumb's leading run meets by construction; MIPS reads its entry block the same way
|
|
325
|
+
* (frontend/mips.ts `prologueSavedRegs`). A frontend that cannot measure it passes nothing here and
|
|
205
326
|
* keeps the parameter it would have got anyway.
|
|
206
327
|
*
|
|
328
|
+
* The one register class exempt from the save is the ABI's scratch set (`target.scratchRegs`):
|
|
329
|
+
* a callee may use one with no save at all, so demanding one would refuse a local the compiler
|
|
330
|
+
* homed there with no prologue (agbcc's `ip`, mwcc's `r0`). `registerLiveIns` composes both
|
|
331
|
+
* halves for every frontend, so this list is never assembled by hand.
|
|
332
|
+
*
|
|
207
333
|
* The frame's sole-writer obligation has no counterpart here and needs none: a register has no
|
|
208
334
|
* address, so nothing outside this function can name it and there is no escape to retract.
|
|
209
335
|
*
|
|
@@ -214,6 +340,41 @@ export interface LiveInModel {
|
|
|
214
340
|
*
|
|
215
341
|
* Declaring this obliges `argRegs` below, and the two must be DISJOINT. */
|
|
216
342
|
uninitRegs?: readonly string[];
|
|
343
|
+
/** Of `uninitRegs`, the ones something outside the C convention hands an input in
|
|
344
|
+
* (`target.hiddenInputRegs`). The entry value of one must reach A JOIN that also takes a
|
|
345
|
+
* definition the entry value does not feed — the shape of a local the compiler assigned on one
|
|
346
|
+
* arm, which may also be read before that assignment. A join alone is not enough: an input
|
|
347
|
+
* carried round a loop meets its own next value at the loop header. Meeting no such join, it is
|
|
348
|
+
* the input, and `finish` refuses rather than spell it `uninit_<reg>`. */
|
|
349
|
+
hiddenInputRegs?: readonly string[];
|
|
350
|
+
/** Registers the ABI requires a callee to PRESERVE (`target.nonArgRegs` less `target.scratchRegs`)
|
|
351
|
+
* that this function did NOT save — the third side of the register partition. No caller passes
|
|
352
|
+
* an argument in one, and the compiler homed no local of this function there, so a value it holds
|
|
353
|
+
* on entry is one the function shares with every other: a FILE-SCOPE GLOBAL REGISTER VARIABLE
|
|
354
|
+
* (`register int g asm("r8");`), which is exactly what agbcc, gcc 2.7.2, KMC and mwcc 2.4 compile
|
|
355
|
+
* `return g;` to (`mov r0, r8`, `move v0,s0`, `mr r3,r14`). gcc's `globalize_reg` (regclass.c)
|
|
356
|
+
* marks such a register fixed and call-used, so no prologue saves it. A def-less read of one
|
|
357
|
+
* refuses: naming it a parameter puts a value in the signature that a C caller would pass in an
|
|
358
|
+
* argument register. A WRITE to one is visible to the caller just the same. Both refuse in
|
|
359
|
+
* `finish`, of the finished function — unless the compiler can spell the global, which is
|
|
360
|
+
* `globalRegs`.
|
|
361
|
+
*
|
|
362
|
+
* Disjoint from `argRegs` and `uninitRegs`, checked with them. */
|
|
363
|
+
unsavedCalleeSavedRegs?: readonly string[];
|
|
364
|
+
/** Of `unsavedCalleeSavedRegs`, the ones the function does save or restore, but not around every
|
|
365
|
+
* use or not restored at every return (a `pop` with no `push` before it, a `push` past a read, a
|
|
366
|
+
* use past the restore). Refused like the rest, and named apart only so the refusal says what it
|
|
367
|
+
* saw. */
|
|
368
|
+
misSavedRegs?: readonly string[];
|
|
369
|
+
/** Where the frontend looks for the saves it measures, for those refusals to name: the partition
|
|
370
|
+
* is only as wide as that look, and a save it does not read is still a save. */
|
|
371
|
+
saveSite?: string;
|
|
372
|
+
/** The registers of `unsavedCalleeSavedRegs`' kind the target can declare a global register
|
|
373
|
+
* variable in (`target.globalRegisterVariables`), each to the name `__asm__` takes. Every read of
|
|
374
|
+
* one is a load of that global and every write a store to it, so a call between two reads may
|
|
375
|
+
* change it, as it may change any global. Disjoint from the other three lists, checked with
|
|
376
|
+
* them. */
|
|
377
|
+
globalRegs?: ReadonlyMap<string, string>;
|
|
217
378
|
/** Registers the ABI DOES pass arguments in — the other side of the register partition, and the
|
|
218
379
|
* only reason the side above is checkable rather than believed. The frame coordinate declares
|
|
219
380
|
* both of its sides and refuses an offset in neither; this one declares both and refuses a key
|
|
@@ -225,6 +386,13 @@ export interface LiveInModel {
|
|
|
225
386
|
* `readRecursive` cannot catch that on its own — it never sees the argument registers, because
|
|
226
387
|
* a read of one takes the parameter path by falling through every other case. */
|
|
227
388
|
argRegs?: readonly string[];
|
|
389
|
+
/** The argument words the function's own declaration fills (`proto.ts` `ownArgumentWords`), where
|
|
390
|
+
* it states them. A register past them is in `uninitRegs`; an INCOMING STACK ARGUMENT at or past
|
|
391
|
+
* them is refused, because no local lives in the caller's frame and the read says the
|
|
392
|
+
* declaration is wrong. Taken as a parameter it would be named by position, one slot below the
|
|
393
|
+
* registers the declaration left out: `int st2(int a,int b,int c,int d,int e){ return g(a)+e; }`
|
|
394
|
+
* declared with two parameters would lift `st2(a0, a1, a2)`, returning the fifth argument as `a2`. */
|
|
395
|
+
ownArgWords?: number;
|
|
228
396
|
}
|
|
229
397
|
|
|
230
398
|
/** The register partition's postcondition, checked at the point of USE. `uninitRegs` asserts "no
|
|
@@ -250,9 +418,203 @@ function checkedLiveInModel(fnName: string, m: LiveInModel): LiveInModel {
|
|
|
250
418
|
`and one the ABI does not pass arguments in`,
|
|
251
419
|
);
|
|
252
420
|
}
|
|
421
|
+
const unsaved = [...(m.unsavedCalleeSavedRegs ?? []), ...(m.globalRegs?.keys() ?? [])];
|
|
422
|
+
const twice = unsaved.filter((r, i) => args.includes(r) || m.uninitRegs?.includes(r) || unsaved.indexOf(r) !== i);
|
|
423
|
+
if (twice.length > 0) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`lifting '${fnName}': the live-in model lists ${twice.join(', ')} as an unsaved callee-saved ` +
|
|
426
|
+
`register AND as an argument register or an uninitialised local's home`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
253
429
|
return m;
|
|
254
430
|
}
|
|
255
431
|
|
|
432
|
+
/** A `scratchRegs` entry outside `nonArgRegs` is inert, and inert is how a partition rots: the list
|
|
433
|
+
* would go on reading as if it exempted something. Refused as a target bug, like the
|
|
434
|
+
* argRegs/uninitRegs contradiction `checkedLiveInModel` refuses, rather than declined as a property
|
|
435
|
+
* of the input. */
|
|
436
|
+
export function assertScratchRegsPartitioned(target: {
|
|
437
|
+
id: string;
|
|
438
|
+
nonArgRegs?: readonly string[];
|
|
439
|
+
scratchRegs?: readonly string[];
|
|
440
|
+
}): ReadonlySet<string> {
|
|
441
|
+
const scratchRegs = new Set(target.scratchRegs ?? []);
|
|
442
|
+
for (const r of scratchRegs) {
|
|
443
|
+
if (!(target.nonArgRegs ?? []).includes(r)) {
|
|
444
|
+
throw new Error(`target '${target.id}': scratch register ${r} is not among the non-argument registers`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return scratchRegs;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** ONE SPELLING PER REGISTER, WHERE A FUNCTION WRITES ONE REGISTER TWO WAYS. The builder keys a
|
|
451
|
+
* register by its operand text, and so does every save measurement a frontend makes, so `sl` and
|
|
452
|
+
* `r10` (or O32's `s8` and `fp`) would be two variables and two partition entries for one register:
|
|
453
|
+
* `mov sl, r0; mov r0, r10` would return a second parameter where the machine returns the first,
|
|
454
|
+
* and a register saved as `s8` and written as `fp` would read as never saved, its writes as a
|
|
455
|
+
* global's. `pairs`
|
|
456
|
+
* is each register's two spellings, the one kept first. Only a function that names BOTH of a pair
|
|
457
|
+
* is rewritten — every other keeps the spelling it was written in, and with it the name of any local
|
|
458
|
+
* homed there — and an alias is rewritten wherever it stands as a whole word of an operand, so a
|
|
459
|
+
* memory operand's base (`8(fp)`) is too. */
|
|
460
|
+
export function unifyRegisterSpellings(
|
|
461
|
+
instrs: Iterable<{ ops: string[] }>,
|
|
462
|
+
pairs: readonly (readonly [keep: string, alias: string])[],
|
|
463
|
+
): void {
|
|
464
|
+
const all = [...instrs];
|
|
465
|
+
const words = new Set(all.flatMap((ins) => ins.ops.flatMap((op) => op.match(/[A-Za-z_$.][\w$.]*/g) ?? [])));
|
|
466
|
+
for (const [keep, alias] of pairs) {
|
|
467
|
+
if (!words.has(keep) || !words.has(alias)) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
const re = new RegExp(`(?<![\\w$.])${alias}(?![\\w$.])`, 'g');
|
|
471
|
+
for (const ins of all) {
|
|
472
|
+
ins.ops = ins.ops.map((op) => op.replace(re, keep));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** THE REGISTER HALF OF THE LIVE-IN MODEL (`LiveInModel.uninitRegs` and `argRegs`), composed here
|
|
478
|
+
* once for every frontend that claims it, so that no frontend can drop a half of the rule. A
|
|
479
|
+
* register is an uninitialised local's home when the ABI passes no argument in it
|
|
480
|
+
* (`target.nonArgRegs`) AND either the ABI lets a callee use it with no save (`target.scratchRegs`,
|
|
481
|
+
* checked against `nonArgRegs`) or this function saved it — `savedRegs`, the frontend's measurement
|
|
482
|
+
* of its saves. The rest — callee-saved, and not saved — is `globalRegs` where the target can
|
|
483
|
+
* declare a global register variable there and `unsavedCalleeSavedRegs` where it cannot, or where
|
|
484
|
+
* the function saves or restores it but not around every use of it (`misSaved`, a measurement only
|
|
485
|
+
* Thumb makes, whose `push` and `pop` are nothing but saves and restores): compiled code never saves
|
|
486
|
+
* a global register variable's register, and asm that saves one restores it too, so its writes are
|
|
487
|
+
* not the caller's and its entry value is no global's. A target stating no `nonArgRegs` claims no
|
|
488
|
+
* partition and gets an empty model, so every def-less read stays a parameter.
|
|
489
|
+
*
|
|
490
|
+
* THE FUNCTION'S OWN DECLARATION MOVES THE PARTITION'S EDGE. `ownWords` is how many argument
|
|
491
|
+
* words it can fill (`proto.ts` `ownArgumentWords`, an upper bound), and an argument register at
|
|
492
|
+
* or past it is one no caller fills for THIS function, so it moves from `argRegs` to `uninitRegs`.
|
|
493
|
+
* It needs no save: every argument register is one a callee may use unsaved (`clobberedByCall`
|
|
494
|
+
* checks they are caller-saved). KMC homes a local in `a2` past `uninit_spill(int k, int *p)`, which
|
|
495
|
+
* without the edge reads as a third parameter. Absent (no declaration, or one that bounds
|
|
496
|
+
* nothing), the ABI's whole list stays arguments. */
|
|
497
|
+
export function registerLiveIns(
|
|
498
|
+
target: {
|
|
499
|
+
id: string;
|
|
500
|
+
argRegs: readonly string[];
|
|
501
|
+
nonArgRegs?: readonly string[];
|
|
502
|
+
scratchRegs?: readonly string[];
|
|
503
|
+
hiddenInputRegs?: readonly string[];
|
|
504
|
+
globalRegisterVariables?: { regs: Readonly<Record<string, string>> };
|
|
505
|
+
},
|
|
506
|
+
savedRegs: ReadonlySet<string>,
|
|
507
|
+
ownWords?: number,
|
|
508
|
+
misSaved: ReadonlySet<string> = new Set(),
|
|
509
|
+
): Pick<
|
|
510
|
+
LiveInModel,
|
|
511
|
+
| 'uninitRegs'
|
|
512
|
+
| 'argRegs'
|
|
513
|
+
| 'hiddenInputRegs'
|
|
514
|
+
| 'ownArgWords'
|
|
515
|
+
| 'unsavedCalleeSavedRegs'
|
|
516
|
+
| 'misSavedRegs'
|
|
517
|
+
| 'globalRegs'
|
|
518
|
+
> {
|
|
519
|
+
const scratchRegs = assertScratchRegsPartitioned(target);
|
|
520
|
+
for (const r of target.hiddenInputRegs ?? []) {
|
|
521
|
+
if (!scratchRegs.has(r)) {
|
|
522
|
+
throw new Error(`target '${target.id}': hidden-input register ${r} is not among the scratch registers`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (target.nonArgRegs === undefined) {
|
|
526
|
+
return {};
|
|
527
|
+
}
|
|
528
|
+
const filled = ownWords === undefined ? target.argRegs.length : Math.min(ownWords, target.argRegs.length);
|
|
529
|
+
const unsaved = target.nonArgRegs.filter((r) => !scratchRegs.has(r) && !savedRegs.has(r));
|
|
530
|
+
const spelled = target.globalRegisterVariables?.regs ?? {};
|
|
531
|
+
return {
|
|
532
|
+
uninitRegs: [
|
|
533
|
+
...target.nonArgRegs.filter((r) => scratchRegs.has(r) || savedRegs.has(r)),
|
|
534
|
+
...target.argRegs.slice(filled),
|
|
535
|
+
],
|
|
536
|
+
argRegs: target.argRegs.slice(0, filled),
|
|
537
|
+
hiddenInputRegs: target.hiddenInputRegs,
|
|
538
|
+
unsavedCalleeSavedRegs: unsaved.filter((r) => !Object.hasOwn(spelled, r) || misSaved.has(r)),
|
|
539
|
+
misSavedRegs: unsaved.filter((r) => misSaved.has(r)),
|
|
540
|
+
globalRegs: new Map(
|
|
541
|
+
unsaved.filter((r) => Object.hasOwn(spelled, r) && !misSaved.has(r)).map((r) => [r, spelled[r]]),
|
|
542
|
+
),
|
|
543
|
+
...(ownWords === undefined ? {} : { ownArgWords: ownWords }),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** REFUSE A WRITE TO A SAVED REGISTER THAT THE CALLER SEES. A register the frontend read as saved
|
|
548
|
+
* (its save store, `LiveInModel.uninitRegs`) is a local's home, and the lift drops what is left
|
|
549
|
+
* in it at a return — right only if every path restores it after its last write. A write past the
|
|
550
|
+
* restore (`lw s0,4(sp); move s0,a0; jr ra`), a reload from a word that is not its save, or a
|
|
551
|
+
* return its restore never reaches leaves the caller holding a value the C never assigns, so the
|
|
552
|
+
* function refuses here rather than drop it. MIPS and PowerPC read a save as an ordinary store in
|
|
553
|
+
* the entry block, so this is asked once the blocks are filled; Thumb asks the same of its
|
|
554
|
+
* `push`/`pop` pairs in `bodySavedRegs`.
|
|
555
|
+
*
|
|
556
|
+
* A forward may-dataflow over the asm: a register is DIRTY from a write (`writes`) until a restore
|
|
557
|
+
* of it (`restores`: the instructions the frontend lowered as reloading its save word), and dirty
|
|
558
|
+
* at the end of a block that returns is the refusal. Compiled code restores every saved register
|
|
559
|
+
* in its epilogue, after its last write, so it never fires there; a path that never writes the
|
|
560
|
+
* register needs no restore. */
|
|
561
|
+
export function refuseSavedRegisterLeaks<I extends { addr: number }>(
|
|
562
|
+
name: string,
|
|
563
|
+
blocks: readonly { instrs: readonly I[]; returns: boolean }[],
|
|
564
|
+
preds: readonly (readonly number[])[],
|
|
565
|
+
saved: ReadonlySet<string>,
|
|
566
|
+
writes: (ins: I, reg: string) => boolean,
|
|
567
|
+
restores: ReadonlyMap<I, ReadonlySet<string>>,
|
|
568
|
+
): void {
|
|
569
|
+
if (saved.size === 0) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
// register → the address of a write no restore has answered since, on some path
|
|
573
|
+
const outs: (Map<string, number> | null)[] = blocks.map(() => null);
|
|
574
|
+
const transfer = (b: number, dirty: Map<string, number>) => {
|
|
575
|
+
for (const ins of blocks[b].instrs) {
|
|
576
|
+
for (const r of saved) {
|
|
577
|
+
if (restores.get(ins)?.has(r)) {
|
|
578
|
+
dirty.delete(r);
|
|
579
|
+
} else if (writes(ins, r)) {
|
|
580
|
+
dirty.set(r, ins.addr);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return dirty;
|
|
585
|
+
};
|
|
586
|
+
const inOf = (b: number) => {
|
|
587
|
+
const dirty = new Map<string, number>();
|
|
588
|
+
for (const p of preds[b]) {
|
|
589
|
+
outs[p]?.forEach((at, r) => dirty.has(r) || dirty.set(r, at));
|
|
590
|
+
}
|
|
591
|
+
return dirty;
|
|
592
|
+
};
|
|
593
|
+
for (let changed = true; changed;) {
|
|
594
|
+
changed = false;
|
|
595
|
+
blocks.forEach((_, b) => {
|
|
596
|
+
if (b !== 0 && !preds[b].some((p) => outs[p] !== null)) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const out = transfer(b, inOf(b));
|
|
600
|
+
if (outs[b] === null || out.size !== outs[b]!.size) {
|
|
601
|
+
outs[b] = out;
|
|
602
|
+
changed = true;
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
blocks.forEach((blk, b) => {
|
|
607
|
+
const [leak] = blk.returns ? [...(outs[b] ?? [])] : [];
|
|
608
|
+
if (leak !== undefined) {
|
|
609
|
+
throw new FrontendUnsupportedError(
|
|
610
|
+
`cannot lift '${name}': the write to saved ${leak[0]} at 0x${leak[1].toString(16)} reaches a return ` +
|
|
611
|
+
`with no restore of ${leak[0]} after it, so the caller sees it — no C local spells a write ` +
|
|
612
|
+
'that outlives the function',
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
256
618
|
/** A read whose register may be holding a callee's leftover — see `refuseStaleCallerSavedReads`. */
|
|
257
619
|
interface StaleRead {
|
|
258
620
|
reg: string;
|
|
@@ -264,6 +626,10 @@ interface StaleRead {
|
|
|
264
626
|
fromOp: number;
|
|
265
627
|
/** a call in this very block destroyed it, so the predecessors have nothing to add. */
|
|
266
628
|
local: boolean;
|
|
629
|
+
/** The OTHER keys this block wrote the read's value into after it — a `move` or a slot store of
|
|
630
|
+
* the destroyed register, which carries the callee's leftover out of the block under a name no
|
|
631
|
+
* call destroyed. */
|
|
632
|
+
carriedBy: Set<string>;
|
|
267
633
|
}
|
|
268
634
|
|
|
269
635
|
/** The registers a call leaves holding nothing the CALLER can name — what {@link SsaBuilder.noteCall}
|
|
@@ -290,6 +656,57 @@ export function clobberedByCall(target: {
|
|
|
290
656
|
return target.callerSaved.filter((r) => r !== target.returnReg);
|
|
291
657
|
}
|
|
292
658
|
|
|
659
|
+
/** One asm block as {@link recordAsmLayout} reads it: its instructions without its control transfer,
|
|
660
|
+
* and whether that transfer is an unconditional jump to a label. `undefined` for a block the
|
|
661
|
+
* frontend made up, which has no place in the asm. An instruction's `reloc` is the relocation that
|
|
662
|
+
* fills it, where the operands do not name the symbol (disasm.ts `DisasmInstr.reloc`). */
|
|
663
|
+
export interface AsmBlockText {
|
|
664
|
+
readonly body: readonly {
|
|
665
|
+
readonly mnemonic: string;
|
|
666
|
+
readonly ops: readonly string[];
|
|
667
|
+
readonly reloc?: { readonly sym: string; readonly addend: number };
|
|
668
|
+
}[];
|
|
669
|
+
readonly endsInJump: boolean;
|
|
670
|
+
/** which instructions of `body` make a call, which only the frontend's ISA knows */
|
|
671
|
+
readonly isCall: (ins: AsmBlockText['body'][number]) => boolean;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** One instruction of {@link AsmBlockText} as the layout record spells it: `mnemonic op,op`, then the
|
|
675
|
+
* symbol its relocation fills in, if any. In a relocatable object's disassembly the operands of a
|
|
676
|
+
* global's load or store, or of a call, read the same whichever symbol it is: gcc 2.7.2's `lui
|
|
677
|
+
* at,0x0` / `sw a2,0(at)` for `gT = c` and for `gW = c`, and `jal 0 <f>` for every callee. Spelled
|
|
678
|
+
* without the symbol, two arms that store to different globals would read as identical bodies. */
|
|
679
|
+
function layoutText(ins: AsmBlockText['body'][number]): string {
|
|
680
|
+
const text = ins.ops.length ? `${ins.mnemonic} ${ins.ops.join(',')}` : ins.mnemonic;
|
|
681
|
+
if (ins.reloc === undefined) {
|
|
682
|
+
return text;
|
|
683
|
+
}
|
|
684
|
+
const addend = ins.reloc.addend === 0 ? '' : `+0x${ins.reloc.addend.toString(16)}`;
|
|
685
|
+
return `${text} <${ins.reloc.sym}${addend}>`;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** The layout record (ir/core.ts `AsmLayout`): asm block `i` is `irBlocks[i]`, which is how every
|
|
689
|
+
* frontend's builder numbers them, and its index is its position among the blocks that have a
|
|
690
|
+
* place in the asm. */
|
|
691
|
+
export function recordAsmLayout(
|
|
692
|
+
irBlocks: readonly Block[],
|
|
693
|
+
asmBlocks: readonly (AsmBlockText | undefined)[],
|
|
694
|
+
): AsmLayout {
|
|
695
|
+
const layout: AsmLayout = new Map();
|
|
696
|
+
let index = 0;
|
|
697
|
+
asmBlocks.forEach((ab, i) => {
|
|
698
|
+
if (ab !== undefined) {
|
|
699
|
+
layout.set(irBlocks[i], {
|
|
700
|
+
index: index++,
|
|
701
|
+
body: ab.body.map(layoutText),
|
|
702
|
+
calls: ab.body.flatMap((ins, at) => (ab.isCall(ins) ? [at] : [])),
|
|
703
|
+
endsInJump: ab.endsInJump,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
return layout;
|
|
708
|
+
}
|
|
709
|
+
|
|
293
710
|
export function makeSsaBuilder(
|
|
294
711
|
name: string,
|
|
295
712
|
blockCount: number,
|
|
@@ -301,6 +718,10 @@ export function makeSsaBuilder(
|
|
|
301
718
|
* phi. A register FILE, not a register: the FPU's keys are floats wherever they arrive, and every
|
|
302
719
|
* other key is `unknown` for type recovery to settle. */
|
|
303
720
|
keyType: (key: string) => IrType | undefined = () => undefined,
|
|
721
|
+
/** `LiveInModel.globalRegs`, which every read and write consults — so it is handed over when the
|
|
722
|
+
* builder is made, not when the model is first used. The frontend passes the same map in its
|
|
723
|
+
* model, where it is checked against the other lists. */
|
|
724
|
+
globalRegs: ReadonlyMap<string, string> = new Map(),
|
|
304
725
|
): SsaBuilder {
|
|
305
726
|
const unset = (key: string): IrType => keyType(key) ?? T.unk(32);
|
|
306
727
|
let modelMemo: LiveInModel | null = null;
|
|
@@ -309,7 +730,33 @@ export function makeSsaBuilder(
|
|
|
309
730
|
// function that reads the mis-listed register. A contradictory or half-declared partition is a
|
|
310
731
|
// bug in the TARGET, not an unliftable function, so it throws a plain Error: a decline would
|
|
311
732
|
// report the target's typo as a property of the input, once per function, forever.
|
|
312
|
-
const model = (): LiveInModel => (modelMemo ??= checkedLiveInModel(name, liveInOf()));
|
|
733
|
+
const model = (): LiveInModel => (modelMemo ??= sameGlobalRegs(checkedLiveInModel(name, liveInOf())));
|
|
734
|
+
// The model's `globalRegs` is the copy checked against the other lists, and the builder's own the
|
|
735
|
+
// one every read and write consults: two copies of one fact, which must be one.
|
|
736
|
+
const sameGlobalRegs = (m: LiveInModel): LiveInModel => {
|
|
737
|
+
const theirs = m.globalRegs ?? new Map<string, string>();
|
|
738
|
+
if (theirs.size !== globalRegs.size || [...theirs].some(([r, n]) => globalRegs.get(r) !== n)) {
|
|
739
|
+
throw new Error(`lifting '${name}': the live-in model's global registers are not the ones the builder was given`);
|
|
740
|
+
}
|
|
741
|
+
return m;
|
|
742
|
+
};
|
|
743
|
+
// What the refusals of unsaved callee-saved registers say of their saves (`misSavedRegs`).
|
|
744
|
+
const unsavedWhy = (regs: readonly string[]): string => {
|
|
745
|
+
const mis = regs.filter((r) => model().misSavedRegs?.includes(r));
|
|
746
|
+
const never = regs.filter((r) => !mis.includes(r));
|
|
747
|
+
return [
|
|
748
|
+
...(mis.length
|
|
749
|
+
? [
|
|
750
|
+
`saves or restores ${mis.join(', ')} but not around every use of it, restored at every return, on every path`,
|
|
751
|
+
]
|
|
752
|
+
: []),
|
|
753
|
+
...(never.length ? [`no ${model().saveSite ?? 'save this lift reads'} saves ${never.join(', ')}`] : []),
|
|
754
|
+
].join(', and ');
|
|
755
|
+
};
|
|
756
|
+
// What compiled code means by such a register — but not by one the function saves and breaks the
|
|
757
|
+
// save of (`misSavedRegs`), which the measurement has shown is no global's.
|
|
758
|
+
const globalNote = (regs: readonly string[]): string =>
|
|
759
|
+
regs.some((r) => model().misSavedRegs?.includes(r)) ? '' : ' (a global register variable)';
|
|
313
760
|
const inRange = (off: number, r?: { from: number; to: number }) => r !== undefined && off >= r.from && off < r.to;
|
|
314
761
|
const irBlocks: Block[] = Array.from({ length: blockCount }, () => ({ params: [] as Value[], ops: [] }));
|
|
315
762
|
// `writeOrder` and `slotHomes` are filled in below, where the builder's counters live.
|
|
@@ -320,6 +767,7 @@ export function makeSsaBuilder(
|
|
|
320
767
|
slotHomes: undefined,
|
|
321
768
|
paramEvidence: undefined,
|
|
322
769
|
localObjects: undefined,
|
|
770
|
+
asmLayout: undefined,
|
|
323
771
|
};
|
|
324
772
|
|
|
325
773
|
const defs: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
@@ -331,6 +779,9 @@ export function makeSsaBuilder(
|
|
|
331
779
|
// arrives as a PHI — which is what happens when the entry block is itself a loop header — is
|
|
332
780
|
// invisible to the escape check below. Braun's construction gives no other way to tell.
|
|
333
781
|
const phiKey = new Map<Value, string>();
|
|
782
|
+
// The entry value of a register a runtime hands a hidden input in (`LiveInModel.hiddenInputRegs`),
|
|
783
|
+
// minted as an uninitialised local, for `finish` to check that a join takes it.
|
|
784
|
+
const hiddenEntryValues = new Map<Value, string>();
|
|
334
785
|
const paramReg = new Map<Value, string>();
|
|
335
786
|
// Parameters created by ensureParam that nothing has read yet. They are deliberately NOT in
|
|
336
787
|
// `defs`: a parameter asserted because a calling convention proves it exists is not evidence that
|
|
@@ -343,6 +794,16 @@ export function makeSsaBuilder(
|
|
|
343
794
|
|
|
344
795
|
// `preds` lists an entry per CFG EDGE; these are the distinct predecessor BLOCKS.
|
|
345
796
|
const distinctPreds = (b: number): number[] => [...new Set(preds[b])];
|
|
797
|
+
// the distinct successor blocks of each, built on first ask (`readAfterEveryReachingWrite`)
|
|
798
|
+
let successorsMemo: number[][] | undefined;
|
|
799
|
+
const successors = (): number[][] => {
|
|
800
|
+
if (successorsMemo === undefined) {
|
|
801
|
+
const succ: number[][] = irBlocks.map(() => []);
|
|
802
|
+
irBlocks.forEach((_, s) => distinctPreds(s).forEach((p) => succ[p].push(s)));
|
|
803
|
+
successorsMemo = succ;
|
|
804
|
+
}
|
|
805
|
+
return successorsMemo;
|
|
806
|
+
};
|
|
346
807
|
|
|
347
808
|
// CALLER-SAVED CLOBBER, for guessed call arities (see trimClobberedCallArgs). Tracked HERE
|
|
348
809
|
// because every register write in every frontend already goes through `writeVar`: a frontend
|
|
@@ -363,7 +824,20 @@ export function makeSsaBuilder(
|
|
|
363
824
|
// predecessors.
|
|
364
825
|
const clobberedLocal: Array<Set<string>> = irBlocks.map(() => new Set());
|
|
365
826
|
const decidedLocal: Array<Set<string>> = irBlocks.map(() => new Set());
|
|
827
|
+
// `imageLocal[b]`: holding a struct's image a call in `b` handed back, with nothing written since
|
|
828
|
+
// (`noteStructImage`) — destroyed for a guessed arity alone
|
|
829
|
+
const imageLocal: Array<Set<string>> = irBlocks.map(() => new Set());
|
|
830
|
+
const structImages = new Set<Value>();
|
|
366
831
|
const staleCandidates: StaleRead[] = [];
|
|
832
|
+
const staleByValue = new Map<Value, StaleRead[]>();
|
|
833
|
+
/** The op-less uses `useOutsideOps` recorded, each at the op count of its block when it happened. */
|
|
834
|
+
const usesOutsideOps: Array<{ value: Value; block: number; atOp: number }> = [];
|
|
835
|
+
/** The sole-reader obligations `requireSoleReader` recorded, checked in `finish`. */
|
|
836
|
+
const soleReaders: Array<{ value: Value; op: Op; refuse: () => void }> = [];
|
|
837
|
+
/** The reader obligations `requireReader` recorded, checked in `finish`. */
|
|
838
|
+
const readObligations: Array<{ value: Value; refuse: () => void }> = [];
|
|
839
|
+
/** The side-effect values `noteIncidentalWrite` recorded. */
|
|
840
|
+
const incidentalWrites = new Set<Value>();
|
|
367
841
|
// `readVar` runs once per operand, so a memo per (register, verdict) keeps the record list
|
|
368
842
|
// proportional to what a block READS rather than to how many times it reads it. The value a key
|
|
369
843
|
// holds only changes at a write, and the verdict only flips at a call, so a run of identical
|
|
@@ -378,6 +852,13 @@ export function makeSsaBuilder(
|
|
|
378
852
|
// every ISA with no frontend code. Per block, because the consumer asks a per-EDGE question.
|
|
379
853
|
const writeCount: number[] = irBlocks.map(() => 0);
|
|
380
854
|
const lastWriteAt: Array<Map<string, number>> = irBlocks.map(() => new Map());
|
|
855
|
+
// the ops block `b` held at each register's last write in it (`lastWriteOp`)
|
|
856
|
+
const lastWriteOp: Array<Map<string, number>> = irBlocks.map(() => new Map());
|
|
857
|
+
// the value an instruction last read of each register block `b` read since its last write or the
|
|
858
|
+
// last call destroying it there (since the block's start where neither), and the value it first
|
|
859
|
+
// read of each before writing it or a call destroying it: `readAfterEveryReachingWrite`
|
|
860
|
+
const readSinceWrite: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
861
|
+
const readBeforeWrite: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
381
862
|
const writeOrder: WriteOrder = { lastWrite: new Map(), writes: new Map() };
|
|
382
863
|
fn.writeOrder = writeOrder;
|
|
383
864
|
|
|
@@ -499,23 +980,101 @@ export function makeSsaBuilder(
|
|
|
499
980
|
}
|
|
500
981
|
};
|
|
501
982
|
|
|
983
|
+
const frameWrites = new Set<Value>();
|
|
984
|
+
// Every key some instruction wrote, for `finish` to find a write to an unsaved callee-saved
|
|
985
|
+
// register (`LiveInModel.unsavedCalleeSavedRegs`).
|
|
986
|
+
const writtenKeys = new Set<string>();
|
|
987
|
+
// A GLOBAL REGISTER VARIABLE (`LiveInModel.globalRegs`) is memory, not an SSA variable: each
|
|
988
|
+
// read loads the global and each write stores it, at the point the instruction runs, so nothing
|
|
989
|
+
// is forwarded across a call that may change it and the caller sees every write. Appended to the
|
|
990
|
+
// block being filled, which is where a frontend puts the op that reads or writes the register.
|
|
991
|
+
const globalLoads = new Set<Value>();
|
|
992
|
+
const globalCell = (reg: string, b: number): Value => {
|
|
993
|
+
if (filled[b]) {
|
|
994
|
+
throw new Error(`lifting '${name}': global register ${reg} accessed in block ${b} after it was filled`);
|
|
995
|
+
}
|
|
996
|
+
const asmName = globalRegs.get(reg)!;
|
|
997
|
+
const cell = mkValue(T.unk(32));
|
|
998
|
+
irBlocks[b].ops.push(
|
|
999
|
+
mkOp('gaddr', { results: [cell], attrs: { sym: globalRegisterSymbol(asmName), globalRegister: asmName } }),
|
|
1000
|
+
);
|
|
1001
|
+
return cell;
|
|
1002
|
+
};
|
|
1003
|
+
// A load of one nothing reads is a frontend reading the register for a question of its own (MIPS
|
|
1004
|
+
// asks of an `addu` operand more than once), not an instruction's read: reading a register does
|
|
1005
|
+
// nothing, so it goes, with its address.
|
|
1006
|
+
const dropUnreadGlobalRegisterLoads = (): void => {
|
|
1007
|
+
if (globalRegs.size === 0) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const read = new Set<Value>();
|
|
1011
|
+
for (const blk of irBlocks) {
|
|
1012
|
+
for (const op of blk.ops) {
|
|
1013
|
+
op.operands.forEach((v) => read.add(v));
|
|
1014
|
+
op.successors.forEach((s) => s.args.forEach((v) => read.add(v)));
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
for (const blk of irBlocks) {
|
|
1018
|
+
const dead = new Set<Value>();
|
|
1019
|
+
for (const op of blk.ops) {
|
|
1020
|
+
if (op.opcode === 'load' && !read.has(op.results[0]) && globalLoads.has(op.results[0])) {
|
|
1021
|
+
dead.add(op.results[0]);
|
|
1022
|
+
dead.add(op.operands[0]);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
blk.ops = blk.ops.filter((op) => !op.results.some((r) => dead.has(r)));
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
502
1028
|
const writeVar = (reg: string, b: number, v: Value) => {
|
|
1029
|
+
if (globalRegs.has(reg)) {
|
|
1030
|
+
irBlocks[b].ops.push(mkOp('store', { operands: [globalCell(reg, b), v], attrs: { off: 0, width: 4 } }));
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
writtenKeys.add(reg);
|
|
1034
|
+
if (slotKeyOffset(reg) !== null || reg.startsWith(STACK_ARG_PREFIX)) {
|
|
1035
|
+
frameWrites.add(v);
|
|
1036
|
+
}
|
|
503
1037
|
noteSlotHome(reg, v);
|
|
504
1038
|
noteSlotTraffic(reg, v);
|
|
1039
|
+
for (const c of staleByValue.get(v) ?? []) {
|
|
1040
|
+
if (c.block === b && c.reg !== reg) {
|
|
1041
|
+
c.carriedBy.add(reg);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
505
1044
|
if (b === 0 && !firstEntryWrite.has(reg)) {
|
|
506
1045
|
firstEntryWrite.set(reg, v);
|
|
507
1046
|
}
|
|
508
1047
|
writtenSinceCall[b].add(reg);
|
|
509
1048
|
clobberedLocal[b].delete(reg);
|
|
1049
|
+
imageLocal[b].delete(reg);
|
|
510
1050
|
decidedLocal[b].add(reg);
|
|
511
1051
|
defs[b].set(reg, v);
|
|
512
1052
|
lastWriteAt[b].set(reg, writeCount[b]++);
|
|
1053
|
+
lastWriteOp[b].set(reg, irBlocks[b].ops.length);
|
|
1054
|
+
readSinceWrite[b].delete(reg);
|
|
513
1055
|
};
|
|
514
1056
|
const readVar = (reg: string, b: number): Value => {
|
|
1057
|
+
if (globalRegs.has(reg)) {
|
|
1058
|
+
const loaded = mkValue(T.unk(32));
|
|
1059
|
+
globalLoads.add(loaded);
|
|
1060
|
+
const cell = globalCell(reg, b);
|
|
1061
|
+
irBlocks[b].ops.push(
|
|
1062
|
+
mkOp('load', { operands: [cell], results: [loaded], attrs: { off: 0, width: 4, signed: true } }),
|
|
1063
|
+
);
|
|
1064
|
+
return loaded;
|
|
1065
|
+
}
|
|
515
1066
|
const v = readAny(reg, b);
|
|
516
1067
|
noteStaleCandidate(reg, b, v);
|
|
1068
|
+
noteRead(reg, b, v);
|
|
517
1069
|
return v;
|
|
518
1070
|
};
|
|
1071
|
+
// an instruction's read, for `readAfterEveryReachingWrite`
|
|
1072
|
+
const noteRead = (reg: string, b: number, v: Value): void => {
|
|
1073
|
+
readSinceWrite[b].set(reg, v);
|
|
1074
|
+
if (!lastWriteOp[b].has(reg) && !clobberedLocal[b].has(reg) && !readBeforeWrite[b].has(reg)) {
|
|
1075
|
+
readBeforeWrite[b].set(reg, v);
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
519
1078
|
/** The lookup, with no claim attached. The FRONTEND's read is what asserts "this register holds
|
|
520
1079
|
* this value here"; the lookups this construction makes on its own — a single predecessor's
|
|
521
1080
|
* definition, a phi's operand at each in-edge — are how that one read is answered, not further
|
|
@@ -537,7 +1096,9 @@ export function makeSsaBuilder(
|
|
|
537
1096
|
return;
|
|
538
1097
|
}
|
|
539
1098
|
staleSeen[b].set(key, v);
|
|
540
|
-
|
|
1099
|
+
const c: StaleRead = { reg, value: v, block: b, fromOp: irBlocks[b].ops.length, local, carriedBy: new Set() };
|
|
1100
|
+
staleCandidates.push(c);
|
|
1101
|
+
staleByValue.set(v, [...(staleByValue.get(v) ?? []), c]);
|
|
541
1102
|
};
|
|
542
1103
|
|
|
543
1104
|
const newPhi = (reg: string, b: number): Value => {
|
|
@@ -575,6 +1136,10 @@ export function makeSsaBuilder(
|
|
|
575
1136
|
// a measurement: a caller cannot pass a value in a register the ABI does not pass arguments
|
|
576
1137
|
// in, so a read of one before any write is an uninitialised local.
|
|
577
1138
|
const off = slotKeyOffset(reg);
|
|
1139
|
+
if (globalRegs.has(reg)) {
|
|
1140
|
+
// only `readVar` reads one, and never through the SSA: a live-in here would be a parameter
|
|
1141
|
+
throw new Error(`lifting '${name}': global register ${reg} read as an SSA variable`);
|
|
1142
|
+
}
|
|
578
1143
|
if (off !== null && !inRange(off, model().ownedLocals) && !inRange(off, model().callerParams)) {
|
|
579
1144
|
throw new FrontendUnsupportedError(
|
|
580
1145
|
`cannot lift '${name}': ${reg} is read on a path that never stores it, and lies outside ` +
|
|
@@ -587,8 +1152,19 @@ export function makeSsaBuilder(
|
|
|
587
1152
|
const op = mkOp('undef', { results: [mkValue(T.unk(32))], attrs: { key: reg } });
|
|
588
1153
|
irBlocks[b].ops.unshift(op); // ahead of everything in a block that nothing precedes
|
|
589
1154
|
defs[b].set(reg, op.results[0]);
|
|
1155
|
+
if (off === null && model().hiddenInputRegs?.includes(reg)) {
|
|
1156
|
+
hiddenEntryValues.set(op.results[0], reg);
|
|
1157
|
+
}
|
|
590
1158
|
return op.results[0];
|
|
591
1159
|
}
|
|
1160
|
+
const rank = stackArgIndex(reg);
|
|
1161
|
+
const declared = model().ownArgWords;
|
|
1162
|
+
if (rank !== null && declared !== undefined && rank >= declared) {
|
|
1163
|
+
throw new FrontendUnsupportedError(
|
|
1164
|
+
`cannot lift '${name}': it reads incoming stack argument ${rank}, past the ${declared} argument ` +
|
|
1165
|
+
`words its own declaration fills — the declaration and the body disagree`,
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
592
1168
|
// an incoming argument register → function parameter.
|
|
593
1169
|
// If one was already asserted for this key (ensureParam), adopt it — minting a second
|
|
594
1170
|
// parameter for the same key would put the key in the signature twice.
|
|
@@ -702,6 +1278,9 @@ export function makeSsaBuilder(
|
|
|
702
1278
|
if (preds[b].length > 0) {
|
|
703
1279
|
return; // a parameter here is a phi; see the precondition on the interface
|
|
704
1280
|
}
|
|
1281
|
+
if (slotKeyOffset(key) === null && model().uninitRegs?.includes(key)) {
|
|
1282
|
+
return; // no caller fills it (an argument register past the own declaration's words)
|
|
1283
|
+
}
|
|
705
1284
|
for (const p of irBlocks[b].params) {
|
|
706
1285
|
if (paramReg.get(p) === key) {
|
|
707
1286
|
return; // already a parameter, however it got there
|
|
@@ -736,6 +1315,28 @@ export function makeSsaBuilder(
|
|
|
736
1315
|
reaches(reg, b, accept, true);
|
|
737
1316
|
const holdsValue = (reg: string, b: number, accept: (v: Value) => boolean = () => true): boolean =>
|
|
738
1317
|
reaches(reg, b, accept, false);
|
|
1318
|
+
const everyReachingDef = (reg: string, b: number, accept: (v: Value) => boolean): boolean => {
|
|
1319
|
+
if (!filled.every(Boolean)) {
|
|
1320
|
+
throw new Error('everyReachingDef: asked before every block is filled');
|
|
1321
|
+
}
|
|
1322
|
+
// A block met again on a cycle adds no path the first visit did not already judge.
|
|
1323
|
+
const seen = new Set<number>();
|
|
1324
|
+
const walk = (at: number): boolean => {
|
|
1325
|
+
if (clobberedLocal[at].has(reg)) {
|
|
1326
|
+
return false;
|
|
1327
|
+
}
|
|
1328
|
+
const own = defs[at].get(reg);
|
|
1329
|
+
if (own !== undefined) {
|
|
1330
|
+
return accept(own);
|
|
1331
|
+
}
|
|
1332
|
+
if (seen.has(at)) {
|
|
1333
|
+
return true;
|
|
1334
|
+
}
|
|
1335
|
+
seen.add(at);
|
|
1336
|
+
return preds[at].length > 0 && preds[at].every(walk);
|
|
1337
|
+
};
|
|
1338
|
+
return walk(b);
|
|
1339
|
+
};
|
|
739
1340
|
|
|
740
1341
|
const holdsEntryValue = (reg: string, b: number): boolean => {
|
|
741
1342
|
// A predecessor is known only once filled, and `b` itself only up to here — so a path that comes
|
|
@@ -788,10 +1389,10 @@ export function makeSsaBuilder(
|
|
|
788
1389
|
* the UNION, against `trimClobberedCallArgs`'s intersection, and for the opposite reason. That
|
|
789
1390
|
* one proves the caller set a register up, which needs every path to agree; this one proves
|
|
790
1391
|
* nobody can name it, which one path is enough for. Meaningful once every block is filled. */
|
|
791
|
-
const destroyedOnEntry = (): Array<Set<string>> => {
|
|
1392
|
+
const destroyedOnEntry = (images = false): Array<Set<string>> => {
|
|
792
1393
|
const destroyedIn: Array<Set<string>> = irBlocks.map(() => new Set());
|
|
793
1394
|
const outOf = (b: number): Set<string> => {
|
|
794
|
-
const out = new Set(clobberedLocal[b]);
|
|
1395
|
+
const out = new Set([...clobberedLocal[b], ...(images ? imageLocal[b] : [])]);
|
|
795
1396
|
for (const r of destroyedIn[b]) {
|
|
796
1397
|
if (!decidedLocal[b].has(r)) {
|
|
797
1398
|
out.add(r);
|
|
@@ -814,6 +1415,35 @@ export function makeSsaBuilder(
|
|
|
814
1415
|
}
|
|
815
1416
|
return destroyedIn;
|
|
816
1417
|
};
|
|
1418
|
+
/** A destroyed value a copy carries OUT of its block (`StaleRead.carriedBy`) is used where any
|
|
1419
|
+
* block the read's block reaches uses it — an edge argument included, which is how a slot the
|
|
1420
|
+
* copy stored reaches its reload at a join. The destroyed register itself needs none of this:
|
|
1421
|
+
* every block it reaches marks it destroyed on entry (`destroyedOnEntry`) and judges its own
|
|
1422
|
+
* reads. Every later use of the value counts, under whatever name, which can only refuse more. */
|
|
1423
|
+
const carriedOut = (c: StaleRead): boolean => {
|
|
1424
|
+
if (![...c.carriedBy].some((k) => defs[c.block].get(k) === c.value)) {
|
|
1425
|
+
return false;
|
|
1426
|
+
}
|
|
1427
|
+
const index = new Map(irBlocks.map((blk, i) => [blk, i]));
|
|
1428
|
+
const usesIt = (op: Op) => op.operands.includes(c.value) || op.successors.some((s) => s.args.includes(c.value));
|
|
1429
|
+
const seen = new Set<number>();
|
|
1430
|
+
const work = irBlocks[c.block].ops.slice(c.fromOp).flatMap((op) => op.successors.map((s) => s.block));
|
|
1431
|
+
if (irBlocks[c.block].ops.slice(c.fromOp).some(usesIt)) {
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
for (let blk = work.pop(); blk !== undefined; blk = work.pop()) {
|
|
1435
|
+
const b = index.get(blk);
|
|
1436
|
+
if (b === undefined || seen.has(b)) {
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
seen.add(b);
|
|
1440
|
+
if (blk.ops.some(usesIt) || usesOutsideOps.some((u) => u.value === c.value && u.block === b)) {
|
|
1441
|
+
return true;
|
|
1442
|
+
}
|
|
1443
|
+
work.push(...blk.ops.flatMap((op) => op.successors.map((s) => s.block)));
|
|
1444
|
+
}
|
|
1445
|
+
return false;
|
|
1446
|
+
};
|
|
817
1447
|
const refuseStaleCallerSavedReads = () => {
|
|
818
1448
|
if (staleCandidates.length === 0) {
|
|
819
1449
|
return;
|
|
@@ -824,7 +1454,11 @@ export function makeSsaBuilder(
|
|
|
824
1454
|
continue;
|
|
825
1455
|
}
|
|
826
1456
|
const ops = irBlocks[c.block].ops;
|
|
827
|
-
if (
|
|
1457
|
+
if (
|
|
1458
|
+
ops.slice(c.fromOp).some((op) => op.operands.includes(c.value)) ||
|
|
1459
|
+
usesOutsideOps.some((u) => u.value === c.value && u.block === c.block && u.atOp >= c.fromOp) ||
|
|
1460
|
+
carriedOut(c)
|
|
1461
|
+
) {
|
|
828
1462
|
throw new FrontendUnsupportedError(
|
|
829
1463
|
`cannot lift '${name}': ${c.reg} is read on a path where a call has destroyed it, so what ` +
|
|
830
1464
|
`it holds is the callee's and not a value this function named — not modelled`,
|
|
@@ -833,6 +1467,297 @@ export function makeSsaBuilder(
|
|
|
833
1467
|
}
|
|
834
1468
|
};
|
|
835
1469
|
|
|
1470
|
+
/** Where each value is read: the joins it is an edge argument of, and `null` for an op's operand. */
|
|
1471
|
+
const readersOf = (): Map<Value, Set<Value | null>> => {
|
|
1472
|
+
const readers = new Map<Value, Set<Value | null>>();
|
|
1473
|
+
const note = (v: Value, r: Value | null) => (readers.get(v) ?? readers.set(v, new Set()).get(v)!).add(r);
|
|
1474
|
+
for (const blk of irBlocks) {
|
|
1475
|
+
for (const op of blk.ops) {
|
|
1476
|
+
op.operands.forEach((v) => note(v, null));
|
|
1477
|
+
for (const s of op.successors) {
|
|
1478
|
+
s.args.forEach((v, i) => note(v, s.block.params[i]));
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return readers;
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
/** Each non-entry block param → the argument every in-edge passes it, in edge order. */
|
|
1486
|
+
const joinArgs = (): Map<Value, Value[]> => {
|
|
1487
|
+
const m = new Map<Value, Value[]>();
|
|
1488
|
+
for (const blk of irBlocks) {
|
|
1489
|
+
for (const op of blk.ops) {
|
|
1490
|
+
for (const s of op.successors) {
|
|
1491
|
+
if (s.block === irBlocks[0]) {
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
s.block.params.forEach((p, i) => {
|
|
1495
|
+
(m.get(p) ?? m.set(p, []).get(p)!).push(s.args[i]);
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return m;
|
|
1501
|
+
};
|
|
1502
|
+
|
|
1503
|
+
/** ANOTHER STORAGE'S UNDEFINED VALUE IS AN ASSIGNMENT. `int x, y; if (c) x = y;` at ido7.1
|
|
1504
|
+
* copies the word at 0(sp) into 4(sp) on one path, so the join keyed `sp@4` takes `sp@0`'s
|
|
1505
|
+
* undefined value as an edge argument. The structurer drops an undefined edge copy (structure.ts
|
|
1506
|
+
* `undefCarriesNothing`), which is right for the join's OWN storage — that path left the local
|
|
1507
|
+
* unassigned — and wrong for another's, where the machine ran a load and a store. Each such edge
|
|
1508
|
+
* argument becomes its own undefined value under the source's key, marked `copied`, which the
|
|
1509
|
+
* structurer spells as the assignment `x = uninit_sp0` and names as the value it copies. A join
|
|
1510
|
+
* whose key the builder did not record is left as it was.
|
|
1511
|
+
*
|
|
1512
|
+
* FRAME WORDS ONLY, both of them. A compiler gives a stack local one word and moves no local from
|
|
1513
|
+
* word to word, so two words are two locals. A register is no such identity: agbcc keeps a local
|
|
1514
|
+
* in r8 and moves it to a low register to use it (`synthetic:uninit_spill`), and a spill writes a
|
|
1515
|
+
* register's local to a word — one source variable under two keys, where a spelled copy would
|
|
1516
|
+
* invent an assignment the source never wrote. So `x = y` between two locals in registers
|
|
1517
|
+
* (`uninit_both_saved` on agbcc) still collapses into one undefined value where nothing else
|
|
1518
|
+
* reads it (below).
|
|
1519
|
+
*
|
|
1520
|
+
* A REGISTER CACHING TWO FRAME WORDS. ido7.1 -O2 gives an uninitialised local its word and then
|
|
1521
|
+
* caches it in a register across a loop or an arm: `int x, y; if (c) x = y; else y = x;
|
|
1522
|
+
* return x - y;` is `lw a0,0(sp); move v1,a0` on one arm and `lw v1,4(sp); move a0,v1` on the
|
|
1523
|
+
* other, so the joins keyed v1 and a0 each take sp@0's undefined value on one edge and sp@4's on
|
|
1524
|
+
* the other, and collapsing them into two unrelated undefined values loses that x - y is 0.
|
|
1525
|
+
* Where the joins one register-keyed join is connected to by its edge arguments (a loop header's
|
|
1526
|
+
* and the arm join inside it, in `for (…) if (i & 1) x = y;`) take the undefined values of TWO
|
|
1527
|
+
* OR MORE frame words between them, the register is no one local's home, and each such edge
|
|
1528
|
+
* argument is marked `copied` too: every one is a load the machine ran. Of ONE frame word only
|
|
1529
|
+
* (an arm that leaves the cached local as it was) the web is that local's, and stays as it was —
|
|
1530
|
+
* so long as nothing else reads the value, below.
|
|
1531
|
+
*
|
|
1532
|
+
* A VALUE READ ELSEWHERE IS A RELATION, whatever the keys. Dropping an edge copy leaves the
|
|
1533
|
+
* join's local unassigned on that path, which is as undefined as the machine's word only where
|
|
1534
|
+
* that word is read through this join alone. `int x, z; if (c) z = x; else z = 7; return z - x;`
|
|
1535
|
+
* at ido7.1 caches x's word 4(sp) in z's register v1 on one arm and reads 4(sp) again for the
|
|
1536
|
+
* subtraction, so `c != 0` returns 0; with the copy dropped, z's local is unassigned there and
|
|
1537
|
+
* the C returns garbage. So an undefined edge argument, of any storage and into a join of any
|
|
1538
|
+
* key (registers too: gcc 2.7.2 keeps x in v1 and runs `move v0,v1`), is a copy here whenever
|
|
1539
|
+
* ANOTHER JOIN also takes it, since that join's local and this one hold the same value on the
|
|
1540
|
+
* machine. One the function also reads directly is left to the structurer, which names the
|
|
1541
|
+
* value as the join's local when it can (`g = x; if (c) x = 5; return x;`) and spells the copy
|
|
1542
|
+
* when it cannot (structure.ts `undefCarriesNothing`); `collapseUndefinedJoins` keeps such a
|
|
1543
|
+
* join for it. What agbcc's prologue relates by staging a high register for its push is not
|
|
1544
|
+
* this function's copy and never reaches here (thumb.ts `prologueSavedRegs`). */
|
|
1545
|
+
const markCopiedUndefs = () => {
|
|
1546
|
+
const defOf = defOpMap(fn);
|
|
1547
|
+
const replaced = new Set<Value>();
|
|
1548
|
+
const frameUndefKey = (v: Value): string | undefined => {
|
|
1549
|
+
const d = defOf.get(v);
|
|
1550
|
+
return d?.opcode === 'undef' && d.attrs.copied !== true && slotKeyOffset(String(d.attrs.key)) !== null
|
|
1551
|
+
? String(d.attrs.key)
|
|
1552
|
+
: undefined;
|
|
1553
|
+
};
|
|
1554
|
+
// The register-keyed joins whose web of joins takes two or more frame words' undefined values.
|
|
1555
|
+
const mixedRegisterJoins = new Set<Value>();
|
|
1556
|
+
{
|
|
1557
|
+
const incoming = joinArgs();
|
|
1558
|
+
const isRegJoin = (p: Value) => {
|
|
1559
|
+
const k = phiKey.get(p);
|
|
1560
|
+
return incoming.has(p) && k !== undefined && slotKeyOffset(k) === null;
|
|
1561
|
+
};
|
|
1562
|
+
const parent = new Map<Value, Value>();
|
|
1563
|
+
const find = (v: Value): Value => {
|
|
1564
|
+
let r = v;
|
|
1565
|
+
while (parent.get(r) !== undefined && parent.get(r) !== r) {
|
|
1566
|
+
r = parent.get(r)!;
|
|
1567
|
+
}
|
|
1568
|
+
parent.set(v, r);
|
|
1569
|
+
return r;
|
|
1570
|
+
};
|
|
1571
|
+
for (const [p, args] of incoming) {
|
|
1572
|
+
if (!isRegJoin(p)) {
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
if (!parent.has(p)) {
|
|
1576
|
+
parent.set(p, p);
|
|
1577
|
+
}
|
|
1578
|
+
for (const a of args) {
|
|
1579
|
+
if (isRegJoin(a)) {
|
|
1580
|
+
if (!parent.has(a)) {
|
|
1581
|
+
parent.set(a, a);
|
|
1582
|
+
}
|
|
1583
|
+
parent.set(find(a), find(p));
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
const keysOf = new Map<Value, Set<string>>();
|
|
1588
|
+
for (const [p, args] of incoming) {
|
|
1589
|
+
if (!isRegJoin(p)) {
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
const root = find(p);
|
|
1593
|
+
const ks = keysOf.get(root) ?? keysOf.set(root, new Set()).get(root)!;
|
|
1594
|
+
for (const a of args) {
|
|
1595
|
+
const k = frameUndefKey(a);
|
|
1596
|
+
if (k !== undefined) {
|
|
1597
|
+
ks.add(k);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
for (const p of parent.keys()) {
|
|
1602
|
+
if ((keysOf.get(find(p))?.size ?? 0) >= 2) {
|
|
1603
|
+
mixedRegisterJoins.add(p);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
const readers = readersOf();
|
|
1608
|
+
const joinedElsewhere = (u: Value, p: Value) => [...(readers.get(u) ?? [])].some((r) => r !== null && r !== p);
|
|
1609
|
+
for (const blk of irBlocks) {
|
|
1610
|
+
for (const op of blk.ops) {
|
|
1611
|
+
for (const s of op.successors) {
|
|
1612
|
+
if (s.block === irBlocks[0]) {
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
s.block.params.forEach((p, i) => {
|
|
1616
|
+
const d = defOf.get(s.args[i]);
|
|
1617
|
+
const key = phiKey.get(p);
|
|
1618
|
+
if (d?.opcode !== 'undef' || d.attrs.copied === true || key === undefined) {
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
const frameCopy =
|
|
1622
|
+
d.attrs.key !== key &&
|
|
1623
|
+
slotKeyOffset(String(d.attrs.key)) !== null &&
|
|
1624
|
+
(slotKeyOffset(key) !== null || mixedRegisterJoins.has(p));
|
|
1625
|
+
if (!frameCopy && !joinedElsewhere(s.args[i], p)) {
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
const copy = mkOp('undef', {
|
|
1629
|
+
results: [mkValue(s.args[i].type)],
|
|
1630
|
+
attrs: { key: d.attrs.key, copied: true },
|
|
1631
|
+
});
|
|
1632
|
+
irBlocks[0].ops.unshift(copy);
|
|
1633
|
+
replaced.add(s.args[i]);
|
|
1634
|
+
s.args[i] = copy.results[0];
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
if (replaced.size > 0) {
|
|
1640
|
+
const used = new Set(
|
|
1641
|
+
irBlocks.flatMap((b) => b.ops.flatMap((o) => [...o.operands, ...o.successors.flatMap((s) => s.args)])),
|
|
1642
|
+
);
|
|
1643
|
+
for (const blk of irBlocks) {
|
|
1644
|
+
blk.ops = blk.ops.filter((o) => !replaced.has(o.results[0]) || used.has(o.results[0]));
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
|
|
1649
|
+
/** A JOIN NO PATH DEFINES IS ONE UNINITIALISED VALUE. A join whose every argument is its own
|
|
1650
|
+
* storage's undefined value (`markCopiedUndefs` has already turned another frame word's into an
|
|
1651
|
+
* assignment) would keep its local and lose every assignment to it, since the structurer drops
|
|
1652
|
+
* each such edge copy, and its own postcondition then reports a local read but never assigned.
|
|
1653
|
+
* Such a join is replaced by one fresh undefined value at the entry, keyed as the join was,
|
|
1654
|
+
* which dominates every use the join had. The greatest such set, so a loop that only carries an
|
|
1655
|
+
* undefined value round (`phi(undef, phi(…))`) collapses with it. Run after the hidden-input
|
|
1656
|
+
* check, which refuses the joins whose undefined arguments are a hand-written routine's inputs.
|
|
1657
|
+
* The fresh value lists the other keys whose undefined values it absorbed (`ABSORBED_KEYS`):
|
|
1658
|
+
* `x = a ? b : c` past a one-word declaration joins r1's and r2's into r4's, and a reader asking
|
|
1659
|
+
* whether the lift still reads r1 must find it there. */
|
|
1660
|
+
const collapseUndefinedJoins = () => {
|
|
1661
|
+
const incoming = joinArgs();
|
|
1662
|
+
const undefs = new Set<Value>();
|
|
1663
|
+
for (const blk of irBlocks) {
|
|
1664
|
+
for (const op of blk.ops) {
|
|
1665
|
+
if (op.opcode === 'undef' && op.attrs.copied !== true) {
|
|
1666
|
+
undefs.add(op.results[0]);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
if (undefs.size === 0) {
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
// A join one of whose undefined arguments is also read directly keeps it: the fresh value it
|
|
1674
|
+
// would collapse into is a second undefined value where the machine holds one, and the
|
|
1675
|
+
// structurer spells the edge copy instead (`markCopiedUndefs`, A VALUE READ ELSEWHERE).
|
|
1676
|
+
const readers = readersOf();
|
|
1677
|
+
const readDirectly = (a: Value) => undefs.has(a) && (readers.get(a)?.has(null) ?? false);
|
|
1678
|
+
const doomed = new Set(
|
|
1679
|
+
[...incoming.keys()].filter((p) => incoming.get(p)!.length > 0 && !incoming.get(p)!.some(readDirectly)),
|
|
1680
|
+
);
|
|
1681
|
+
for (let changed = true; changed;) {
|
|
1682
|
+
changed = false;
|
|
1683
|
+
for (const p of doomed) {
|
|
1684
|
+
if (incoming.get(p)!.some((a) => a !== p && !undefs.has(a) && !doomed.has(a))) {
|
|
1685
|
+
doomed.delete(p);
|
|
1686
|
+
changed = true;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
// ...and reaches an undefined value at all: a param only its own back edge feeds is a cycle no
|
|
1691
|
+
// entry reaches (`readRecursive`'s unreachable walk), which carries no value to collapse into.
|
|
1692
|
+
const grounded = new Set<Value>();
|
|
1693
|
+
for (let changed = true; changed;) {
|
|
1694
|
+
changed = false;
|
|
1695
|
+
for (const p of doomed) {
|
|
1696
|
+
if (!grounded.has(p) && incoming.get(p)!.some((a) => undefs.has(a) || grounded.has(a))) {
|
|
1697
|
+
grounded.add(p);
|
|
1698
|
+
changed = true;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
for (const p of [...doomed]) {
|
|
1703
|
+
if (!grounded.has(p)) {
|
|
1704
|
+
doomed.delete(p);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
if (doomed.size === 0) {
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
const orphans = new Set([...doomed].flatMap((p) => incoming.get(p)!.filter((a) => undefs.has(a))));
|
|
1711
|
+
const defOf = defOpMap(fn);
|
|
1712
|
+
// The keys of the undefined values a join absorbs, through the doomed joins it takes.
|
|
1713
|
+
const absorbed = (p: Value, seen = new Set<Value>()): string[] =>
|
|
1714
|
+
seen.has(p)
|
|
1715
|
+
? []
|
|
1716
|
+
: (seen.add(p),
|
|
1717
|
+
incoming
|
|
1718
|
+
.get(p)!
|
|
1719
|
+
.flatMap((a) =>
|
|
1720
|
+
undefs.has(a) ? [String(defOf.get(a)!.attrs.key)] : doomed.has(a) ? absorbed(a, seen) : [],
|
|
1721
|
+
));
|
|
1722
|
+
for (const p of doomed) {
|
|
1723
|
+
const firstUndef = incoming.get(p)!.find((a) => undefs.has(a));
|
|
1724
|
+
const key = phiKey.get(p) ?? (firstUndef && defOf.get(firstUndef)?.attrs.key) ?? '';
|
|
1725
|
+
const from = [...new Set(absorbed(p))].filter((k) => k !== key).sort();
|
|
1726
|
+
const op = mkOp('undef', {
|
|
1727
|
+
results: [mkValue(T.unk(32))],
|
|
1728
|
+
attrs: from.length === 0 ? { key } : { key, [ABSORBED_KEYS]: from.join(',') },
|
|
1729
|
+
});
|
|
1730
|
+
irBlocks[0].ops.unshift(op);
|
|
1731
|
+
replaceAllUsesWith(fn, p, op.results[0]);
|
|
1732
|
+
}
|
|
1733
|
+
for (const blk of irBlocks) {
|
|
1734
|
+
for (let i = blk.params.length - 1; i >= 0; i--) {
|
|
1735
|
+
const p = blk.params[i];
|
|
1736
|
+
if (!doomed.has(p)) {
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
dropPhiRecord(p);
|
|
1740
|
+
blk.params.splice(i, 1);
|
|
1741
|
+
for (const b2 of irBlocks) {
|
|
1742
|
+
for (const o of b2.ops) {
|
|
1743
|
+
for (const s of o.successors) {
|
|
1744
|
+
if (s.block === blk) {
|
|
1745
|
+
s.args.splice(i, 1);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
// The undefined arguments the collapsed joins took and nothing else reads.
|
|
1753
|
+
const used = new Set(
|
|
1754
|
+
irBlocks.flatMap((b) => b.ops.flatMap((o) => [...o.operands, ...o.successors.flatMap((s) => s.args)])),
|
|
1755
|
+
);
|
|
1756
|
+
for (const blk of irBlocks) {
|
|
1757
|
+
blk.ops = blk.ops.filter((o) => !orphans.has(o.results[0]) || used.has(o.results[0]));
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
|
|
836
1761
|
const dropPhiRecord = (p: Value) => {
|
|
837
1762
|
phiBlock.delete(p);
|
|
838
1763
|
phiKey.delete(p);
|
|
@@ -844,13 +1769,130 @@ export function makeSsaBuilder(
|
|
|
844
1769
|
irBlocks,
|
|
845
1770
|
readVar,
|
|
846
1771
|
readGuessedArg: readAny, // see the interface: the trim is this read's answer, not a refusal
|
|
1772
|
+
noteGuessedArgRead: noteRead,
|
|
1773
|
+
readAtEnd: (reg: string, b: number) => (filled[b] ? readAny(reg, b) : null),
|
|
847
1774
|
writeVar,
|
|
1775
|
+
frameWrites,
|
|
848
1776
|
paramReg,
|
|
849
1777
|
ensureParam,
|
|
850
1778
|
hasReachingDef,
|
|
851
1779
|
holdsValue,
|
|
1780
|
+
everyReachingDef,
|
|
852
1781
|
entryValue,
|
|
853
1782
|
holdsEntryValue,
|
|
1783
|
+
writtenSinceCall: (reg: string, b: number) => writtenSinceCall[b].has(reg),
|
|
1784
|
+
lastWriteOp: (reg: string, b: number) => lastWriteOp[b].get(reg),
|
|
1785
|
+
readAfterEveryReachingWrite: (reg, b, { passes, accept, unlowered, answersIfRead }) => {
|
|
1786
|
+
const writes = (at: number) => lastWriteOp[at].has(reg) || clobberedLocal[at].has(reg);
|
|
1787
|
+
// the blocks whose last write of `reg` reaches `b`, and whether a path from the entry wrote none.
|
|
1788
|
+
// `b` is judged up to here: met again round a loop, its end is no write that reaches here yet
|
|
1789
|
+
const writers: number[] = [];
|
|
1790
|
+
let fromEntry = false;
|
|
1791
|
+
const back = new Set<number>([b]);
|
|
1792
|
+
const walkBack = (at: number): void => {
|
|
1793
|
+
for (const p of distinctPreds(at)) {
|
|
1794
|
+
if (back.has(p)) {
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
back.add(p);
|
|
1798
|
+
if (filled[p] && writes(p)) {
|
|
1799
|
+
writers.push(p);
|
|
1800
|
+
} else if (preds[p].length === 0) {
|
|
1801
|
+
fromEntry = true;
|
|
1802
|
+
} else {
|
|
1803
|
+
walkBack(p);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
walkBack(b);
|
|
1808
|
+
const succs = (at: number): number[] => successors()[at].filter((s) => s !== b);
|
|
1809
|
+
// forward from a write of `written` (none for the entry's): the first read on each path, until
|
|
1810
|
+
// another write ends it
|
|
1811
|
+
const ahead = (from: number, seen: Set<number>, written: Value | undefined): boolean =>
|
|
1812
|
+
succs(from).some((s) => {
|
|
1813
|
+
if (seen.has(s)) {
|
|
1814
|
+
return false;
|
|
1815
|
+
}
|
|
1816
|
+
seen.add(s);
|
|
1817
|
+
if (!filled[s]) {
|
|
1818
|
+
return written !== undefined && unlowered(written);
|
|
1819
|
+
}
|
|
1820
|
+
const read = readBeforeWrite[s].get(reg);
|
|
1821
|
+
if (read !== undefined) {
|
|
1822
|
+
return accept(read);
|
|
1823
|
+
}
|
|
1824
|
+
return !writes(s) && ahead(s, seen, written);
|
|
1825
|
+
});
|
|
1826
|
+
const readAfter = (w: number): boolean => {
|
|
1827
|
+
const read = readSinceWrite[w].get(reg);
|
|
1828
|
+
return read !== undefined ? accept(read) : ahead(w, new Set(), defs[w].get(reg));
|
|
1829
|
+
};
|
|
1830
|
+
// a call's destroying it is no write of this function's: it neither counts nor answers; and a
|
|
1831
|
+
// write of a value `answersIfRead` takes answers only where it was read after
|
|
1832
|
+
const own = writers.filter((w) => {
|
|
1833
|
+
const v = defs[w].get(reg)!;
|
|
1834
|
+
return !clobberedLocal[w].has(reg) && passes(v) && (!answersIfRead(v) || readAfter(w));
|
|
1835
|
+
});
|
|
1836
|
+
// nor does the entry's value unread: nothing wrote it for the call. Read, it answers by its
|
|
1837
|
+
// value too (`unlowered`): no write of this function's made it, so no path out of one places
|
|
1838
|
+
// the read, and a path from the entry into a block not lowered yet holds reads nobody has seen
|
|
1839
|
+
const param = irBlocks[0].params.find((v) => paramReg.get(v) === reg);
|
|
1840
|
+
const entryRead =
|
|
1841
|
+
fromEntry &&
|
|
1842
|
+
((param !== undefined && unlowered(param)) ||
|
|
1843
|
+
(readBeforeWrite[0].has(reg) ? accept(readBeforeWrite[0].get(reg)!) : ahead(0, new Set(), undefined)));
|
|
1844
|
+
// where a call destroyed it on every path in, it holds what a callee left there and nothing
|
|
1845
|
+
// this function named: no argument, whatever an earlier lookup's phi of the bytes before
|
|
1846
|
+
// those calls says (counted, sbk func_800B6E5C_A6D0C's a1 and a2 would be minted as entry
|
|
1847
|
+
// parameters the trim cuts from the call but leaves in the signature)
|
|
1848
|
+
if (!fromEntry && writers.length > 0 && writers.every((w) => clobberedLocal[w].has(reg))) {
|
|
1849
|
+
return true;
|
|
1850
|
+
}
|
|
1851
|
+
// no write of this function's left to judge, and no entry value read: nothing to answer
|
|
1852
|
+
if (own.length === 0 && !entryRead) {
|
|
1853
|
+
return undefined;
|
|
1854
|
+
}
|
|
1855
|
+
return own.every(readAfter);
|
|
1856
|
+
},
|
|
1857
|
+
writtenOnEveryPath: (reg, b, stop, visit) => {
|
|
1858
|
+
// `b` is judged up to here on the first visit; met again round a loop, it is the end of `b`,
|
|
1859
|
+
// which a back edge brings, and a cycle adds no path the first visit did not judge
|
|
1860
|
+
const seen = new Set<number>([b]);
|
|
1861
|
+
const walk = (at: number): boolean => {
|
|
1862
|
+
if (writtenSinceCall[at].has(reg) || clobberedLocal[at].has(reg)) {
|
|
1863
|
+
return true;
|
|
1864
|
+
}
|
|
1865
|
+
const ps = distinctPreds(at);
|
|
1866
|
+
return (
|
|
1867
|
+
ps.length > 0 &&
|
|
1868
|
+
ps.every((p) => {
|
|
1869
|
+
if (seen.has(p)) {
|
|
1870
|
+
return true;
|
|
1871
|
+
}
|
|
1872
|
+
seen.add(p);
|
|
1873
|
+
if (!filled[p] || stop(p)) {
|
|
1874
|
+
return false;
|
|
1875
|
+
}
|
|
1876
|
+
visit?.(p);
|
|
1877
|
+
return walk(p);
|
|
1878
|
+
})
|
|
1879
|
+
);
|
|
1880
|
+
};
|
|
1881
|
+
return walk(b);
|
|
1882
|
+
},
|
|
1883
|
+
requireSoleReader: (value: Value, op: Op, refuse: () => void) => {
|
|
1884
|
+
soleReaders.push({ value, op, refuse });
|
|
1885
|
+
},
|
|
1886
|
+
requireReader: (value: Value, refuse: () => void) => {
|
|
1887
|
+
readObligations.push({ value, refuse });
|
|
1888
|
+
},
|
|
1889
|
+
noteIncidentalWrite: (v: Value) => {
|
|
1890
|
+
incidentalWrites.add(v);
|
|
1891
|
+
},
|
|
1892
|
+
isIncidentalWrite: (v: Value) => incidentalWrites.has(v),
|
|
1893
|
+
useOutsideOps: (v: Value, b: number) => {
|
|
1894
|
+
usesOutsideOps.push({ value: v, block: b, atOp: irBlocks[b].ops.length });
|
|
1895
|
+
},
|
|
854
1896
|
noteCall: (b: number, clobbers: readonly string[]) => {
|
|
855
1897
|
callsIn.add(b);
|
|
856
1898
|
// the callee clobbers the caller-saved registers, its own result register included — see
|
|
@@ -859,26 +1901,41 @@ export function makeSsaBuilder(
|
|
|
859
1901
|
for (const r of clobbers) {
|
|
860
1902
|
clobberedLocal[b].add(r);
|
|
861
1903
|
decidedLocal[b].add(r);
|
|
1904
|
+
readSinceWrite[b].delete(r);
|
|
862
1905
|
}
|
|
1906
|
+
imageLocal[b] = new Set();
|
|
1907
|
+
},
|
|
1908
|
+
noteStructImage: (b: number, reg: string, image: Value) => {
|
|
1909
|
+
imageLocal[b].add(reg);
|
|
1910
|
+
structImages.add(image);
|
|
863
1911
|
},
|
|
864
1912
|
recordGuessedCall: (
|
|
865
1913
|
op: Op,
|
|
866
1914
|
b: number,
|
|
867
1915
|
abi: { argRegs: string[]; returnReg: string },
|
|
868
1916
|
passesCalleeResult?: (result: Value) => boolean,
|
|
1917
|
+
trimmed?: (cut: readonly string[], kept: readonly string[]) => void,
|
|
1918
|
+
withholdsSetupArgs?: () => boolean,
|
|
1919
|
+
asksEveryCut?: () => boolean,
|
|
1920
|
+
floor?: number,
|
|
869
1921
|
) => {
|
|
870
1922
|
abiSeen = abi;
|
|
871
1923
|
guessedCalls.push({
|
|
872
1924
|
block: b,
|
|
873
1925
|
op,
|
|
874
1926
|
passesCalleeResult,
|
|
1927
|
+
trimmed,
|
|
1928
|
+
withholdsSetupArgs,
|
|
1929
|
+
asksEveryCut,
|
|
1930
|
+
...(floor ? { floor } : {}),
|
|
875
1931
|
freshBefore: new Set(writtenSinceCall[b]),
|
|
876
1932
|
afterCallInBlock: callsIn.has(b), // `noteCall` runs after this, so this means an EARLIER call
|
|
877
|
-
returnRegBefore:
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1933
|
+
returnRegBefore:
|
|
1934
|
+
clobberedLocal[b].has(abi.returnReg) || imageLocal[b].has(abi.returnReg)
|
|
1935
|
+
? 'destroyed'
|
|
1936
|
+
: decidedLocal[b].has(abi.returnReg)
|
|
1937
|
+
? 'written'
|
|
1938
|
+
: 'inherited',
|
|
882
1939
|
});
|
|
883
1940
|
},
|
|
884
1941
|
markFilled: (b: number) => {
|
|
@@ -904,13 +1961,26 @@ export function makeSsaBuilder(
|
|
|
904
1961
|
argRegs: abiSeen.argRegs,
|
|
905
1962
|
returnReg: abiSeen.returnReg,
|
|
906
1963
|
calleeResults,
|
|
907
|
-
destroyedIn: destroyedOnEntry(),
|
|
1964
|
+
destroyedIn: destroyedOnEntry(true),
|
|
908
1965
|
preds,
|
|
909
1966
|
blocks: irBlocks,
|
|
910
1967
|
freshAtEnd: writtenSinceCall,
|
|
911
1968
|
callsIn,
|
|
912
1969
|
sites: guessedCalls,
|
|
913
1970
|
});
|
|
1971
|
+
// what the trim leaves a guess passing a struct's image: a copy of it into another argument
|
|
1972
|
+
// register (`add r1, r0, #0`), which is as undecided as the image itself
|
|
1973
|
+
for (const site of guessedCalls) {
|
|
1974
|
+
const at = callArgs(site.op).findIndex((v) => structImages.has(v));
|
|
1975
|
+
if (at >= 0) {
|
|
1976
|
+
// worded as `trimClobberedCallArgs` refuses the image left in the return register itself
|
|
1977
|
+
const callee = site.op.attrs.target;
|
|
1978
|
+
throw new FrontendUnsupportedError(
|
|
1979
|
+
`cannot lift '${name}': argument ${at + 1} of the call to ${typeof callee === 'string' ? `'${callee}'` : 'a callee'} ` +
|
|
1980
|
+
`is a struct an earlier call handed back in ${abiSeen.returnReg}, and nothing here passes one on as an argument — not modelled`,
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
914
1984
|
}
|
|
915
1985
|
irBlocks.forEach((blk, i) => writeOrder.writes.set(blk, writeCount[i]));
|
|
916
1986
|
// The phis nothing reads at all — a register two paths leave holding different junk (a loop
|
|
@@ -929,6 +1999,155 @@ export function makeSsaBuilder(
|
|
|
929
1999
|
refuseStaleCallerSavedReads();
|
|
930
2000
|
simplifyTrivialPhis(fn, dropPhiRecord);
|
|
931
2001
|
pruneDeadParams(fn, dropPhiRecord);
|
|
2002
|
+
// A SOLE-READER OBLIGATION (`requireSoleReader`), over the finished function: after both
|
|
2003
|
+
// prunes, so a join nothing reads is no reader.
|
|
2004
|
+
for (const { value, op, refuse } of soleReaders) {
|
|
2005
|
+
const reads = (o: Op) =>
|
|
2006
|
+
o !== op && (o.operands.includes(value) || o.successors.some((e) => e.args.includes(value)));
|
|
2007
|
+
if (
|
|
2008
|
+
frameWrites.has(value) ||
|
|
2009
|
+
usesOutsideOps.some((u) => u.value === value) ||
|
|
2010
|
+
irBlocks.some((blk) => blk.ops.some(reads))
|
|
2011
|
+
) {
|
|
2012
|
+
refuse();
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
// …AND A READER OBLIGATION (`requireReader`), over the same finished function. A join the
|
|
2016
|
+
// value reaches is followed to its own readers, and a `ret` is no reader: a frontend returns
|
|
2017
|
+
// the return register at every `ret` whatever the function's declared type.
|
|
2018
|
+
for (const { value, refuse } of readObligations) {
|
|
2019
|
+
const reached = new Set<Value>([value]);
|
|
2020
|
+
for (let grew = true; grew;) {
|
|
2021
|
+
grew = false;
|
|
2022
|
+
for (const blk of irBlocks) {
|
|
2023
|
+
for (const op of blk.ops) {
|
|
2024
|
+
for (const e of op.successors) {
|
|
2025
|
+
e.block.params.forEach((p, i) => {
|
|
2026
|
+
if (reached.has(e.args[i]) && !reached.has(p)) {
|
|
2027
|
+
reached.add(p);
|
|
2028
|
+
grew = true;
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
const read = [...reached].some(
|
|
2036
|
+
(v) =>
|
|
2037
|
+
frameWrites.has(v) ||
|
|
2038
|
+
usesOutsideOps.some((u) => u.value === v) ||
|
|
2039
|
+
irBlocks.some((blk) => blk.ops.some((o) => o.opcode !== 'ret' && o.operands.includes(v))),
|
|
2040
|
+
);
|
|
2041
|
+
if (!read) {
|
|
2042
|
+
refuse();
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
// A HIDDEN INPUT IS NO UNINITIALISED LOCAL (`LiveInModel.hiddenInputRegs`). After the
|
|
2046
|
+
// trivial-phi pass, so a join every path of which arrives undefined is no join. The
|
|
2047
|
+
// compiler's local is assigned on SOME path, and its entry value meets that definition as a
|
|
2048
|
+
// join's edge argument; once it does, a direct use is the same local read before that
|
|
2049
|
+
// assignment (`uninitfirst`). An entry value that meets no such definition is the input.
|
|
2050
|
+
//
|
|
2051
|
+
// MEETING A JOIN IS NOT MEETING A DEFINITION. A register that carries an input round a loop
|
|
2052
|
+
// (`addiu t0,t0,-1; bnez t0,top` on a hand-written count) sends its entry value into the
|
|
2053
|
+
// loop header's join too, and the back edge's argument is that input's own next value. So
|
|
2054
|
+
// the join must also take an INDEPENDENT definition (`independentOfEntry`): a value no chain
|
|
2055
|
+
// of operands or join arguments derives from the entry value, other than through a join one
|
|
2056
|
+
// of whose other arguments is itself independent. A conditional assignment inside a loop
|
|
2057
|
+
// (`for (…) if (c) x = i;`) meets that through the inner join, and so does one the loop then
|
|
2058
|
+
// transforms before its back edge — a bit reader's `if (!bits) code = *src++; … code <<= 1;`,
|
|
2059
|
+
// whose shift reaches the entry value only through the join the load also feeds. A counter
|
|
2060
|
+
// that only advances the entry value, and two undefined values joined (`phi(t0, v1)`), do
|
|
2061
|
+
// not, and refuse. A bit reader HANDED its buffer meets the same join and lifts with the
|
|
2062
|
+
// input dropped: the rule's blind side, at `O32_SCRATCH_REGS` in target.ts.
|
|
2063
|
+
if (hiddenEntryValues.size > 0) {
|
|
2064
|
+
const incoming = joinArgs();
|
|
2065
|
+
const defOf = defOpMap(fn);
|
|
2066
|
+
// Read forwards: who reads each value, as an operand or as a join's edge argument.
|
|
2067
|
+
const readers = new Map<Value, Value[]>();
|
|
2068
|
+
const addReader = (v: Value, r: Value) => (readers.get(v) ?? readers.set(v, []).get(v)!).push(r);
|
|
2069
|
+
for (const blk of irBlocks) {
|
|
2070
|
+
for (const op of blk.ops) {
|
|
2071
|
+
for (const v of op.operands) {
|
|
2072
|
+
for (const r of op.results) {
|
|
2073
|
+
addReader(v, r);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
for (const [p, args] of incoming) {
|
|
2079
|
+
for (const a of args) {
|
|
2080
|
+
addReader(a, p);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
const isJoin = (v: Value) => incoming.has(v) && !irBlocks[0].params.includes(v);
|
|
2084
|
+
/** The values independent of `u`, as a least fixpoint. A join is independent once one of
|
|
2085
|
+
* its arguments is; any other value once no chain of operands or join arguments reaches
|
|
2086
|
+
* `u` without passing through an independent join. Grown from nothing, so no join is
|
|
2087
|
+
* independent on the strength of its own back edge (`addiu t0,t0,-1` round a loop). */
|
|
2088
|
+
const independentOfEntry = (u: Value): ((v: Value) => boolean) => {
|
|
2089
|
+
const absorbing = new Set<Value>();
|
|
2090
|
+
for (;;) {
|
|
2091
|
+
const derived = new Set<Value>([u]);
|
|
2092
|
+
const work = [u];
|
|
2093
|
+
for (let w = work.pop(); w !== undefined; w = work.pop()) {
|
|
2094
|
+
for (const r of readers.get(w) ?? []) {
|
|
2095
|
+
if (!derived.has(r) && !absorbing.has(r)) {
|
|
2096
|
+
derived.add(r);
|
|
2097
|
+
work.push(r);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
const indep = (v: Value) =>
|
|
2102
|
+
v !== u && defOf.get(v)?.opcode !== 'undef' && (absorbing.has(v) || (!isJoin(v) && !derived.has(v)));
|
|
2103
|
+
let grew = false;
|
|
2104
|
+
for (let changed = true; changed;) {
|
|
2105
|
+
changed = false;
|
|
2106
|
+
for (const [p, args] of incoming) {
|
|
2107
|
+
if (isJoin(p) && !absorbing.has(p) && args.some(indep)) {
|
|
2108
|
+
absorbing.add(p);
|
|
2109
|
+
changed = grew = true;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
if (!grew) {
|
|
2114
|
+
return indep;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
};
|
|
2118
|
+
const metMemo = new Map<Value, boolean>();
|
|
2119
|
+
const met = (u: Value): boolean => {
|
|
2120
|
+
let m = metMemo.get(u);
|
|
2121
|
+
if (m === undefined) {
|
|
2122
|
+
const joins = [...incoming.entries()].filter(([p, args]) => isJoin(p) && args.includes(u));
|
|
2123
|
+
const indep = joins.length === 0 ? undefined : independentOfEntry(u);
|
|
2124
|
+
m = indep !== undefined && joins.some(([, args]) => args.some(indep));
|
|
2125
|
+
metMemo.set(u, m);
|
|
2126
|
+
}
|
|
2127
|
+
return m;
|
|
2128
|
+
};
|
|
2129
|
+
for (const blk of irBlocks) {
|
|
2130
|
+
for (const op of blk.ops) {
|
|
2131
|
+
const reg = [...op.operands, ...op.successors.flatMap((s) => s.args)]
|
|
2132
|
+
.filter((v) => hiddenEntryValues.has(v) && !met(v))
|
|
2133
|
+
.map((v) => hiddenEntryValues.get(v))
|
|
2134
|
+
.find((r) => r !== undefined);
|
|
2135
|
+
if (reg !== undefined) {
|
|
2136
|
+
throw new FrontendUnsupportedError(
|
|
2137
|
+
`cannot lift '${name}': ${reg} is read before anything writes it, and ` +
|
|
2138
|
+
`no join meets it with a definition of its own — an input handed over outside the ` +
|
|
2139
|
+
`C convention (a save routine's frame address, a member-function pointer, a register ` +
|
|
2140
|
+
`hand-written asm passes a value in, a count it carries round a loop), not modelled ` +
|
|
2141
|
+
`as an uninitialised local`,
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
dropUnreadGlobalRegisterLoads();
|
|
2148
|
+
markCopiedUndefs();
|
|
2149
|
+
collapseUndefinedJoins();
|
|
2150
|
+
simplifyTrivialPhis(fn, dropPhiRecord);
|
|
932
2151
|
// SEAL THE PARAMETER EVIDENCE (ir/core.ts `ParamEvidence`). Here and not at the store or the
|
|
933
2152
|
// write, because `pruneDeadParams` above is the last thing that can retire an entry
|
|
934
2153
|
// parameter, and an observation about a value no longer in the signature is one the reader
|
|
@@ -945,6 +2164,12 @@ export function makeSsaBuilder(
|
|
|
945
2164
|
}
|
|
946
2165
|
}
|
|
947
2166
|
const defs0 = defOpMap(fn);
|
|
2167
|
+
for (const v of deadHomed) {
|
|
2168
|
+
const d = defs0.get(v);
|
|
2169
|
+
if (d?.opcode === 'undef') {
|
|
2170
|
+
d.attrs[HOMED_UNDEF] = true;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
948
2173
|
for (const p of irBlocks[0].params) {
|
|
949
2174
|
const reg = paramReg.get(p);
|
|
950
2175
|
const first = reg === undefined ? undefined : firstEntryWrite.get(reg);
|
|
@@ -991,6 +2216,24 @@ export function makeSsaBuilder(
|
|
|
991
2216
|
`(partially-initialised local, or storage this function does not own) — not modelled`,
|
|
992
2217
|
);
|
|
993
2218
|
}
|
|
2219
|
+
// …AND NEITHER MAY A CALLEE-SAVED REGISTER THIS FUNCTION NEVER SAVED
|
|
2220
|
+
// (`LiveInModel.unsavedCalleeSavedRegs`): no caller passes an argument there, so as a
|
|
2221
|
+
// parameter it would be read from the argument register a C caller fills instead.
|
|
2222
|
+
if (key !== undefined && koff === null && model().unsavedCalleeSavedRegs?.includes(key)) {
|
|
2223
|
+
throw new FrontendUnsupportedError(
|
|
2224
|
+
`cannot lift '${name}': it reads callee-saved ${key} before writing it, and ${unsavedWhy([key])} — ` +
|
|
2225
|
+
`a value no caller passes as an argument${globalNote([key])} — not modelled`,
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
// A write to one is handed back to the caller (a global register variable's store,
|
|
2230
|
+
// `mov r8, r0`): no C local spells it, and dropping it drops an effect.
|
|
2231
|
+
const leaked = (model().unsavedCalleeSavedRegs ?? []).filter((r) => writtenKeys.has(r));
|
|
2232
|
+
if (leaked.length > 0) {
|
|
2233
|
+
throw new FrontendUnsupportedError(
|
|
2234
|
+
`cannot lift '${name}': it writes callee-saved ${leaked.join(', ')}, and ${unsavedWhy(leaked)} — ` +
|
|
2235
|
+
`the caller sees the write${globalNote(leaked)} — not modelled`,
|
|
2236
|
+
);
|
|
994
2237
|
}
|
|
995
2238
|
},
|
|
996
2239
|
};
|
|
@@ -1016,8 +2259,11 @@ export function makeSsaBuilder(
|
|
|
1016
2259
|
* the first drops that argument and every one after it: `ac-decomp:evw_anime_colreg_manual` sets
|
|
1017
2260
|
* up seven registers for `evw_color_set` and, with r4 at its incoming value, lifts to
|
|
1018
2261
|
* `evw_color_set(a0);` — its divide, its multiply and five arguments gone. Which reading it is
|
|
1019
|
-
* cannot be decided
|
|
1020
|
-
* and names the gap rather than guessing
|
|
2262
|
+
* cannot be decided from the register file alone — the function's own arity is exactly what is
|
|
2263
|
+
* missing — so this refuses and names the gap rather than guessing, unless `decide` answers it:
|
|
2264
|
+
* the lowering may show that the register above the gap is surely an argument, and where argument
|
|
2265
|
+
* words fill in order the gap below it is then this function's own arguments passed on
|
|
2266
|
+
* (frontend/call-plan.ts `decideGap`). A declaration answers it outright, and this scan is never
|
|
1021
2267
|
* weighed against one: it is a guess, and a guess that cannot fail cannot witness a width a
|
|
1022
2268
|
* declaration left open. */
|
|
1023
2269
|
export function fallbackArgc(
|
|
@@ -1025,27 +2271,170 @@ export function fallbackArgc(
|
|
|
1025
2271
|
argRegs: readonly string[],
|
|
1026
2272
|
bi: number,
|
|
1027
2273
|
opts: {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
2274
|
+
/** whether `v`, reaching argument register `reg`, is a value the caller may have set up */
|
|
2275
|
+
accept?: (v: Value, reg: string) => boolean;
|
|
2276
|
+
/** the call's function, where it is (`CallSite`'s `guess.at`), and the frontend's own refusal */
|
|
2277
|
+
gap: { name: string; at: number | string; fail: FrontendRefusal };
|
|
2278
|
+
/** the leading arguments a declaration states (`GuessedCallSite.floor`): counted whether or not
|
|
2279
|
+
* the caller set them up, so only the registers above them are guessed */
|
|
2280
|
+
floor?: number;
|
|
2281
|
+
/** whether argument register `reg` still holds this function's own incoming value at the call
|
|
2282
|
+
* (an entry parameter, which a read leaves behind as a definition) on some path the caller
|
|
2283
|
+
* did not set it up on: one a call destroyed it on ends there, and one this block wrote it on
|
|
2284
|
+
* since its last call set it up (`CallDeclarations.lower` asks it) */
|
|
2285
|
+
entry?: (reg: string) => boolean;
|
|
2286
|
+
/** whether the gap `gap` — argument registers nothing reaches, in order — below `set`, the next
|
|
2287
|
+
* one a value reaches, is decided: the frontend shows `set` holds an argument, so `gap` holds
|
|
2288
|
+
* arguments passed on unchanged (`CallDeclarations.lower`'s `decideGap`). Handed the gap's
|
|
2289
|
+
* refusal, owed should a proof it defers fail. Absent, or false, the gap refuses. It is asked
|
|
2290
|
+
* again with an empty `gap` for every register the count takes past a decided gap. Each must be
|
|
2291
|
+
* proven an argument the same way, or the gap refuses. */
|
|
2292
|
+
decide?: (gap: readonly string[], set: string, refuse: () => void) => boolean;
|
|
2293
|
+
/** whether what a jump-table dispatch parked in argument register `reg` reaches the call
|
|
2294
|
+
* (`CallSite.parked`). Where nothing `accept` takes reaches it too, the count stops below it: no
|
|
2295
|
+
* compiler hands a callee the dispatch's own register, so no register above it is an argument.
|
|
2296
|
+
*
|
|
2297
|
+
* WHY THIS STOPS WHERE A CALL-DESTROYED REGISTER REFUSES (`r2 holds a value and r1 holds
|
|
2298
|
+
* none`): where the value above came from. A call destroys every caller-saved register, so one
|
|
2299
|
+
* holding a value after it was written since, for a call — an argument as likely as not. A
|
|
2300
|
+
* dispatch clobbers only its idiom's registers, and agbcc moves a value it keeps live across the
|
|
2301
|
+
* switch out of them, upward, before the idiom runs (`add r2,r1,#0` ahead of the `lsl` in a
|
|
2302
|
+
* switch on r0 whose cases call): the value above the parked register was set up for the switch,
|
|
2303
|
+
* not for the call. The cost is a value a case itself writes above the parked register, dropped
|
|
2304
|
+
* too (`mov r2,#5; mov r0,#1; bl g` lifts `g(1)`): no compiler measured writes one there. A
|
|
2305
|
+
* parked register a call in the case destroyed is that call's hole, not the dispatch's, and it
|
|
2306
|
+
* stops the count only where no register above holds a value written since that call
|
|
2307
|
+
* (`SsaBuilder.holdsValue`): `bl h; mov r2,#5; mov r0,#1; bl g` refuses, while `add r3,r1,#0`
|
|
2308
|
+
* ahead of the idiom and `bl h; mov r0,#1; bl g` in a case lifts `g(1)` — the gap rule's
|
|
2309
|
+
* `setUp` walks through calls and would count r3's copy, which `h` destroyed. */
|
|
2310
|
+
parked?: (reg: string) => boolean;
|
|
2311
|
+
/** how many argument words the callee's own body reads at most (frontend/callee-body.ts),
|
|
2312
|
+
* asked only where it can answer something. It is an upper bound on what the callee READS, and
|
|
2313
|
+
* a source call passes more than that whenever a parameter goes unused or a K&R call hands
|
|
2314
|
+
* extra arguments — `setx(a + 1, 4)` to a body reading only its first, `rd1(a, b, 7)` to one
|
|
2315
|
+
* reading r0 — so it never caps what the caller set up for the call: such a register stays
|
|
2316
|
+
* counted, and a value above a gap still decides the gap below it (`decide`). It answers only
|
|
2317
|
+
* for registers at or past it, whose being arguments changes nothing the callee computes:
|
|
2318
|
+
* - one that is a `leftover` stops the count: a write something else read since, which the
|
|
2319
|
+
* call found there (`gH = 5; gA = rnd()` leaves the store's 5 in r0, which the guess took as
|
|
2320
|
+
* `rnd(5)`). A copy made for the call is none, whatever read its value before
|
|
2321
|
+
* (`g4(a, b, c, d); un(a, b, c, d)` copies a..d back from their homes for `un`);
|
|
2322
|
+
* - a gap starting there, under a register that is a leftover or that no proof makes an
|
|
2323
|
+
* argument, stops the count at the gap rather than refuse.
|
|
2324
|
+
* A gap BELOW the bound refuses as ever, whatever lies above it: the callee may read it, and it
|
|
2325
|
+
* holds this function's own incoming value or nothing. `gp = 3; gq = rd1(a)` leaves the store's 3
|
|
2326
|
+
* in r1 above r0's a, and stopping there would drop the `a` rd1 reads. */
|
|
2327
|
+
bound?: () => number | undefined;
|
|
2328
|
+
/** whether what reaches argument register `reg` was written for something other than the call,
|
|
2329
|
+
* which read it since, and left there (frontend/call-plan.ts `leftover`). Asked only at or past
|
|
2330
|
+
* `bound`. */
|
|
2331
|
+
leftover?: (reg: string) => boolean;
|
|
2332
|
+
},
|
|
1032
2333
|
): number {
|
|
2334
|
+
const accept = opts.accept ?? (() => true);
|
|
1033
2335
|
// 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,
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
2336
|
+
const setUp = (k: number) => ssa.hasReachingDef(argRegs[k], bi, (v) => accept(v, argRegs[k]));
|
|
2337
|
+
const { name, at, fail } = opts.gap;
|
|
2338
|
+
const call = typeof at === 'number' ? `at 0x${at.toString(16)}` : at;
|
|
2339
|
+
const floor = opts.floor ?? 0;
|
|
2340
|
+
// the registers past the lowest parked one are none of the call's: neither counted nor a gap's top
|
|
2341
|
+
const parkedAt = argRegs.findIndex((r, k) => k >= floor && opts.parked?.(r) === true && !setUp(k));
|
|
2342
|
+
const top = parkedAt < 0 ? argRegs.length : parkedAt;
|
|
2343
|
+
// the run of argument registers the caller set up from `from`, each past a declaration's
|
|
2344
|
+
// stated words asked whether it still holds the incoming value
|
|
2345
|
+
// whether argument register `k` lies at or past what the callee's own body reads
|
|
2346
|
+
const unread = (k: number): boolean => {
|
|
2347
|
+
const b = opts.bound?.();
|
|
2348
|
+
return b !== undefined && k >= b;
|
|
2349
|
+
};
|
|
2350
|
+
// past the bound, a leftover is no argument (`bound`): leftover first, the cheaper question
|
|
2351
|
+
const notPassed = (k: number): boolean => opts.leftover?.(argRegs[k]) === true && unread(k);
|
|
2352
|
+
const run = (from: number): number => {
|
|
2353
|
+
let n = from;
|
|
2354
|
+
while (n < top && setUp(n) && !notPassed(n)) {
|
|
2355
|
+
// PAST A DECLARATION'S STATED WORDS, A REGISTER STILL HOLDING ITS INCOMING VALUE REFUSES. A
|
|
2356
|
+
// read of it (a home store, a compare, a store to a global) leaves the entry parameter behind
|
|
2357
|
+
// as a definition, so it reads as set up, and it is exactly as undecidable as the gap below: the
|
|
2358
|
+
// function's own argument passed on, or one the call is not handed. Counted, it is an argument
|
|
2359
|
+
// the source never passed — `int logv(const char *, int, ...)` and `logv(s, c) + 1` in a
|
|
2360
|
+
// function whose unused third parameter IDO home-stores lifts `logv(a0, a1, a2)`. Below a
|
|
2361
|
+
// floor the scan never asks, and with none the guess keeps counting it: there it is the far
|
|
2362
|
+
// commoner `if (a) g(a)`, and the same refusal declines 17 lifts of kleod's 20 units and 40 of
|
|
2363
|
+
// sa3's (`DecompressAlloc(a0)`, `m4aSongNumStart(a1)`), where the floored rule moves none.
|
|
2364
|
+
//
|
|
2365
|
+
// ONLY A PATH THE ENTRY VALUE STILL ARRIVES BY IS ASKED. `setUp` walks through calls, so an
|
|
2366
|
+
// entry value a call destroyed reaches it too — `g(x); dprint(s)` — and that register holds
|
|
2367
|
+
// nothing; the trim cuts it (`trimClobberedCallArgs`) and `dprint(a0)` is the bytes. And a
|
|
2368
|
+
// register this block wrote since its last call was set up here, by the copy that put the
|
|
2369
|
+
// value back — `g(y); dprint(s, x)` re-sets r1 from a saved copy — whatever value SSA names.
|
|
2370
|
+
if (floor > 0 && opts.entry?.(argRegs[n])) {
|
|
2371
|
+
fail(
|
|
2372
|
+
`cannot lift '${name}': the call ${call} has a prototype stating only its first ${floor} ` +
|
|
2373
|
+
`argument${floor === 1 ? '' : 's'}, and ${argRegs[n]} past them still holds this function's own incoming ` +
|
|
2374
|
+
'value — an argument passed on unchanged and one the call is not handed look the same here, so the ' +
|
|
2375
|
+
'argument count is not decidable',
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2378
|
+
n++;
|
|
2379
|
+
}
|
|
2380
|
+
return n;
|
|
2381
|
+
};
|
|
2382
|
+
// where the run stopped at a register set up but no argument (`bound`), every one above is past
|
|
2383
|
+
// the bound too, and the count ends there
|
|
2384
|
+
const stoppedPast = (n: number): boolean => n < top && setUp(n);
|
|
2385
|
+
let n = run(floor);
|
|
2386
|
+
if (stoppedPast(n)) {
|
|
2387
|
+
return n;
|
|
1038
2388
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
2389
|
+
// A GAP BELOW A REGISTER THAT HOLDS A VALUE refuses, unless the frontend decides it: then the run
|
|
2390
|
+
// goes on through the gap and the register above it, and the next gap is asked the same.
|
|
2391
|
+
//
|
|
2392
|
+
// PAST A DECIDED GAP, EVERY REGISTER THE RUN COUNTS MUST BE PROVEN TOO (asked of `decide` with an
|
|
2393
|
+
// empty gap), or the gap refuses. The decision says the register above the gap is an argument and
|
|
2394
|
+
// nothing about the ones above that. Without the gap's refusal the run would count whatever reaches
|
|
2395
|
+
// them: a store's address or value, a discarded volatile read, a temp, an incoming value read
|
|
2396
|
+
// elsewhere. Measured 2026-10-18 at canonical flags: `gp[1] = 3; return g(a, -gy) + 1;` would
|
|
2397
|
+
// lift `g(a0, -gy, &gp)` on agbcc, `return g(a, c + 1) + 1;` `g(a0, a2 + 1, a2)` on all seven
|
|
2398
|
+
// toolchains, and `*(volatile int *)K; g(a, b, (short)c)` made agbcc's discarded read a fourth
|
|
2399
|
+
// argument.
|
|
2400
|
+
for (let k = n + 1; k < top; k++) {
|
|
2401
|
+
if (setUp(k)) {
|
|
2402
|
+
const [gapAt, setAt] = [n, k];
|
|
2403
|
+
const refuse = () =>
|
|
1043
2404
|
fail(
|
|
1044
|
-
`cannot lift '${name}': the call
|
|
1045
|
-
|
|
2405
|
+
`cannot lift '${name}': the call ${call} ` +
|
|
2406
|
+
(opts.floor
|
|
2407
|
+
? `has a prototype stating only its first ${opts.floor} argument${opts.floor === 1 ? '' : 's'}`
|
|
2408
|
+
: 'has no prototype') +
|
|
2409
|
+
`, ${argRegs[setAt]} holds a ` +
|
|
2410
|
+
`value and ${argRegs[gapAt]} holds none — an argument register left at its incoming value and one ` +
|
|
1046
2411
|
`the call does not pass look the same here, so the argument count is not decidable`,
|
|
1047
2412
|
);
|
|
2413
|
+
// the gap and the register above it are past what the callee reads: no argument it can tell
|
|
2414
|
+
// from a missing one, so the count stops below the gap as at a gapless call
|
|
2415
|
+
if (opts.leftover?.(argRegs[k]) === true && unread(gapAt)) {
|
|
2416
|
+
return gapAt;
|
|
2417
|
+
}
|
|
2418
|
+
if (!opts.decide?.(argRegs.slice(n, k), argRegs[k], refuse)) {
|
|
2419
|
+
if (unread(gapAt)) {
|
|
2420
|
+
return gapAt;
|
|
2421
|
+
}
|
|
2422
|
+
refuse();
|
|
1048
2423
|
}
|
|
2424
|
+
n = run(k);
|
|
2425
|
+
const stopped = stoppedPast(n);
|
|
2426
|
+
for (let above = k + 1; above < n; above++) {
|
|
2427
|
+
if (!opts.decide?.([], argRegs[above], refuse)) {
|
|
2428
|
+
if (unread(above)) {
|
|
2429
|
+
return above;
|
|
2430
|
+
}
|
|
2431
|
+
refuse();
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
if (stopped) {
|
|
2435
|
+
return n;
|
|
2436
|
+
}
|
|
2437
|
+
k = n;
|
|
1049
2438
|
}
|
|
1050
2439
|
}
|
|
1051
2440
|
return n;
|
|
@@ -1068,6 +2457,25 @@ export interface GuessedCallSite {
|
|
|
1068
2457
|
/** which earlier callee's result in the return register is an argument here, not a leftover:
|
|
1069
2458
|
* the site's lowering says so ({@link SsaBuilder.recordGuessedCall}). Absent where none is. */
|
|
1070
2459
|
passesCalleeResult?: (result: Value) => boolean;
|
|
2460
|
+
/** handed the FRESH argument registers the trim cuts from this site's guess, and the ones it
|
|
2461
|
+
* keeps — the lowering's `CallSite.leftOut` question, asked again where the arity shrinks a
|
|
2462
|
+
* second time. An unfresh cut is the trim's own proof; a fresh one went with a hole below it
|
|
2463
|
+
* (see {@link trimClobberedCallArgs}), which proves nothing. The `/setup-args` reading asks it a
|
|
2464
|
+
* third time, of the cut registers the call's own block wrote (of every one it cuts, where
|
|
2465
|
+
* `asksEveryCut`), and a refusal there withholds that reading for the site instead of declining.
|
|
2466
|
+
* Absent where nothing is asked. */
|
|
2467
|
+
trimmed?: (cut: readonly string[], kept: readonly string[]) => void;
|
|
2468
|
+
/** whether the frontend withholds the `/setup-args` reading from this site whatever it would cut
|
|
2469
|
+
* (`CallSite.withholdsSetupArgs`). Absent where it does not. */
|
|
2470
|
+
withholdsSetupArgs?: () => boolean;
|
|
2471
|
+
/** whether the `/setup-args` reading of this site asks `trimmed` of every register it cuts, an
|
|
2472
|
+
* earlier block's leftovers included ({@link narrowingKeepsFrameAddresses};
|
|
2473
|
+
* `CallSite.asksEveryCut`). Absent where it asks only of what the call's own block wrote. */
|
|
2474
|
+
asksEveryCut?: () => boolean;
|
|
2475
|
+
/** how many leading arguments a declaration STATES, so neither reading cuts below them: the
|
|
2476
|
+
* fixed parameters of a variadic declaration that are each one word (frontend/call-plan.ts
|
|
2477
|
+
* `variadicFloor`). Only the arguments past them are guessed. Absent where none is stated. */
|
|
2478
|
+
floor?: number;
|
|
1071
2479
|
}
|
|
1072
2480
|
|
|
1073
2481
|
export interface CallArgTrim {
|
|
@@ -1313,8 +2721,14 @@ export function trimClobberedCallArgs(inp: CallArgTrim): void {
|
|
|
1313
2721
|
};
|
|
1314
2722
|
for (const s of sites) {
|
|
1315
2723
|
const fresh = s.afterCallInBlock ? s.freshBefore : new Set([...freshIn[s.block], ...s.freshBefore]);
|
|
1316
|
-
const n = argcAt(s, fresh);
|
|
2724
|
+
const n = Math.max(s.floor ?? 0, argcAt(s, fresh));
|
|
1317
2725
|
if (n < callArgs(s.op).length) {
|
|
2726
|
+
// only the FRESH ones: an unfresh register is the trim's proof (a path reaches here with
|
|
2727
|
+
// nothing set up in it since a call), a fresh one went with a hole below it
|
|
2728
|
+
s.trimmed?.(
|
|
2729
|
+
argRegs.slice(n, callArgs(s.op).length).filter((r) => fresh.has(r)),
|
|
2730
|
+
argRegs.slice(0, n),
|
|
2731
|
+
);
|
|
1318
2732
|
truncateCallArgs(s.op, n);
|
|
1319
2733
|
}
|
|
1320
2734
|
// The SHORTER arity the same evidence also allows, recorded for {@link narrowToSetupArgs}: the
|
|
@@ -1334,8 +2748,12 @@ export function trimClobberedCallArgs(inp: CallArgTrim): void {
|
|
|
1334
2748
|
const dropsPassed = passed && provenConstant(new Set([result, callArgs(s.op)[0]]), s.block);
|
|
1335
2749
|
const localFresh =
|
|
1336
2750
|
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 (
|
|
2751
|
+
const local = Math.min(Math.max(s.floor ?? 0, runOfFresh(localFresh, 0)), callArgs(s.op).length);
|
|
2752
|
+
if (
|
|
2753
|
+
local < callArgs(s.op).length &&
|
|
2754
|
+
s.withholdsSetupArgs?.() !== true &&
|
|
2755
|
+
narrowingKeepsFrameAddresses(s, argRegs, local)
|
|
2756
|
+
) {
|
|
1339
2757
|
setupArgc.set(s.op, local);
|
|
1340
2758
|
if (local === 0 && dropsPassed) {
|
|
1341
2759
|
discardsPassedResult.add(s.op);
|
|
@@ -1344,6 +2762,45 @@ export function trimClobberedCallArgs(inp: CallArgTrim): void {
|
|
|
1344
2762
|
}
|
|
1345
2763
|
}
|
|
1346
2764
|
|
|
2765
|
+
/** Whether the narrower reading of `s` ({@link narrowToSetupArgs}, cutting it to `local` arguments)
|
|
2766
|
+
* passes the lowering's `CallSite.leftOut` question too (`GuessedCallSite.trimmed`). That reading
|
|
2767
|
+
* is applied in the ranker AFTER the lift and its frame audit, so nothing downstream re-asks it: a
|
|
2768
|
+
* register it cuts can hold a frame address set up in the call's own block (`mov r2, sp`) above an
|
|
2769
|
+
* argument that came from an earlier one, and the cut lifts `g(a, b, s)` as `g(a)`, leaving the
|
|
2770
|
+
* stores to `s` dead.
|
|
2771
|
+
*
|
|
2772
|
+
* ONLY WHAT THE CALL'S OWN BLOCK WROTE is asked (`freshBefore`). That a value an earlier block left
|
|
2773
|
+
* in a register is dead is this reading's whole guess, and a frame address is no exception: agbcc
|
|
2774
|
+
* keeps `mov r3, sp` as a store base across blocks and calls `TransformSprite(s, t)` with it still
|
|
2775
|
+
* in r3 (sa3 sub_805DADC), where the cut is the right arity. A register this block wrote is not
|
|
2776
|
+
* that guess — the block set it up for the call — so where it may hold a frame address the call
|
|
2777
|
+
* passes, the cut would drop an argument. A refusal withholds this candidate for this site only;
|
|
2778
|
+
* the default reading stands. A leftover the guess was wrong about is caught of the lifted IR
|
|
2779
|
+
* instead, where the cut leaves the object it addresses unread ({@link cutsUnreadFrameObjects}).
|
|
2780
|
+
*
|
|
2781
|
+
* …AND EVERY REGISTER IT CUTS where the frontend says so (`GuessedCallSite.asksEveryCut`): a
|
|
2782
|
+
* function whose frame verdict reads every argument register a call holds as possibly handed
|
|
2783
|
+
* over, where a frame address an earlier block left is not the dead base this reading guesses at,
|
|
2784
|
+
* and the IR question cannot settle it when the object is read elsewhere too
|
|
2785
|
+
* (`if (a) h(b, a, t); else *q = t[0];` with `&t` left in r2). The question refuses only frame
|
|
2786
|
+
* addresses, so a call that cuts none still narrows. */
|
|
2787
|
+
function narrowingKeepsFrameAddresses(s: GuessedCallSite, argRegs: readonly string[], local: number): boolean {
|
|
2788
|
+
const every = s.asksEveryCut?.() === true;
|
|
2789
|
+
const cut = argRegs.slice(local, callArgs(s.op).length).filter((r) => every || s.freshBefore.has(r));
|
|
2790
|
+
if (s.trimmed === undefined || cut.length === 0) {
|
|
2791
|
+
return true;
|
|
2792
|
+
}
|
|
2793
|
+
try {
|
|
2794
|
+
s.trimmed(cut, argRegs.slice(0, local));
|
|
2795
|
+
return true;
|
|
2796
|
+
} catch (e) {
|
|
2797
|
+
if (e instanceof FrontendUnsupportedError) {
|
|
2798
|
+
return false;
|
|
2799
|
+
}
|
|
2800
|
+
throw e;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
|
|
1347
2804
|
/** The opcodes whose result is a literal when every operand is one ({@link trimClobberedCallArgs}). */
|
|
1348
2805
|
const LITERAL_ARITHMETIC: ReadonlySet<string> = new Set([
|
|
1349
2806
|
'add',
|
|
@@ -1379,9 +2836,157 @@ export function setupArgsDiscardsPassedResult(fn: Fn): boolean {
|
|
|
1379
2836
|
|
|
1380
2837
|
/** Whether anything in `fn` HAS the narrower reading — the variation's gate, so the ~99% of functions
|
|
1381
2838
|
* with no narrowable call cost no re-lift. Read it off the lift itself: a later pipeline stage may
|
|
1382
|
-
* replace a `call` op (softdiv rewrites one to a division), and the table is keyed by op.
|
|
2839
|
+
* replace a `call` op (softdiv rewrites one to a division), and the table is keyed by op. A call
|
|
2840
|
+
* {@link cutsUnreadFrameObjects} withholds has none. */
|
|
1383
2841
|
export function hasSetupArgsNarrowing(fn: Fn): boolean {
|
|
1384
|
-
|
|
2842
|
+
if (!fn.blocks.some((b) => b.ops.some((op) => setupArgc.has(op)))) {
|
|
2843
|
+
return false;
|
|
2844
|
+
}
|
|
2845
|
+
const withheld = cutsUnreadFrameObjects(fn);
|
|
2846
|
+
return fn.blocks.some((b) => b.ops.some((op) => setupArgc.has(op) && !withheld.has(op)));
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
/** A frame address in the lifted IR: a `laddr`'s offset plus the constants added to it. `exact` is
|
|
2850
|
+
* false once a term that is not a constant was added (`&buf[i]`), and `off` is then the address
|
|
2851
|
+
* that term was added to. `end` is where the `laddr`'s storage ends. */
|
|
2852
|
+
interface FrameAddress {
|
|
2853
|
+
readonly off: number;
|
|
2854
|
+
readonly exact: boolean;
|
|
2855
|
+
readonly end: number;
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
/** The calls whose `/setup-args` reading cuts the address of a frame object nothing else in `fn`
|
|
2859
|
+
* reads, under that reading — which is then withheld for that call, the default reading standing.
|
|
2860
|
+
*
|
|
2861
|
+
* THE QUESTION THE IR CAN ANSWER of a frame address an earlier block left in a register the reading
|
|
2862
|
+
* cuts. That such a leftover is dead is the reading's whole guess, and agbcc does leave a dead
|
|
2863
|
+
* `mov r3, sp` store base in a call's argument registers (sa3 sub_805DADC, which reloads the
|
|
2864
|
+
* object after the call). Where the leftover was an argument after all, cutting it leaves the
|
|
2865
|
+
* object's stores with no reader: `if (a) g(b, s)` with `s` left in r1 by the entry block lifts as
|
|
2866
|
+
* `g(a1)` beside `s[0] = a0; s[1] = a1;`, which nothing reads. A source that compiled to those stores
|
|
2867
|
+
* read the object somewhere, so a cut that leaves it unread drops an argument. The register file
|
|
2868
|
+
* cannot tell the two leftovers apart; the object's readers can.
|
|
2869
|
+
*
|
|
2870
|
+
* A READER is any use of an address into the object but the address operand of a store into it
|
|
2871
|
+
* and an argument the narrower reading cuts — a load, an address stored or returned or carried
|
|
2872
|
+
* to another block, an argument some call still passes. The other calls are read under the same
|
|
2873
|
+
* narrowing, since the ranker applies it to every call at once.
|
|
2874
|
+
*
|
|
2875
|
+
* THE OBJECT is the span between the offsets the function forms addresses at or accesses. Where the
|
|
2876
|
+
* frame audit sized one `laddr` over two source objects (an escaped `sp+k`, which excuses every word
|
|
2877
|
+
* from k up), the address the compiler formed for the second one (`add r2, sp, #0xc`) is where it
|
|
2878
|
+
* starts: `int s[3]; char t[16]; ... if (a) g(b, s, t)` cut to `g(a1, sp0)` leaves `t` unread
|
|
2879
|
+
* though `sp0` is passed. An access's offset counts as well, because the lift folds a formed
|
|
2880
|
+
* `add r4, sp, #8` into the offsets of the loads and stores through r4, so the `laddr` they share
|
|
2881
|
+
* no longer says where the second object starts. Splitting finer only withholds more. A load or
|
|
2882
|
+
* argument at an offset reads the span it falls in. An address with a term that is not a constant
|
|
2883
|
+
* reads its base's span: an index stays inside the object its base addresses.
|
|
2884
|
+
*
|
|
2885
|
+
* NOT ANSWERED HERE: a cut whose object is read elsewhere too. That leftover cannot be told from the
|
|
2886
|
+
* sub_805DADC dead base, by the IR or by the register file, so it narrows, except where the frontend
|
|
2887
|
+
* withholds the reading at that call (`GuessedCallSite.withholdsSetupArgs`) or asks the register
|
|
2888
|
+
* question of every cut there (`GuessedCallSite.asksEveryCut`). */
|
|
2889
|
+
function cutsUnreadFrameObjects(fn: Fn): Set<Op> {
|
|
2890
|
+
const def = defOpMap(fn);
|
|
2891
|
+
const memo = new Map<Value, FrameAddress | null>();
|
|
2892
|
+
const frameAddress = (v: Value): FrameAddress | null => {
|
|
2893
|
+
if (memo.has(v)) {
|
|
2894
|
+
return memo.get(v)!;
|
|
2895
|
+
}
|
|
2896
|
+
memo.set(v, null); // a cycle through a phi's operands is no frame address
|
|
2897
|
+
const d = def.get(v);
|
|
2898
|
+
let r: FrameAddress | null = null;
|
|
2899
|
+
if (d?.opcode === 'laddr') {
|
|
2900
|
+
const off = d.attrs.off as number;
|
|
2901
|
+
r = { off, exact: true, end: off + (d.attrs.width as number) * (d.attrs.count as number) };
|
|
2902
|
+
} else if ((d?.opcode === 'add' || d?.opcode === 'sub') && d.operands.length === 2) {
|
|
2903
|
+
const konst = (x: Value): number | undefined => {
|
|
2904
|
+
const k = def.get(x);
|
|
2905
|
+
return k?.opcode === 'const' && typeof k.attrs.value === 'number' ? k.attrs.value : undefined;
|
|
2906
|
+
};
|
|
2907
|
+
const [x, y] = d.operands;
|
|
2908
|
+
const fx = frameAddress(x);
|
|
2909
|
+
const fy = d.opcode === 'add' ? frameAddress(y) : null; // `k - &s` is no address
|
|
2910
|
+
const base = fx === null ? fy : fy === null ? fx : null;
|
|
2911
|
+
if (base !== null) {
|
|
2912
|
+
const k = konst(fx === null ? x : y);
|
|
2913
|
+
r =
|
|
2914
|
+
k === undefined || !base.exact
|
|
2915
|
+
? { ...base, exact: false }
|
|
2916
|
+
: { ...base, off: d.opcode === 'add' ? base.off + k : base.off - k };
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
memo.set(v, r);
|
|
2920
|
+
return r;
|
|
2921
|
+
};
|
|
2922
|
+
const bounds = new Set<number>();
|
|
2923
|
+
for (const b of fn.blocks) {
|
|
2924
|
+
for (const op of b.ops) {
|
|
2925
|
+
for (const r of op.results) {
|
|
2926
|
+
frameAddress(r);
|
|
2927
|
+
}
|
|
2928
|
+
const at = op.opcode === 'load' || op.opcode === 'store' ? frameAddress(op.operands[0]) : null;
|
|
2929
|
+
if (at?.exact) {
|
|
2930
|
+
bounds.add(at.off + (op.attrs.off as number)); // an access's offset (see THE OBJECT)
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
for (const a of memo.values()) {
|
|
2935
|
+
if (a !== null) {
|
|
2936
|
+
bounds.add(a.end);
|
|
2937
|
+
if (a.exact) {
|
|
2938
|
+
bounds.add(a.off);
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
const sorted = [...bounds].sort((x, y) => x - y);
|
|
2943
|
+
/** the span an access at `a` (plus `extra` bytes) reads — see THE OBJECT above */
|
|
2944
|
+
const span = (a: FrameAddress, extra = 0): [number, number] => {
|
|
2945
|
+
const at = a.off + extra;
|
|
2946
|
+
const lo = sorted.filter((x) => x <= at).pop() ?? at;
|
|
2947
|
+
const hi = sorted.find((x) => x > at) ?? a.end;
|
|
2948
|
+
return [lo, Math.max(hi, lo + 1)];
|
|
2949
|
+
};
|
|
2950
|
+
const reads: [number, number][] = [];
|
|
2951
|
+
const cuts = new Map<Op, [number, number][]>();
|
|
2952
|
+
const read = (v: Value, extra = 0): void => {
|
|
2953
|
+
const a = frameAddress(v);
|
|
2954
|
+
if (a !== null) {
|
|
2955
|
+
reads.push(span(a, extra));
|
|
2956
|
+
}
|
|
2957
|
+
};
|
|
2958
|
+
for (const b of fn.blocks) {
|
|
2959
|
+
for (const op of b.ops) {
|
|
2960
|
+
if ((op.opcode === 'add' || op.opcode === 'sub') && frameAddress(op.results[0]) !== null) {
|
|
2961
|
+
continue; // a derivation: its result's uses are the ones that read
|
|
2962
|
+
}
|
|
2963
|
+
const args = op.opcode === 'call' ? callArgs(op) : [];
|
|
2964
|
+
const keep = setupArgc.get(op) ?? args.length;
|
|
2965
|
+
op.operands.forEach((v, i) => {
|
|
2966
|
+
if ((op.opcode === 'store' || op.opcode === 'astore') && i === 0) {
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2969
|
+
if (op.opcode === 'call' && i >= keep && i < args.length) {
|
|
2970
|
+
const a = frameAddress(v);
|
|
2971
|
+
if (a !== null) {
|
|
2972
|
+
cuts.set(op, [...(cuts.get(op) ?? []), span(a)]);
|
|
2973
|
+
}
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
read(v, op.opcode === 'load' && i === 0 ? (op.attrs.off as number) : 0);
|
|
2977
|
+
});
|
|
2978
|
+
for (const s of op.successors) {
|
|
2979
|
+
s.args.forEach((v) => read(v));
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
const withheld = new Set<Op>();
|
|
2984
|
+
for (const [op, spans] of cuts) {
|
|
2985
|
+
if (spans.some(([lo, hi]) => !reads.some(([rlo, rhi]) => rlo < hi && rhi > lo))) {
|
|
2986
|
+
withheld.add(op);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
return withheld;
|
|
1385
2990
|
}
|
|
1386
2991
|
|
|
1387
2992
|
/** Cut every guessed call to the arity its OWN BLOCK set up, and report whether anything moved.
|
|
@@ -1399,8 +3004,13 @@ export function hasSetupArgsNarrowing(fn: Fn): boolean {
|
|
|
1399
3004
|
* Applies only to arities that were GUESSED — a declared prototype never recorded the fact. */
|
|
1400
3005
|
export function narrowToSetupArgs(fn: Fn): boolean {
|
|
1401
3006
|
let changed = false;
|
|
3007
|
+
const withheld = cutsUnreadFrameObjects(fn);
|
|
1402
3008
|
for (const b of fn.blocks) {
|
|
1403
3009
|
for (const op of b.ops) {
|
|
3010
|
+
if (withheld.has(op)) {
|
|
3011
|
+
setupArgc.delete(op);
|
|
3012
|
+
discardsPassedResult.delete(op);
|
|
3013
|
+
}
|
|
1404
3014
|
const setup = setupArgc.get(op);
|
|
1405
3015
|
if (setup !== undefined && setup < callArgs(op).length) {
|
|
1406
3016
|
truncateCallArgs(op, setup);
|
|
@@ -1419,6 +3029,18 @@ export function narrowToSetupArgs(fn: Fn): boolean {
|
|
|
1419
3029
|
|
|
1420
3030
|
// ── shared frontend tail helpers ──
|
|
1421
3031
|
|
|
3032
|
+
/** The `undef` attribute naming the other keys whose undefined values a collapsed join absorbed
|
|
3033
|
+
* (`collapseUndefinedJoins`), comma-separated. Read once, by the own-entry bound (pipeline.ts `readDroppedArgRegs`),
|
|
3034
|
+
* and removed before anything else sees the lift. */
|
|
3035
|
+
export const ABSORBED_KEYS = 'absorbed';
|
|
3036
|
+
|
|
3037
|
+
/** The `undef` attribute marking an undefined value the machine stored to a stack slot nothing reads
|
|
3038
|
+
* back (`finish`, the same test as an entry parameter's `deadHome`). The store reaches no op, so the
|
|
3039
|
+
* value looks dead, yet the asm read the register: IDO at -g3 and -O0 homes every argument register
|
|
3040
|
+
* its definition declares (`sw a1,4(sp)`) whether or not the body uses it. Read once, by the
|
|
3041
|
+
* own-entry bound (pipeline.ts `readDroppedArgRegs`), and removed with `ABSORBED_KEYS`. */
|
|
3042
|
+
export const HOMED_UNDEF = 'homed';
|
|
3043
|
+
|
|
1422
3044
|
/** The stack-slot key the MIPS, Thumb and PowerPC frontends use for a word-sized local in the
|
|
1423
3045
|
* function's own frame. Shared so they spell it identically and the frame-partition rule can
|
|
1424
3046
|
* recognise any frontend's slots. See the virtual-key note in the module header. */
|
|
@@ -1430,6 +3052,27 @@ export const stackSlotKey = (off: number): string => `${SLOT_PREFIX}${off}`;
|
|
|
1430
3052
|
export const slotKeyOffset = (key: string): number | null =>
|
|
1431
3053
|
key.startsWith(SLOT_PREFIX) ? Number(key.slice(SLOT_PREFIX.length)) : null;
|
|
1432
3054
|
|
|
3055
|
+
/** The virtual key of incoming stack argument `index` (its ABI argument rank, the registers
|
|
3056
|
+
* counted), which the Thumb, MIPS and PPC frontends read an incoming stack word through. Reading it goes
|
|
3057
|
+
* through the ordinary Braun live-in path, which turns a read with no reaching def into a function
|
|
3058
|
+
* parameter — so it needs NO new representation, opcode or pass. The `@` cannot appear in a real
|
|
3059
|
+
* register token, so the key cannot collide with one. */
|
|
3060
|
+
const STACK_ARG_PREFIX = '@sarg';
|
|
3061
|
+
export const stackArgKey = (index: number): string => `${STACK_ARG_PREFIX}${index}`;
|
|
3062
|
+
/** The {@link stackArgKey} of the incoming word at `entryOff` bytes above the caller's stack pointer,
|
|
3063
|
+
* on an ABI whose caller's parameter area starts `paramAreaAt` bytes above it and whose first
|
|
3064
|
+
* `regArgs` argument words travel in registers: word k past them is at `paramAreaAt + 4k`
|
|
3065
|
+
* (O32: 16, past the a0..a3 home; the PPC EABI: 8, past the back chain and the LR save word).
|
|
3066
|
+
* Undefined below the area and for an offset off the word grid. */
|
|
3067
|
+
export const incomingStackArgKey = (entryOff: number, paramAreaAt: number, regArgs: number): string | undefined =>
|
|
3068
|
+
entryOff >= paramAreaAt && entryOff % 4 === 0 ? stackArgKey(regArgs + (entryOff - paramAreaAt) / 4) : undefined;
|
|
3069
|
+
/** The argument rank a {@link stackArgKey} names, or null for any other key. Defined beside its mint
|
|
3070
|
+
* site on purpose: the format string and its parser once drifted 650 lines apart. */
|
|
3071
|
+
export const stackArgIndex = (key: string): number | null => {
|
|
3072
|
+
const m = /^@sarg(\d+)$/.exec(key);
|
|
3073
|
+
return m ? +m[1] : null;
|
|
3074
|
+
};
|
|
3075
|
+
|
|
1433
3076
|
/** Where a live-in of the entry block sits in the calling convention: `slotOf` gives its ABI
|
|
1434
3077
|
* argument rank (a register or an incoming stack word), or null for a live-in no argument arrives
|
|
1435
3078
|
* in — an uninitialised register the body reads; `holes` gives the keys of the argument slots the
|
|
@@ -1457,6 +3100,28 @@ export const registerArgSlots = (argRegs: readonly string[]): ArgSlots => ({
|
|
|
1457
3100
|
holes: (readKeys) => argRegs.slice(0, Math.max(0, ...readKeys.map((k) => argRegs.indexOf(k)))),
|
|
1458
3101
|
});
|
|
1459
3102
|
|
|
3103
|
+
/** The slots of a convention that passes arguments past `argRegs` on the stack: `regs` for the
|
|
3104
|
+
* registers, then incoming stack word k as slot k ({@link stackArgKey}). Reading a stack word
|
|
3105
|
+
* proves every register slot and every lower stack word precede it, one word per argument.
|
|
3106
|
+
* `besideStackWords` is the convention's own refusal of a read it cannot place beside them (O32's
|
|
3107
|
+
* float arguments, which take integer slots this rule does not count), called with the highest
|
|
3108
|
+
* stack slot read. */
|
|
3109
|
+
export const stackArgSlots = (
|
|
3110
|
+
regs: ArgSlots,
|
|
3111
|
+
argRegs: readonly string[],
|
|
3112
|
+
besideStackWords?: (readKeys: readonly string[], top: number) => void,
|
|
3113
|
+
): ArgSlots => ({
|
|
3114
|
+
slotOf: (key) => stackArgIndex(key) ?? regs.slotOf(key),
|
|
3115
|
+
holes: (readKeys) => {
|
|
3116
|
+
const top = Math.max(-1, ...readKeys.map((k) => stackArgIndex(k) ?? -1));
|
|
3117
|
+
if (top < 0) {
|
|
3118
|
+
return regs.holes(readKeys);
|
|
3119
|
+
}
|
|
3120
|
+
besideStackWords?.(readKeys, top);
|
|
3121
|
+
return Array.from({ length: top }, (_, k) => (k < argRegs.length ? argRegs[k] : stackArgKey(k)));
|
|
3122
|
+
},
|
|
3123
|
+
});
|
|
3124
|
+
|
|
1460
3125
|
/** A rank past every argument slot, for a live-in that is not one. */
|
|
1461
3126
|
const NOT_AN_ARGUMENT = Number.MAX_SAFE_INTEGER;
|
|
1462
3127
|
|
|
@@ -1493,8 +3158,21 @@ export function abiSortEntryParams(
|
|
|
1493
3158
|
assertTrueEntry(entryHasPreds);
|
|
1494
3159
|
const rank = (v: Value): number => slots.slotOf(paramReg.get(v) ?? '') ?? NOT_AN_ARGUMENT;
|
|
1495
3160
|
entry.params.sort((x, y) => rank(x) - rank(y));
|
|
3161
|
+
for (const v of entry.params) {
|
|
3162
|
+
const r = rank(v);
|
|
3163
|
+
if (r !== NOT_AN_ARGUMENT) {
|
|
3164
|
+
argRanks.set(v, r);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
1496
3167
|
}
|
|
1497
3168
|
|
|
3169
|
+
/** The ABI argument rank each entry parameter arrived in, as {@link abiSortEntryParams} ranked it. */
|
|
3170
|
+
const argRanks = new WeakMap<Value, number>();
|
|
3171
|
+
|
|
3172
|
+
/** The argument rank (registers counted, then incoming stack words) entry parameter `v` arrived in,
|
|
3173
|
+
* or undefined for a live-in that is no argument slot, or a value no entry sort ranked. */
|
|
3174
|
+
export const entryArgRank = (v: Value): number | undefined => argRanks.get(v);
|
|
3175
|
+
|
|
1498
3176
|
function assertTrueEntry(entryHasPreds: boolean): void {
|
|
1499
3177
|
if (entryHasPreds) {
|
|
1500
3178
|
throw new Error('internal: the entry-parameter rule needs an entry block with no predecessors');
|