@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,159 @@
1
+ // asmlift — does a test placed ahead of a loop compute the loop's own test on the loop's entry values?
2
+ //
3
+ // Three questions ask exactly that, at two levels, and all read the answer from this one predicate:
4
+ // • structure's GUARD FUSION (`structure.ts`, through `hazards.ts`): the `if` a rotated loop is
5
+ // wrapped in is deleted when it is the loop's test at the first iteration, and the loop re-tests;
6
+ // • raise's RE-ROLL (`raise/reroll.ts`): a loop test the compiler peeled into the loop's entry is
7
+ // deleted when it is the latch's test at the values the entry hands the loop;
8
+ // • the re-roll's TOP-TEST RESTORE (`entryGuard`, same file): the guard a restored top test
9
+ // makes redundant is deleted when it is that test at the entry values, with the same exit args.
10
+ //
11
+ // The reading is STRUCTURAL, never semantic: two values are the same when they are one value, or
12
+ // when their defining ops have one opcode, one set of attributes and operands that are the same in
13
+ // turn. A false negative costs a loud decline, the direction to be wrong in. `entry` models the
14
+ // first iteration: on the loop side (`a`), a value it maps is read as the value it maps to, and
15
+ // below that substitution the walk is in the entry's world, where the map no longer applies — a
16
+ // loop-invariant back-edge argument read inside the entry's own expression is itself there, not the
17
+ // param it feeds.
18
+ //
19
+ // `normalize` adds the equalities the re-roll needs and guard fusion does not take: an `add` or
20
+ // `or` of a constant 0 on EITHER side, commutative operands in either order, and a load off
21
+ // `base + c` as the same load off `base` at its offset plus `c` (`str[0]` against `str[i]` at
22
+ // `i = 0`). Each is an identity of the integer operators, so turning it on for guard fusion is a
23
+ // spelling decision, not a soundness one.
24
+ import { Op, Value } from './core';
25
+ import { spelledWhenDead } from './discipline';
26
+ import { NEGATED_ICMP } from './opcodes';
27
+
28
+ export interface EntryReading {
29
+ /** the value each loop-side value holds on the loop's first iteration */
30
+ readonly entry: ReadonlyMap<Value, Value>;
31
+ /** values whose defining op may be read through; everything else compares by identity. Default:
32
+ * every value. An operand-free op that reads no memory (a constant, a symbol address) is always
33
+ * read through: it is its attributes wherever it sits. */
34
+ readonly through?: (v: Value) => boolean;
35
+ /** the identities above, beyond `x + 0` / `x - 0` / `x | 0` with the zero on the right */
36
+ readonly normalize?: boolean;
37
+ }
38
+
39
+ /** Opcodes whose two operands may be read in either order, under `normalize`. */
40
+ const COMMUTES = new Set(['add', 'mul', 'and', 'or', 'xor', 'icmp_eq', 'icmp_ne']);
41
+
42
+ /** Is `a` (loop side), read on the loop's entry values, the same value as `b`? With `negated`, is it
43
+ * `b`'s logical opposite — the usual case where a guard spells the loop's test the other way round. */
44
+ export function sameAtEntry(
45
+ defs: ReadonlyMap<Value, Op>,
46
+ a: Value,
47
+ b: Value,
48
+ reading: EntryReading,
49
+ negated = false,
50
+ ): boolean {
51
+ const { entry, through = () => true, normalize = false } = reading;
52
+ const readable = (v: Value): boolean => {
53
+ const d = defs.get(v);
54
+ return d !== undefined && (through(v) || (d.operands.length === 0 && !spelledWhenDead(d)));
55
+ };
56
+ const constOf = (v: Value): number | null => {
57
+ const d = defs.get(v);
58
+ return d?.opcode === 'const' && typeof d.attrs?.value === 'number' ? d.attrs.value : null;
59
+ };
60
+ /** A constant `v` reads as: under `normalize`, as its entry substitution reads. */
61
+ const constAt = (v: Value, atEntry: boolean): number | null => constOf(normalize ? view(v, atEntry).x : v);
62
+ /** `x0` as the comparison reads it: through the entry map while on the loop side (`!atEntry`), and
63
+ * with `x + 0` / `x - 0` / `x | 0` read as `x` — substituting a loop variable by its init constant
64
+ * turns ordinary index arithmetic into exactly these. */
65
+ function view(x0: Value, atEntry0: boolean): { x: Value; atEntry: boolean } {
66
+ let x = x0;
67
+ let atEntry = atEntry0;
68
+ for (;;) {
69
+ const mapped = atEntry ? undefined : entry.get(x);
70
+ if (mapped !== undefined) {
71
+ x = mapped;
72
+ atEntry = true;
73
+ continue;
74
+ }
75
+ const d = readable(x) ? defs.get(x) : undefined;
76
+ if (!d || d.operands.length !== 2 || !['add', 'sub', 'or'].includes(d.opcode)) {
77
+ return { x, atEntry };
78
+ }
79
+ if (constAt(d.operands[1], atEntry) === 0) {
80
+ x = d.operands[0];
81
+ } else if (normalize && d.opcode !== 'sub' && constAt(d.operands[0], atEntry) === 0) {
82
+ x = d.operands[1];
83
+ } else {
84
+ return { x, atEntry };
85
+ }
86
+ }
87
+ }
88
+ /** A load's address as a base and a constant offset, the `+ c` of an `add` folded in. */
89
+ const address = (d: Op, atEntry0: boolean): { base: Value; off: number; atEntry: boolean } => {
90
+ const off = Number(d.attrs.off ?? 0);
91
+ const { x: base, atEntry } = view(d.operands[0], atEntry0);
92
+ const ad = readable(base) ? defs.get(base) : undefined;
93
+ if (ad?.opcode === 'add' && ad.operands.length === 2) {
94
+ for (const k of [0, 1]) {
95
+ const c = constAt(ad.operands[k], atEntry);
96
+ if (c !== null) {
97
+ return { base: ad.operands[1 - k], off: off + c, atEntry };
98
+ }
99
+ }
100
+ }
101
+ return { base, off, atEntry };
102
+ };
103
+ const attrsOf = (d: Op, drop: string | null) =>
104
+ JSON.stringify(Object.fromEntries(Object.entries(d.attrs ?? {}).filter(([k]) => k !== drop)));
105
+ // memoised per (x, y, side): a value its own consumer reads twice would otherwise double the work
106
+ // at every level
107
+ const memo = new Map<string, boolean>();
108
+ const ids = new Map<Value, number>();
109
+ const id = (v: Value) => ids.get(v) ?? (ids.set(v, ids.size), ids.size - 1);
110
+ /** `da` (read with `atEntry`) and `db` (the entry's own) compute the same value. */
111
+ const sameOp = (da: Op, db: Op, opcodeOk: boolean, atEntry: boolean): boolean => {
112
+ if (!opcodeOk || da.operands.length !== db.operands.length) {
113
+ return false;
114
+ }
115
+ if (normalize && da.opcode === 'load' && db.opcode === 'load') {
116
+ const x = address(da, atEntry);
117
+ const y = address(db, true);
118
+ return x.off === y.off && attrsOf(da, 'off') === attrsOf(db, 'off') && same(x.base, x.atEntry, y.base);
119
+ }
120
+ if (attrsOf(da, null) !== attrsOf(db, null)) {
121
+ return false;
122
+ }
123
+ if (da.operands.every((o, i) => same(o, atEntry, db.operands[i]))) {
124
+ return true;
125
+ }
126
+ return (
127
+ normalize &&
128
+ COMMUTES.has(da.opcode) &&
129
+ da.operands.length === 2 &&
130
+ same(da.operands[0], atEntry, db.operands[1]) &&
131
+ same(da.operands[1], atEntry, db.operands[0])
132
+ );
133
+ };
134
+ function same(x0: Value, atEntry0: boolean, y0: Value): boolean {
135
+ const { x, atEntry } = view(x0, atEntry0);
136
+ const { x: y } = view(y0, true);
137
+ if (x === y) {
138
+ return true;
139
+ }
140
+ const key = `${id(x)},${id(y)},${atEntry ? 1 : 0}`;
141
+ const hit = memo.get(key);
142
+ if (hit !== undefined) {
143
+ return hit;
144
+ }
145
+ const da = readable(x) ? defs.get(x) : undefined;
146
+ const db = readable(y) ? defs.get(y) : undefined;
147
+ const r = !!da && !!db && sameOp(da, db, da.opcode === db.opcode, atEntry);
148
+ memo.set(key, r);
149
+ return r;
150
+ }
151
+ if (!negated) {
152
+ return same(a, false, b);
153
+ }
154
+ const { x, atEntry } = view(a, false);
155
+ const { x: y } = view(b, true);
156
+ const da = readable(x) ? defs.get(x) : undefined;
157
+ const db = readable(y) ? defs.get(y) : undefined;
158
+ return !!da && !!db && sameOp(da, db, NEGATED_ICMP[da.opcode] === db.opcode, atEntry);
159
+ }
@@ -0,0 +1,47 @@
1
+ // asmlift IR — a double literal: the bit pattern the IR carries it as, and the C that spells it.
2
+ //
3
+ // THE IR CARRIES THE BITS, NOT A NUMBER, because a number has one value that is two doubles:
4
+ // `-0` prints and serializes as `0`, so a literal keyed or dumped by its number would be the
5
+ // other zero. Sixteen hex digits name exactly one double, NaN payloads included.
6
+
7
+ const view = new DataView(new ArrayBuffer(8));
8
+
9
+ /** The bit pattern, as sixteen hex digits, of the double whose high word (sign and exponent) is
10
+ * `hi` and low word is `lo`. */
11
+ export function doubleBits(hi: number, lo: number): string {
12
+ return (hi >>> 0).toString(16).padStart(8, '0') + (lo >>> 0).toString(16).padStart(8, '0');
13
+ }
14
+
15
+ /** Whether `bits` is a double's bit pattern: sixteen hex digits, as `doubleBits` spells one. A
16
+ * single's eight would name another number read as a double, so nothing here takes one. */
17
+ export const isDoubleBits = (bits: unknown): bits is string => typeof bits === 'string' && /^[0-9a-f]{16}$/.test(bits);
18
+
19
+ /** The double a bit pattern names. */
20
+ export function doubleOf(bits: string): number {
21
+ if (!isDoubleBits(bits)) {
22
+ throw new Error(`'${bits}' is not a double's bit pattern`);
23
+ }
24
+ view.setUint32(0, parseInt(bits.slice(0, 8), 16));
25
+ view.setUint32(4, parseInt(bits.slice(8, 16), 16));
26
+ return view.getFloat64(0);
27
+ }
28
+
29
+ /** The C spelling of a finite double: the shortest decimal that reads back as the same double
30
+ * (ECMAScript's Number::toString, whose digits are the fewest that do), with a `.0` where it has
31
+ * neither a point nor an exponent, so C reads a `double` and not an `int`, and `-0.0` for the
32
+ * negative zero.
33
+ *
34
+ * THAT IT READS BACK IS A PREMISE ABOUT THE COMPILER, and the one producer of a literal
35
+ * (`raise/floathelpers.ts`) runs only where the target states it
36
+ * (`compilerBehaviors.roundTripsDoubleLiterals`, which cites agbcc's source for it). */
37
+ export function doubleLiteral(bits: string): string {
38
+ const x = doubleOf(bits);
39
+ if (!Number.isFinite(x)) {
40
+ throw new Error(`the double ${bits} is not finite and has no C literal`);
41
+ }
42
+ if (Object.is(x, -0)) {
43
+ return '-0.0';
44
+ }
45
+ const s = String(x);
46
+ return /[.e]/.test(s) ? s : `${s}.0`;
47
+ }
@@ -1,4 +1,8 @@
1
- // asmlift — natural-loop discovery: the pure CFG analysis the structurer consumes.
1
+ // asmlift — natural-loop discovery: the pure CFG analysis the structurer consumes, and the raise
2
+ // passes that rewrite a loop's edges before it (`raise/latch.ts`, `raise/reroll.ts`).
3
+ //
4
+ // A substrate fact like dominance, so it lives here and not under `structure/`: that keeps the
5
+ // dependency between the levels one way — structure imports raise, never the reverse.
2
6
  //
