@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
@@ -0,0 +1,530 @@
1
+ // asmlift — what the jump-table recognizers share: the MIPS and PowerPC idiom matchers below, and,
2
+ // for all three frontends, what an elided dispatch leaves in its registers (at the end of this file).
3
+ //
4
+ // Both frontends recover a dense switch from its dispatch idiom BEFORE the CFG is built, because
5
+ // the case count decides the block leaders: the bounds branch becomes a `switch_br`, and the
6
+ // dispatch block it guards is pruned unread. Two facts about that pruning are ISA-neutral, and live
7
+ // here so a fix to one reaches both:
8
+ //
9
+ // - THE DISPATCH BLOCK HAS ONE ENTRY. A branch landing inside it skips the bounds check, and the
10
+ // block it enters is the one pruned (`entersBetween`). Nor may one land between the bounds
11
+ // compare and the branch that reads it: that path brings its own compare, and the `switch_br`
12
+ // would bound it by one it never ran (PowerPC's `boundsCompare`; MIPS's `mipsBounds` reads each
13
+ // edge into the block instead, because IDO puts the `sltiu` in their delay slots). The pruning is
14
+ // then CHECKED on the built CFG, not assumed (`refuseReachableDispatch`): a table's case is an
15
+ // edge too, and no table existed while the recognizer looked, so a case landing in its own or
16
+ // another table's dispatch block keeps that block reachable, and its indirect jump would lift as
17
+ // whatever the frontend reads it as (MIPS reads a non-`ra` `jr` as a return).
18
+ // - A TABLE BASE FORMED OUTSIDE THAT BLOCK IS NOT PRUNED WITH IT. KMC and gcc 2.7.2 hoist the
19
+ // `lui ; addiu` pair into a loop's preheader, mwcc 2.3.3 hoists `lis ; addi` to the entry, and a
20
+ // register allocator puts the base wherever it likes — a temporary in a leaf, a callee-saved
21
+ // register across a call, an argument register once its argument is dead. The recognizer can only
22
+ // GUESS which pair that is, because it reads instructions, not dataflow. So the pair is lifted
23
+ // like any address — its high half paired with its low half through SSA — and what the low half
24
+ // completes is not a value but a TABLE BASE (`TableBases`): a definition no C expression spells,
25
+ // refused wherever it is read and wherever it reaches a merge, as a pending high half is
26
+ // (frontend/high-half.ts). The dispatch's one claim on it is a PROOF answered by SSA once every
27
+ // block is filled: every path into the bounds block brings that very base in the register the
28
+ // dispatch reads, and no call destroyed it on the way (`SsaBuilder.everyReachingDef`). Keyed by
29
+ // value, not by register — the same reason high-half.ts gives — so a second pair, a rewrite, a
30
+ // path around the pair, a call that clobbers it and a callee-saved restore all answer "no" here,
31
+ // by the SSA's own bookkeeping rather than by a count of the register's mentions.
32
+ import { type Block, type Op, type Value, mkOp, mkValue } from '../ir/core';
33
+ import { T } from '../ir/types';
34
+ import { type FnProto, declaresVoidReturn } from '../proto';
35
+ import type { CallDeclarations } from './call-plan';
36
+ import type { FrontendRefusal } from './errors';
37
+ import type { SsaBuilder } from './ssa';
38
+
39
+ /** Whether any branch lands after the word at `afterAddr`, up to and including the word at
40
+ * `lastAddr`. Asked of a dispatch block (after its bounds branch, up to its indirect jump), where
41
+ * such a path skips the bounds check and enters the block a recovered table prunes, and of the
42
+ * words between a bounds compare and its branch, where it brings a compare of its own. */
43
+ export const entersBetween = (
44
+ instrs: ReadonlyArray<{ target?: number }>,
45
+ afterAddr: number,
46
+ lastAddr: number,
47
+ ): boolean => instrs.some((x) => x.target !== undefined && x.target > afterAddr && x.target <= lastAddr);
48
+
49
+ /** Refuse a function whose CFG, pruned to what the entry reaches, still holds a block ending in a
50
+ * recovered dispatch's indirect jump. Recovery replaces that jump with the bounds block's
51
+ * `switch_br` and lifts nothing of the block it ends, so the block must be unreachable; a branch
52
+ * or a table case into it says otherwise. `blocks` are the reachable blocks, `recovered` the
53
+ * addresses of the recovered indirect jumps. */
54
+ export function refuseReachableDispatch(
55
+ name: string,
56
+ blocks: ReadonlyArray<{ branch: { addr: number; mnemonic: string } | null }>,
57
+ recovered: ReadonlySet<number>,
58
+ fail: FrontendRefusal,
59
+ ): void {
60
+ for (const { branch } of blocks) {
61
+ if (branch !== null && recovered.has(branch.addr)) {
62
+ fail(
63
+ `cannot lift '${name}': the recovered jump table's '${branch.mnemonic}' at 0x${branch.addr.toString(16)} ` +
64
+ `is still reachable — a branch or a table case enters its dispatch block past the bounds check`,
65
+ );
66
+ }
67
+ }
68
+ }
69
+
70
+ /** Where a jump table is: the symbol a base's relocations name, and the byte offset into it. */
71
+ export interface TableKey {
72
+ sym: string;
73
+ addend: number;
74
+ }
75
+
76
+ const sameKey = (a: TableKey, b: TableKey) => a.sym === b.sym && a.addend === b.addend;
77
+
78
+ /** The one table base a function's candidate pairs agree on, or null. The recognizer's GUESS for a
79
+ * base formed outside the dispatch block: several distinct candidates leave it undecided, and the
80
+ * lift's proof (`TableBases.prove`) is what makes the guess safe to act on. */
81
+ export const agreedTable = (candidates: readonly TableKey[]): TableKey | null =>
82
+ candidates.length > 0 && candidates.every((c) => sameKey(c, candidates[0])) ? candidates[0] : null;
83
+
84
+ /** A recovered dispatch whose table base its own block does not form. */
85
+ export interface HoistedDispatch {
86
+ /** the register the dispatch reads the base from */
87
+ reg: string;
88
+ table: TableKey;
89
+ }
90
+
91
+ export interface TableBases {
92
+ /** Whether the low half completing `key` makes a table base: some recovered dispatch reads that
93
+ * table through a hoisted base. Every other pair is an ordinary address. */
94
+ wants(key: TableKey): boolean;
95
+ /** Record `v` as the address of the table `key`, completed by the instruction at `addr`. */
96
+ record(v: Value, key: TableKey, addr: number): void;
97
+ /** True when `v` is a table base — for a caller that must EXCLUDE one (a guessed call's argument
98
+ * count must not take a base parked in an argument register for an argument). */
99
+ has(v: Value): boolean;
100
+ /** Reading a register AS A VALUE. A table base is not one, so this refuses. */
101
+ guardRead(name: string, reg: string, v: Value): Value;
102
+ /** The dispatch whose `switch_br` block `b` emits reads `d.reg` as the base of `d.table` at the end
103
+ * of that block. Checked by `prove`. */
104
+ demand(d: HoistedDispatch, b: number, jumpAddr: number): void;
105
+ /** Every demand holds: each path into its block brings the very table base it reads, undestroyed.
106
+ * Asked once every block is filled. */
107
+ prove(name: string, ssa: Pick<SsaBuilder, 'everyReachingDef'>): void;
108
+ /** The last line of defence, as for the high halves: a base reaching an operand, a block
109
+ * parameter or a successor argument has been read as a value through a join, which no
110
+ * `guardRead` saw. Checked on the finished function. */
111
+ assertNoneEscaped(name: string, blocks: Block[]): void;
112
+ }
113
+
114
+ export function makeTableBases(hoisted: Iterable<HoistedDispatch>, fail: FrontendRefusal): TableBases {
115
+ const wanted = [...hoisted].map((d) => d.table);
116
+ const bases = new Map<Value, TableKey & { addr: number }>();
117
+ const demands: Array<{ d: HoistedDispatch; b: number; jumpAddr: number }> = [];
118
+ const hex = (a: number) => `0x${a.toString(16)}`;
119
+ const refuse = (name: string, v: Value, how: string): never => {
120
+ const t = bases.get(v)!;
121
+ return fail(
122
+ `cannot lift '${name}': the jump table '${t.sym}'+${t.addend}'s base, formed at ${hex(t.addr)}, ${how} — ` +
123
+ `a table's address is the dispatch's alone, and no C expression spells it`,
124
+ );
125
+ };
126
+ return {
127
+ wants: (key) => wanted.some((w) => sameKey(w, key)),
128
+ record: (v, key, addr) => void bases.set(v, { ...key, addr }),
129
+ has: (v) => bases.has(v),
130
+ guardRead(name, reg, v) {
131
+ return bases.has(v) ? refuse(name, v, `is read from ${reg} as a value`) : v;
132
+ },
133
+ demand: (d, b, jumpAddr) => void demands.push({ d, b, jumpAddr }),
134
+ prove(name, ssa) {
135
+ for (const { d, b, jumpAddr } of demands) {
136
+ const isBase = (v: Value) => {
137
+ const t = bases.get(v);
138
+ return t !== undefined && sameKey(t, d.table);
139
+ };
140
+ if (!ssa.everyReachingDef(d.reg, b, isBase)) {
141
+ fail(
142
+ `cannot lift '${name}': the dispatch at ${hex(jumpAddr)} reads the base of jump table '${d.table.sym}'` +
143
+ `+${d.table.addend} from ${d.reg}, and ${d.reg} does not hold that table's address on every path to ` +
144
+ `it — a path around the base, a rewrite, or a call that destroys it`,
145
+ );
146
+ }
147
+ }
148
+ },
149
+ assertNoneEscaped(name, blocks) {
150
+ for (const b of blocks) {
151
+ for (const v of [
152
+ ...b.params,
153
+ ...b.ops.flatMap((op) => [...op.operands, ...op.successors.flatMap((s) => s.args)]),
154
+ ]) {
155
+ if (bases.has(v)) {
156
+ refuse(name, v, 'reaches a use through a merge');
157
+ }
158
+ }
159
+ }
160
+ },
161
+ };
162
+ }
163
+
164
+ // WHAT AN ELIDED DISPATCH LEAVES BEHIND. Every recognizer prunes the dispatch block unread, so the
165
+ // registers its idiom writes — the scaled index, the table's base, the entry's address, the loaded
166
+ // code address, PowerPC's `ctr` — are written on no path the lift models. The default path never
167
+ // ran the idiom, so nothing may be written at the bounds block; the case edges did, so the case
168
+ // targets are where it is written: at the ENTRY of each, before its first instruction. A register
169
+ // holds what the idiom's LAST write of it left:
170
+ // - the index shift (`lsl`/`sll`/`slwi rI,rS,2`) leaves `rS << 2`, a value C spells. mwcc 2.4.2
171
+ // at -O4 reuses it in the case bodies (`p[i] = 7` in a switch on `i` is `stwx r0,r4,r5` with r5
172
+ // the dispatch's own `slwi`), so it is modelled, not refused. The shift reads the scrutinee as
173
+ // the switch does: every recognizer refuses an idiom that rewrites it before the shift, and the
174
+ // switch reads it where the bounds test did, so the scrutinee at a case's entry is the one the
175
+ // shift read — on a target only the table's case edges enter (EXACT);
176
+ // - every other write leaves an address no C expression spells, so it is a MARKER: an `undef`
177
+ // value that `refuseStaleIdiomReads` declines wherever an operand demands it.
178
+ // A target some other edge also enters (the default, a hole naming the default, a loop's back
179
+ // edge, a fallthrough from another case: MIXED) cannot tell the paths apart at its entry, so its
180
+ // markers stand for what the other paths brought too, and a read of one declines. Its scaled index
181
+ // may stand: mwcc 2.4.2 at -O4 keeps it live into such targets — a do-while case re-entered on its
182
+ // back edge (`lwzx r0,r4,r3` with r3 the dispatch's `slwi r3,r3,2`), a case falling into the next
183
+ // with `slwi r31,r27,2` still in r31. It is the shift of the scrutinee where the `switch_br` read
184
+ // it, not where the target reads it (a back edge brings whatever the case left in the scrutinee's
185
+ // register), and it stands only where every other path in brings that same scaled index
186
+ // (`unsettledScaledIndices`); elsewhere it is a marker. A target two tables enter, and the
187
+ // function's entry, whose call edge no `preds` lists, make every register a marker.
188
+
189
+ /** What an elided dispatch leaves in a register it writes: the scrutinee scaled by four, or an
190
+ * address no C expression spells. */
191
+ export type IdiomLeft = 'scaled' | 'opaque';
192
+
193
+ /** The registers an elided dispatch's idiom writes, in program order, each with whether it is the
194
+ * index shift: what each register holds after it, decided by its last writer. */
195
+ export function idiomLeaves(writes: ReadonlyArray<{ reg: string; scaled: boolean }>): Map<string, IdiomLeft> {
196
+ const left = new Map<string, IdiomLeft>();
197
+ for (const { reg, scaled } of writes) {
198
+ left.set(reg, scaled ? 'scaled' : 'opaque');
199
+ }
200
+ return left;
201
+ }
202
+
203
+ /** A table whose dispatch block was elided, by block index. */
204
+ export interface ElidedDispatch {
205
+ /** the block emitting the `switch_br` */
206
+ at: number;
207
+ /** the bounds-checked scrutinee's register */
208
+ scrutReg: string;
209
+ /** the case targets, one per case */
210
+ cases: readonly number[];
211
+ /** the default target, or null when the out-of-range path is a return of its own */
212
+ defaultBlock: number | null;
213
+ leaves: ReadonlyMap<string, IdiomLeft>;
214
+ }
215
+
216
+ /** A MARKER ({@link staleMarker}): what the idiom left in `reg` that no C expression spells, and
217
+ * whether the dispatch that left it was kept (its block lifted, holding the code the compiler
218
+ * placed in it) or elided. A read of one declines ({@link refuseStaleIdiomReads}). */
219
+ export interface StaleMarker {
220
+ kind: 'marker';
221
+ reg: string;
222
+ form: 'kept' | 'elided';
223
+ /** Whether the marker stands for the register on EVERY path that brings it ({@link parkedIn}): a
224
+ * kept dispatch's, and an EXACT target's. A MIXED target's marker also replaced what the other
225
+ * paths into it brought, so it is read like any value, and its read declines. */
226
+ parks: boolean;
227
+ }
228
+
229
+ /** The scaled index a case target is entered with in `reg` ({@link enterCaseTarget}): no marker
230
+ * but a value a case may read, recorded for {@link scaledIndexIn} and, on a MIXED target, to be
231
+ * settled. */
232
+ export interface StaleScaledIndex {
233
+ kind: 'scaled';
234
+ reg: string;
235
+ /** the dispatch's block: two tables' scaled indices are different values */
236
+ table: number;
237
+ /** the scrutinee the shift reads, where the dispatch's `switch_br` read it */
238
+ of: Value;
239
+ /** On a MIXED target: the `shl` defining it, the frontend's key for `reg`, and the predecessors
240
+ * ({@link CaseEntry.others}) every path into whose end must bring the same scaled index. Where
241
+ * one does not, `refuseStaleIdiomReads` declines a read of it and makes the `shl` a marker. */
242
+ settle?: { op: Op; key: string; others: readonly number[] };
243
+ }
244
+
245
+ /** What an elided or kept dispatch left in a register, by the value standing for it: a marker no
246
+ * read may reach, or the scaled index a read may reach but a guessed arity may not count. */
247
+ export type StaleIdiomRead = StaleMarker | StaleScaledIndex;
248
+
249
+ /** What a case target's entry holds from an elided dispatch: the scrutinee's register (read for a
250
+ * `scaled` entry) and each register the idiom wrote. */
251
+ export interface CaseEntry {
252
+ scrutReg: string;
253
+ leaves: ReadonlyMap<string, IdiomLeft>;
254
+ /** only the table's case edges enter the target */
255
+ exact: boolean;
256
+ /** the block emitting the table's `switch_br` */
257
+ at: number;
258
+ /** A MIXED target's predecessors other than the table's case edges — `at` itself when the target
259
+ * is also the default — each of whose paths must bring the scaled index for it to stand there
260
+ * (`StaleScaledIndex.settle`). Empty on an EXACT target. */
261
+ others: readonly number[];
262
+ }
263
+
264
+ /** Each case target's entry: EXACT (every edge into it is one table's case edge) and MIXED (other
265
+ * edges enter it too) keep that table's `leaves`, the scaled index settled later on a MIXED one; a
266
+ * target two tables enter makes every register they wrote a marker. Block 0 does too: the call
267
+ * that enters a function is an edge no `preds` lists. */
268
+ export function caseEntries(
269
+ tables: readonly ElidedDispatch[],
270
+ preds: ReadonlyArray<readonly number[]>,
271
+ ): Map<number, CaseEntry> {
272
+ const owners = new Map<number, ElidedDispatch[]>();
273
+ for (const t of tables) {
274
+ for (const c of new Set(t.cases)) {
275
+ owners.set(c, [...(owners.get(c) ?? []), t]);
276
+ }
277
+ }
278
+ const out = new Map<number, CaseEntry>();
279
+ for (const [b, ts] of owners) {
280
+ const [t] = ts;
281
+ const own = ts.length === 1 && b !== 0;
282
+ const exact = own && b !== t.defaultBlock && preds[b].every((p) => p === t.at);
283
+ out.set(
284
+ b,
285
+ own
286
+ ? {
287
+ scrutReg: t.scrutReg,
288
+ leaves: t.leaves,
289
+ exact,
290
+ at: t.at,
291
+ others: exact ? [] : [...new Set(preds[b])].filter((p) => p !== t.at || b === t.defaultBlock),
292
+ }
293
+ : {
294
+ scrutReg: t.scrutReg,
295
+ leaves: new Map(ts.flatMap((u) => [...u.leaves.keys()]).map((r) => [r, 'opaque' as const])),
296
+ exact,
297
+ at: t.at,
298
+ others: [],
299
+ },
300
+ );
301
+ }
302
+ return out;
303
+ }
304
+
305
+ /** Write what an elided dispatch left at the entry of case target `entry`'s block, before its first
306
+ * instruction: `scrutinee` reads the switch's scrutinee there (before any of these writes, which may
307
+ * overwrite its register), `dispatchScrutinee` at the end of the dispatch's block `entry.at` (null
308
+ * while that block is unfilled, where a MIXED target's scaled index is a marker instead), `write`
309
+ * defines a register in the block. Each marker is recorded in `stale` against the register it
310
+ * stands in, for {@link refuseStaleIdiomReads}, and the scaled index as its own kind, for
311
+ * {@link scaledIndexIn} and, on a MIXED target, to be settled. */
312
+ export function enterCaseTarget(
313
+ entry: CaseEntry,
314
+ ops: Op[],
315
+ io: {
316
+ key: (reg: string) => string;
317
+ scrutinee: (reg: string) => Value;
318
+ dispatchScrutinee: (reg: string) => Value | null;
319
+ write: (reg: string, v: Value) => void;
320
+ },
321
+ stale: Map<Value, StaleIdiomRead>,
322
+ ): void {
323
+ // On an EXACT target the two reads are one value: its only predecessor is the dispatch's block.
324
+ const of = [...entry.leaves.values()].includes('scaled')
325
+ ? entry.exact
326
+ ? io.scrutinee(entry.scrutReg)
327
+ : io.dispatchScrutinee(entry.scrutReg)
328
+ : null;
329
+ const shl = of === null ? null : mkOp('shl', { operands: [of], results: [mkValue(T.unk(32))], attrs: { imm: 2 } });
330
+ if (shl !== null) {
331
+ ops.push(shl);
332
+ }
333
+ for (const [r, left] of entry.leaves) {
334
+ if (left === 'scaled' && shl !== null) {
335
+ const [scaled] = shl.results;
336
+ io.write(r, scaled);
337
+ const settle = entry.exact ? {} : { settle: { op: shl, key: io.key(r), others: entry.others } };
338
+ stale.set(scaled, { kind: 'scaled', reg: r, table: entry.at, of: of!, ...settle });
339
+ continue;
340
+ }
341
+ io.write(r, staleMarker(ops, io.key(r), { reg: r, form: 'elided', parks: entry.exact }, stale));
342
+ }
343
+ }
344
+
345
+ /** A marker for what the unlifted idiom left in `read.reg`, keyed `key` in the SSA. */
346
+ export function staleMarker(
347
+ ops: Op[],
348
+ key: string,
349
+ read: Omit<StaleMarker, 'kind'>,
350
+ stale: Map<Value, StaleIdiomRead>,
351
+ ): Value {
352
+ const op = mkOp('undef', { results: [mkValue(T.unk(32))], attrs: { key } });
353
+ ops.push(op);
354
+ stale.set(op.results[0], { kind: 'marker', ...read });
355
+ return op.results[0];
356
+ }
357
+
358
+ /** Whether `v`, read from `reg`, is the marker the idiom left in that very register on every path
359
+ * that brings it — where the compiler parked the dispatch's own value, which no call is handed and
360
+ * no return hands back. A guessed arity stops below it (`CallSite.parked`). A return reaching only
361
+ * it returns nothing where the frontend already asks whether a value reaches the return register
362
+ * (PowerPC's r3, MIPS's v0, each written by the idiom on mwcc and gcc). Thumb asks no such thing — a
363
+ * return with nothing written in r0 hands back the caller's r0 — so its return reads the marker, and
364
+ * the read declines unless the function is declared void. A copy into another register is
365
+ * something the case did with it, and a MIXED target's marker stands in for what another path
366
+ * brought too (`StaleIdiomRead.parks`): both are read like any value. `key` is the frontend's
367
+ * register spelling. */
368
+ export const parkedIn =
369
+ (stale: ReadonlyMap<Value, StaleIdiomRead>, key: (reg: string) => string = (r) => r) =>
370
+ (v: Value, reg: string): boolean => {
371
+ const s = stale.get(v);
372
+ return s?.kind === 'marker' && s.parks && key(s.reg) === key(reg);
373
+ };
374
+
375
+ /** Whether `v`, read from `reg`, is the scrutinee scaled by four that an elided dispatch left in that
376
+ * very register (`enterCaseTarget`). A case reads it freely (`p[i]`), but a guessed arity may not
377
+ * count it: mwcc 2.4.2 keeps the dispatch's `slwi r4,r3,2` live into its `p[i]` cases, and an
378
+ * undeclared `g(2)` there is `li r3,2; bl g`, the bytes `g(2, i * 4)` compiles to too. */
379
+ export const scaledIndexIn =
380
+ (stale: ReadonlyMap<Value, StaleIdiomRead>, key: (reg: string) => string = (r) => r) =>
381
+ (v: Value, reg: string): boolean => {
382
+ const s = stale.get(v);
383
+ return s?.kind === 'scaled' && key(s.reg) === key(reg);
384
+ };
385
+
386
+ /** A DISPATCH'S STALE IDIOM REGISTERS MAY NOT BE READ. The dispatch idiom is not lifted, so a marker
387
+ * ({@link staleMarker}) stands for the table's address, an entry's address or the case's own code
388
+ * address, none of which a C expression spells. Run on the filled SSA, before `finish` collapses a
389
+ * join of an undefined value into its other argument: a marker some operand demands, directly or
390
+ * through the joins that carry it, declines. A `ret` operand demands nothing under a declared
391
+ * `void` return, which drops it (pipeline.ts `dropVoidReturnOperands`). A MIXED target's scaled
392
+ * index is settled first ({@link unsettledScaledIndices}): one that does not stand is a marker,
393
+ * read or not. `fail` is the frontend's own refusal. Last, `calls` refuses a guessed arity's
394
+ * trailing pass-through where the function lifts only through this modelling: a parked ceiling that
395
+ * hid a register, or a case reading a scaled index that stands
396
+ * (`CallDeclarations.refuseDispatchUnlockedPassThrough`). */
397
+ export function refuseStaleIdiomReads(
398
+ name: string,
399
+ irBlocks: readonly Block[],
400
+ stale: ReadonlyMap<Value, StaleIdiomRead>,
401
+ ssa: Pick<SsaBuilder, 'everyReachingDef'>,
402
+ own: FnProto | undefined,
403
+ fail: FrontendRefusal,
404
+ calls: Pick<CallDeclarations, 'refuseDispatchUnlockedPassThrough'>,
405
+ ): void {
406
+ if (stale.size === 0) {
407
+ return;
408
+ }
409
+ const incoming = new Map<Value, Value[]>();
410
+ for (const blk of irBlocks) {
411
+ for (const op of blk.ops) {
412
+ for (const s of op.successors) {
413
+ s.args.forEach((a, i) => {
414
+ const p = s.block.params[i];
415
+ incoming.set(p, [...(incoming.get(p) ?? []), a]);
416
+ });
417
+ }
418
+ }
419
+ }
420
+ const unsettled = unsettledScaledIndices(irBlocks, stale, incoming, ssa);
421
+ const demanded = new Set<Value>();
422
+ const work: Value[] = [];
423
+ const demand = (v: Value) => {
424
+ if (!demanded.has(v)) {
425
+ demanded.add(v);
426
+ work.push(v);
427
+ }
428
+ };
429
+ const voidReturn = declaresVoidReturn(own);
430
+ for (const blk of irBlocks) {
431
+ for (const op of blk.ops) {
432
+ if (!(op.opcode === 'ret' && voidReturn)) {
433
+ op.operands.forEach(demand);
434
+ }
435
+ }
436
+ }
437
+ while (work.length > 0) {
438
+ (incoming.get(work.pop()!) ?? []).forEach(demand);
439
+ }
440
+ const read = [...stale].flatMap(([v, s]) =>
441
+ demanded.has(v) && (s.kind === 'marker' || unsettled.has(v))
442
+ ? [s.kind === 'marker' ? s : { reg: s.reg, form: 'elided' as const }]
443
+ : [],
444
+ )[0];
445
+ if (read !== undefined) {
446
+ const { reg, form } = read;
447
+ const why =
448
+ "which the unlifted dispatch idiom wrote — the lift would read the value from before the dispatch, not the table's " +
449
+ 'index, address or target';
450
+ // one literal per form: the decline classifier (apps/web declines.ts) keys on the text up to `reads`
451
+ fail(
452
+ form === 'kept'
453
+ ? `cannot lift '${name}': a case of the kept jump table reads ${reg}, ${why}`
454
+ : `cannot lift '${name}': a case of the elided jump table reads ${reg}, ${why}`,
455
+ );
456
+ }
457
+ calls.refuseDispatchUnlockedPassThrough(
458
+ [...stale].some(([v, s]) => s.kind === 'scaled' && demanded.has(v) && !unsettled.has(v)),
459
+ );
460
+ // Read by nothing, so no C spells it; a marker keeps the SSA honest, as the shift's scrutinee need
461
+ // not reach a target some other path enters.
462
+ for (const v of unsettled) {
463
+ const s = stale.get(v) as StaleScaledIndex;
464
+ Object.assign(s.settle!.op, { opcode: 'undef', operands: [], attrs: { key: s.settle!.key } });
465
+ }
466
+ }
467
+
468
+ /** The scaled indices written at MIXED targets that do not stand. One stands where every path into
469
+ * the end of each of its target's other predecessors brings, in its register, a scaled index of the
470
+ * same table shifted from the same scrutinee — directly or through joins all of whose inputs do —
471
+ * with no call destroying it (`SsaBuilder.everyReachingDef`). Those paths include ones through
472
+ * other MIXED targets, so it is a greatest fixpoint: every guess stands until a path refutes it, and
473
+ * a refuted one refutes whatever leaned on it. What stands is then exactly what the register holds
474
+ * on every path into the target: the table's case edges bring the shift the target computes, and
475
+ * each other edge brings a value computing that same shift. The scrutinee is compared through
476
+ * joins all of whose other inputs are it, which the trivial-phi pass has yet to fold. */
477
+ function unsettledScaledIndices(
478
+ irBlocks: readonly Block[],
479
+ stale: ReadonlyMap<Value, StaleIdiomRead>,
480
+ incoming: ReadonlyMap<Value, readonly Value[]>,
481
+ ssa: Pick<SsaBuilder, 'everyReachingDef'>,
482
+ ): Set<Value> {
483
+ const pending = [...stale].flatMap(([v, s]) => (s.kind === 'scaled' && s.settle !== undefined ? [{ v, s }] : []));
484
+ const refuted = new Set<Value>();
485
+ if (pending.length === 0) {
486
+ return refuted;
487
+ }
488
+ // the entry block's parameters are the caller's values, whatever its back edges bring
489
+ const entryParams = new Set(irBlocks[0].params);
490
+ const joined = (v: Value) => (entryParams.has(v) ? undefined : incoming.get(v));
491
+ const resolve = (v: Value, seen: Set<Value>): Value => {
492
+ const ins = joined(v);
493
+ if (ins === undefined || seen.has(v)) {
494
+ return v;
495
+ }
496
+ seen.add(v);
497
+ const others = new Set(ins.map((i) => resolve(i, seen)).filter((u) => u !== v));
498
+ return others.size === 1 ? [...others][0] : v;
499
+ };
500
+ const brings = (s: StaleScaledIndex) => {
501
+ const of = resolve(s.of, new Set());
502
+ const seen = new Set<Value>();
503
+ const ok = (u: Value): boolean => {
504
+ const t = stale.get(u);
505
+ if (t?.kind === 'scaled' && t.table === s.table && !refuted.has(u) && resolve(t.of, new Set()) === of) {
506
+ return true;
507
+ }
508
+ const ins = joined(u);
509
+ if (ins === undefined) {
510
+ return false;
511
+ }
512
+ if (seen.has(u)) {
513
+ return true; // a cycle of joins adds no input the first visit did not judge
514
+ }
515
+ seen.add(u);
516
+ return ins.every(ok);
517
+ };
518
+ return ok;
519
+ };
520
+ for (let changed = true; changed;) {
521
+ changed = false;
522
+ for (const { v, s } of pending) {
523
+ if (!refuted.has(v) && !s.settle!.others.every((p) => ssa.everyReachingDef(s.settle!.key, p, brings(s)))) {
524
+ refuted.add(v);
525
+ changed = true;
526
+ }
527
+ }
528
+ }
529
+ return refuted;
530
+ }