@asmlift/core 0.8.1 → 0.10.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 +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
package/src/frontend/opaque.ts
CHANGED
|
@@ -50,13 +50,70 @@ export interface OpaquePolicy {
|
|
|
50
50
|
* destination — so there is nothing to degrade and the instruction is REFUSED, not skipped
|
|
51
51
|
* (`teq zero, zero` is a trap). Default: nothing is zero. */
|
|
52
52
|
isZero?: (r: string) => boolean;
|
|
53
|
+
/** The FPU's register operands in this ISA, and the FPU's ALONE — the message this produces
|
|
54
|
+
* names the floating-point register file in so many words, so a second unmodelled file (MIPS
|
|
55
|
+
* CP0, `hi`/`lo`, PowerPC's condition registers and SPRs) needs its own field and its own
|
|
56
|
+
* sentence rather than one more alternative here. An unmodelled instruction touching one is
|
|
57
|
+
* refused with a message naming that file, because the missing capability is the FILE, not the
|
|
58
|
+
* instruction: there is no value kind to hold what it computes, no ABI home to receive it, and
|
|
59
|
+
* no C spelling to print it. `docs/floating-point.md` measures the gap and prices the layers.
|
|
60
|
+
*
|
|
61
|
+
* Tested against EVERY operand, not just `ops[0]`, and the two shapes that force it are on
|
|
62
|
+
* opposite sides: `fcmpo cr0,f1,f2` has a destination `isReg` REJECTS (a condition register) and
|
|
63
|
+
* `mfc1 v0,$f12` one it ACCEPTS. A destination-only test files the first under "no register
|
|
64
|
+
* destination to degrade" and lets the second fabricate an opaque whose sources silently drop
|
|
65
|
+
* the FP operand it actually read. Both are floating point and neither is about the destination.
|
|
66
|
+
*
|
|
67
|
+
* IT MUST COVER EVERY DIALECT THE FRONTEND READS AND EVERY SPELLING EACH OF THEM USES, because
|
|
68
|
+
* a frontend can be fed more than one name for the same register: MIPS objdump numbers the file
|
|
69
|
+
* (`$f12`), the Splat trees use o32 ABI names (`$ft2`), and those names take a trailing `f` for
|
|
70
|
+
* the odd half of a double-precision pair (`$ft0f`) — so a pattern anchored on a final digit
|
|
71
|
+
* covers two of the three. Every miss is SILENT in the same way: the reader strips the sigil
|
|
72
|
+
* off a token it did not recognise, `isMipsReg` ACCEPTS the bare result, and the gap ends as an
|
|
73
|
+
* opaque on a register in a file nothing models. MIPS answers this with ONE predicate — the
|
|
74
|
+
* exported `MIPS_FP_REG` in `frontend/splat.ts`, which the reader and the frontend both use —
|
|
75
|
+
* because which tokens keep their sigil and which tokens are the FPU's are two halves of one
|
|
76
|
+
* decision, and two copies of it can disagree silently in either direction.
|
|
77
|
+
*
|
|
78
|
+
* A CONTROL TRANSFER IS NOT THIS MODULE'S, even when the FPU is what it is missing. MIPS
|
|
79
|
+
* `bc1t`/`bc1f` test an FP condition code, and PowerPC branch targets are bare lower-case hex
|
|
80
|
+
* that PowerPC's own `fpReg` matches — but a branch has no destination to degrade, so nothing
|
|
81
|
+
* here could answer one, and both frontends refuse unmodelled control flow in a whole-function
|
|
82
|
+
* pre-pass before a block is filled. The report keeps them as their own class for the same
|
|
83
|
+
* reason (`fp-cond-branch` in `apps/web/src/pages/benchmark/lib/declines.ts`): the missing file
|
|
84
|
+
* is shared, the mechanism is not.
|
|
85
|
+
*
|
|
86
|
+
* It is consulted FIRST, ahead of `storeClass`, so `swc1`/`stfs` are named by the file they move
|
|
87
|
+
* rather than by the fact that they move it to memory. That ordering is what the web report's
|
|
88
|
+
* decline table already assumes, and it could only assume it by re-deriving "is this floating
|
|
89
|
+
* point?" from a list of mnemonics this module never told it.
|
|
90
|
+
*
|
|
91
|
+
* REQUIRED, and `null` is the way to say "this ISA has no FPU" — an optional refusal is one the
|
|
92
|
+
* next frontend switches off by forgetting it, and the symptom is the message this field exists
|
|
93
|
+
* to remove. */
|
|
94
|
+
fpReg: RegExp | null;
|
|
95
|
+
/** FPU instructions that name no `fpReg` operand at all, matched on the MNEMONIC because there
|
|
96
|
+
* is nothing else to match: they address the FPU's CONTROL register, which is spelled in the
|
|
97
|
+
* ISA's OTHER namespace — MIPS `cfc1 rt,$31` / `ctc1 rt,$31` take a GPR and a coprocessor
|
|
98
|
+
* control number, PowerPC's `mtfsfi`/`mtfsb0`/`mtfsb1`/`mcrfs` take a field number and a
|
|
99
|
+
* condition register. A register-name predicate cannot see them, so without this they fall to
|
|
100
|
+
* the general arms and are filed as the generic opaque gap — and `ctc1`'s `ops[0]` is the
|
|
101
|
+
* instruction's SOURCE, so an opaque would be fabricated on a register it only reads.
|
|
102
|
+
*
|
|
103
|
+
* The FPU moves that DO name a data register (`mfc1`, `mtc1`, `mffs`, `mtfsf`) are `fpReg`'s,
|
|
104
|
+
* not this one's. Same message phrase either way, because it is the same missing capability.
|
|
105
|
+
*
|
|
106
|
+
* REQUIRED for the same reason as `fpReg`; `null` says the ISA has no FPU control register to
|
|
107
|
+
* reach. */
|
|
108
|
+
fpControl: RegExp | null;
|
|
53
109
|
/** Mnemonics that WRITE MEMORY in this ISA: the "no register destination ⇒ safe to fall
|
|
54
110
|
* through" premise is FALSE for stores — skipping one silently deletes the write (Thumb
|
|
55
111
|
* `stmia r0!, {…}`), and a store whose FIRST token is a register (MIPS `swl rt, off(base)`)
|
|
56
112
|
* would otherwise fabricate an opaque write to what is actually a SOURCE. An unmodelled
|
|
57
|
-
* instruction matching this pattern throws instead.
|
|
58
|
-
* every store
|
|
59
|
-
|
|
113
|
+
* instruction matching this pattern throws instead. REQUIRED, with `null` for a frontend that
|
|
114
|
+
* models every store in its ISA — held to it by the type, because the sentence that said
|
|
115
|
+
* "registered frontends are held to supplying one" was held to it by nothing. */
|
|
116
|
+
storeClass: RegExp | null;
|
|
60
117
|
/** attribution for thrown declines: the function being lifted (optionally "+ site"). */
|
|
61
118
|
context?: string;
|
|
62
119
|
/** How to SPELL the mnemonic in messages, when that differs from the name used to classify it.
|
|
@@ -80,10 +137,14 @@ export interface OpaqueDest {
|
|
|
80
137
|
|
|
81
138
|
/** Decide the opaque destination + register source operands for an unmodelled instruction
|
|
82
139
|
* `mnemonic` with operand list `ops` (operands in objdump order — destination first). Returns
|
|
83
|
-
* `null` only for a policy.skipSafe mnemonic.
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
140
|
+
* `null` only for a policy.skipSafe mnemonic. Three arms throw instead, in this order: an
|
|
141
|
+
* instruction that touches the FLOATING-POINT unit — naming one of its registers (policy.fpReg)
|
|
142
|
+
* or, for the control-register moves that name none, matching policy.fpControl — is refused
|
|
143
|
+
* first, because the file is the capability and the instruction is only where it surfaced; an
|
|
144
|
+
* unmodelled STORE-CLASS instruction (policy.storeClass) throws next (a skipped memory write is a
|
|
145
|
+
* silent miscompile); and so does any instruction with no degradable destination: with no register
|
|
146
|
+
* to carry the live-`?` sentinel, skipping would silently delete a side effect (swi/syscall/sync/
|
|
147
|
+
* cache).
|
|
87
148
|
*
|
|
88
149
|
* When non-null, the caller MUST emit an `opaque` op that writes `dst` and consumes `srcRegs`
|
|
89
150
|
* (read through the frontend's own SSA). It reaches structuring as the sentinel `?` and trips
|
|
@@ -97,23 +158,42 @@ export interface OpaqueDest {
|
|
|
97
158
|
* So `skipSafe`, a short per-ISA list of provably transparent mnemonics, is the only way an
|
|
98
159
|
* unmodelled instruction leaves no trace.
|
|
99
160
|
*
|
|
100
|
-
* `storeClass` is
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
161
|
+
* None of `fpReg`, `fpControl` and `storeClass` is load-bearing for SOUNDNESS — every instruction
|
|
162
|
+
* that reaches this function declines whatever arm takes it, because an `opaque` carries
|
|
163
|
+
* `effects: true` and reaches `assertResolved` whether or not anything reads it. What they buy is
|
|
164
|
+
* WHICH capability the decline names, and that is the whole content of a decline. Two of them buy
|
|
165
|
+
* one thing more: they catch the shapes where `ops[0]` is a SOURCE (MIPS `swl rt, off(base)`,
|
|
166
|
+
* `ctc1 rt, $31`) before a `dst` is fabricated from a register the instruction only reads.
|
|
167
|
+
* They and `skipSafe` match case-INSENSITIVELY: mnemonic case is a property of the disassembler,
|
|
168
|
+
* not of the instruction. */
|
|
105
169
|
export function opaqueDest(mnemonic: string, ops: string[], policy: OpaquePolicy): OpaqueDest | null {
|
|
106
170
|
const shown = policy.display ?? mnemonic;
|
|
171
|
+
const norm = policy.normalize ?? ((s) => s);
|
|
172
|
+
// `context` names the function (and, where the ISA has addresses, the site) — every message
|
|
173
|
+
// below lands verbatim in annotate-mode stub headers, where an un-attributed decline is
|
|
174
|
+
// unactionable in a multi-function run.
|
|
175
|
+
const where = policy.context ? `cannot lift '${policy.context}': ` : '';
|
|
176
|
+
const fpReg = policy.fpReg;
|
|
177
|
+
// DEDUPED: `fadds f1,f1,f2` names two registers, not three, and the message is read by a human
|
|
178
|
+
// asking which file is missing.
|
|
179
|
+
const fp = fpReg ? [...new Set(ops.map(norm).filter((o) => fpReg.test(o)))] : [];
|
|
180
|
+
if (fp.length > 0) {
|
|
181
|
+
throw new FrontendUnsupportedError(
|
|
182
|
+
`${where}unmodelled floating-point instruction '${shown}' — it uses the floating-point ` +
|
|
183
|
+
`register file (${fp.join(', ')}), which this frontend does not model`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (policy.fpControl?.test(mnemonic)) {
|
|
187
|
+
throw new FrontendUnsupportedError(
|
|
188
|
+
`${where}unmodelled floating-point instruction '${shown}' — it uses the floating-point ` +
|
|
189
|
+
`control register, which this frontend does not model`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
107
192
|
if (policy.storeClass?.test(mnemonic)) {
|
|
108
|
-
// `context` names the function (and, where the ISA has addresses, the site) — this message
|
|
109
|
-
// lands verbatim in annotate-mode stub headers, where an un-attributed decline is
|
|
110
|
-
// unactionable in a multi-function run.
|
|
111
|
-
const where = policy.context ? `cannot lift '${policy.context}': ` : '';
|
|
112
193
|
throw new FrontendUnsupportedError(
|
|
113
194
|
`${where}unmodelled store-class instruction '${shown}' — a memory write cannot be skipped or degraded to a register opaque`,
|
|
114
195
|
);
|
|
115
196
|
}
|
|
116
|
-
const norm = policy.normalize ?? ((s) => s);
|
|
117
197
|
const dst = norm(ops[0] ?? '');
|
|
118
198
|
// No DEGRADABLE destination: `ops[0]` is not a register, or it is the hardwired zero. Same answer
|
|
119
199
|
// for both — a zero write is a genuine no-op only for a MODELLED instruction, and by construction
|
|
@@ -122,7 +202,6 @@ export function opaqueDest(mnemonic: string, ops: string[], policy: OpaquePolicy
|
|
|
122
202
|
if (policy.skipSafe?.test(mnemonic)) {
|
|
123
203
|
return null;
|
|
124
204
|
} // explicitly transparent for this ISA
|
|
125
|
-
const where = policy.context ? `cannot lift '${policy.context}': ` : '';
|
|
126
205
|
throw new FrontendUnsupportedError(
|
|
127
206
|
`${where}unmodelled effect instruction '${shown}' — no register destination to degrade, and skipping it would silently delete its effect`,
|
|
128
207
|
);
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
// asmlift — THE CALLEE HALF OF A STRUCT RETURNED THROUGH MEMORY.
|
|
2
|
+
//
|
|
3
|
+
// A function declared to return a struct by value, on a target whose rule (aggregate.ts
|
|
4
|
+
// `returnsInMemory`) returns it through memory, is handed its caller's storage in the first
|
|
5
|
+
// argument register. Where its compiler hands the same address back in the return register (O32
|
|
6
|
+
// `move v0,a0`; agbcc leaves r0 holding it, or restores it), the machine code is the function
|
|
7
|
+
// `S *f(S *a0, ...)`; where it does not (mwcc leaves in r3 whatever was last there, `target.ts`
|
|
8
|
+
// `structReturnHandsBackPointer`), it is `void f(S *a0, ...)`. Its result is the stores through that
|
|
9
|
+
// pointer either way, which is also what `void f(S *)` and `S *f(S *)` compile to, and why only the
|
|
10
|
+
// declaration can say which one the source wrote.
|
|
11
|
+
//
|
|
12
|
+
// Where the declaration says `struct S f(...)` and the lift PROVES the pointer is that storage and
|
|
13
|
+
// nothing else, the pointer becomes a local of the struct, its stores become that local's members,
|
|
14
|
+
// every return hands the local back, and the parameters are the declared ones: `struct S f(s32 a0)
|
|
15
|
+
// { struct S ret; ret.a = a0; return ret; }`. Compiled, the compiler builds that local in a frame
|
|
16
|
+
// temp or straight in the caller's storage, and copies it out as its own lowering of `return ret;`.
|
|
17
|
+
//
|
|
18
|
+
// THE PROOF, over the whole lifted function — each condition its own refusal:
|
|
19
|
+
// - the function takes argument register 0 at all (the entry block's first parameter);
|
|
20
|
+
// - where the compiler hands the pointer back (`structReturnHandsBackPointer`), every `ret`
|
|
21
|
+
// returns that incoming pointer and nothing else; where it does not, what a `ret` hands back
|
|
22
|
+
// is read as nothing — save a value COMPUTED FOR THE RETURN ALONE (`returnedAlone`): one that
|
|
23
|
+
// nothing but a `ret` reads, through joins of nothing else. mwcc's struct-returning callee
|
|
24
|
+
// leaves in r3 what was last there, which is a value the body needed for something else (a
|
|
25
|
+
// reloaded word it stores, the pointer itself); a value nothing else reads is a scalar the
|
|
26
|
+
// function computed to return, which the declaration would silently drop — `int f(struct S12
|
|
27
|
+
// *p, int x) { ...; return x * 2; }` under a header that says `struct S12 f(int x)`. It does
|
|
28
|
+
// not catch a returned value the body also stores (`return x;` past `p->a = x`), which the
|
|
29
|
+
// machine code cannot tell from a struct return;
|
|
30
|
+
// - outside the ops `dce` deletes (agbcc's `stmia r2!` writes back an address nothing reads),
|
|
31
|
+
// the pointer is read only as the base of an unqualified store, at a byte offset that keeps the
|
|
32
|
+
// store inside the struct, and is never the stored value — so it is not read through, does
|
|
33
|
+
// not escape into a call, a global or another object, and joins no phi;
|
|
34
|
+
// - at least one store goes through it: a function that writes nothing there returns the
|
|
35
|
+
// address only, which a struct return cannot be told from.
|
|
36
|
+
// A refusal leaves the function as lifted. What the caller does with it is the caller's rule
|
|
37
|
+
// (pipeline.ts `liftStamped`): a stated declaration refuses, a context one keeps the pointer lift
|
|
38
|
+
// with a note.
|
|
39
|
+
//
|
|
40
|
+
// THE FRAME COPY. Where the compiler built the struct in a frame object and copied it out word by
|
|
41
|
+
// word (IDO's `addiu t7,sp,4; lw t9,0(t7); sw t9,0(a0)`, agbcc's `ldmia`/`stmia` pair), that frame
|
|
42
|
+
// object IS the returned local and the copy is `return ret;`'s lowering, so lifting the copy as
|
|
43
|
+
// member stores would compile to a second copy. `foldFrameCopy` folds it: the object's stores
|
|
44
|
+
// become the return object's, and the copy goes. It folds only a copy it can prove is that one —
|
|
45
|
+
// see its own note — and otherwise leaves the stores of the copied words, which is the same
|
|
46
|
+
// function spelled with one copy too many.
|
|
47
|
+
//
|
|
48
|
+
// A STRUCT RETURNED IN THE RETURN REGISTER is the other half of the target's rule, and only its
|
|
49
|
+
// simplest case is spelled as the struct (`liftOwnRegisterStruct`): one word that is all one
|
|
50
|
+
// integer member.
|
|
51
|
+
import {
|
|
52
|
+
type Block,
|
|
53
|
+
type Fn,
|
|
54
|
+
OWN_RETURN_OBJECT,
|
|
55
|
+
type Op,
|
|
56
|
+
RETURNS_OBJECT,
|
|
57
|
+
type Value,
|
|
58
|
+
defOpMap,
|
|
59
|
+
fallThroughOf,
|
|
60
|
+
mergeClasses,
|
|
61
|
+
mkOp,
|
|
62
|
+
mkValue,
|
|
63
|
+
predecessors,
|
|
64
|
+
successorsOf,
|
|
65
|
+
} from '../ir/core';
|
|
66
|
+
import { deadOps, qualified } from '../ir/discipline';
|
|
67
|
+
import { type IrType, T } from '../ir/types';
|
|
68
|
+
import type { TargetDescription } from '../target';
|
|
69
|
+
|
|
70
|
+
type StructType = Extract<IrType, { kind: 'struct' }>;
|
|
71
|
+
|
|
72
|
+
/** Rewrites `fn` so the hidden struct-return pointer in argument register 0 is a local of `type`,
|
|
73
|
+
* where the proof above holds; otherwise leaves `fn` as it is and answers why. */
|
|
74
|
+
export function liftOwnStructReturn(fn: Fn, type: StructType, target: TargetDescription): string | undefined {
|
|
75
|
+
const entry = fn.blocks[0];
|
|
76
|
+
const hidden = entry?.params[0];
|
|
77
|
+
const reg = target.argRegs[0];
|
|
78
|
+
if (hidden === undefined) {
|
|
79
|
+
return `it never reads ${reg}, where its caller hands it the address to return the struct at`;
|
|
80
|
+
}
|
|
81
|
+
const size = type.size ?? 0;
|
|
82
|
+
const dead = deadOps(fn.blocks);
|
|
83
|
+
const addresses = storageAddresses(fn, hidden, dead);
|
|
84
|
+
const handsBack = target.compilerBehaviors.structReturnHandsBackPointer !== false;
|
|
85
|
+
const why =
|
|
86
|
+
provedReturnStorage(fn, addresses, size, reg, dead, handsBack) ??
|
|
87
|
+
(handsBack ? undefined : returnedAlone(fn, addresses, dead, target.returnReg));
|
|
88
|
+
if (why !== undefined) {
|
|
89
|
+
return why;
|
|
90
|
+
}
|
|
91
|
+
const object = mkOp('laddr', {
|
|
92
|
+
results: [mkValue(T.ptr(type))],
|
|
93
|
+
attrs: { off: 0, width: 1, signed: false, count: size, aggregate: true, [OWN_RETURN_OBJECT]: true },
|
|
94
|
+
});
|
|
95
|
+
const local = object.results[0];
|
|
96
|
+
for (const b of fn.blocks) {
|
|
97
|
+
for (const op of b.ops) {
|
|
98
|
+
const k = addresses.get(op.operands[0]);
|
|
99
|
+
if (!dead.has(op) && op.opcode === 'store' && k !== undefined) {
|
|
100
|
+
op.operands = [local, ...op.operands.slice(1)];
|
|
101
|
+
op.attrs = { ...op.attrs, off: (op.attrs.off as number) + k };
|
|
102
|
+
} else if (op.opcode === 'ret') {
|
|
103
|
+
op.operands = [local];
|
|
104
|
+
op.attrs = { ...op.attrs, [RETURNS_OBJECT]: true };
|
|
105
|
+
}
|
|
106
|
+
// what is left reading the pointer is an op `dce` deletes: an address derived from it
|
|
107
|
+
op.operands = op.operands.map((v) => (v === hidden ? local : v));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
entry.params = entry.params.slice(1);
|
|
111
|
+
entry.ops.unshift(object);
|
|
112
|
+
foldFrameCopy(fn, local, size, dead);
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** THE ONE-WORD STRUCT A TARGET RETURNS IN ITS RETURN REGISTER (agbcc's and mwcc's `struct W {
|
|
117
|
+
* u32 x; }` in r0/r3): the register holds the struct's one member, so each `ret`'s word is stored
|
|
118
|
+
* to that member of a local of `type` and the local is returned by value, `ret.x = a0 + 5; return
|
|
119
|
+
* ret;`. Only a struct of exactly one word whose member at byte 0 is an integer of that word fills
|
|
120
|
+
* the register with nothing else; any other struct, or a `ret` that hands back no word, leaves `fn`
|
|
121
|
+
* as it is (its word the lift's return), and answers why. */
|
|
122
|
+
export function liftOwnRegisterStruct(fn: Fn, type: StructType): string | undefined {
|
|
123
|
+
const member = type.fields.find((f) => f.off === 0 && f.type.kind === 'int' && f.type.width === 32);
|
|
124
|
+
if (type.size !== 4 || type.fields.length !== 1 || member === undefined) {
|
|
125
|
+
return 'it is not one word that is all one integer member';
|
|
126
|
+
}
|
|
127
|
+
const rets = fn.blocks.map((b) => b.ops[b.ops.length - 1]).filter((op) => op?.opcode === 'ret');
|
|
128
|
+
if (rets.some((op) => op.operands.length !== 1)) {
|
|
129
|
+
return 'a return hands back no word';
|
|
130
|
+
}
|
|
131
|
+
const object = mkOp('laddr', {
|
|
132
|
+
results: [mkValue(T.ptr(type))],
|
|
133
|
+
attrs: { off: 0, width: 1, signed: false, count: 4, aggregate: true, [OWN_RETURN_OBJECT]: true },
|
|
134
|
+
});
|
|
135
|
+
const local = object.results[0];
|
|
136
|
+
for (const b of fn.blocks) {
|
|
137
|
+
const ret = b.ops[b.ops.length - 1];
|
|
138
|
+
if (ret?.opcode === 'ret') {
|
|
139
|
+
b.ops.splice(
|
|
140
|
+
b.ops.length - 1,
|
|
141
|
+
0,
|
|
142
|
+
mkOp('store', { operands: [local, ret.operands[0]], attrs: { off: 0, width: 4 } }),
|
|
143
|
+
);
|
|
144
|
+
ret.operands = [local];
|
|
145
|
+
ret.attrs = { ...ret.attrs, [RETURNS_OBJECT]: true };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
fn.blocks[0].ops.unshift(object);
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The incoming pointer `hidden` and every address derived from it by adding a constant (O32's
|
|
153
|
+
* `addu a3,a0,zero` copy; agbcc's `add r2, r0, #0`), each with the bytes it lies past `hidden`.
|
|
154
|
+
* Ops in `dead` derive nothing. */
|
|
155
|
+
function storageAddresses(fn: Fn, hidden: Value, dead: ReadonlySet<Op>): Map<Value, number> {
|
|
156
|
+
const consts = new Map<Value, number>();
|
|
157
|
+
for (const b of fn.blocks) {
|
|
158
|
+
for (const op of b.ops) {
|
|
159
|
+
if (op.opcode === 'const' && typeof op.attrs.value === 'number') {
|
|
160
|
+
consts.set(op.results[0], op.attrs.value);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const addresses = new Map<Value, number>([[hidden, 0]]);
|
|
165
|
+
for (let grew = true; grew;) {
|
|
166
|
+
grew = false;
|
|
167
|
+
for (const b of fn.blocks) {
|
|
168
|
+
for (const op of b.ops) {
|
|
169
|
+
if (op.opcode !== 'add' || dead.has(op) || addresses.has(op.results[0]) || op.operands.length !== 2) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const [x, y] = op.operands;
|
|
173
|
+
const k =
|
|
174
|
+
addresses.has(x) && consts.has(y)
|
|
175
|
+
? addresses.get(x)! + consts.get(y)!
|
|
176
|
+
: addresses.has(y) && consts.has(x)
|
|
177
|
+
? addresses.get(y)! + consts.get(x)!
|
|
178
|
+
: undefined;
|
|
179
|
+
if (k !== undefined) {
|
|
180
|
+
addresses.set(op.results[0], k);
|
|
181
|
+
grew = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return addresses;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Why the incoming pointer is not provably the storage a struct of `size` bytes is returned in —
|
|
190
|
+
* undefined where it is. `addresses` are it and what is derived from it (`storageAddresses`);
|
|
191
|
+
* uses in `dead` ops are no reads, and so is a `ret`'s where the compiler does not hand the
|
|
192
|
+
* pointer back (`handsBack` false), since what the return register then holds is no part of the
|
|
193
|
+
* struct return (`returnedAlone` asks the rest of it). */
|
|
194
|
+
function provedReturnStorage(
|
|
195
|
+
fn: Fn,
|
|
196
|
+
addresses: ReadonlyMap<Value, number>,
|
|
197
|
+
size: number,
|
|
198
|
+
reg: string,
|
|
199
|
+
dead: ReadonlySet<Op>,
|
|
200
|
+
handsBack: boolean,
|
|
201
|
+
): string | undefined {
|
|
202
|
+
let stores = 0;
|
|
203
|
+
for (const [i, b] of fn.blocks.entries()) {
|
|
204
|
+
if (b.params.slice(i === 0 ? 1 : 0).some((v) => addresses.has(v))) {
|
|
205
|
+
return `${reg} reaches a join`;
|
|
206
|
+
}
|
|
207
|
+
for (const op of b.ops) {
|
|
208
|
+
if (op.successors.some((s) => s.args.some((v) => addresses.has(v)))) {
|
|
209
|
+
return `${reg} reaches a join`;
|
|
210
|
+
}
|
|
211
|
+
if (op.opcode === 'ret') {
|
|
212
|
+
if (handsBack && (op.operands.length !== 1 || addresses.get(op.operands[0]) !== 0)) {
|
|
213
|
+
return `a return hands back something other than the address it was handed in ${reg}`;
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const at = op.operands.flatMap((v, k) => (addresses.has(v) ? [k] : []));
|
|
218
|
+
if (at.length === 0 || dead.has(op) || (op.opcode === 'add' && addresses.has(op.results[0]))) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const why = storeThrough(op, at, addresses.get(op.operands[at[0]])!, size);
|
|
222
|
+
if (why !== undefined) {
|
|
223
|
+
return `${reg} ${why}`;
|
|
224
|
+
}
|
|
225
|
+
stores++;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return stores === 0 ? `nothing is stored through ${reg}` : undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Where the compiler does not hand the pointer back: why a `ret` hands back a value the function
|
|
232
|
+
* computed for that return alone — undefined where none does. A ret's value and every value joined
|
|
233
|
+
* with it (`mergeClasses`) form its web; the web is the return's alone where nothing but a `ret` or
|
|
234
|
+
* a jump into the web reads any of it (ops in `dead` read nothing), and it is a computed value
|
|
235
|
+
* where some member is an op's result other than an `undef` (a register the machine left unset),
|
|
236
|
+
* or an incoming register other than the pointer. */
|
|
237
|
+
function returnedAlone(
|
|
238
|
+
fn: Fn,
|
|
239
|
+
addresses: ReadonlyMap<Value, number>,
|
|
240
|
+
dead: ReadonlySet<Op>,
|
|
241
|
+
reg: string,
|
|
242
|
+
): string | undefined {
|
|
243
|
+
const readers = new Map<Value, Op[]>();
|
|
244
|
+
const defs = defOpMap(fn);
|
|
245
|
+
for (const b of fn.blocks) {
|
|
246
|
+
for (const op of b.ops) {
|
|
247
|
+
if (!dead.has(op)) {
|
|
248
|
+
for (const v of [...op.operands, ...op.successors.flatMap((s) => s.args)]) {
|
|
249
|
+
(readers.get(v) ?? readers.set(v, []).get(v)!).push(op);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const webs = mergeClasses(fn);
|
|
255
|
+
const entry = new Set(fn.blocks[0]?.params ?? []);
|
|
256
|
+
for (const b of fn.blocks) {
|
|
257
|
+
const ret = b.ops[b.ops.length - 1];
|
|
258
|
+
if (ret?.opcode !== 'ret' || ret.operands.length !== 1 || addresses.has(ret.operands[0])) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const web = webs.get(ret.operands[0]) ?? [ret.operands[0]];
|
|
262
|
+
const alone = web.every((v) =>
|
|
263
|
+
(readers.get(v) ?? []).every(
|
|
264
|
+
(op) => op.opcode === 'ret' || (op.successors.length > 0 && !op.operands.includes(v)),
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
const computed = web.some((v) =>
|
|
268
|
+
defs.has(v) ? defs.get(v)!.opcode !== 'undef' : entry.has(v) && !addresses.has(v),
|
|
269
|
+
);
|
|
270
|
+
if (alone && computed) {
|
|
271
|
+
return `a return hands back in ${reg} a value nothing else reads, which a function returning the struct through memory has no reason to compute`;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Why `op`, which reads an address `k` bytes into the struct at operand indexes `at`, is not a
|
|
278
|
+
* store of a value into the struct through it. */
|
|
279
|
+
function storeThrough(op: Op, at: readonly number[], k: number, size: number): string | undefined {
|
|
280
|
+
if (op.opcode !== 'store') {
|
|
281
|
+
return `is read by a ${op.opcode}, not only as the base of a store`;
|
|
282
|
+
}
|
|
283
|
+
if (at.length !== 1 || at[0] !== 0) {
|
|
284
|
+
return 'is stored as a value';
|
|
285
|
+
}
|
|
286
|
+
if (qualified(op)) {
|
|
287
|
+
return 'is the base of a qualified store';
|
|
288
|
+
}
|
|
289
|
+
const off = k + (op.attrs.off as number);
|
|
290
|
+
const width = op.attrs.width as number;
|
|
291
|
+
if (!(off >= 0 && off + width <= size)) {
|
|
292
|
+
return `is stored through at bytes ${off}..${off + width}, outside the ${size} the struct takes`;
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** One access through a frame object: the op, and the byte it reaches from the object's start. */
|
|
298
|
+
interface FrameAccess {
|
|
299
|
+
op: Op;
|
|
300
|
+
at: number;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** FOLDS A FRAME OBJECT COPIED WHOLE INTO THE RETURN OBJECT, where every store to the return object
|
|
304
|
+
* (`ret`, the hidden pointer's local) is a COPY: an unqualified store of a load of the same width,
|
|
305
|
+
* read through one frame object (`laddr`) at one fixed shift from the stored byte. Then the frame
|
|
306
|
+
* object's stores are the return object's, the copy is deleted, and the frame object goes with it.
|
|
307
|
+
*
|
|
308
|
+
* It folds only where that is the same function, which needs every one of:
|
|
309
|
+
* - the copies in each block tile the struct, `[0, size)`, each byte once: the compiler copies the
|
|
310
|
+
* whole of it, once per block (IDO repeats the copy at each `return r;`);
|
|
311
|
+
* - each copied load is read by its copy alone, and is a load of the frame object at
|
|
312
|
+
* `shift + off` for the copy at `off`;
|
|
313
|
+
* - the frame object is reached only through `laddr`s of its offset, as the base of unqualified
|
|
314
|
+
* loads and stores — never passed, stored or added to — and every access lies in the copied
|
|
315
|
+
* range `[shift, shift + size)`, so the struct is all the object holds;
|
|
316
|
+
* - at least one store reaches the frame object: one nothing stores to is a struct returned
|
|
317
|
+
* wholly unset, whose copy is all that writes `ret`;
|
|
318
|
+
* - no store to the frame object follows a copied load: not later in a copy's block, and not in
|
|
319
|
+
* any block reachable from one — so each copy reads what was stored — every store to the object
|
|
320
|
+
* is on a path into a copy block, and no return is reached through no copy, so each store is
|
|
321
|
+
* one the returned struct holds and not a write to storage a path that skips the copy leaves
|
|
322
|
+
* alone (`storeFollowsCopy`).
|
|
323
|
+
* Anything else leaves `fn` as it is. Where the copy is in several blocks, each returns the struct
|
|
324
|
+
* itself (`returnAtEachCopy`). */
|
|
325
|
+
function foldFrameCopy(fn: Fn, ret: Value, size: number, dead: ReadonlySet<Op>): void {
|
|
326
|
+
const defs = new Map<Value, Op>();
|
|
327
|
+
const blockOf = new Map<Op, Block>();
|
|
328
|
+
for (const b of fn.blocks) {
|
|
329
|
+
for (const op of b.ops) {
|
|
330
|
+
op.results.forEach((r) => defs.set(r, op));
|
|
331
|
+
blockOf.set(op, b);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const uses = new Map<Value, Op[]>();
|
|
335
|
+
for (const b of fn.blocks) {
|
|
336
|
+
for (const op of b.ops) {
|
|
337
|
+
if (dead.has(op)) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
for (const v of [...op.operands, ...op.successors.flatMap((s) => s.args)]) {
|
|
341
|
+
(uses.get(v) ?? uses.set(v, []).get(v)!).push(op);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const copies = (uses.get(ret) ?? []).filter((op) => op.opcode === 'store');
|
|
346
|
+
if (copies.length === 0) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
// each copy: the load it stores, its frame object's offset, and the shift from the stored byte
|
|
350
|
+
let frame: number | undefined;
|
|
351
|
+
let shift: number | undefined;
|
|
352
|
+
const copiedLoads = new Set<Op>();
|
|
353
|
+
// the copies by the block they are in: one whole copy per block, as IDO repeats it at each return
|
|
354
|
+
const tiles = new Map<Block, [number, number][]>();
|
|
355
|
+
for (const c of copies) {
|
|
356
|
+
const load = defs.get(c.operands[1]);
|
|
357
|
+
const base = load?.opcode === 'load' ? defs.get(load.operands[0]) : undefined;
|
|
358
|
+
if (
|
|
359
|
+
load === undefined ||
|
|
360
|
+
base?.opcode !== 'laddr' ||
|
|
361
|
+
base.attrs[OWN_RETURN_OBJECT] === true ||
|
|
362
|
+
qualified(c) ||
|
|
363
|
+
qualified(load) ||
|
|
364
|
+
load.attrs.width !== c.attrs.width ||
|
|
365
|
+
(uses.get(load.results[0]) ?? []).length !== 1
|
|
366
|
+
) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const s = (load.attrs.off as number) - (c.attrs.off as number);
|
|
370
|
+
if ((frame ?? base.attrs.off) !== base.attrs.off || (shift ?? s) !== s) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
frame = base.attrs.off as number;
|
|
374
|
+
shift = s;
|
|
375
|
+
copiedLoads.add(load);
|
|
376
|
+
const at = blockOf.get(c)!;
|
|
377
|
+
(tiles.get(at) ?? tiles.set(at, []).get(at)!).push([c.attrs.off as number, c.attrs.width as number]);
|
|
378
|
+
}
|
|
379
|
+
for (const t of tiles.values()) {
|
|
380
|
+
t.sort((a, b) => a[0] - b[0]);
|
|
381
|
+
if (t.reduce((end, [off, w]) => (end === off ? off + w : -1), 0) !== size) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// every access to the frame object, and nothing else that reaches it
|
|
386
|
+
const accesses: FrameAccess[] = [];
|
|
387
|
+
const laddrs = fn.blocks
|
|
388
|
+
.flatMap((b) => b.ops)
|
|
389
|
+
.filter((op) => op.opcode === 'laddr' && op.attrs.off === frame && op.attrs[OWN_RETURN_OBJECT] !== true);
|
|
390
|
+
for (const l of laddrs) {
|
|
391
|
+
for (const u of uses.get(l.results[0]) ?? []) {
|
|
392
|
+
const access = (u.opcode === 'load' || u.opcode === 'store') && u.operands[0] === l.results[0];
|
|
393
|
+
const stored = u.opcode === 'store' && u.operands[1] === l.results[0];
|
|
394
|
+
if (!access || stored || qualified(u) || u.operands.filter((v) => v === l.results[0]).length !== 1) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const at = u.attrs.off as number;
|
|
398
|
+
if (at < shift! || at + (u.attrs.width as number) > shift! + size) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (u.opcode === 'load' && !copiedLoads.has(u)) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
accesses.push({ op: u, at });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const stores = accesses.filter((a) => a.op.opcode === 'store');
|
|
408
|
+
// a frame object nothing stores to is a struct returned wholly unset (`struct S12 r; return r;`):
|
|
409
|
+
// folding it would leave `ret` read and never written, so the copy of its unset words stays
|
|
410
|
+
if (stores.length === 0 || storeFollowsCopy(fn, [...tiles.keys()], copiedLoads, stores, blockOf)) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
for (const { op, at } of stores) {
|
|
414
|
+
op.operands = [ret, ...op.operands.slice(1)];
|
|
415
|
+
op.attrs = { ...op.attrs, off: at - shift! };
|
|
416
|
+
}
|
|
417
|
+
const gone = new Set<Op>([...copies, ...copiedLoads]);
|
|
418
|
+
for (const b of fn.blocks) {
|
|
419
|
+
b.ops = b.ops.filter((op) => !gone.has(op));
|
|
420
|
+
}
|
|
421
|
+
// …and the frame object's addresses, where nothing still reads them (an op `dce` deletes may)
|
|
422
|
+
const read = new Set(fn.blocks.flatMap((b) => b.ops.flatMap((op) => op.operands)));
|
|
423
|
+
for (const b of fn.blocks) {
|
|
424
|
+
b.ops = b.ops.filter((op) => !laddrs.includes(op) || read.has(op.results[0]));
|
|
425
|
+
}
|
|
426
|
+
if (tiles.size > 1) {
|
|
427
|
+
returnAtEachCopy(fn, [...tiles.keys()]);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** A COPY REPEATED IN SEVERAL BLOCKS IS A `return` IN EACH: IDO lowers every `return r;` to its own
|
|
432
|
+
* copy and branches on to the one epilogue, where the lift has a block that only returns. So each
|
|
433
|
+
* copy block that branches straight to such a block returns there itself, and the block goes where
|
|
434
|
+
* nothing else reaches it — `if (x) { …; return ret; } …; return ret;`, the function IDO compiled,
|
|
435
|
+
* where one `return ret;` past the join compiles to one copy. */
|
|
436
|
+
function returnAtEachCopy(fn: Fn, at: readonly Block[]): void {
|
|
437
|
+
for (const b of at) {
|
|
438
|
+
const br = b.ops[b.ops.length - 1];
|
|
439
|
+
const m = br?.opcode === 'br' && br.successors.length === 1 ? br.successors[0].block : undefined;
|
|
440
|
+
const ret = m?.ops.length === 1 ? m.ops[0] : undefined;
|
|
441
|
+
if (ret?.opcode !== 'ret' || ret.operands.some((v) => m!.params.includes(v))) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const { fallthrough: _, ...attrs } = ret.attrs;
|
|
445
|
+
b.ops[b.ops.length - 1] = mkOp('ret', { operands: [...ret.operands], attrs: { ...attrs, ...fallThroughOf(br) } });
|
|
446
|
+
}
|
|
447
|
+
const preds = predecessors(fn);
|
|
448
|
+
fn.blocks = fn.blocks.filter((b, k) => k === 0 || (preds.get(b) ?? []).length > 0);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Whether a store in `stores` is not one a copy reads, or a copy is not one every return reads. The
|
|
452
|
+
* copies are one whole copy per block in `at`. A store is not read by one where it can run after a
|
|
453
|
+
* copied load — later in its copy's block, or in any block reachable from a copy's (which includes
|
|
454
|
+
* that block itself, where it heads a loop) — or where it runs on a path that reaches no copy at
|
|
455
|
+
* all, where the caller's storage was left unwritten and folding would write it. And a return
|
|
456
|
+
* reached on a path through no copy hands back storage the machine leaves alone, which a folded
|
|
457
|
+
* `ret` would hold the frame object's stores in. */
|
|
458
|
+
function storeFollowsCopy(
|
|
459
|
+
fn: Fn,
|
|
460
|
+
at: readonly Block[],
|
|
461
|
+
loads: ReadonlySet<Op>,
|
|
462
|
+
stores: readonly FrameAccess[],
|
|
463
|
+
blockOf: ReadonlyMap<Op, Block>,
|
|
464
|
+
): boolean {
|
|
465
|
+
const copying = new Set(at);
|
|
466
|
+
const first = new Map(at.map((b) => [b, b.ops.findIndex((op) => loads.has(op))]));
|
|
467
|
+
if ([...first.values()].some((i) => i < 0) || [...loads].some((l) => !copying.has(blockOf.get(l)!))) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
const walk = (from: Block[], next: (b: Block) => Block[]): Set<Block> => {
|
|
471
|
+
const seen = new Set<Block>();
|
|
472
|
+
const work = [...from];
|
|
473
|
+
while (work.length > 0) {
|
|
474
|
+
const b = work.pop()!;
|
|
475
|
+
if (!seen.has(b)) {
|
|
476
|
+
seen.add(b);
|
|
477
|
+
work.push(...next(b));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return seen;
|
|
481
|
+
};
|
|
482
|
+
const after = walk(at.flatMap(successorsOf), successorsOf);
|
|
483
|
+
const preds = predecessors(fn);
|
|
484
|
+
const before = walk(
|
|
485
|
+
at.flatMap((b) => preds.get(b) ?? []),
|
|
486
|
+
(b) => preds.get(b) ?? [],
|
|
487
|
+
);
|
|
488
|
+
// the blocks the entry reaches through no copy, none of which may return
|
|
489
|
+
const copyless =
|
|
490
|
+
fn.blocks.length === 0 || copying.has(fn.blocks[0])
|
|
491
|
+
? new Set<Block>()
|
|
492
|
+
: walk([fn.blocks[0]], (b) => successorsOf(b).filter((s) => !copying.has(s)));
|
|
493
|
+
if ([...copyless].some((b) => b.ops.some((op) => op.opcode === 'ret'))) {
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
return stores.some(({ op }) => {
|
|
497
|
+
const b = blockOf.get(op)!;
|
|
498
|
+
return after.has(b) || (copying.has(b) ? b.ops.indexOf(op) > first.get(b)! : !before.has(b));
|
|
499
|
+
});
|
|
500
|
+
}
|