3
7
  // Produces a DEFINITE header set computed structurally from back-edges — a block is a loop header
4
8
  // iff it is a real back-edge target (`h ∈ dom(t)` for an edge `t→h`), never inferred from `cond_br`
@@ -8,7 +12,7 @@
8
12
  //
9
13
  // Dominance itself is a substrate fact and lives in `ir/core.ts`; `analyzeLoops` takes the map as
10
14
  // a parameter, so a caller passes `dominators(fn)` from there.
11
- import { Block, Fn, predecessors, successorsOf } from '../ir/core';
15
+ import { Block, Fn, predecessors, successorsOf } from './core';
12
16
 
13
17
  /** One natural loop, keyed by its header. */
14
18
  export interface NaturalLoop {
package/src/ir/opcodes.ts CHANGED
@@ -13,8 +13,8 @@ export interface OpSig {
13
13
  successors?: number | 'variadic';
14
14
  requiredAttrs?: readonly string[];
15
15
  /** observable side effect (memory write / call): never deleted when dead, never hoisted into
16
- * an unconditional position. THE one effect vocabulary — DCE (pattern/engine.ts) and the
17
- * short-circuit hoist guard (raise/shortcircuit.ts) both derive from this flag. */
16
+ * an unconditional position. THE one effect vocabulary — every question ir/discipline.ts asks of an
17
+ * op derives from this flag and the two below. */
18
18
  effects?: boolean;
19
19
  /** reads memory. Deletable when dead — nothing observes a load nobody reads — but NOT movable:
20
20
  * a load answers whichever stores ran before it, so crossing one changes the value it yields.
@@ -38,7 +38,14 @@ export const OPCODES = {
38
38
  // Effect-free (no `effects` flag) so DCE reaps a dead one.
39
39
  mulh: { operands: 2, results: 1 },
40
40
  mulhu: { operands: 2, results: 1 },
41
- neg: { operands: 1, results: 1 },
41
+ neg: { operands: 1, results: 1 }, // the wrapping negation, 0 - x as a 32-bit word
42
+ // The MATHEMATICAL negation: what Thumb's `cmn rA, rB` compares rA against when a signed branch
43
+ // reads it (frontend/thumb.ts `lowerTerminator`). It differs from `neg` at exactly one operand,
44
+ // 0x80000000, where its value (+2^31) is no 32-bit word at all. A separate opcode rather than an
45
+ // attr on `neg` because every word evaluator in core folds `neg` as the wrapping one, and an
46
+ // attr each of them must remember to read fails OPEN: one that does not know `neg_s` falls to
47
+ // its default arm and declines. Only the structurer spells it, as `-(s32)x`.
48
+ neg_s: { operands: 1, results: 1 },
42
49
  not: { operands: 1, results: 1 }, // bitwise complement (`mvn`) → ~x
43
50
  or: { operands: 2, results: 1 },
44
51
  and: { operands: 2, results: 1 },
@@ -109,6 +116,74 @@ export const OPCODES = {
109
116
  // which recompiles to the branch diamond the source emitted.
110
117
  logic_and: { operands: 2, results: 1 },
111
118
  logic_or: { operands: 2, results: 1 },
119
+ // --- a 64-bit value and its two halves ---
120
+ // A 64-bit integer is ONE value of `{kind:'int'|'unknown', width:64}`, never two results and never
121
+ // a new `IrType` kind. The rule that decides it: for TYPES, widen the number that is already
122
+ // there; for OPERATIONS, add a new opcode. Existing code tests `t.kind === 'int'` and reads
123
+ // `t.width`, so a wider width makes a pass that is wrong about it fail LOUDLY, while a new kind
124
+ // would make the same pass skip SILENTLY. Existing code switches on `op.opcode`, so a new opcode
125
+ // makes an existing pass skip, which is the safe direction there.
126
+ //
127
+ // So the arithmetic opcodes above gain NOTHING: `add`, `sub`, `mul`, the shifts and the `icmp_*`
128
+ // family carry the width in their operand types. Only the three ops that have no 32-bit spelling
129
+ // are new — building a 64-bit value out of two halves, and reading either half back.
130
+ //
131
+ // `concat lo, hi` is TRANSIENT the way `mulh` is: it renders only in the shapes something above
132
+ // it recognises (a widen, a literal, an ABI pair), and one that survives to the structurer is a
133
+ // loud gap. `lo32`/`hi32` invert that property and are PERMANENT, because C spells them exactly —
134
+ // `(u32)v` and `(u32)(v >> 32)`.
135
+ //
136
+ // WHY `hi32` AND NOT `lo32(shr_u v {imm:32})`, which is the same value in two ops rather than
137
+ // three: spelling a projection as a 64-bit SHIFT puts a 64-bit arithmetic op into the IR at every
138
+ // pair site, and hands it to every pass that matches a shift — `raise/divpow2.ts` on
139
+ // `shr_s {imm}`, `raise/extscale.ts` on `shl`, `raise/narrow.ts` on a narrow/re-widen pair. Those
140
+ // passes would then be REACHED by a 64-bit operand instead of skipping it. A third opcode nothing
141
+ // else matches is what buys the isolation.
142
+ concat: { operands: 2, results: 1 }, // concat lo, hi → the 64-bit value whose low half is `lo`
143
+ lo32: { operands: 1, results: 1 },
144
+ hi32: { operands: 1, results: 1 },
145
+ // --- hardware floating point ---
146
+ // IEEE arithmetic on `{kind:'float'}` values, and on nothing else (ir/verify.ts checks both
147
+ // directions). NEW OPCODES rather than `add`/`sub` over a float-typed operand, by the rule the
148
+ // 64-bit section above states for OPERATIONS: every pass that matches `add` is an integer rewrite
149
+ // — a pointer walk, a reassociation, a constant fold, an idiom pattern — and a new opcode is what
150
+ // makes each of them skip. Pure: under the default IEEE environment none of them traps, so a
151
+ // dead one is reaped like any integer op.
152
+ fadd: { operands: 2, results: 1 },
153
+ fsub: { operands: 2, results: 1 },
154
+ fmul: { operands: 2, results: 1 },
155
+ fdiv: { operands: 2, results: 1 },
156
+ fneg: { operands: 1, results: 1 },
157
+ // A double LITERAL: `bits` is its IEEE pattern as sixteen hex digits (ir/float-bits.ts). Its own
158
+ // opcode rather than a `const` over a float type for the reason the arithmetic above has its own:
159
+ // every pass that matches `const` is an integer fold.
160
+ fconst: { operands: 0, results: 1, requiredAttrs: ['bits'] },
161
+ // The CROSSINGS between a float and an integer, C's conversions: `fptosi`/`fptoui` truncate toward
162
+ // zero into a signed or unsigned integer of the result's width (32 or 64), `sitofp` converts a
163
+ // signed one, `fpext`/`fptrunc` move between the precisions. The only ops a float and an integer
164
+ // meet in (ir/verify.ts). No `uitofp`: no compiler here converts an unsigned integer in one step
165
+ // (agbcc open-codes it around `__floatsisf`), so it would be an opcode with no producer.
166
+ fptosi: { operands: 1, results: 1 },
167
+ fptoui: { operands: 1, results: 1 },
168
+ sitofp: { operands: 1, results: 1 },
169
+ fpext: { operands: 1, results: 1 },
170
+ fptrunc: { operands: 1, results: 1 },
171
+ // The float COMPARES, a 0/1 integer out of two floats of one type. Ten and not six, because
172
+ // NaN makes C's relations a partial order: `!(a > b)` is true on a NaN and `a <= b` is not, and
173
+ // every compiler here compiles the two differently. So each ordered relation (`o`, false on a
174
+ // NaN) has its logical negation as an unordered opcode (`u`, true on a NaN): `fcmp_ule` IS
175
+ // `!(a > b)`. C's `!=` is `fcmp_une`. They stay out of `NEGATED_ICMP`, whose integer negation
176
+ // (`<` ↔ `>=`) would be the wrong program here; `NEGATED_FCMP` below is theirs.
177
+ fcmp_oeq: { operands: 2, results: 1 },
178
+ fcmp_une: { operands: 2, results: 1 },
179
+ fcmp_olt: { operands: 2, results: 1 },
180
+ fcmp_uge: { operands: 2, results: 1 },
181
+ fcmp_ole: { operands: 2, results: 1 },
182
+ fcmp_ugt: { operands: 2, results: 1 },
183
+ fcmp_ogt: { operands: 2, results: 1 },
184
+ fcmp_ule: { operands: 2, results: 1 },
185
+ fcmp_oge: { operands: 2, results: 1 },
186
+ fcmp_ult: { operands: 2, results: 1 },
112
187
  // --- memory ---
113
188
  load: { operands: 1, results: 1, requiredAttrs: ['off', 'width', 'signed'], reads: true },
114
189
  store: { operands: 2, results: 0, requiredAttrs: ['off', 'width'], effects: true },
@@ -119,8 +194,10 @@ export const OPCODES = {
119
194
  aload: { operands: 2, results: 1, requiredAttrs: ['elemSize', 'signed'], reads: true }, // aload base, index
120
195
  astore: { operands: 3, results: 0, requiredAttrs: ['elemSize'], effects: true }, // astore base, index, value
121
196
  // --- call: operands are the argument values (r0..), result is the return value (r0),
122
- // `target` attr is the callee symbol. Caller-saved clobbering is implicit. ---
123
- call: { operands: 'variadic', results: 1, requiredAttrs: ['target'], effects: true },
197
+ // `target` attr is the callee symbol — or, with `indirect: true` and no `target`, the
198
+ // callee is the address in the LAST operand (ir/core.ts `calleeValue`). Exactly one of the
199
+ // two, which `ir/verify.ts` checks. Caller-saved clobbering is implicit. ---
200
+ call: { operands: 'variadic', results: 1, effects: true },
124
201
  // The ADDRESS of a named global (agbcc `ldr rD, .Lpool` where the pool word is `.word gSym`).
125
202
  // Pure, 0 operands. Globals come from the project headers, so they are referenced by name, never
126
203
  // declared as locals. The structurer lowers it three ways (see scalarGlobals in structure.ts):
@@ -134,10 +211,17 @@ export const OPCODES = {
134
211
  // proving every access agrees, and the structurer declares the local and renders `&name` exactly
135
212
  // as it renders a gaddr's `&sym`. Operand-free and pure, so GVN numbers it like gaddr and a dead
136
213
  // one is reaped.
137
- // `width`/`signed` are stamped by the frontend's frame-object AUDIT — requiring them makes
138
- // "the audit ran" a verifier-checkable fact instead of a convention: a frontend that emits a
139
- // laddr and skips the audit fails verify loudly instead of rendering `&undefined`.
140
- laddr: { operands: 0, results: 1, requiredAttrs: ['off', 'width', 'signed'] },
214
+ // `width`/`signed`/`count` are stamped by the frontend's frame-object AUDIT — requiring them
215
+ // makes "the audit ran" a verifier-checkable fact instead of a convention: a frontend that emits
216
+ // a laddr and skips the audit fails verify loudly instead of rendering `&undefined`. The SHAPE
217
+ // the three of them encode is checked there too (ir/verify.ts), because the attrs are
218
+ // disjunctive and only `count` tells the two arms apart.
219
+ // The object is `count` elements of `width` bytes, so its storage spans `width * count`. A
220
+ // SCALAR is count 1, typed by the access width and signedness every access agreed on. Count
221
+ // above 1 is STORAGE the audit sized without typing — no access pins an element type, so it is
222
+ // width 1, unsigned, and declares as `u8 name[count]`; nothing else may mint one, because the
223
+ // only thing that licenses an untyped object is the frame accounting the audit solves.
224
+ laddr: { operands: 0, results: 1, requiredAttrs: ['off', 'width', 'signed', 'count'] },
141
225
  // An UNDEFINED value: a read of storage that carries no INPUT — nothing was entitled to hand this
142
226
  // function a value there, and none of its own stores reached it on this path. Deliberately NOT
143
227
  // "storage nobody could have written": a callee-saved register holds the CALLER's value at entry,
@@ -153,7 +237,7 @@ export const OPCODES = {
153
237
  // merely "owns the storage", because a frame the function owns can still be written by
154
238
  // someone else once an address into it escapes to a callee, which fills a wider object than
155
239
  // any in-function access reveals. Whoever mints one owes the retraction on escape
156
- // (frontend/thumb.ts, after the frame-object audit).
240
+ // (the frame-object audit, frontend/frame-objects.ts).
157
241
  // • a REGISTER the ABI does not pass arguments in AND this function's prologue SAVED cannot
158
242
  // carry a value a caller handed over, and has no address for anything else to reach it by, so
159
243
  // there is nothing to retract. The save is half of the premise, not a corroboration of it:
@@ -163,13 +247,16 @@ export const OPCODES = {
163
247
  // live-in, and a live-in of the entry block is a PARAMETER — right for an argument register, a
164
248
  // fabricated argument for anything else.
165
249
  //
166
- // Operand-free and pure like `laddr`, and out of raise/gvn.ts's NUMBERABLE set — where numbering
167
- // it would be VACUOUS rather than harmful, since two undefs in one function always carry
168
- // different keys. "Same key, therefore same value" is empty for a value that has none.
250
+ // Operand-free and pure like `laddr`, and out of raise/gvn.ts's NUMBERABLE set, which must keep
251
+ // it out: two undefs of one key are a storage's content and a `copied` move of it, and numbering
252
+ // them one value would erase the move.
169
253
  //
170
254
  // `key` names the storage (`sp@0`, `r4`); the structurer reads it to name the local
171
255
  // (`uninit_sp0`) and emits NO assignment — that absence is the recovery, and an edge argument
172
- // that is one emits no copy either (structure.ts undefCarriesNothing).
256
+ // that is one emits no copy either (structure.ts undefCarriesNothing). OPTIONAL `copied: true`:
257
+ // the value another storage's content arrives at a join keyed elsewhere as (`if (c) x = y;` of
258
+ // two uninitialised locals, frontend/ssa.ts `markCopiedUndefs`) — the machine moved it, so that
259
+ // edge copy IS emitted, under the name of the undef its key names.
173
260
  undef: { operands: 0, results: 1, requiredAttrs: ['key'] },
174
261
  // --- black-box escape hatch (keeps lifting total) ---
175
262
  // `effects: true`: an instruction asmlift could not model may do anything — write memory, trap,
@@ -192,6 +279,11 @@ export const OPCODES = {
192
279
  // Many-way switch dispatch (Regime B, jump table). The single operand is the scrutinee;
193
280
  // successors are the N case blocks followed by the default block (the LAST successor);
194
281
  // `cases` is the index-aligned list of the first N successors' case values.
282
+ // `exhaustive: true` — OPTIONAL, set by a frontend whose dispatch block is entered only past a
283
+ // bounds test that already sent every value outside `cases` elsewhere, so the default edge is
284
+ // never taken. It still names a block, because the op needs a last successor; the structurer
285
+ // reads the attr to emit no `default:` arm for it. A pass that rebuilds the op leaves it off and
286
+ // gets the default arm back, which is the conservative answer.
195
287
  switch_br: { operands: 1, results: 0, terminator: true, successors: 'variadic', requiredAttrs: ['cases'] },
196
288
  } as const satisfies Record<string, OpSig>;
197
289
 
@@ -199,13 +291,87 @@ export const OPCODES = {
199
291
  * does not. */
200
292
  export type Opcode = keyof typeof OPCODES;
201
293
 
294
+ /** The width of a 64-bit IR value, named once so the three places that build one and the verifier
295
+ * that checks them cannot come to disagree about it. */
296
+ export const WIDE_BITS = 64;
297
+
202
298
  /** The widths a `zext`/`sext` carries — a fact about those two opcodes, so it lives with them
203
299
  * rather than re-declared per consumer (raise/narrow.ts pairs a narrowing op with its re-widening,
204
300
  * raise/paramwidth.ts declares a parameter at one). Every frontend that produces the pair produces
205
301
  * one of these: agbcc's gated shift-pair fold (pattern/engine.ts CAST_PATTERNS) and PPC's
206
- * `extsb`/`extsh` (frontend/ppc.ts). A third width would be a C type the backend cannot spell. */
302
+ * `extsb`/`extsh` (frontend/ppc.ts). A third width would be a C type the backend cannot spell.
303
+ *
304
+ * 32 IS NOT ONE OF THEM, and the widening to 64 does not come here. The machine spells that widen
305
+ * as a PAIR — agbcc `asr rN,rM,#31` or `mov rN,#0`, PPC `srawi`, MIPS `sra` — so it is a `concat`
306
+ * shape (`concat(x, shr_s(x,31))` signed, `concat(x, const 0)` unsigned), which is also the only
307
+ * form that says HOW the half was widened. This set keeps its one job: refuse an extension with no
308
+ * C spelling. */
207
309
  export const CAST_WIDTHS: ReadonlySet<number> = new Set([8, 16]);
208
310
 
311
+ /** The float opcodes: the only ops a `{kind:'float'}` value may be an operand or a result of,
312
+ * besides `ret` (ir/verify.ts). Authored beside the registry because no signature field says what
313
+ * an op computes ON. */
314
+ export const FLOAT_OPS: ReadonlySet<string> = new Set(['fadd', 'fsub', 'fmul', 'fdiv', 'fneg', 'fconst']);
315
+
316
+ /** The conversions out of a float into an integer, and into a float out of an integer. */
317
+ export const FLOAT_TO_INT: ReadonlySet<string> = new Set(['fptosi', 'fptoui']);
318
+ export const INT_TO_FLOAT: ReadonlySet<string> = new Set(['sitofp']);
319
+ /** The conversions between the two precisions, each named by the width it converts from and to. */
320
+ export const FLOAT_RESIZE: Readonly<Record<string, { from: 32 | 64; to: 32 | 64 }>> = {
321
+ fpext: { from: 32, to: 64 },
322
+ fptrunc: { from: 64, to: 32 },
323
+ };
324
+
325
+ /** The logical negation of each float compare, NaN included (the registry's note on `fcmp_*`):
326
+ * expanded both ways from the pairs, as `NEGATED_ICMP` is. */
327
+ const FCMP_NEGATION_PAIRS: readonly (readonly [Opcode, Opcode])[] = [
328
+ ['fcmp_oeq', 'fcmp_une'],
329
+ ['fcmp_olt', 'fcmp_uge'],
330
+ ['fcmp_ole', 'fcmp_ugt'],
331
+ ['fcmp_ogt', 'fcmp_ule'],
332
+ ['fcmp_oge', 'fcmp_ult'],
333
+ ];
334
+ export const NEGATED_FCMP: Readonly<Record<string, Opcode>> = Object.fromEntries(
335
+ FCMP_NEGATION_PAIRS.flatMap(([a, b]) => [
336
+ [a, b],
337
+ [b, a],
338
+ ]),
339
+ );
340
+
341
+ /** What each float compare answers when its first operand is LESS than, EQUAL to or GREATER than
342
+ * its second, and when the two are UNORDERED (either is a NaN): the definition of the ten, from
343
+ * which a compare a runtime computes some other way is read (raise/floathelpers.ts). */
344
+ export const FCMP_TRUTH: Readonly<Record<string, readonly [lt: boolean, eq: boolean, gt: boolean, un: boolean]>> = {
345
+ fcmp_olt: [true, false, false, false],
346
+ fcmp_ole: [true, true, false, false],
347
+ fcmp_ogt: [false, false, true, false],
348
+ fcmp_oge: [false, true, true, false],
349
+ fcmp_oeq: [false, true, false, false],
350
+ fcmp_une: [true, false, true, true],
351
+ fcmp_uge: [false, true, true, true],
352
+ fcmp_ugt: [false, false, true, true],
353
+ fcmp_ule: [true, true, false, true],
354
+ fcmp_ult: [true, false, false, true],
355
+ };
356
+
357
+ /** The float compare whose truth table (`FCMP_TRUTH`'s order: less, equal, greater, unordered) is
358
+ * `truth`, or undefined where none is — always true, say. The one reader of the table backwards,
359
+ * for every place that computes a compare's answers some other way (agbcc's helper tests,
360
+ * raise/floathelpers.ts; PowerPC's `cror` of two bits, frontend/ppc.ts). */
361
+ export function fcmpOfTruth(truth: readonly boolean[]): Opcode | undefined {
362
+ return Object.keys(FCMP_TRUTH).find((k) => FCMP_TRUTH[k].every((t, i) => t === truth[i])) as Opcode | undefined;
363
+ }
364
+
365
+ /** Every op that computes on a float: the arithmetic, the conversions and the compares. A runtime
366
+ * helper whose op is one of these is a float helper (runtime-helpers.ts `isFloatHelper`). */
367
+ export const FLOAT_COMPUTING: ReadonlySet<string> = new Set([
368
+ ...FLOAT_OPS,
369
+ ...FLOAT_TO_INT,
370
+ ...INT_TO_FLOAT,
371
+ ...Object.keys(FLOAT_RESIZE),
372
+ ...Object.keys(NEGATED_FCMP),
373
+ ]);
374
+
209
375
  /** Signature lookup by RUNTIME opcode string (Op.opcode is a plain string — IR consumers switch
210
376
  * on it); undefined for an unregistered opcode. */
211
377
  export function opSig(opcode: string): OpSig | undefined {
@@ -214,21 +380,20 @@ export function opSig(opcode: string): OpSig | undefined {
214
380
 
215
381
  /** The comparison whose result is the logical NEGATION of each `icmp_*` — `!(a < b)` is `a >= b`.
216
382
  *
217
- * Unlike EFFECTFUL_OPS/HOIST_UNSAFE_OPS below, this is AUTHORED data seated beside the registry,
218
- * not a view derived from it: nothing in `OPCODES` states which comparison opposes which. What is
383
+ * AUTHORED data seated beside the registry, not a view derived from it: nothing in `OPCODES`
384
+ * states which comparison opposes which. What is
219
385
  * derived is its SYMMETRY — the five involutive pairs are expanded both ways, so `neg(neg(c)) === c`
220
386
  * holds by construction (a hand-written map is one typo away from breaking it, and the symptom is a
221
387
  * plainly inverted condition in the emitted C). Completeness against the icmp family is the part
222
388
  * construction cannot give, so a test asserts it (test/pattern.test.ts) — an eleventh comparison
223
389
  * added to `OPCODES` would otherwise degrade its consumers several different ways.
224
390
  *
225
- * It lives here for the reason HOIST_UNSAFE_OPS does: every consumer that has to say "the opposite
226
- * of this compare" reads THIS one, so they
227
- * cannot drift apart the way inline copies did. Two adjacent facts worth knowing: raise/
228
- * shortcircuit.ts derives its `BOOL_OPS` from these keys (asserting negatable-icmp == boolean-op,
229
- * true today), and l3/ast.ts `NEGATE_REL` is the SAME relation over the neutral L3 operator
230
- * vocabulary — deliberately separate, because signedness lives in the operand types there, so the
231
- * two tables are not candidates for further consolidation. */
391
+ * It lives here so that every consumer that has to say "the opposite of this compare" reads THIS
392
+ * one, and they cannot drift apart the way inline copies did. Two adjacent facts worth knowing: raise/
393
+ * shortcircuit.ts derives its `BOOL_OPS` from these keys and `NEGATED_FCMP`'s (asserting
394
+ * negatable compare == boolean-op, true today), and l3/ast.ts `NEGATE_REL` is the SAME relation
395
+ * over the neutral L3 operator vocabulary — deliberately separate, because signedness lives in the
396
+ * operand types there, so the two tables are not candidates for further consolidation. */
232
397
  const ICMP_NEGATION_PAIRS: readonly (readonly [Opcode, Opcode])[] = [
233
398
  ['icmp_eq', 'icmp_ne'],
234
399
  ['icmp_slt', 'icmp_sge'],
@@ -243,103 +408,17 @@ export const NEGATED_ICMP: Readonly<Record<string, Opcode>> = Object.fromEntries
243
408
  ]),
244
409
  );
245
410
 
246
- /** Ops with an observable side effect: the flag on the signature, derived rather than re-listed.
247
- * `isDceSafe` asks the same question of the FLAG through `opSig` rather than of this set, so the
248
- * two cannot disagree. The SET's own consumers are `HOIST_UNSAFE_OPS` below, structure.ts's
249
- * block-purity tests (may this block be folded into a loop header, is this exit owned),
250
- * analysis.ts's memory-write barrier, divpow2's bias block (which is DELETED rather than moved),
251
- * and the idiom layer's de-sequencing guard (pattern/engine.ts). Derived rather than re-listed
252
- * because three of those consumers each carried a hand-written copy of this membership, which is
253
- * how the models drifted apart. */
254
- export const EFFECTFUL_OPS: ReadonlySet<string> = new Set(
255
- (Object.keys(OPCODES) as Opcode[]).filter((k) => (OPCODES[k] as OpSig).effects),
256
- );
257
-
258
- /** Ops that may not be SPECULATED — run on a path that did not run them before. Identical to
259
- * `EFFECTFUL_OPS`, and kept as its own name because its call sites ask the speculation question
260
- * rather than the deletion one.
261
- *
262
- * A memory read is deliberately absent, and that is the one entry worth arguing: its only consumer
263
- * is raise/shortcircuit.ts, which hoists an arm's body into the block above, and the structurer
264
- * inlines an unnamed value back into the `&&`/`||` right-hand side, where C's own short-circuit
265
- * re-guards it. Adding the two reads
266
- * here cost three byte-matches (kleod:UpdateHUDCounterDisplay, retired 2026-09-13 with its source,
267
- * plus synthetic:breakloop and synthetic:strcmp1), so the argument was load-bearing rather than
268
- * merely plausible. Not re-measured since the kleod row's retirement.
269
- *
270
- * KNOWN GAP: the trapping divides are absent too, and there the re-guard argument does NOT carry
271
- * — a hoisted `sdiv` that the structurer NAMES becomes an unconditional statement. Left as it is
272
- * because closing it is a separate change with its own measurement; `REEVAL_UNSAFE_OPS` does
273
- * refuse them, so the pre-update sink is not exposed to it.
274
- *
275
- * AND THE EXEMPTION IS NOT TRANSFERABLE, which is worth saying beside it: a second consumer once
276
- * read this set to answer "would gcc have SPECULATED this arm above a compare", where nothing
277
- * re-guards anything, and admitted single-load arms `gcc/jump.c:483`'s `! may_trap_p` refuses.
278
- * Pointing that consumer back at this set costs `synthetic:mergeldcast:agbcc` its byte-match
279
- * (MATCH -> diff:6), measured. That consumer is `raise/narrowlocal.ts` and it reads
280
- * `REEVAL_UNSAFE_OPS` instead. Reach for this set only when the argument above — a C-level
281
- * re-guard at the new point — actually holds at your call site. */
282
- export const HOIST_UNSAFE_OPS: ReadonlySet<string> = EFFECTFUL_OPS;
283
-
284
411
  /** The ops whose `operands[0]` is a memory-access BASE: `load base`, `store base, value`,
285
412
  * `aload base, index`, `astore base, index, value` (the operand roles are in the registry above).
286
413
  * Authored rather than derived — no registry field records the operand ROLE, and the only other
287
414
  * memory-touching opcode is `call`, whose variadic operands are arguments and not a base.
288
415
  *
289
- * Two consumers ask two different questions of it and both need the same answer, which is why it
290
- * is here and not next to either: `structure/analysis.ts` uses it for the address-home variation's slot
291
- * model, and `raise/const.ts` to recognise a folded literal that IS an address. */
416
+ * Three consumers ask three different questions of it and all need the same answer, which is why
417
+ * it is here and not next to any: `structure/analysis.ts` uses it for the address-home variation's
418
+ * slot model, `raise/const.ts` to recognise a folded literal that IS an address, and
419
+ * `ir/discipline.ts` to tell a device access's `volatile` from a frame object's. */
292
420
  export const MEM_BASE_OPS: ReadonlySet<string> = new Set(['load', 'store', 'aload', 'astore']);
293
421
 
294
- /** Ops whose answer depends on WHERE they run: an effect (its order against other effects is
295
- * observable) or a memory read (it answers whichever stores ran before it). The question a pass
296
- * asks before moving a computation to another point on the SAME path. `SPELLED_WHEN_DEAD_OPS`
297
- * below asks a DIFFERENT question and derives, today, the same set. */
298
- export const ORDER_SENSITIVE_OPS: ReadonlySet<string> = new Set(
299
- (Object.keys(OPCODES) as Opcode[]).filter((k) => {
300
- const sig = OPCODES[k] as OpSig;
301
- return sig.effects || sig.reads;
302
- }),
303
- );
304
-
305
- /** Ops the structurer must still SPELL when nothing consumes their result — an effect, or a memory
306
- * READ. Consumer: structure.ts's `sideEffects` walk.
307
- *
308
- * The read half is the entry worth arguing, because `reads` documents the OPPOSITE about C — a
309
- * load nobody reads is deletable, nothing observes it. That is the C claim. The COMPILER claim
310
- * points the other way: an optimizing compiler deletes every dead read it is ALLOWED to delete, so
311
- * one still in the target is evidence the source's access was `volatile`. Membership here only
312
- * says the structurer may not drop the op silently; whether a `volatile` can actually reach the
313
- * access is a second, ADDRESS-level question the call site asks separately
314
- * (structure.ts `volatileQualifiable`), and a read it answers no to is dropped as before.
315
- *
316
- * DERIVED FROM THE REGISTRY, not aliased to `ORDER_SENSITIVE_OPS`, even though the two are
317
- * extensionally identical today and `HOIST_UNSAFE_OPS` above does alias `EFFECTFUL_OPS`. An alias
318
- * makes two DIFFERENT questions incapable of ever differing, so the day one of them acquires an
319
- * opcode the other should not have, the edit lands on both silently. The identity is pinned by a
320
- * test instead (test/pattern.test.ts), where a future divergence surfaces as a decision to make
321
- * rather than a coupling nobody sees. */
322
- export const SPELLED_WHEN_DEAD_OPS: ReadonlySet<string> = new Set(
323
- (Object.keys(OPCODES) as Opcode[]).filter((k) => {
324
- const sig = OPCODES[k] as OpSig;
325
- return sig.effects || sig.reads;
326
- }),
327
- );
328
-
329
- /** Ops that may not be RE-EVALUATED at another program point — order-sensitive, or trapping. The
330
- * trap half is what separates this from `ORDER_SENSITIVE_OPS`: it only matters when the new point
331
- * can be reached on a path the old one was not, so a consumer that merely re-orders on one path
332
- * wants the smaller set. Both are needed because the two consumers differ on exactly the divides:
333
- * a collapsed switch re-renders a test block's ops AT THEIR USES, and every use is dominated by
334
- * the def, so it evaluates them on a SUBSET of the original paths — a narrowing, never a
335
- * speculation. */
336
- export const REEVAL_UNSAFE_OPS: ReadonlySet<string> = new Set(
337
- (Object.keys(OPCODES) as Opcode[]).filter((k) => {
338
- const sig = OPCODES[k] as OpSig;
339
- return sig.effects || sig.reads || sig.traps;
340
- }),
341
- );
342
-
343
422
  /** Ops that MATERIALIZE a value out of nothing: no operands, no state read, no effect, no trap, no
344
423
  * control flow — so where one sits in a block says nothing about what ran before it. Derived, so a
345
424
  * future pure nullary opcode joins without a second edit. Consumed by raise/paramwidth.ts, whose
@@ -352,10 +431,3 @@ export const MATERIALIZING_OPS: ReadonlySet<string> = new Set(
352
431
  return sig.operands === 0 && !sig.terminator && !sig.effects && !sig.reads && !sig.traps;
353
432
  }),
354
433
  );
355
-
356
- /** May a dead result of this opcode be deleted? Registered, no observable effects, not control
357
- * flow. `opaque` is excluded via its `effects` flag — see the note on its signature. */
358
- export function isDceSafe(opcode: string): boolean {
359
- const sig = opSig(opcode);
360
- return !!sig && !sig.effects && !sig.terminator;
361
- }