@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
@@ -3,7 +3,7 @@
3
3
  // A loop whose back-edge carries a register copy — `add r3, r0, #0` then `b .L6` — lifts to a block
4
4
  // holding nothing but that branch, because SSA construction turns the copy into an EDGE ARGUMENT.
5
5
  // The loop is unchanged; its latch has just become a separate empty block between the exit test and
6
- // the header. Loop DISCOVERY still finds the loop — `structure/loops.ts` reads the same back-edge —
6
+ // the header. Loop DISCOVERY still finds the loop — `ir/loops.ts` reads the same back-edge —
7
7
  // but the do-while emitter requires the latch to end in a `cond_br`, an empty one ends in `br`, and
8
8
  // the test is at the bottom so the `while` form is unavailable too. The back-edge survives to the
9
9
  // `onStack` refusal and the whole function declines with "unrecovered back-edge".
@@ -24,11 +24,13 @@
24
24
  // `if` (declining loud on the hazards). The guard therefore survives the fold either way; what the
25
25
  // gate preserves is the SHAPE — a forward trampoline is not a latch, and folding one re-casts an
26
26
  // unrelated branch as a loop guard — which is the `target-dominates` entry's own note below.
27
- import { Fn, dominators, foldWriteOrder } from '../ir/core';
27
+ import { Block, Fn, dominators, foldWriteOrder, predecessors, reachForward, successorsOf } from '../ir/core';
28
+ import { effectful } from '../ir/discipline';
29
+ import { analyzeLoops } from '../ir/loops';
28
30
  import { type Gate, firstRejection } from '../l3/gates';
29
31
 
30
32
  /** What the gates below judge: one candidate block and the block its `br` goes to. */
