@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,594 @@
1
+ // asmlift — undo a partial-redundancy insertion: a block parameter whose every in-edge hands it the
2
+ // same pure expression is that expression, recomputed in the block (ISA-neutral CFG pass).
3
+ //
4
+ // agbcc's gcse PRE (gcse.c `pre_insert` :4337, at the end of a predecessor by `insert_insn_end_bb`
5
+ // :4210) makes an expression that is computed on SOME paths into a block fully redundant by
6
+ // inserting it on the others, keeps each original computation's result in the same pseudo
7
+ // (`pre_insert_copy_insn` :4400), and deletes the copy in the block (`pre_delete` :4508). In
8
+ // `for (i = 0; i < n; i++) switch (gId[i]) { … case 2: case 3: if (gK[i + 1] != 0) break; case 4: … }`
9
+ // (`synthetic:sw_fallpre:agbcc`) the loop's `i + 1` is inserted above the jump table, case 2 keeps
10
+ // its own `gK[i + 1]` sum in that register, and SSA construction reads the register as a parameter
11
+ // of `case 4:` fed `i + 1` by both the switch edge and the arm falling into it. The structurer
12
+ // spells such a parameter as a C variable assigned on each edge, and on a switch edge that a
13
+ // fall-through path also reaches that is an edge copy it refuses to re-run ('the case fallen into
14
+ // takes a value from the switch edge'). Recomputing the expression in the block removes the
15
+ // parameter, and with it the copy.
16
+ //
17
+ // SOUND BY SSA: every in-edge's argument is the SAME VALUE, so reading it in the block is reading
18
+ // it on every edge. This is ir/simplify.ts's trivial phi (`trivialPhiValue`, which this calls) with
19
+ // identity widened to congruence, and with its waiver: a back edge that hands the parameter itself
20
+ // hands whatever the last entry did. "The same" is `sameAtEntry` (ir/entry.ts) with an empty entry
21
+ // map: one opcode, one set of attributes, operands the same in turn, down to LEAVES that are one SSA
22
+ // value on every edge. The walk reads through an op only when it can be re-evaluated anywhere (`clonable`: no
23
+ // effect, no memory read, nothing that traps, no placement stamp), so the expression's value is a
24
+ // function of its leaves alone; and every leaf must be defined in a block that strictly dominates
25
+ // the block, so it holds the same value there. C names need no argument of their own: the
26
+ // recomputed expression is fresh SSA ops reading the leaves, and namecoalesce gives a leaf that is
27
+ // live at the block a name no other live value shares. The edge ops the fold leaves unread are
28
+ // deleted with whatever pure ops only they read, as the derived fold below deletes its own: KMC
29
+ // steps `gA[i]`'s pointer in the default arm and in the slot of the branch into the latch's second
30
+ // entry, and once the latch reads the step itself the default arm's dead copy would still be a body
31
+ // (`synthetic:sw_gotocase2:gcc2.7.2kmc`), which the structurer reads as one more case it reaches.
32
+ // A block the sweep leaves holding only its `br` is then threaded out of the CFG (`threadEmptied`).
33
+ //
34
+ // SCOPE, not soundness (the gates the precondition does not need, tabled as `UNPRE_GATES`):
35
+ // • THE BLOCK IS A JOIN OF ONE SWITCH (`not-switch-join`): one in-edge is a `switch_br`'s, at least
36
+ // one comes from another block, and every other comes from a block the dispatch dominates — an
37
+ // arm of that switch. Two joins pass:
38
+ // (1) A CASE FALLEN INTO (`case 4:` above). The structurer cannot spell its parameter as a
39
+ // variable, because the switch-edge copy would re-run on the fall path, so it declines; the
40
+ // fold is what lifts it.
41
+ // (2) THE SWITCH'S EXIT, entered by the dispatch's default edge (the bounds test's, when the
42
+ // source has no `default:`) and by the arms' `break`s. Its parameter IS spellable — a copy at
43
+ // the end of every arm and a `default:` arm holding only the copy — so here the fold changes
44
+ // the spelling, not whether the function lifts. `sw_fallpre`'s MATCH rests on it: PRE's
45
+ // `i + 1` reaches the loop latch from every edge, the default one included, and with the
46
+ // exit refused (only the case fallen into recomputed) the row lifts at 6/61 as `v1 = v0 + 1;`
47
+ // in each arm, a `default:` arm, and `v0 = v1;` where the source has `i++`. Off the bench it
48
+ // also re-spells functions that already lift, host-correct both ways (4 agbcc fuzz
49
+ // seeds): per-arm `v2 = v0 + 1` becomes the latch's `v0 = v0 + 1`, and per-arm
50
+ // `v1 = a1 * 3` (a source's `u = v * 3` above the switch, which every edge hands the join)
51
+ // one `a1 * 3` read at the join.
52
+ // Everywhere else the parameter is spellable too, and which spelling the source wrote the asm
53
+ // does not say: a sum agbcc kept in each arm is also what a source computing it in each arm
54
+ // compiles to. Measured at every join (full bench, the gate lifted): `synthetic:sw_fallpre` and
55
+ // `synthetic:jt_pre_midg` MATCH, but `synthetic:bgshare:agbcc` and `synthetic:bgswitch:agbcc` lose
56
+ // their MATCH, `synthetic:sw_jtloop:agbcc` goes 8/57 -> 32/58, and 21 rows move in all. Gated, the
57
+ // full bench moves `sw_fallpre` alone (declined -> MATCH). The wider fold would be a lift
58
+ // variation, and a variation never ships a function its default declines (rank.ts), so it could
59
+ // not have lifted this row: the gate is what lets the default carry it. `unpre.test.ts` pins both
60
+ // joins and an ordinary `if` join the gate keeps out.
61
+ // A LOOP'S LATCH passes too: the block holding the back edge to a header that dominates it.
62
+ // IDO computes a counted loop's loop-invariant bound `hi + 1` (`addiu v0,a1,1`) in the header
63
+ // and again in the delay slot of every arm's `b latch`, so the latch of `for (i = lo; i <= hi;
64
+ // i++) switch (..) { .. }` (`synthetic:sw_gotocase2:ido7.1`) takes `hi + 1` as a parameter every
65
+ // in-edge hands it. The parameter is spellable — a copy at the end of every arm — but the
66
+ // structurer reads a latch with a parameter as no `for` (`forPlanAt`'s `latch-params`), so the
67
+ // fold is what lets the `for` read the latch at all. It is a pre-structure fold on every
68
+ // toolchain, so it also folds a latch param no `for` reads: a `do`-`while` latch every arm hands
69
+ // `p + 2` (KMC's `loop_elsebreak`) is spelled as the update `p = p + 2` rather than as a copy at
70
+ // the end of each arm. Over the synthetic tier the latch scope alone moves 16 cells, loses no
71
+ // match and turns no decline: closer on six (uninit_bits ido 13 -> 6, sw_loopret gcc2.7.2
72
+ // 81 -> 73), further on four (loop_elsebreak gcc2.7.2kmc 16 -> 23 the most), so the scope stays.
73
+ // A SIBLING'S STRIDE below is kept out of the pre-structure folds by the same measurement, where
74
+ // the moves were one way (brk2out_ctl ido 151 -> 214). `unpre.test.ts` pins an ido-shaped latch
75
+ // folding and the `if` join staying out.
76
+ // • the argument must be an op with operands (`operand-free`). An operand-free argument (a constant, a symbol's
77
+ // address) is what a merge of `return 0;` arms or of a register the compiler set to one constant
78
+ // on several paths carries, and those are spellings other passes own. A default arm that both the
79
+ // switch edge and a falling case hand the constant 7 (the KMC/gcc2.7.2 shape thumb-switch.test.ts
80
+ // pins) therefore still declines.
81
+ //
82
+ // A SIBLING'S LEAF (`recomputeDerivedParams`, its own trace stage just ahead of this one): the
83
+ // congruence above with one leaf allowed to differ per edge, where on every edge it is the argument
84
+ // that same edge hands a sibling parameter `i`. gcc 2.7.2 strength-reduces `for (i = lo; i <= hi;
85
+ // i++) .. gE[i] ..` to a header that carries `i << 3` beside `i`: the guard hands it `(lo, lo << 3)`
86
+ // and the latch `(i + 1, (i + 1) << 3)`. SOUND BY SSA, with no induction: on edge `e` the parameter
87
+ // `j` receives F(a_e) and `i` receives a_e, so at every entry `j` holds F(i), and reading F(i) in the
88
+ // block is reading `j`. F is one `clonable` op of one shape and result type on every edge; each other
89
+ // operand is one value every edge shares, defined in a block that strictly dominates this one, or a
90
+ // constant of one value (the latch's own `3` against the entry's). An argument that is not such an op
91
+ // on some edge (a back edge handing the parameter itself, a load, a call) leaves the parameter alone.
92
+ // The edge ops the fold leaves unread are deleted with whatever pure ops only they read, so a latch's
93
+ // `i + 1` the dead `(i + 1) << 3` was the second reader of is not named for it.
94
+ //
95
+ // UNGATED, by measurement: a loop header is no switch join, so this is exactly the widening the
96
+ // `not-switch-join` gate keeps from the congruent fold. Over the synthetic tier it moves only gcc
97
+ // 2.7.2 and KMC cells and loses no match (retpost_tail, uninit_bits, sw_h3, sw_h4 and bcpy_ind
98
+ // match through it; the brk2out rows lift a few insns further, with C that reads correct). It
99
+ // cannot be asked per loop as structure.ts's rerun asks its readings: the pre-structure folds run
100
+ // once, ahead of every attempt. The recomputed F(i) has no name, so a loop test that reads it reads
101
+ // the op of the IV that test sees, never a value one trip stale.
102
+ import {
103
+ type Block,
104
+ type Fn,
105
+ type Op,
106
+ type Successor,
107
+ type Value,
108
+ defOpMap,
109
+ dominators,
110
+ foldWriteOrder,
111
+ forwardingTarget,
112
+ isBodyless,
113
+ mkValue,
114
+ opShape,
115
+ } from '../ir/core';
116
+ import { deletableWhenDead, placedAt, reevalUnsafe, spelledWhenDead } from '../ir/discipline';
117
+ import { sameAtEntry } from '../ir/entry';
118
+ import { inEdgesOf, retireParam, trivialPhiValue } from '../ir/simplify';
119
+ import { type IrType, typeEquals } from '../ir/types';
120
+ import { type Gate, firstRejection } from '../l3/gates';
121
+
122
+ /** two integer types of one width, which may differ only in sign */
123
+ const sameIntWidth = (a: IrType, b: IrType): boolean => a.kind === 'int' && b.kind === 'int' && a.width === b.width;
124
+
125
+ /** May this op be evaluated again at another program point to the same value? */
126
+ const clonable = (d: Op): boolean =>
127
+ d.opcode !== 'call' && d.results.length === 1 && !reevalUnsafe(d) && !spelledWhenDead(d) && placedAt(d) === null;
128
+
129
+ /** A parameter the precondition admits: every in-edge (but a back edge handing the parameter itself)
130
+ * hands it one pure expression over leaves that strictly dominate the block. */
131
+ export interface RedundantParam {
132
+ /** one in-edge is a `switch_br`'s, another comes from elsewhere, and every other comes from a block
133
+ * the dispatch dominates */
134
+ readonly switchJoin: boolean;
135
+ /** the block holds a loop's back edge: one of its successors is a header that dominates it */
136
+ readonly latchJoin: boolean;
137
+ /** the expression is an op with operands, not a constant or a symbol's address */
138
+ readonly hasOperands: boolean;
139
+ }
140
+
141
+ /** THE SCOPE (the header's), as data: neither rule is needed for soundness — the precondition is —
142
+ * so neither is `sound`, and a test ablates one with `without()` to run the fold at every join.
143
+ * THE RESIDUE, untabled: the precondition itself (congruent arguments over `clonable` ops, leaves
144
+ * that strictly dominate the block). It is what the fold IS, not a refusal of it — drop any part
145
+ * and the recompute reads a different value — and the gates are asked only of a parameter it
146
+ * admits, so a census counts scope refusals, not shapes the fold could never take. */
147
+ export const UNPRE_GATES: readonly Gate<RedundantParam>[] = [
148
+ {
149
+ id: 'not-switch-join',
150
+ why: 'outside a switch or a loop latch the parameter is spellable as a variable, and recomputing it re-spells functions that match',
151
+ sound: false,
152
+ rejects: (c) => !c.switchJoin && !c.latchJoin,
153
+ },
154
+ {
155
+ id: 'operand-free',
156
+ why: 'a merged constant or symbol address is a spelling the passes that merge constants own',
157
+ sound: false,
158
+ rejects: (c) => !c.hasOperands,
159
+ },
160
+ ];
161
+
162
+ /** Recompute every parameter of a switch's join (a case fallen into, or the switch's exit) or of a
163
+ * loop's latch whose in-edges all hand it one pure expression; returns how many parameters it
164
+ * removed. */
165
+ export function recomputeRedundantParams(fn: Fn, gates: readonly Gate<RedundantParam>[] = UNPRE_GATES): number {
166
+ // The CFG does not change, so dominance and the in-edge index are computed once; the def map and
167
+ // the homes grow with each clone.
168
+ const dom = dominators(fn);
169
+ const defs = defOpMap(fn);
170
+ const home = new Map<Value, Block>();
171
+ for (const b of fn.blocks) {
172
+ b.params.forEach((p) => home.set(p, b));
173
+ for (const op of b.ops) {
174
+ op.results.forEach((r) => home.set(r, b));
175
+ }
176
+ }
177
+ const inEdges = inEdgesOf(fn);
178
+ /** a block the switch enters — a case, or by the default edge its exit — whose every other edge
179
+ * comes from inside the switch */
180
+ const switchJoin = (ins: readonly { from: Block; op: Op }[]): boolean => {
181
+ const dispatch = ins.find((e) => e.op.opcode === 'switch_br');
182
+ return (
183
+ dispatch !== undefined &&
184
+ ins.some((e) => e.from !== dispatch.from) &&
185
+ ins.every((e) => e.from === dispatch.from || dom.get(e.from)!.has(dispatch.from))
186
+ );
187
+ };
188
+ /** a block holding a back edge: a successor that dominates it is the header of a loop it closes */
189
+ const latchJoin = (b: Block): boolean => b.ops[b.ops.length - 1].successors.some((s) => dom.get(b)!.has(s.block));
190
+ const through = (v: Value) => {
191
+ const d = defs.get(v);
192
+ return d !== undefined && clonable(d);
193
+ };
194
+ /** structural congruence: one pure expression over the same leaves */
195
+ const congruent = (a: Value, b: Value) => sameAtEntry(defs, a, b, { entry: new Map(), through });
196
+ /** the edge ops a fold stopped reading, swept once no sweep removes anything */
197
+ const orphaned: Op[] = [];
198
+ let removed = 0;
199
+ // A recomputed parameter can make a later block's arguments equal (a second case fallen into
200
+ // whose argument reads the first's parameter), so sweep until a sweep removes nothing.
201
+ for (let changed = true; changed;) {
202
+ changed = false;
203
+ for (const b of fn.blocks.slice(1)) {
204
+ const ins = inEdges.get(b) ?? [];
205
+ const incoming = ins.map((e) => e.succ);
206
+ for (let k = 0; k < b.params.length;) {
207
+ // THE trivial-phi predicate (ir/simplify.ts), held to congruence instead of identity: a back
208
+ // edge handing the parameter itself is waived there, for the reason it is waived for a
209
+ // trivial phi.
210
+ const first = trivialPhiValue(incoming, k, b.params[k], congruent);
211
+ const d0 = first === null ? undefined : defs.get(first);
212
+ const clones: Op[] = [];
213
+ const memo = new Map<Value, Value>();
214
+ let leavesDominate = true;
215
+ const clone = (v: Value): Value => {
216
+ const seen = memo.get(v);
217
+ if (seen !== undefined) {
218
+ return seen;
219
+ }
220
+ const d = defs.get(v);
221
+ let out = v;
222
+ if (d !== undefined && clonable(d)) {
223
+ // THE ROOT TAKES THE PARAMETER'S TYPE, which every reader of the parameter was typed
224
+ // against: type recovery reads a block parameter off its uses (an unsigned compare makes
225
+ // it `u32`) and an arm's op off its operands, so the two can differ in sign, and the
226
+ // declaration-signedness reconciliation reads the value that stands in the parameter's
227
+ // place. Typed as the arm's `s32` add, a counter compared unsigned would declare `s32`
228
+ // and overflow (UB) where the asm's `u32` wraps (kleod's `sub_080240F4`).
229
+ const p = b.params[k];
230
+ const r = mkValue(v === first && sameIntWidth(p.type, v.type) ? p.type : v.type);
231
+ const op: Op = {
232
+ opcode: d.opcode,
233
+ operands: d.operands.map(clone),
234
+ results: [r],
235
+ attrs: { ...d.attrs },
236
+ successors: [],
237
+ };
238
+ clones.push(op);
239
+ out = r;
240
+ } else {
241
+ const h = home.get(v);
242
+ if (h === undefined || h === b || !dom.get(b)!.has(h)) {
243
+ leavesDominate = false;
244
+ }
245
+ }
246
+ memo.set(v, out);
247
+ return out;
248
+ };
249
+ const value = first !== null && d0 !== undefined && clonable(d0) ? clone(first) : undefined;
250
+ if (
251
+ value === undefined ||
252
+ !leavesDominate ||
253
+ firstRejection(gates, {
254
+ switchJoin: switchJoin(ins),
255
+ latchJoin: latchJoin(b),
256
+ hasOperands: d0!.operands.length > 0,
257
+ }) !== null
258
+ ) {
259
+ k++;
260
+ continue;
261
+ }
262
+ b.ops.unshift(...clones);
263
+ for (const op of clones) {
264
+ defs.set(op.results[0], op);
265
+ home.set(op.results[0], b);
266
+ }
267
+ for (const s of incoming) {
268
+ const d = defs.get(s.args[k]);
269
+ if (d !== undefined) {
270
+ orphaned.push(d);
271
+ }
272
+ }
273
+ retireParam(fn, b, k, value, incoming);
274
+ removed++;
275
+ changed = true;
276
+ }
277
+ }
278
+ }
279
+ sweepOrphans(fn, orphaned);
280
+ return removed;
281
+ }
282
+
283
+ /** Is `x` the same op as `y` apart from its operands' values: one opcode, attributes, operand count
284
+ * and result type? */
285
+ const sameShape = (x: Op, y: Op): boolean =>
286
+ opShape(x) === opShape(y) &&
287
+ x.operands.length === y.operands.length &&
288
+ x.results.length === 1 &&
289
+ typeEquals(x.results[0].type, y.results[0].type);
290
+
291
+ /** Recompute every block parameter `j` whose in-edges each hand it one pure op F over the argument
292
+ * that edge hands a sibling parameter `i` (A SIBLING'S LEAF, in the header): `j` is F(i). Returns
293
+ * how many parameters it removed. */
294
+ export function recomputeDerivedParams(fn: Fn): number {
295
+ const dom = dominators(fn);
296
+ const defs = defOpMap(fn);
297
+ const home = new Map<Value, Block>();
298
+ for (const b of fn.blocks) {
299
+ b.params.forEach((p) => home.set(p, b));
300
+ for (const op of b.ops) {
301
+ op.results.forEach((r) => home.set(r, b));
302
+ }
303
+ }
304
+ const inEdges = inEdgesOf(fn);
305
+ /** the edge ops a fold stopped reading, swept once every block is done */
306
+ const orphaned: Op[] = [];
307
+ let removed = 0;
308
+ for (const b of fn.blocks.slice(1)) {
309
+ const ins = inEdges.get(b) ?? [];
310
+ if (ins.length < 2) {
311
+ continue;
312
+ }
313
+ const incoming = ins.map((e) => e.succ);
314
+ /** F(i) as fresh ops at the head of `b`, or null where `j` is no one op of `i`'s arguments */
315
+ const derive = (j: number): { ops: Op[]; value: Value } | null => {
316
+ const ds = incoming.map((s) => defs.get(s.args[j]));
317
+ const d0 = ds[0];
318
+ if (d0 === undefined || !ds.every((d) => d !== undefined && clonable(d) && sameShape(d, d0))) {
319
+ return null;
320
+ }
321
+ for (let i = 0; i < b.params.length; i++) {
322
+ if (i === j) {
323
+ continue;
324
+ }
325
+ const ivSlot = d0.operands.map((_, k) => incoming.every((s, x) => ds[x]!.operands[k] === s.args[i]));
326
+ if (!ivSlot.some(Boolean)) {
327
+ continue;
328
+ }
329
+ const ops: Op[] = [];
330
+ const operands: Value[] = [];
331
+ const ok = d0.operands.every((o, k) => {
332
+ if (ivSlot[k]) {
333
+ operands.push(b.params[i]);
334
+ return true;
335
+ }
336
+ const vals = ds.map((d) => d!.operands[k]);
337
+ if (vals.every((v) => v === o)) {
338
+ // one value every edge shares, which must hold that value here too
339
+ const h = home.get(o);
340
+ operands.push(o);
341
+ return h !== undefined && h !== b && dom.get(b)!.has(h);
342
+ }
343
+ // or a constant of one value on every edge, made again here
344
+ const cs = vals.map((v) => defs.get(v));
345
+ const c0 = cs[0];
346
+ if (c0?.opcode !== 'const' || !cs.every((c) => c !== undefined && c.opcode === 'const' && sameShape(c, c0))) {
347
+ return false;
348
+ }
349
+ const r = mkValue(o.type);
350
+ ops.push({ opcode: 'const', operands: [], results: [r], attrs: { ...c0.attrs }, successors: [] });
351
+ operands.push(r);
352
+ return true;
353
+ });
354
+ if (!ok) {
355
+ continue;
356
+ }
357
+ const r = mkValue(d0.results[0].type);
358
+ ops.push({ opcode: d0.opcode, operands, results: [r], attrs: { ...d0.attrs }, successors: [] });
359
+ return { ops, value: r };
360
+ }
361
+ return null;
362
+ };
363
+ for (let j = 0; j < b.params.length;) {
364
+ const got = derive(j);
365
+ if (got === null) {
366
+ j++;
367
+ continue;
368
+ }
369
+ orphaned.push(...incoming.map((s) => defs.get(s.args[j])!));
370
+ b.ops.unshift(...got.ops);
371
+ for (const op of got.ops) {
372
+ defs.set(op.results[0], op);
373
+ home.set(op.results[0], b);
374
+ }
375
+ retireParam(fn, b, j, got.value, incoming);
376
+ removed++;
377
+ }
378
+ }
379
+ sweepOrphans(fn, orphaned);
380
+ return removed;
381
+ }
382
+
383
+ /** Delete each of `seeds` nothing reads any more, and then each pure op only the deleted read: never
384
+ * an op the fold did not orphan, which other passes own. */
385
+ function sweepOrphans(fn: Fn, seeds: readonly Op[]): void {
386
+ const defs = defOpMap(fn);
387
+ const uses = new Map<Value, number>();
388
+ const read = (v: Value, by: number) => uses.set(v, (uses.get(v) ?? 0) + by);
389
+ for (const b of fn.blocks) {
390
+ for (const op of b.ops) {
391
+ op.operands.forEach((o) => read(o, 1));
392
+ op.successors.forEach((s) => s.args.forEach((a) => read(a, 1)));
393
+ }
394
+ }
395
+ const dead = new Set<Op>();
396
+ const work = [...seeds];
397
+ while (work.length > 0) {
398
+ const op = work.pop()!;
399
+ if (dead.has(op) || !clonable(op) || !deletableWhenDead(op) || (uses.get(op.results[0]) ?? 0) > 0) {
400
+ continue;
401
+ }
402
+ dead.add(op);
403
+ for (const o of op.operands) {
404
+ read(o, -1);
405
+ const d = defs.get(o);
406
+ if (d !== undefined) {
407
+ work.push(d);
408
+ }
409
+ }
410
+ }
411
+ if (dead.size === 0) {
412
+ return;
413
+ }
414
+ const emptied: Block[] = [];
415
+ for (const b of fn.blocks) {
416
+ const kept = b.ops.filter((op) => !dead.has(op));
417
+ if (kept.length < b.ops.length && b !== fn.blocks[0] && isBodyless({ params: b.params, ops: kept })) {
418
+ emptied.push(b);
419
+ }
420
+ b.ops = kept;
421
+ }
422
+ threadEmptied(fn, emptied);
423
+ }
424
+
425
+ /** Splice out each of `emptied` that the sweep left a transparent forwarder (no params, a lone `br`
426
+ * handing no value), every edge into it now going where it forwards.
427
+ *
428
+ * THE PASS THAT EMPTIES A BLOCK THREADS IT. Such a block held nothing but the edge copies a fold
429
+ * made redundant, so it stands for no code of the source: KMC's default arm that held only the
430
+ * pointer step (`synthetic:sw_gotocase2:gcc2.7.2kmc`), and each arm of IDO's
431
+ * `synthetic:forexit_old:ido7.1` that held only its copy of the latch's bound `hi + 1`. Left in the
432
+ * CFG, the structurer reads it as one more sibling case every `continue` falls into, or one more
433
+ * region on the way to the latch. An edge into it carries no args (it has no params) and its own
434
+ * carries none, so retargeting changes no value on any path;
435
+ * `foldExitTrampolines` (raise/latch.ts) folds the same block on a loop's exit, for its own scope,
436
+ * and this one asks no scope because the fold made it, not the compiler. A forwarder whose chain
437
+ * ends in a cycle of forwarders (`while (1);`) stays. */
438
+ function threadEmptied(fn: Fn, emptied: readonly Block[]): void {
439
+ const forwards = (b: Block) => isBodyless(b) && b.ops[0].opcode === 'br' && b.ops[0].successors[0].args.length === 0;
440
+ const gone = new Set<Block>();
441
+ for (const b of emptied) {
442
+ const onward = forwardingTarget(b);
443
+ if (!forwards(b) || forwards(onward)) {
444
+ continue;
445
+ }
446
+ for (const p of fn.blocks) {
447
+ let repointed = false;
448
+ for (const op of p.ops) {
449
+ op.successors.forEach((s, i) => {
450
+ if (s.block === b) {
451
+ op.successors[i] = { block: onward, args: [] };
452
+ repointed = true;
453
+ }
454
+ });
455
+ }
456
+ if (repointed) {
457
+ foldWriteOrder(fn.writeOrder, b, p);
458
+ }
459
+ }
460
+ gone.add(b);
461
+ }
462
+ if (gone.size > 0) {
463
+ fn.blocks = fn.blocks.filter((b) => !gone.has(b));
464
+ }
465
+ }
466
+
467
+ // A SIBLING'S STRIDE (`strideDerivedParams`): the fact `recomputeDerivedParams` folds — a header
468
+ // parameter `j` holds G(i) at every entry, `i` a sibling — proved over a loop's two edges by
469
+ // INDUCTION where the back edge hands `j + K` rather than G of the argument it hands `i`. IDO
470
+ // strength-reduces `gE[i]` in `for (i = lo; i <= hi; i++)` (`synthetic:sw_gotocase2:ido7.1`) to a
471
+ // pointer that starts at `gE + (lo << 3)` and steps by 8 beside `i`: the entry hands the header
472
+ // `(gE + (lo << 3), lo)` and the back edge `(p + 8, i + 1)`. G is `B + T(x)` or `T(x)`, T one of
473
+ // `x << s`, `x * M` (M a constant) or `x` (the last only beside a base), and B one value from outside
474
+ // the loop. The entry hands `j` exactly G(x0), x0 being what it hands `i`; the back edge hands `i + D`
475
+ // and `j + K` with K = T(D). T is linear in 32-bit words, so G(i + D) = G(i) + T(D) with no overflow
476
+ // argument: if `j` = G(i) on one entry, it is G(i + D) = G(i') on the next.
477
+ //
478
+ // A FACT, NOT A FOLD, by measurement: recomputed in the header ahead of every structuring, as
479
+ // `recomputeDerivedParams` recomputes its params, it re-spells every IDO loop that stays `while (1)`
480
+ // as an index recomputed per trip, and those lift further from the bytes (synthetic:brk2out_ctl:ido7.1
481
+ // 151/176 -> 214/224, forexit_old 163 -> 173). So the structurer reads it only where it spells the
482
+ // loop as a `for` (structure.ts `forPlanAt`, A SIBLING'S STRIDE), which writes `j` nowhere and reads
483
+ // G(i) at each of its uses. The caller states which values are from outside the loop.
484
+
485
+ /** A header parameter that is G of a sibling at every entry (A SIBLING'S STRIDE). */
486
+ export interface StrideDerived {
487
+ /** the derived parameter's slot in the header */
488
+ readonly j: number;
489
+ /** the sibling's (the counter's) slot */
490
+ readonly i: number;
491
+ /** G's ops as the entry edge computes them, innermost first; the last one's result is G(x0) */
492
+ readonly ops: readonly Op[];
493
+ /** what the entry hands the sibling: the one leaf of G that is not from outside the loop */
494
+ readonly x0: Value;
495
+ /** G's other operands (the base, a multiplier, a shift amount), each from outside the loop */
496
+ readonly leaves: readonly Value[];
497
+ }
498
+
499
+ /** Every header parameter that is G of a sibling at each entry, read over the loop's one entry edge
500
+ * and one back edge (A SIBLING'S STRIDE). `outside` says a value is defined in a block that strictly
501
+ * dominates the header. */
502
+ export function strideDerivedParams(
503
+ defs: ReadonlyMap<Value, Op>,
504
+ header: Block,
505
+ entry: Successor,
506
+ back: Successor,
507
+ outside: (v: Value) => boolean,
508
+ ): StrideDerived[] {
509
+ const constOf = (v: Value): number | null => {
510
+ const d = defs.get(v);
511
+ return d?.opcode === 'const' && typeof d.attrs.value === 'number' ? d.attrs.value | 0 : null;
512
+ };
513
+ /** `v` is `p + c`, `c` a constant: `c` */
514
+ const stepOf = (v: Value, p: Value): number | null => {
515
+ const d = defs.get(v);
516
+ if (d?.opcode !== 'add' || d.operands.length !== 2) {
517
+ return null;
518
+ }
519
+ const [a, b] = d.operands;
520
+ return a === p ? constOf(b) : b === p ? constOf(a) : null;
521
+ };
522
+ /** `v` is T(x0): its ops, its constant operands, and T(d) */
523
+ const term = (v: Value, x0: Value, d: number): { ops: Op[]; leaves: Value[]; k: number } | null => {
524
+ if (v === x0) {
525
+ return { ops: [], leaves: [], k: d };
526
+ }
527
+ const t = defs.get(v);
528
+ if (t === undefined || !clonable(t) || (t.opcode === 'shl' && t.operands[0] !== x0)) {
529
+ return null;
530
+ }
531
+ if (t.opcode === 'shl') {
532
+ const amount = t.operands.length === 1 ? t.attrs.imm : t.operands.length === 2 ? constOf(t.operands[1]) : null;
533
+ return typeof amount !== 'number' || amount < 0 || amount > 31
534
+ ? null
535
+ : { ops: [t], leaves: t.operands.slice(1), k: (d << amount) | 0 };
536
+ }
537
+ if (t.opcode === 'mul' && t.operands.length === 2) {
538
+ const at = t.operands.indexOf(x0);
539
+ const m = at < 0 ? null : constOf(t.operands[1 - at]);
540
+ return m === null ? null : { ops: [t], leaves: [t.operands[1 - at]], k: Math.imul(d, m) };
541
+ }
542
+ return null;
543
+ };
544
+ /** `j` as G of the sibling `i`, or null */
545
+ const fit = (j: number, i: number): StrideDerived | null => {
546
+ const k = stepOf(back.args[j], header.params[j]);
547
+ const d = i === j ? null : stepOf(back.args[i], header.params[i]);
548
+ if (k === null || d === null || d === 0) {
549
+ return null;
550
+ }
551
+ const x0 = entry.args[i];
552
+ const g = entry.args[j];
553
+ let found: { ops: Op[]; leaves: Value[]; k: number } | null = null;
554
+ const bare = term(g, x0, d);
555
+ if (bare !== null && bare.ops.length > 0) {
556
+ found = bare;
557
+ } else {
558
+ const add = defs.get(g);
559
+ if (add?.opcode === 'add' && add.operands.length === 2 && clonable(add)) {
560
+ for (const at of [0, 1]) {
561
+ const t = term(add.operands[at], x0, d);
562
+ const base = add.operands[1 - at];
563
+ if (t !== null && base !== x0 && outside(base)) {
564
+ found = { ops: [...t.ops, add], leaves: [...t.leaves, base], k: t.k };
565
+ break;
566
+ }
567
+ }
568
+ }
569
+ }
570
+ return found !== null && found.k === k && found.leaves.every(outside)
571
+ ? { j, i, ops: found.ops, x0, leaves: found.leaves }
572
+ : null;
573
+ };
574
+ const slots = header.params.map((_, x) => x);
575
+ // A sibling that is itself G of another is asked last. The one reader, structure.ts's `for`, drops
576
+ // every slot in `strides` from its copies, so no such slot is the counter its update writes, and it
577
+ // takes a stride only as G of that counter's param: asking the other slots first asks exactly the
578
+ // slots that can be the counter, and a pair asked later is one the `for` would refuse anyway, so the
579
+ // order turns no lift into a decline. (KMC strength-reduces both `i << 3` and `gE + (i << 3)` beside
580
+ // `i`, and the pointer is `x + gE` of the shift as well as G of `i`.)
581
+ const strides = new Set(slots.filter((j) => slots.some((i) => fit(j, i) !== null)));
582
+ const order = [...slots.filter((x) => !strides.has(x)), ...slots.filter((x) => strides.has(x))];
583
+ const out: StrideDerived[] = [];
584
+ for (const j of strides) {
585
+ for (const i of order) {
586
+ const sd = fit(j, i);
587
+ if (sd !== null) {
588
+ out.push(sd);
589
+ break;
590
+ }
591
+ }
592
+ }
593
+ return out;
594
+ }