@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.
Files changed (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
@@ -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. Default: none (a frontend that models
58
- * every store may omit it, but registered frontends are held to supplying one). */
59
- storeClass?: RegExp;
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. An unmodelled STORE-CLASS instruction
84
- * (policy.storeClass) throws loud (a skipped memory write is a silent miscompile), and so does any
85
- * instruction with no degradable destination: with no register to carry the live-`?` sentinel,
86
- * skipping would silently delete a side effect (swi/syscall/sync/cache).
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 not load-bearing for soundness — a missed store degrades loudly like anything
101
- * else — but it throws naming the memory write instead of an unresolvable value, and it catches
102
- * the shape where `ops[0]` is a SOURCE (MIPS `swl rt, off(base)`) before a `dst` is fabricated
103
- * from it. It and `skipSafe` match case-INSENSITIVELY: mnemonic case is a property of the
104
- * disassembler, not of the instruction. */
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
+ }