31
- interface LatchCandidate {
33
+ export interface LatchCandidate {
32
34
  block: Fn['blocks'][number];
33
35
  target: Fn['blocks'][number];
34
36
  dominatesBlock: boolean;
@@ -80,9 +82,9 @@ export const LATCH_GATES: readonly Gate<LatchCandidate>[] = [
80
82
  * SUCCESSOR changes, so an edge that was not a back-edge becomes one. (Dominator sets themselves
81
83
  * only ever shrink — removing a block removes it from every set it was in.)
82
84
  *
83
- * A predecessor that already branches to the target ends up with two edges into one block. That is
84
- * a block whose every edge continues the loop, so it has no exit and loop recovery declines — the
85
- * same loud decline it gave before the fold.
85
+ * A predecessor that already branches to the target ends up with two edges into one block, each
86
+ * carrying its own arguments. Where that predecessor has no other edge the loop has no exit, and
87
+ * loop recovery spells it `while (1)` with each edge's own update copy.
86
88
  */
87
89
  export function foldEmptyLatches(fn: Fn, gates: readonly Gate<LatchCandidate>[] = LATCH_GATES): number {
88
90
  let folded = 0;
@@ -124,3 +126,157 @@ export function foldEmptyLatches(fn: Fn, gates: readonly Gate<LatchCandidate>[]
124
126
  folded++;
125
127
  }
126
128
  }
129
+
130
+ // ── EMPTY EXIT TRAMPOLINES ──────────────────────────────────────────────────────────────────────
131
+ //
132
+ // The same empty block on a loop's way OUT. `for (…) { if (t[i] == k) { r = i; break; } }` keeps
133
+ // `r` in a register the break copies into (`add r2, r1, #0`), and the copy becomes an edge argument
134
+ // of a block that holds nothing but its `br` to the code after the loop — beside a second exit edge
135
+ // (the loop's own test failing) that reaches that code directly. The loop then has two exit targets
136
+ // and none of them is a `return`, so every loop form refuses it, and the back-edge declines loud at
137
+ // `onStack`. Which exit edge carries the block is the compiler's choice: agbcc puts it on the break
138
+ // (`synthetic:break_value`) or on the latch's own exit with the break going direct (kleod
139
+ // `sub_0801E664`).
140
+ //
141
+ // The second exit need not land on the target itself. ido, gcc2.7.2kmc and mwcc lay an `else` the
142
+ // loop sits beside between the loop and its join — `if (f) { for (..) if (a(i) == k) { id = i + 100;
143
+ // break; } } else id = f + 26; out(id);` (`synthetic:break_elsetail`) — so the loop's own exit is an
144
+ // empty `b`/`j` over the `else`, and the break reaches the join only through the code its arm runs.
145
+ // Folding the block there leaves the loop two exit targets all the same — the break arm's head is
146
+ // one. What it buys is that the loop's own exit becomes the join the break arm runs on into, which
147
+ // is the exit a `break` that runs code has to leave for (`codeBreakArm`'s region must reach it). So
148
+ // the scope gate asks whether another exit edge's target REACHES the target, going forward and
149
+ // outside the loop's body.
150
+ //
151
+ // Splicing it out re-points each edge into it at its target with its own arguments, and the loop
152
+ // exits to one block: a `break` that carries its copies, which the structurer already spells. The
153
+ // arguments move soundly for the reason the latch's do: the block has no params and one op, so
154
+ // every value its `br` passes dominates it, and therefore the end of each of its predecessors.
155
+
156
+ /** One candidate trampoline: the block, where its `br` goes, and the loops it may leave. */
157
+ export interface ExitTrampoline {
158
+ block: Block;
159
+ target: Block;
160
+ /** the predecessors of `block` */
161
+ preds: readonly Block[];
162
+ /** does some loop hold every predecessor of `block` but not `block` itself, and leave by another
163
+ * edge for `target` as well — landing on it, or on a block that reaches it forward without
164
+ * re-entering the loop (`reachForward`)? */
165
+ sharedExit: boolean;
166
+ /** does every path from `target` reach a `ret` without an effect on the way? */
167
+ pureReturn: boolean;
168
+ }
169
+
170
+ export const EXIT_TRAMPOLINE_GATES: readonly Gate<ExitTrampoline>[] = [
171
+ {
172
+ id: 'trampoline-has-params',
173
+ why: 'a param is a join: the br args may read it, and they would not exist at a predecessor',
174
+ // the fold then hands a predecessor a value it does not dominate, which `verify` refuses loud
175
+ sound: false,
176
+ rejects: (c) => c.block.params.length > 0,
177
+ },
178
+ {
179
+ id: 'trampoline-does-work',
180
+ why: 'the sole terminator must be an unconditional `br`: folding a `cond_br` block discards its second successor',
181
+ sound: true,
182
+ guardedBy: 'latch.test.ts: an exit block whose sole op is a cond_br is never folded',
183
+ rejects: (c) => c.block.ops[0]?.opcode !== 'br',
184
+ },
185
+ {
186
+ id: 'trampoline-self-branch',
187
+ why: 'a block branching to itself is an infinite loop rather than a trampoline',
188
+ sound: false,
189
+ rejects: (c) => c.target === c.block,
190
+ },
191
+ {
192
+ // The SCOPE of the fold, not its soundness: an exit edge's empty block is harmless to the
193
+ // structurer wherever it is the loop's one way out, and folding it there would only re-spell
194
+ // functions that already lift.
195
+ id: 'trampoline-not-a-second-exit',
196
+ why: 'only a loop that also leaves for the same target by another edge, or for a block that runs on into it, gains from the fold an exit every edge out can leave for',
197
+ sound: false,
198
+ rejects: (c) => !c.sharedExit,
199
+ },
200
+ {
201
+ // A target that only returns already lifts: the structurer takes the edge through the
202
+ // trampoline as an early `return` and copies the pure tail into it. Kept that way, so the fold
203
+ // re-spells no function that lifted before it.
204
+ id: 'trampoline-returns-already',
205
+ why: 'an exit that only returns is an early `return` arm, which the structurer spells without the fold',
206
+ sound: false,
207
+ rejects: (c) => c.pureReturn,
208
+ },
209
+ ];
210
+
211
+ /** Every path from `b` ends in a `ret` and runs no effect on the way. */
212
+ export function returnsPurely(b: Block): boolean {
213
+ const seen = new Set<Block>();
214
+ for (const stack = [b]; stack.length > 0;) {
215
+ const x = stack.pop()!;
216
+ if (seen.has(x)) {
217
+ continue; // a join two paths share, or a cycle — either way judged on its first visit
218
+ }
219
+ seen.add(x);
220
+ if (x.ops.some(effectful)) {
221
+ return false;
222
+ }
223
+ const t = x.ops[x.ops.length - 1];
224
+ if (t?.opcode === 'ret') {
225
+ continue;
226
+ }
227
+ if (t?.opcode !== 'br' && t?.opcode !== 'cond_br') {
228
+ return false;
229
+ }
230
+ stack.push(...successorsOf(x));
231
+ }
232
+ return true;
233
+ }
234
+
235
+ /** Splice out every empty EXIT TRAMPOLINE the gates above admit; returns how many were removed.
236
+ * Iterated, as the latch fold is: one splice can make another block's exit edges shared. */
237
+ export function foldExitTrampolines(fn: Fn, gates: readonly Gate<ExitTrampoline>[] = EXIT_TRAMPOLINE_GATES): number {
238
+ let folded = 0;
239
+ for (;;) {
240
+ const preds = predecessors(fn);
241
+ const dom = dominators(fn);
242
+ const loops = [...analyzeLoops(fn, dom).byHeader.values()];
243
+ const found = fn.blocks.find((b) => {
244
+ const target = b.ops[0]?.successors[0]?.block;
245
+ if (target === undefined) {
246
+ return false;
247
+ }
248
+ const into = preds.get(b) ?? [];
249
+ const sharedExit =
250
+ into.length > 0 &&
251
+ loops.some(
252
+ (nl) =>
253
+ !nl.body.has(b) &&
254
+ into.every((p) => nl.body.has(p)) &&
255
+ nl.exitEdges.some((e) => e.to !== b && reachForward(e.to, dom, nl.body).has(target)),
256
+ );
257
+ return (
258
+ firstRejection(gates, { block: b, target, preds: into, sharedExit, pureReturn: returnsPurely(target) }) === null
259
+ );
260
+ });
261
+ if (!found) {
262
+ return folded;
263
+ }
264
+ const onward = found.ops[0].successors[0];
265
+ for (const b of fn.blocks) {
266
+ let repointed = false;
267
+ for (const op of b.ops) {
268
+ op.successors.forEach((s, i) => {
269
+ if (s.block === found) {
270
+ op.successors[i] = { block: onward.block, args: [...onward.args] };
271
+ repointed = true;
272
+ }
273
+ });
274
+ }
275
+ if (repointed) {
276
+ foldWriteOrder(fn.writeOrder, found, b);
277
+ }
278
+ }
279
+ fn.blocks = fn.blocks.filter((b) => b !== found);
280
+ folded++;
281
+ }
282
+ }
@@ -71,6 +71,7 @@
71
71
  import { type Fn, type Op, type Value, defOpMap } from '../ir/core';
72
72
  import { type IrType, type StructField, T, scalarTypeForAccess } from '../ir/types';
73
73
  import { type Gate, firstRejection } from '../l3/gates';
74
+ import { storeSigned } from './storesign';
74
75
  import { firstFreeStructIndex } from './structs';
75
76
 
76
77
  /** One variable-index access, resolved onto (base value, member offset). */
@@ -260,7 +261,9 @@ function stripExt(v: Value, defs: Map<Value, Op>): Value {
260
261
  }
261
262
  }
262
263
 
263
- /** `add(x, const)` read as (other operand, constant), or null. The add is commutative. */
264
+ /** `add(x, const)` read as (other operand, constant), or null. The add is commutative. ONE link, not
265
+ * a chain, and no `sub`: the shapes this pass reads are a single `add` of a scaled index or of a
266
+ * member offset. A chain of constant displacements is ir/alias.ts `constDisplacementOf`. */
264
267
  function constAddend(op: Op, defs: Map<Value, Op>): { of: Value; k: number } | null {
265
268
  if (op.opcode !== 'add' || op.operands.length !== 2) {
266
269
  return null;
@@ -389,7 +392,7 @@ function accessesByBase(fn: Fn, defs: Map<Value, Op>): Map<Value, MemberAccess[]
389
392
  op,
390
393
  off: add === null ? 0 : add.k,
391
394
  elemSize,
392
- signed: isLoad ? (op.attrs.signed as boolean) : elemSize === 4,
395
+ signed: isLoad ? (op.attrs.signed as boolean) : storeSigned(op, elemSize),
393
396
  isLoad,
394
397
  index: op.operands[1],
395
398
  addr: add === null ? null : op.operands[0],
@@ -583,8 +586,11 @@ export function recognizeMemberArrays(
583
586
  a.op.operands[0] = g.base;
584
587
  a.op.attrs.memberOff = a.off;
585
588
  if (!a.isLoad) {
586
- // an astore carries no signedness of its own — the member's decides how the index node
587
- // types, so the store spells through the same declaration the load does
589
+ // the member's declaration decides how the index node types, so the store spells through
590
+ // the same declaration the load does, over any stamp raise/storesign.ts left. No probed
591
+ // input reaches here with a stamp that disagrees: agbcc lifts a signed narrow constant as
592
+ // `neg`/`shl`, and the MIPS shapes (`p->t[i] = -1`, `-104` beside a u16 read) take a flat
593
+ // cast. One that does should keep its stamp, and the index node cast the cell to it.
588
594
  a.op.attrs.signed = g.members.find((m) => m.off === a.off)!.signed;
589
595
  }
590
596
  }
@@ -23,6 +23,16 @@
23
23
  // A signed and an unsigned right shift of the same value by the same amount differ only in the bits
24
24
  // the shift brings in, so `%134` is `sext(%133, 32 - 16)` — the same rewrite, and the reason this
25
25
  // pass matches the pair rather than either extension on its own.
26
+ //
27
+ // And a third, where the variable is carried SHIFTED and never extended at all: an `s8` walked down
28
+ // a list, `i = buf[i].next`, is kept as `next << 24` and the test reads it back with `asr #24`.
29
+ // The cast idiom folds that `asr` of the `lsl` into a `sext` of the load, which is the same bits
30
+ // and no longer reads the carried value:
31
+ //
32
+ // %41 = shl %40 {imm=24} what `i` holds next iteration, shifted
33
+ // %42 = sext %40 {8} the loop test
34
+ //
35
+ // `sext(v, 32 - k) === shr_s(shl(v, k), k)`, so `%42` is re-rooted as `shr_s %41 {k}`.
26
36
  import { Fn, Op, Value, defOpMap } from '../ir/core';
27
37
  import { CAST_WIDTHS, Opcode } from '../ir/opcodes';
28
38
 
@@ -41,8 +51,8 @@ const domainOf = (op: Op, low: Opcode, high: Opcode): { key: string; width: numb
41
51
  return null;
42
52
  };
43
53
 
44
- /** Re-root a sign extension on the co-existing zero extension of the same bits. Returns the number
45
- * of rewrites. */
54
+ /** Re-root a sign extension on the co-existing zero extension of the same bits, or on the carried
55
+ * left shift that holds them. Returns the number of rewrites. */
46
56
  export function rerootNarrowReads(fn: Fn): number {
47
57
  // LOOP-CARRIED edge arguments only. The rewrite is sound for ANY co-existing pair — it only
48
58
  // re-associates two extensions of the same bits — so this gate is about what it BUYS, not about
@@ -87,6 +97,8 @@ export function rerootNarrowReads(fn: Fn): number {
87
97
  // else this could be a use before a def. That is the shape the idiom emits — one increment,
88
98
  // both extensions of it, in the block that computes it.
89
99
  const unsigned = new Map<Value, Map<string, Value>>();
100
+ // …and the carried left shifts, by operand and then by amount, under the same two rules
101
+ const shifted = new Map<Value, Map<number, Value>>();
90
102
  for (const op of b.ops) {
91
103
  const u = domainOf(op, 'zext', 'shr_u');
92
104
  if (u !== null && carried.has(op.results[0])) {
@@ -95,6 +107,21 @@ export function rerootNarrowReads(fn: Fn): number {
95
107
  unsigned.set(op.operands[0], byDomain);
96
108
  continue;
97
109
  }
110
+ if (op.opcode === 'shl' && typeof op.attrs.imm === 'number' && carried.has(op.results[0])) {
111
+ const byAmount = shifted.get(op.operands[0]) ?? new Map<number, Value>();
112
+ byAmount.set(op.attrs.imm, op.results[0]);
113
+ shifted.set(op.operands[0], byAmount);
114
+ continue;
115
+ }
116
+ const k = 32 - (op.attrs.width as number);
117
+ const sh = op.opcode === 'sext' && CAST_WIDTHS.has(32 - k) ? shifted.get(op.operands[0])?.get(k) : undefined;
118
+ if (sh !== undefined) {
119
+ op.opcode = 'shr_s';
120
+ op.operands = [sh];
121
+ op.attrs = { imm: k };
122
+ rewritten++;
123
+ continue;
124
+ }
98
125
  const sgn = domainOf(op, 'sext', 'shr_s');
99
126
  if (sgn === null) {
100
127
  continue;
@@ -58,6 +58,12 @@
58
58
  // u16 v; … *out = v; zext16 4 0 MATCH
59
59
  // s32 v; … *out = (u16)v; zext16 0 MATCH 4
60
60
  //
61
+ // A THIRD SPELLING of rows three and four is the `/escape-home` variation's
62
+ // (structure/analysis.ts `escapingExtensions`): keep `s32 v` and give the truncation a NAME where
63
+ // it happened, `t = (u16)v`. It is not an alternative to this pass — it judges an extension's
64
+ // result where this one judges a block PARAMETER — and it is a ranked candidate where this is a
65
+ // decision, so neither can take the other's input.
66
+ //
61
67
  // Rows one and two are DECIDABLE by the carrier's own readers: a `zext_w` read by a `sext_w` is the
62
68
  // write-back truncation followed by the declaration's own sign extension, and no cast on a wide
63
69
  // local writes that pair. Rows three and four are the same IR in this pass's READER vocabulary —
@@ -131,7 +137,8 @@
131
137
  // admits), and `runPreRecovery` reads the shape ahead of the pass that manufactures it —
132
138
  // `mergeShapes`.
133
139
  import { type Block, type Fn, type Op, type Value, defOpMap, predecessors, replaceAllUsesWith } from '../ir/core';
134
- import { CAST_WIDTHS, REEVAL_UNSAFE_OPS } from '../ir/opcodes';
140
+ import { reevalUnsafe } from '../ir/discipline';
141
+ import { CAST_WIDTHS } from '../ir/opcodes';
135
142
  import { T } from '../ir/types';
136
143
  import { type Gate, firstRejection } from '../l3/gates';
137
144
 
@@ -350,8 +357,8 @@ function mergeArms(preds: Map<Block, Block[]>, fn: Fn, blk: Block): [Block, Bloc
350
357
  * • a MEMORY READ is one op and is NOT one speculatable SET. `gcc/rtlanal.c:1770-1771` sends a
351
358
  * MEM to `rtx_addr_can_trap_p` and `:144-147` says an address held in a plain pseudo CAN trap,
352
359
  * so `jump.c` never hoists a load and the diamond survives under BOTH spellings, carrying no
353
- * information. `REEVAL_UNSAFE_OPS` answers this (effects, reads, or traps — `may_trap_p`
354
- * refuses the trapping divides too, at `rtlanal.c:1774-1784`); `HOIST_UNSAFE_OPS` does NOT,
360
+ * information. `reevalUnsafe` answers this (effects, reads, or traps — `may_trap_p`
361
+ * refuses the trapping divides too, at `rtlanal.c:1774-1784`); `speculationUnsafe` does NOT,
355
362
  * because it omits reads for `raise/shortcircuit.ts`, whose arm C's own `&&` re-guards, and
356
363
  * that exemption does not transfer to speculation above a compare.
357
364
  * • a CONSTANT is not free. `v = a + 0x12345` is a literal-pool `ldr` plus the `add` — two
@@ -382,9 +389,7 @@ function mergeArms(preds: Map<Block, Block[]>, fn: Fn, blk: Block): [Block, Bloc
382
389
  * clause in that table is `sound: false`), so the shared conservative predicate is the right one
383
390
  * until a row asks for the cost model neither pass has. */
384
391
  export function armIsOneSet(b: Block): boolean {
385
- return (
386
- !b.ops.some((op) => REEVAL_UNSAFE_OPS.has(op.opcode)) && b.ops.filter((op) => op.results.length > 0).length === 1
387
- );
392
+ return !b.ops.some(reevalUnsafe) && b.ops.filter((op) => op.results.length > 0).length === 1;
388
393
  }
389
394
 
390
395
  /** The join shape of every block, read ONCE off the IR it is handed.