@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.
Files changed (117) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
@@ -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
+ }