@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,628 @@
1
+ /** THE DEAD-READ PIN: a memory read the lifted function never uses was `volatile` in the source, so
2
+ * it is stamped `volatile` (the `device` placement, ir/discipline.ts) before anything can delete
3
+ * it, and the structurer spells it through a qualified pointer or declines.
4
+ *
5
+ * The claim is the COMPILER's, and it is the one ir/discipline.ts `spelledWhenDead` already rests
6
+ * on: an optimizing compiler's dead-code pass deletes every plain read whose value nothing uses.
7
+ * A pass that runs after it can still leave one behind — a split, a merge, a spill — and the gates
8
+ * below name each one found. agbcc expands an expression statement with no target (stmt.c
9
+ * `expand_expr_stmt`) and its flow pass keeps an insn whose result is dead only when
10
+ * `volatile_refs_p` says it touches a `MEM_VOLATILE_P` cell
11
+ * (flow.c:1296, rtlanal.c:1628); measured, agbcc at -O2 and -O0, ido 7.1, gcc 2.7.2 (KMC) and
12
+ * mwcc 2.4.2 each emit no read for `gR; *p; *(int *)0x4000004;` and keep every one of them
13
+ * qualified. So a read still in the target whose result is overwritten or never consumed is the
14
+ * `dmaRegs[2];` that ends a DMA macro, a `REG_KEYINPUT;` acknowledge, an `extern volatile` poll
15
+ * read once for its side effect — whatever the address, symbol or literal or a pointer parameter.
16
+ * Left plain, the lift's own dead-code pass (pattern/engine.ts `dce`) drops it, and the recompile
17
+ * loses an access the machine made. This pin is the ONE place that claim is decided: the structurer
18
+ * spells a read nothing uses only when it is qualified (structure.ts `volatileQualifiable`).
19
+ *
20
+ * DEAD means what that pass would delete (ir/discipline.ts `deadOps`, the one definition): a
21
+ * `load`/`aload` whose result reaches no op the pass keeps, through any chain of ops it would delete
22
+ * with it — an extension of the read nothing reads is as dead as the read. It is decided on the RAW
23
+ * lift, after the frontend's SSA is final and before any raise rewrite, so a read only asmlift's
24
+ * own folds orphan is never counted — save one the frontend orphans itself by dropping a spill
25
+ * nothing reloads, which `spilled` refuses. One use the pass counts is not one here: the operand of the
26
+ * `ret` of a function its OWN declaration says is void, which the structurer suppresses — the
27
+ * read-back that ends `void f(int b) { REG = b; REG; }` is in the return register at `bx lr`.
28
+ *
29
+ * THE READS ARE DECIDED OUTERMOST FIRST. A dead read whose value, through dead ops, is the address
30
+ * of another dead read waits until that one is decided: kept, it is a use, and the inner read is
31
+ * live — `*(volatile int *)*pp;` qualifies the read of `*pp`'s target, not the read of `pp`.
32
+ *
33
+ * The reads the rule must not claim are tabled (`DEAD_READ_GATES`, in this order) and REPORTED: a
34
+ * caller may hand its own table (`tallying`, l3/gates.ts) to census the refusals.
35
+ * • a read of a function built at a level where the compiler keeps a plain read nothing uses
36
+ * (target.ts `keepsUnusedReadsAt`) on a compiler that names the register it discards a
37
+ * volatile read into (target.ts `discardsUnusedReadsInto`), save one the machine loaded into
38
+ * that register and left there (`discarded`): mwcc at `-O0` keeps `x = p[5]; x = 0;`'s load in
39
+ * r31 and IDO at `-O1` in t6, so a read in a home says nothing about `volatile`, where `*p;`
40
+ * through a volatile pointer is `lwz r0` and `lw zero` — and a level no flag names, or one a
41
+ * word the flag table cannot read may set (mwcc `-opt off`), is judged as keeping. A load whose
42
+ * value, or a value computed from it, is written to another register is not discarded
43
+ * (`leftInDiscardRegister`: mwcc's `lwz r0` + `extsb r31,r0` of `signed char x; x = p[5];
44
+ * x = 0;`); the second word of IDO's doubleword discard, `lw at,4(a0)` after `lw zero,0(a0)`,
45
+ * is (frontend/mips.ts `emitLoad`);
46
+ * • a read of a global the project declares with no volatile byte where it reads (`declared-plain`:
47
+ * the symbol map where it lays the object out, else the declaration context): the source spelled
48
+ * the read plain, so the compiler kept it for some other reason — agbcc merges the identical
49
+ * arms of `if (g.c) x = 0x800; else x = 0x800;` (kleod's `sub_08042024`) after flow has decided
50
+ * which insns are dead, and the read the deleted branch tested survives, plain;
51
+ * • a read of the function's own frame object (`laddr`): a local nothing outside the function
52
+ * observes, whose dead read is a reload, not an access to keep;
53
+ * • any read of a function that holds an unmodelled instruction (`opaque`, frontend/opaque.ts):
54
+ * the lift does not know what that instruction reads, so it cannot call a value unused —
55
+ * mwcc's `lbz r0, 76(r3)` + `rlwimi r0, r4, 7, 24, 24` + `stb` reads the byte it inserts into,
56
+ * and the opaque standing for the `rlwimi` does not name it;
57
+ * • a read still held, at a return whose width the lift guessed, in the register that carries
58
+ * the second word of a two-word return (`unreadPairHalf`): asmlift returns one word where no
59
+ * declaration pins the width, so a value in the pair's second register is unused by the LIFT,
60
+ * not by the source — agbcc's leaf `long long k(long long *p) { return *p; }` is
61
+ * `ldr r1, [r0, #4]; ldr r0, [r0]; bx lr`, and stamping the r1 load would publish a one-word
62
+ * return that drops the high half beside an invented volatile read of RAM, byte-exact;
63
+ * • a read still held in an argument register at a call whose arity no declaration states
64
+ * (call-plan.ts `heldAtUndeclaredCall`): the guess passes what it counts, so a value it leaves
65
+ * out is unused by the lift, not by the source — agbcc's `g(a, c ? p[1] : p[2])` leaves r0 at
66
+ * its incoming value, the guess passes nothing, and stamping the r1 loads publishes `g();` beside
67
+ * two invented volatile reads;
68
+ * • a read whose value was stored to the function's own stack (`spilled`, ssa.ts `frameWrites`):
69
+ * each frontend keeps a stack slot as a variable and drops a store nothing reloads, so a read
70
+ * only that store consumed looks dead because the lift dropped its consumer — KMC's `struct S
71
+ * s = *p; return s.a;` stores both words to the frame, IDO a `long long` it truncates;
72
+ * • a read that is the address of a read the pin left plain (`addresses-plain-read`): the outer
73
+ * read was decided first, and the pointer it read through was the compiler's to compute;
74
+ * • a read the compiler could not have made of a `volatile` (target.ts `readCouldBeVolatile`): on
75
+ * a compiler that extends a qualified narrow read in a register, a sign-extending load is a
76
+ * plain read in the source, the same rule frontend/device-pins.ts applies to a device read;
77
+ * • `undecided-at-keeping-level`, whose verdict REFUSES THE LIFT instead of leaving the read
78
+ * plain: a read at a keeping level on a compiler that names no discard register, which none of
79
+ * the gates above claims. agbcc, gcc 2.7.2 and KMC at `-O0` load a volatile `*p;` into a
80
+ * register nothing then reads (`ldr r0`, `lw v0`), and a `register` local assigned and
81
+ * overwritten (`register int x; x = p[5]; x = 0;`) is the same instructions; agbcc's
82
+ * `if (*vp) {}` is the same `ldr; cmp; beq` as the plain `if (p[0]) {}`. Neither spelling is in
83
+ * evidence, and either one, published, is a function the machine's source did not write:
84
+ * dropped, the volatile access is gone; stamped, the plain one gains an access. It sits BEFORE
85
+ * the two gates below, which rest on an optimiser's premise a keeping level does not keep;
86
+ * • a read a compare consumed (`compared`, gathered by each frontend that fuses a compare into the
87
+ * branch that reads it): a compare no branch reads leaves no op, so the read looks dead only
88
+ * because the lift has no op for its consumer — mwcc 2.3.3 at `-O4,s` keeps
89
+ * `lwz r0, g; cmpwi r0, 0` for `if (g) { (g != 0); }`;
90
+ * • a read the machine computed with (`computed`): an op other than an extension or a read
91
+ * consumes the value, or an extension of it — an op dead in the lift too, but one the compiler
92
+ * emitted, so it did not judge the value unused there. KMC splits a `long long` sum after flow,
93
+ * and keeps the high word's `addu` of `v += 3; return (int)v;`.
94
+ * RESIDUE, untabled: reads stamped with a `volatile` the source did not write, or spelled at
95
+ * another width. The first four recompile byte-exact and compute the same values; the next two
96
+ * inherit a wrong input; the last performs the same accesses and does not recompile byte-exact.
97
+ * • a bare read an inline `asm("" :: "r"(x))` took whole. agbcc leaves the statement as a bare
98
+ * `.code 16` the Thumb frontend skips, so the read it keeps alive looks dead; where the asm took
99
+ * an extraction (sa3's `Task_150_80A619C` reads `gPlayers[0].charFlags.character` with
100
+ * `lsl`/`lsr` first), `computed` leaves it plain;
101
+ * • the unused half of a DImode move under a declaration that pins a one-word return. agbcc splits
102
+ * the move into two word loads AFTER flow has deleted dead insns
103
+ * (`thumb_load_double_from_address`), so `int f(long long *p) { long long v = *p; return
104
+ * (int)v; }` keeps `ldr r1, [r0, #4]`. With no such declaration the pair gate refuses it on Thumb
105
+ * and PowerPC; O32 declines the v0:v1 pair before the pin is asked;
106
+ * • a plain read under an arithmetic or a sign-extending cast nothing uses, at mwcc `-O0`:
107
+ * `p[5] + 1;` and `(char)p[5];` keep `lwz r0, 20(r3)` (the cast with an `extsb`/`extsh` of r0
108
+ * where `char` is signed), which is the register the compiler discards a volatile read into,
109
+ * so the read is stamped. A zero-extending cast (`clrlwi r0,r0,24`: `(unsigned char)`, and
110
+ * `(char)` under `-char unsigned`, as the 2.4.7 and 2.3.3 projects build) lowers to an `and`,
111
+ * which `computed` rejects, so that read is left plain. No unit of Mario Party 4's 409 at
112
+ * `-O0,p` holds one;
113
+ * • merged identical arms where no declaration is in evidence: the same instructions as `gR;` on an
114
+ * object nothing declares, and as `*p;` through a pointer parameter;
115
+ * • a read returned by a getter wrongly declared void: the declaration is trusted, as the pair
116
+ * gate trusts a declared width;
117
+ * • hand-written assembly, which is outside the compiler claim: klonoa's m4a `sub_0804FE6C` returns
118
+ * a byte in r3, which no ABI the lift models reads, so the lift's return model has already lost
119
+ * that byte and its read is stamped;
120
+ * • IDO's doubleword discard (`volatile long long`/`double` `*p;`, `lw zero` + `lw at`) is
121
+ * stamped as two word reads, which recompile to two `lw zero`.
122
+ * DROPPED, untabled: volatile reads left plain, which the lift's dead-code pass deletes.
123
+ * • a volatile read assigned to a local nothing reads (`x = *vp; x = 0;`, `int x = *vp;`) at a
124
+ * keeping level: the same instructions as its plain twin on every compiler measured (mwcc's
125
+ * r31 home, or `lwz r0` + `extsb r31,r0` for a `char` local, IDO's `t6` and a frame store,
126
+ * agbcc's and gcc's frame store), so no fact tells the two apart, and the plain one is the
127
+ * population: of the 925 non-frame loads nothing reads over Mario Party 4's -O0,p units, 924
128
+ * sit in r17-r31 and f18-f31 homes, and the other is a volatile read in r0;
129
+ * • a read held in the return pair's second register at a return whose width the lift guessed,
130
+ * at a keeping level on a compiler with no discard register: agbcc `-O0` keeps
131
+ * `return (*p, 1);`'s volatile read in r1, and `held-at-word-return` leaves it plain before
132
+ * the refusal is asked. A declaration that pins the return width sends it to the refusal;
133
+ * • a read in the discard register that a dead compare or computation consumes, at mwcc `-O0`:
134
+ * `-*vp;` (`neg r0,r0`), `!*vp;` and `*vp ? 1 : 2;` are the same instructions as their plain
135
+ * twins, which mwcc keeps too; the volatile bitfield `s->b;` (`lbz r0` + `clrlwi r0,r0,27`) is
136
+ * not — its plain twin compiles to nothing — and is dropped all the same;
137
+ * • a read in a home that a dead compare consumes at IDO `-O1`: `if (*vp) {}` and `*vp ? 1 : 2;`
138
+ * are `lw t6` + `beqz t6`, where the plain twins compile to nothing, so the compare is evidence
139
+ * of `volatile` there, and no gate reads it (at `-O0` the plain twin is the same code). */
140
+ import { globalBaseOf, globalCellOf } from '../ir/alias';
141
+ import { type Block, type Op, type Successor, type Value, forwardingTarget } from '../ir/core';
142
+ import { deadOps, spelledWhenDead } from '../ir/discipline';
143
+ import { type Gate, firstRejection } from '../l3/gates';
144
+ import { type FnProto, declaresVoidReturn, ownReturnWidth } from '../proto';
145
+ import type { ContextGlobals } from '../proto-context';
146
+ import { type SymbolInfo, type SymbolMap, declaresVolatile, symbolsByName } from '../symbols';
147
+ import { type TargetDescription, readCouldBeVolatile } from '../target';
148
+ import { FrontendUnsupportedError } from './errors';
149
+ import type { SsaBuilder } from './ssa';
150
+
151
+ /** What a dead-read gate judges: the read, and the facts about it the gates read. */
152
+ export interface DeadRead {
153
+ readonly op: Op;
154
+ /** its address is this function's own frame object, an offset of a `laddr` */
155
+ readonly fromFrame: boolean;
156
+ /** the function holds an instruction the lift could not model (an `opaque`) */
157
+ readonly unmodelled: boolean;
158
+ /** its value, or a value only it feeds, is still in the return pair's second register at a return
159
+ * whose width the lift guessed (`unreadPairHalf`) */
160
+ readonly heldAtWordReturn: boolean;
161
+ /** its value, or a value only it feeds, is still in an argument register at a call whose arity no
162
+ * declaration states (call-plan.ts `heldAtUndeclaredCall`) */
163
+ readonly heldAtUndeclaredCall: boolean;
164
+ /** its value, or a value only it feeds, is an operand of a compare (`DeadReadFacts.compared`) */
165
+ readonly compared: boolean;
166
+ /** its value, or a value only it feeds, was stored to the function's own stack
167
+ * (`DeadReadFacts.spilled`) */
168
+ readonly spilled: boolean;
169
+ /** its value, or an extension of it, is an operand of an op that is neither an extension nor a
170
+ * read (`usesOf`): arithmetic the machine emitted */
171
+ readonly computed: boolean;
172
+ /** its value, or an extension of it, is the address of a read the pin left plain (`usesOf`) */
173
+ readonly addressesPlainRead: boolean;
174
+ /** its address reaches one global, declared with no volatile byte where it reads (`declaredPlain`) */
175
+ readonly declaredPlain: boolean;
176
+ /** the machine loaded it into the register the compiler discards an unused read into
177
+ * (`DeadReadFacts.discarded`, target.ts `discardsUnusedReadsInto`) */
178
+ readonly discarded: boolean;
179
+ readonly target: TargetDescription;
180
+ }
181
+
182
+ /** The gate whose verdict is a REFUSAL of the lift, not a read left plain: see its entry in the
183
+ * file header. A table without it (`without`) leaves the read to the gates after it. */
184
+ const UNDECIDED = 'undecided-at-keeping-level';
185
+
186
+ export const DEAD_READ_GATES: readonly Gate<DeadRead>[] = [
187
+ {
188
+ id: 'keeps-unused-reads',
189
+ why: 'at these flags the compiler keeps a plain read nothing uses, and this one is not in the register it discards a volatile read into',
190
+ sound: true,
191
+ guardedBy: 'dead-reads.test.ts: keeps-unused-reads: mwcc -O0 keeps a dead assignment plain',
192
+ rejects: (c) =>
193
+ c.target.compilerBehaviors.keepsUnusedReads === true &&
194
+ c.target.compilerBehaviors.discardsUnusedReadsInto !== undefined &&
195
+ !c.discarded,
196
+ },
197
+ {
198
+ id: 'declared-plain',
199
+ why: 'the declaration of the object read has no volatile byte there, so the source spelled the read plain',
200
+ sound: false,
201
+ guardedBy: 'dead-reads.test.ts: declared-plain: a dead read of a global declared plain stays deletable',
202
+ rejects: (c) => c.declaredPlain,
203
+ },
204
+ {
205
+ id: 'frame-object',
206
+ why: 'a dead read of a local of this function is a reload, which nothing outside it observes',
207
+ sound: false,
208
+ guardedBy: 'dead-reads.test.ts: frame-object: a dead read of a frame object stays deletable',
209
+ rejects: (c) => c.fromFrame,
210
+ },
211
+ {
212
+ id: 'unmodelled-instruction',
213
+ why: 'an instruction the lift could not model may be what reads the value, so the read is not proved dead',
214
+ sound: false,
215
+ guardedBy: 'dead-reads.test.ts: unmodelled-instruction: a read an unmodelled instruction may consume stays plain',
216
+ rejects: (c) => c.unmodelled,
217
+ },
218
+ {
219
+ id: 'held-at-word-return',
220
+ why: 'the value may be the second word of a two-word return the lift lowered as one word',
221
+ sound: false,
222
+ guardedBy: 'dead-reads.test.ts: held-at-word-return: a leaf 64-bit return from memory keeps no volatile high word',
223
+ rejects: (c) => c.heldAtWordReturn,
224
+ },
225
+ {
226
+ id: 'held-at-undeclared-call',
227
+ why: 'the value may be an argument a guessed arity left out of the call',
228
+ sound: false,
229
+ guardedBy: 'dead-reads.test.ts: held-at-undeclared-call: a read the guessed arity leaves out stays plain',
230
+ rejects: (c) => c.heldAtUndeclaredCall,
231
+ },
232
+ {
233
+ id: 'spilled',
234
+ why: 'the machine stored the value to its stack; the lift keeps the slot as a variable and drops a store nothing reloads',
235
+ sound: false,
236
+ guardedBy: 'dead-reads.test.ts: spilled: a read whose value was stored to the frame stays plain',
237
+ rejects: (c) => c.spilled,
238
+ },
239
+ {
240
+ id: 'addresses-plain-read',
241
+ why: 'the value is the address of a read left plain, which the compiler used it to make',
242
+ sound: false,
243
+ guardedBy:
244
+ 'dead-reads.test.ts: addresses-plain-read: a read that is the address of a read the pin left plain is that read’s use',
245
+ rejects: (c) => c.addressesPlainRead,
246
+ },
247
+ {
248
+ id: 'not-a-volatile-read',
249
+ why: 'a read that extends in a way the compiler never extends a volatile one was plain in the source',
250
+ sound: false,
251
+ guardedBy: 'dead-reads.test.ts: not-a-volatile-read: a dead sign-extending narrow read stays plain on agbcc',
252
+ rejects: (c) => !readCouldBeVolatile(c.target.compilerBehaviors, c.op),
253
+ },
254
+ {
255
+ id: UNDECIDED,
256
+ why: 'at these flags the compiler keeps a plain read nothing uses and has no register of its own for a volatile one, so the lift refuses',
257
+ sound: true,
258
+ guardedBy:
259
+ 'dead-reads.test.ts: undecided-at-keeping-level: a keeping level on a compiler with no discard register refuses a read no evidence claims',
260
+ rejects: (c) =>
261
+ c.target.compilerBehaviors.keepsUnusedReads === true &&
262
+ c.target.compilerBehaviors.discardsUnusedReadsInto === undefined,
263
+ },
264
+ {
265
+ id: 'compared',
266
+ why: 'a compare consumed the value; the lift has no op for a compare no branch reads',
267
+ sound: false,
268
+ guardedBy: 'dead-reads.test.ts: compared: a read only a dead compare consumes stays plain',
269
+ rejects: (c) => c.compared,
270
+ },
271
+ {
272
+ id: 'computed',
273
+ why: 'the machine computed with the value, so the compiler did not judge it unused where it emitted that op',
274
+ sound: false,
275
+ guardedBy: 'dead-reads.test.ts: computed: a read a dead add consumes stays plain',
276
+ rejects: (c) => c.computed,
277
+ },
278
+ ];
279
+
280
+ /** What a frontend knows about its lift that the IR does not carry, for `pinDeadReads`. */
281
+ export interface DeadReadFacts {
282
+ /** the function's own name, for the refusal `undecided-at-keeping-level` makes */
283
+ readonly name?: string;
284
+ /** the function's own declaration: a declared void return's `ret` operand is no use */
285
+ readonly own?: FnProto;
286
+ /** what the return pair's second register may hold at a guessed word return (`unreadPairHalf`) */
287
+ readonly atWordReturn?: ReadonlySet<Value>;
288
+ /** what an argument register may hold at a call of undeclared arity (call-plan.ts) */
289
+ readonly atUndeclaredCall?: ReadonlySet<Value>;
290
+ /** every value a compare consumed, where a compare leaves no op of its own: Thumb's `cmp` and
291
+ * PowerPC's `cmpw`/`cmpwi`/`cmplw`/`cmplwi` and record forms fuse into the branch that reads
292
+ * them, and one no branch reads is gone */
293
+ readonly compared?: ReadonlySet<Value>;
294
+ /** every value the machine stored to its own stack, which the lift keeps as a slot variable
295
+ * (ssa.ts `frameWrites`) */
296
+ readonly spilled?: ReadonlySet<Value>;
297
+ /** every value a load put in the register the compiler discards an unused read into
298
+ * (target.ts `discardsUnusedReadsInto`): mwcc's `lwz r0`, IDO's `lw zero` */
299
+ readonly discarded?: ReadonlySet<Value>;
300
+ /** where a read's object is declared: the project's symbol map and the declaration context */
301
+ readonly declarations?: Declarations;
302
+ }
303
+
304
+ /** The loads a frontend saw the discard register take (`discarded`) that no write outside that
305
+ * register carried, which is what `DeadReadFacts.discarded` holds. `written` is every value the
306
+ * frontend wrote to another register; each is walked back through the ops that computed it, and a
307
+ * discarded load the walk reaches was put in a home, so it is no read the compiler discarded:
308
+ * mwcc at -O0,p reads `signed char x; x = p[5]; x = 0;` as `lwz r0,20(r3)` + `extsb r31,r0`, its
309
+ * `unsigned char` twin as `clrlwi r31,r0,24`, and a volatile twin of either is the same
310
+ * instructions. Keyed on the write, not on the mnemonics that move or extend, so how a frontend
311
+ * lowers the op does not decide it. A block parameter ends the walk. An extension the discard
312
+ * register keeps (`(char)*vp;` is `extsb r0,r0`) stays discarded. */
313
+ export function leftInDiscardRegister(
314
+ irBlocks: readonly Block[],
315
+ discarded: ReadonlySet<Value>,
316
+ written: readonly Value[],
317
+ ): Set<Value> {
318
+ if (discarded.size === 0) {
319
+ return new Set();
320
+ }
321
+ const defOf = new Map<Value, Op>();
322
+ for (const blk of irBlocks) {
323
+ for (const op of blk.ops) {
324
+ op.results.forEach((r) => defOf.set(r, op));
325
+ }
326
+ }
327
+ const seen = new Set<Value>();
328
+ const homed = new Set<Value>();
329
+ const stack = [...written];
330
+ while (stack.length > 0) {
331
+ const v = stack.pop()!;
332
+ if (seen.has(v)) {
333
+ continue;
334
+ }
335
+ seen.add(v);
336
+ if (discarded.has(v)) {
337
+ homed.add(v);
338
+ continue;
339
+ }
340
+ stack.push(...(defOf.get(v)?.operands ?? []));
341
+ }
342
+ return new Set([...discarded].filter((v) => !homed.has(v)));
343
+ }
344
+
345
+ /** The declarations `declared-plain` reads. */
346
+ export interface Declarations {
347
+ readonly symbols?: SymbolMap;
348
+ readonly context?: ContextGlobals;
349
+ }
350
+
351
+ /** IS THE GLOBAL `sym` DECLARED WITH NO VOLATILE BYTE AT `byte`? (`declared-plain`) `null` is an
352
+ * access that names no byte, which is plain only where no byte of the object is qualified. The map
353
+ * answers before the context where it lays the object out, as raise/declared-volatile.ts reads them;
354
+ * a struct the map names with no layout, and an object neither declares, answer no — nothing is in
355
+ * evidence. So does an array whose element the map does not name a base type (no `elemSigned`):
356
+ * the map carries the qualifiers of the array's element chain and no element layout, so klonoa's
357
+ * `struct EntityAnimationInfo gEntityAnimationInfo[]`, whose `frame` member is `volatile u8`,
358
+ * reads as an array with no volatile byte. A struct member that nests a struct holding a volatile
359
+ * field is still judged by the member's own qualifier: the map's member facts do not tell it from
360
+ * an enum, and calling every enum member unknown would stamp the merged-arm reads this gate
361
+ * refuses. */
362
+ export function declaredPlain(
363
+ sym: string,
364
+ byte: number | null,
365
+ info: SymbolInfo | undefined,
366
+ context: ContextGlobals | undefined,
367
+ ): boolean {
368
+ const layout = Array.isArray(info?.layout) ? info.layout : [];
369
+ if (layout.length > 0) {
370
+ return byte === null
371
+ ? info?.volatile !== true && !layout.some((f) => f.volatile === true)
372
+ : !declaresVolatile(info, byte);
373
+ }
374
+ if (context?.volatiles.has(sym) === true) {
375
+ return false;
376
+ }
377
+ if (info?.declared === true && info.shape !== undefined && info.shape !== 'struct') {
378
+ if (info.shape === 'array' && typeof info.elemSigned !== 'boolean') {
379
+ return info.volatile !== true && context?.declared.has(sym) === true;
380
+ }
381
+ return info.volatile !== true;
382
+ }
383
+ return context?.declared.has(sym) === true;
384
+ }
385
+
386
+ /** THE RETURN PAIR'S SECOND REGISTER AT A WORD RETURN, gathered by a frontend as it emits a `ret`
387
+ * of one word (`held-at-word-return`): every value `pairReg` may hold at the end of block `bi`,
388
+ * added to `into` — asked through `holdsValue`, which reads no register (a read of one some path
389
+ * leaves undefined would mint a parameter) and ends a path where a call destroyed it.
390
+ *
391
+ * Nothing is gathered where the function's own declaration pins the return: a void one, or a
392
+ * scalar whose width it states. A declared one-word return leaves the pair's second register
393
+ * unused by the source too, and a declared wider one is refused after the lift (pipeline.ts
394
+ * `refuseNarrowReturnsOfWideDeclaration`). */
395
+ export function unreadPairHalf(
396
+ ssa: Pick<SsaBuilder, 'holdsValue'>,
397
+ own: FnProto | undefined,
398
+ pairReg: string,
399
+ bi: number,
400
+ into: Set<Value>,
401
+ ): void {
402
+ if (declaresVoidReturn(own) || ownReturnWidth(own) !== undefined) {
403
+ return;
404
+ }
405
+ ssa.holdsValue(pairReg, bi, (v) => {
406
+ into.add(v);
407
+ return false; // keep walking: every path's value, not the first
408
+ });
409
+ }
410
+
411
+ /** Whether a value was COMPUTED ONLY TO BE RETURNED: an op with no effect and no memory read
412
+ * (`spelledWhenDead` answers no) whose value nothing but a return reads, directly or through the
413
+ * joins that carry it there, or a join of such values. An entry parameter is the caller's value and
414
+ * merges nothing, so it never is one. The evidence is the compiler's, the claim `spelledWhenDead`
415
+ * already rests on: an optimizing compiler deletes a pure computation nobody reads, so one that
416
+ * survives with a return as its only reader was written for that return. A value no op reads at
417
+ * all answers yes too, which is what a frontend asks of the value a return home holds where the
418
+ * `ret` does not read it (frontend/fpu.ts `settleReturnHome`). Read over `blocks` as they stand. */
419
+ export function computedOnlyToReturn(blocks: readonly Block[]): (v: Value) => boolean {
420
+ const defOp = new Map<Value, Op>();
421
+ const incoming = new Map<Value, Value[]>();
422
+ const uses = new Map<Value, Array<Op | Value | undefined>>();
423
+ const push = <K, V>(m: Map<K, V[]>, k: K, v: V) => {
424
+ const l = m.get(k);
425
+ if (l) {
426
+ l.push(v);
427
+ } else {
428
+ m.set(k, [v]);
429
+ }
430
+ };
431
+ for (const blk of blocks) {
432
+ for (const op of blk.ops) {
433
+ op.results.forEach((r) => defOp.set(r, op));
434
+ op.operands.forEach((v) => push(uses, v, op));
435
+ for (const s of op.successors) {
436
+ s.args.forEach((v, i) => {
437
+ const p = s.block.params[i] as Value | undefined;
438
+ push(uses, v, p);
439
+ if (p !== undefined) {
440
+ push(incoming, p, v);
441
+ }
442
+ });
443
+ }
444
+ }
445
+ }
446
+ // A join is followed to what it merges, and every value on the way may have only returns, or
447
+ // joins that are themselves read only by returns, as readers. A cycle adds nothing the first
448
+ // visit did not judge.
449
+ const returnOnly = (v: Value, seen: Set<Value>): boolean => {
450
+ if (seen.has(v)) {
451
+ return true;
452
+ }
453
+ seen.add(v);
454
+ return (uses.get(v) ?? []).every(
455
+ (u) => u !== undefined && ('opcode' in u ? u.opcode === 'ret' : returnOnly(u, seen)),
456
+ );
457
+ };
458
+ const computed = (v: Value, seen: Set<Value>): boolean => {
459
+ if (seen.has(v)) {
460
+ return true;
461
+ }
462
+ seen.add(v);
463
+ if (!returnOnly(v, new Set())) {
464
+ return false;
465
+ }
466
+ const op = defOp.get(v);
467
+ if (op !== undefined) {
468
+ return !spelledWhenDead(op);
469
+ }
470
+ const ins = incoming.get(v);
471
+ return ins !== undefined && ins.every((a) => computed(a, seen));
472
+ };
473
+ return (v) => computed(v, new Set());
474
+ }
475
+
476
+ /** Stamp `volatile` on every read these blocks never use that no gate refuses, outermost first.
477
+ * `facts` is what the frontend gathered while it filled the blocks.
478
+ *
479
+ * CALLED BY EACH FRONTEND, at the end of its lift, and not once in pipeline.ts `liftStamped`: the
480
+ * pair, call and compare gates need what a return, a call or a compare left in a register no op
481
+ * reads, which only the frontend's SSA builder answers, and only while it is filling the blocks.
482
+ * The IR it hands back has no register left to ask. */
483
+ export function pinDeadReads(
484
+ irBlocks: readonly Block[],
485
+ target: TargetDescription,
486
+ facts: DeadReadFacts = {},
487
+ gates: readonly Gate<DeadRead>[] = DEAD_READ_GATES,
488
+ ): void {
489
+ const defOf = new Map<Value, Op>();
490
+ for (const b of irBlocks) {
491
+ for (const op of b.ops) {
492
+ for (const r of op.results) {
493
+ defOf.set(r, op);
494
+ }
495
+ }
496
+ }
497
+ // An offset of a frame object: the `laddr` itself, or a sum or difference with one beside it.
498
+ const fromFrame = (v: Value, depth = 0): boolean => {
499
+ const d = defOf.get(v);
500
+ if (d === undefined || depth > 8) {
501
+ return false;
502
+ }
503
+ if (d.opcode === 'laddr') {
504
+ return true;
505
+ }
506
+ return (d.opcode === 'add' || d.opcode === 'sub') && d.operands.some((o) => fromFrame(o, depth + 1));
507
+ };
508
+ const unmodelled = irBlocks.some((b) => b.ops.some((op) => op.opcode === 'opaque'));
509
+ let byName: Map<string, SymbolInfo> | undefined;
510
+ const plainGlobal = (op: Op): boolean => {
511
+ const sym = globalBaseOf(defOf, op.operands[0]);
512
+ if (sym === null) {
513
+ return false;
514
+ }
515
+ const symbols = facts.declarations?.symbols;
516
+ byName ??= symbols === undefined ? new Map() : symbolsByName(symbols);
517
+ const cell = op.opcode === 'load' ? globalCellOf(defOf, op.operands[0], op.attrs.off as number) : null;
518
+ return declaredPlain(sym, cell?.byte ?? null, byName.get(sym), facts.declarations?.context);
519
+ };
520
+ const voidReturn = declaresVoidReturn(facts.own);
521
+ // A branch whose two edges reach one block with the same arguments chooses nothing, so its
522
+ // condition is no use either: agbcc at -O0 keeps `if (*vp) {}` as `ldr; cmp; beq` to the next
523
+ // instruction, which the structurer would spell `*p == 0;` with the read plain. The edges are
524
+ // compared by DESTINATION (ir/core.ts `forwardingTarget`), not by identity: agbcc keeps
525
+ // `if (*vp) {} else {}` and `*vp ? 1 : 2;` as `beq .L4; b .L4`, whose fallthrough edge reaches
526
+ // the join through a bodyless forwarder. An edge carrying arguments is compared as it stands,
527
+ // since a forwarder past it would drop the values it supplies.
528
+ const destination = (e: Successor): Block => (e.args.length === 0 ? forwardingTarget(e.block) : e.block);
529
+ const choosesNothing = (op: Op): boolean =>
530
+ op.opcode === 'cond_br' &&
531
+ op.successors.length === 2 &&
532
+ destination(op.successors[0]) === destination(op.successors[1]) &&
533
+ op.successors[0].args.length === op.successors[1].args.length &&
534
+ op.successors[0].args.every((a, i) => a === op.successors[1].args[i]);
535
+ const usesOperands = (op: Op): boolean => !(voidReturn && op.opcode === 'ret') && !choosesNothing(op);
536
+ const isRead = (op: Op): boolean => op.opcode === 'load' || op.opcode === 'aload';
537
+ const decided = new Set<Op>();
538
+ for (;;) {
539
+ const dead = deadOps(irBlocks, usesOperands);
540
+ // The dead ops whose value reaches one of `values`, through the dead ops it feeds: an extension
541
+ // of the read a register holds is the read held.
542
+ const reaching = (values: ReadonlySet<Value> | undefined): Set<Op> => {
543
+ const out = new Set<Op>();
544
+ const walk = (v: Value): void => {
545
+ const d = defOf.get(v);
546
+ if (d !== undefined && dead.has(d) && !out.has(d)) {
547
+ out.add(d);
548
+ d.operands.forEach(walk);
549
+ }
550
+ };
551
+ values?.forEach(walk);
552
+ return out;
553
+ };
554
+ const open = [...dead].filter((op) => isRead(op) && !decided.has(op));
555
+ // A read that, through dead ops, computes another open read's address waits for it.
556
+ const feeds = new Set<Op>();
557
+ const feed = (v: Value): void => {
558
+ const d = defOf.get(v);
559
+ if (d !== undefined && dead.has(d) && !feeds.has(d)) {
560
+ feeds.add(d);
561
+ if (!isRead(d)) {
562
+ d.operands.forEach(feed);
563
+ }
564
+ }
565
+ };
566
+ open.forEach((r) => r.operands.forEach(feed));
567
+ const ready = open.filter((r) => !feeds.has(r));
568
+ if (ready.length === 0) {
569
+ return; // nothing open, or (never in SSA) only reads that wait on each other
570
+ }
571
+ const held = reaching(facts.atWordReturn);
572
+ const atCall = reaching(facts.atUndeclaredCall);
573
+ const compared = reaching(facts.compared);
574
+ const spilled = reaching(facts.spilled);
575
+ const discarded = reaching(facts.discarded);
576
+ // Who consumes a value among the dead ops: an extension passes the read on, any other op the
577
+ // machine computed with it. A read among them was decided before this one (outermost first)
578
+ // and left plain, so the address it took was the compiler's use.
579
+ const usersOf = new Map<Value, Op[]>();
580
+ for (const d of dead) {
581
+ for (const o of d.operands) {
582
+ usersOf.set(o, [...(usersOf.get(o) ?? []), d]);
583
+ }
584
+ }
585
+ // What consumes the value through its extensions: an op that computes with it, and a read it
586
+ // is the address of.
587
+ const usesOf = (v: Value, out = { computed: false, addresses: false }, depth = 0) => {
588
+ for (const u of usersOf.get(v) ?? []) {
589
+ if ((u.opcode === 'zext' || u.opcode === 'sext') && depth < 8) {
590
+ usesOf(u.results[0], out, depth + 1);
591
+ } else if (isRead(u) && u.operands[0] === v) {
592
+ out.addresses = true;
593
+ } else {
594
+ out.computed = true;
595
+ }
596
+ }
597
+ return out;
598
+ };
599
+ for (const op of ready) {
600
+ decided.add(op);
601
+ const uses = usesOf(op.results[0]);
602
+ const read: DeadRead = {
603
+ op,
604
+ fromFrame: fromFrame(op.operands[0]),
605
+ unmodelled,
606
+ heldAtWordReturn: held.has(op),
607
+ heldAtUndeclaredCall: atCall.has(op),
608
+ compared: compared.has(op),
609
+ spilled: spilled.has(op),
610
+ computed: uses.computed,
611
+ addressesPlainRead: uses.addresses,
612
+ declaredPlain: plainGlobal(op),
613
+ discarded: discarded.has(op),
614
+ target,
615
+ };
616
+ const rejection = firstRejection(gates, read);
617
+ if (rejection === UNDECIDED) {
618
+ throw new FrontendUnsupportedError(
619
+ `cannot lift '${facts.name ?? '?'}': a read nothing uses at a level that keeps plain ones, which ${target.compiler} ` +
620
+ `loads into no register of its own — a volatile read and a register local are the same code here`,
621
+ );
622
+ }
623
+ if (rejection === null) {
624
+ op.attrs = { ...op.attrs, volatile: true };
625
+ }
626
+ }
627
+ }
628
+ }