@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,157 @@
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, attrsKey } 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
+ // memoised per (x, y, side): a value its own consumer reads twice would otherwise double the work
104
+ // at every level
105
+ const memo = new Map<string, boolean>();
106
+ const ids = new Map<Value, number>();
107
+ const id = (v: Value) => ids.get(v) ?? (ids.set(v, ids.size), ids.size - 1);
108
+ /** `da` (read with `atEntry`) and `db` (the entry's own) compute the same value. */
109
+ const sameOp = (da: Op, db: Op, opcodeOk: boolean, atEntry: boolean): boolean => {
110
+ if (!opcodeOk || da.operands.length !== db.operands.length) {
111
+ return false;
112
+ }
113
+ if (normalize && da.opcode === 'load' && db.opcode === 'load') {
114
+ const x = address(da, atEntry);
115
+ const y = address(db, true);
116
+ return x.off === y.off && attrsKey(da, 'off') === attrsKey(db, 'off') && same(x.base, x.atEntry, y.base);
117
+ }
118
+ if (attrsKey(da) !== attrsKey(db)) {
119
+ return false;
120
+ }
121
+ if (da.operands.every((o, i) => same(o, atEntry, db.operands[i]))) {
122
+ return true;
123
+ }
124
+ return (
125
+ normalize &&
126
+ COMMUTES.has(da.opcode) &&
127
+ da.operands.length === 2 &&
128
+ same(da.operands[0], atEntry, db.operands[1]) &&
129
+ same(da.operands[1], atEntry, db.operands[0])
130
+ );
131
+ };
132
+ function same(x0: Value, atEntry0: boolean, y0: Value): boolean {
133
+ const { x, atEntry } = view(x0, atEntry0);
134
+ const { x: y } = view(y0, true);
135
+ if (x === y) {
136
+ return true;
137
+ }
138
+ const key = `${id(x)},${id(y)},${atEntry ? 1 : 0}`;
139
+ const hit = memo.get(key);
140
+ if (hit !== undefined) {
141
+ return hit;
142
+ }
143
+ const da = readable(x) ? defs.get(x) : undefined;
144
+ const db = readable(y) ? defs.get(y) : undefined;
145
+ const r = !!da && !!db && sameOp(da, db, da.opcode === db.opcode, atEntry);
146
+ memo.set(key, r);
147
+ return r;
148
+ }
149
+ if (!negated) {
150
+ return same(a, false, b);
151
+ }
152
+ const { x, atEntry } = view(a, false);
153
+ const { x: y } = view(b, true);
154
+ const da = readable(x) ? defs.get(x) : undefined;
155
+ const db = readable(y) ? defs.get(y) : undefined;
156
+ return !!da && !!db && sameOp(da, db, NEGATED_ICMP[da.opcode] === db.opcode, atEntry);
157
+ }
@@ -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
@@ -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 },
@@ -151,6 +158,32 @@ export const OPCODES = {
151
158
  // opcode rather than a `const` over a float type for the reason the arithmetic above has its own:
152
159
  // every pass that matches `const` is an integer fold.
153
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 },
154
187
  // --- memory ---
155
188
  load: { operands: 1, results: 1, requiredAttrs: ['off', 'width', 'signed'], reads: true },
156
189
  store: { operands: 2, results: 0, requiredAttrs: ['off', 'width'], effects: true },
@@ -171,6 +204,12 @@ export const OPCODES = {
171
204
  // - a load/store through an off-0 SCALAR gaddr → a bare global `gSym` / `gSym = v`;
172
205
  // - an indexed or non-zero-offset AGGREGATE access → the address-cast `((T *)&gSym)[i]`;
173
206
  // - any other use (e.g. `&gSym` passed to a call) → the `{k:'addr'}` L3 node, printed `&gSym`.
207
+ // Optional `fromMap: true` marks a name asmlift READ OFF THE SYMBOL MAP (a promoted numeric pool
208
+ // word, an interior attribution, a named walk) rather than one the asm spells by relocation.
209
+ // With it, `mapAddr` is the map address the name was read at: a name the map places at several
210
+ // addresses (a file static per unit) then denotes that one, and two such reads at different
211
+ // addresses, one of them read off the map, are two objects under one spelling, which
212
+ // raise/offsetnames.ts `cellSpelledTwice` declines. Two relocations of one name are the asm's own.
174
213
  gaddr: { operands: 0, results: 1, requiredAttrs: ['sym'] },
175
214
  // The address of a FRAME-LOCAL object — gaddr's local twin, for the address-taken stack local
176
215
  // (`mov rD, sp` feeding a DMA register or a callee). `off` is the byte offset inside the frame's
@@ -214,13 +253,16 @@ export const OPCODES = {
214
253
  // live-in, and a live-in of the entry block is a PARAMETER — right for an argument register, a
215
254
  // fabricated argument for anything else.
216
255
  //
217
- // Operand-free and pure like `laddr`, and out of raise/gvn.ts's NUMBERABLE set — where numbering
218
- // it would be VACUOUS rather than harmful, since two undefs in one function always carry
219
- // different keys. "Same key, therefore same value" is empty for a value that has none.
256
+ // Operand-free and pure like `laddr`, and out of raise/gvn.ts's NUMBERABLE set, which must keep
257
+ // it out: two undefs of one key are a storage's content and a `copied` move of it, and numbering
258
+ // them one value would erase the move.
220
259
  //
221
260
  // `key` names the storage (`sp@0`, `r4`); the structurer reads it to name the local
222
261
  // (`uninit_sp0`) and emits NO assignment — that absence is the recovery, and an edge argument
223
- // that is one emits no copy either (structure.ts undefCarriesNothing).
262
+ // that is one emits no copy either (structure.ts undefCarriesNothing). OPTIONAL `copied: true`:
263
+ // the value another storage's content arrives at a join keyed elsewhere as (`if (c) x = y;` of
264
+ // two uninitialised locals, frontend/ssa.ts `markCopiedUndefs`) — the machine moved it, so that
265
+ // edge copy IS emitted, under the name of the undef its key names.
224
266
  undef: { operands: 0, results: 1, requiredAttrs: ['key'] },
225
267
  // --- black-box escape hatch (keeps lifting total) ---
226
268
  // `effects: true`: an instruction asmlift could not model may do anything — write memory, trap,
@@ -243,6 +285,11 @@ export const OPCODES = {
243
285
  // Many-way switch dispatch (Regime B, jump table). The single operand is the scrutinee;
244
286
  // successors are the N case blocks followed by the default block (the LAST successor);
245
287
  // `cases` is the index-aligned list of the first N successors' case values.
288
+ // `exhaustive: true` — OPTIONAL, set by a frontend whose dispatch block is entered only past a
289
+ // bounds test that already sent every value outside `cases` elsewhere, so the default edge is
290
+ // never taken. It still names a block, because the op needs a last successor; the structurer
291
+ // reads the attr to emit no `default:` arm for it. A pass that rebuilds the op leaves it off and
292
+ // gets the default arm back, which is the conservative answer.
246
293
  switch_br: { operands: 1, results: 0, terminator: true, successors: 'variadic', requiredAttrs: ['cases'] },
247
294
  } as const satisfies Record<string, OpSig>;
248
295
 
@@ -272,6 +319,65 @@ export const CAST_WIDTHS: ReadonlySet<number> = new Set([8, 16]);
272
319
  * an op computes ON. */
273
320
  export const FLOAT_OPS: ReadonlySet<string> = new Set(['fadd', 'fsub', 'fmul', 'fdiv', 'fneg', 'fconst']);
274
321
 
322
+ /** The conversions out of a float into an integer, and into a float out of an integer. */
323
+ export const FLOAT_TO_INT: ReadonlySet<string> = new Set(['fptosi', 'fptoui']);
324
+ export const INT_TO_FLOAT: ReadonlySet<string> = new Set(['sitofp']);
325
+ /** The conversions between the two precisions, each named by the width it converts from and to. */
326
+ export const FLOAT_RESIZE: Readonly<Record<string, { from: 32 | 64; to: 32 | 64 }>> = {
327
+ fpext: { from: 32, to: 64 },
328
+ fptrunc: { from: 64, to: 32 },
329
+ };
330
+
331
+ /** The logical negation of each float compare, NaN included (the registry's note on `fcmp_*`):
332
+ * expanded both ways from the pairs, as `NEGATED_ICMP` is. */
333
+ const FCMP_NEGATION_PAIRS: readonly (readonly [Opcode, Opcode])[] = [
334
+ ['fcmp_oeq', 'fcmp_une'],
335
+ ['fcmp_olt', 'fcmp_uge'],
336
+ ['fcmp_ole', 'fcmp_ugt'],
337
+ ['fcmp_ogt', 'fcmp_ule'],
338
+ ['fcmp_oge', 'fcmp_ult'],
339
+ ];
340
+ export const NEGATED_FCMP: Readonly<Record<string, Opcode>> = Object.fromEntries(
341
+ FCMP_NEGATION_PAIRS.flatMap(([a, b]) => [
342
+ [a, b],
343
+ [b, a],
344
+ ]),
345
+ );
346
+
347
+ /** What each float compare answers when its first operand is LESS than, EQUAL to or GREATER than
348
+ * its second, and when the two are UNORDERED (either is a NaN): the definition of the ten, from
349
+ * which a compare a runtime computes some other way is read (raise/floathelpers.ts). */
350
+ export const FCMP_TRUTH: Readonly<Record<string, readonly [lt: boolean, eq: boolean, gt: boolean, un: boolean]>> = {
351
+ fcmp_olt: [true, false, false, false],
352
+ fcmp_ole: [true, true, false, false],
353
+ fcmp_ogt: [false, false, true, false],
354
+ fcmp_oge: [false, true, true, false],
355
+ fcmp_oeq: [false, true, false, false],
356
+ fcmp_une: [true, false, true, true],
357
+ fcmp_uge: [false, true, true, true],
358
+ fcmp_ugt: [false, false, true, true],
359
+ fcmp_ule: [true, true, false, true],
360
+ fcmp_ult: [true, false, false, true],
361
+ };
362
+
363
+ /** The float compare whose truth table (`FCMP_TRUTH`'s order: less, equal, greater, unordered) is
364
+ * `truth`, or undefined where none is — always true, say. The one reader of the table backwards,
365
+ * for every place that computes a compare's answers some other way (agbcc's helper tests,
366
+ * raise/floathelpers.ts; PowerPC's `cror` of two bits, frontend/ppc.ts). */
367
+ export function fcmpOfTruth(truth: readonly boolean[]): Opcode | undefined {
368
+ return Object.keys(FCMP_TRUTH).find((k) => FCMP_TRUTH[k].every((t, i) => t === truth[i])) as Opcode | undefined;
369
+ }
370
+
371
+ /** Every op that computes on a float: the arithmetic, the conversions and the compares. A runtime
372
+ * helper whose op is one of these is a float helper (runtime-helpers.ts `isFloatHelper`). */
373
+ export const FLOAT_COMPUTING: ReadonlySet<string> = new Set([
374
+ ...FLOAT_OPS,
375
+ ...FLOAT_TO_INT,
376
+ ...INT_TO_FLOAT,
377
+ ...Object.keys(FLOAT_RESIZE),
378
+ ...Object.keys(NEGATED_FCMP),
379
+ ]);
380
+
275
381
  /** Signature lookup by RUNTIME opcode string (Op.opcode is a plain string — IR consumers switch
276
382
  * on it); undefined for an unregistered opcode. */
277
383
  export function opSig(opcode: string): OpSig | undefined {
@@ -290,10 +396,10 @@ export function opSig(opcode: string): OpSig | undefined {
290
396
  *
291
397
  * It lives here so that every consumer that has to say "the opposite of this compare" reads THIS
292
398
  * one, and they cannot drift apart the way inline copies did. Two adjacent facts worth knowing: raise/
293
- * shortcircuit.ts derives its `BOOL_OPS` from these keys (asserting negatable-icmp == boolean-op,
294
- * true today), and l3/ast.ts `NEGATE_REL` is the SAME relation over the neutral L3 operator
295
- * vocabulary — deliberately separate, because signedness lives in the operand types there, so the
296
- * two tables are not candidates for further consolidation. */
399
+ * shortcircuit.ts derives its `BOOL_OPS` from these keys and `NEGATED_FCMP`'s (asserting
400
+ * negatable compare == boolean-op, true today), and l3/ast.ts `NEGATE_REL` is the SAME relation
401
+ * over the neutral L3 operator vocabulary — deliberately separate, because signedness lives in the
402
+ * operand types there, so the two tables are not candidates for further consolidation. */
297
403
  const ICMP_NEGATION_PAIRS: readonly (readonly [Opcode, Opcode])[] = [
298
404
  ['icmp_eq', 'icmp_ne'],
299
405
  ['icmp_slt', 'icmp_sge'],
@@ -308,6 +414,28 @@ export const NEGATED_ICMP: Readonly<Record<string, Opcode>> = Object.fromEntries
308
414
  ]),
309
415
  );
310
416
 
417
+ /** The comparison each `icmp_*` is with its operands SWAPPED — `a < b` is `b > a`; `==` and `!=`
418
+ * are their own. Not a negation: `NEGATED_ICMP` keeps the operands and flips the answer, this
419
+ * keeps the answer and flips the operands. Seated here for the reason `NEGATED_ICMP` is, and
420
+ * expanded both ways from its pairs the same way, so `swap(swap(c)) === c` by construction; a test
421
+ * asserts it covers the icmp family (test/pattern.test.ts). */
422
+ const ICMP_SWAP_PAIRS: readonly (readonly [Opcode, Opcode])[] = [
423
+ ['icmp_slt', 'icmp_sgt'],
424
+ ['icmp_sle', 'icmp_sge'],
425
+ ['icmp_ult', 'icmp_ugt'],
426
+ ['icmp_ule', 'icmp_uge'],
427
+ ];
428
+ export const SWAPPED_ICMP: Readonly<Record<string, Opcode>> = {
429
+ icmp_eq: 'icmp_eq',
430
+ icmp_ne: 'icmp_ne',
431
+ ...Object.fromEntries(
432
+ ICMP_SWAP_PAIRS.flatMap(([a, b]) => [
433
+ [a, b],
434
+ [b, a],
435
+ ]),
436
+ ),
437
+ };
438
+
311
439
  /** The ops whose `operands[0]` is a memory-access BASE: `load base`, `store base, value`,
312
440
  * `aload base, index`, `astore base, index, value` (the operand roles are in the registry above).
313
441
  * Authored rather than derived — no registry field records the operand ROLE, and the only other
package/src/ir/parse.ts CHANGED
@@ -128,6 +128,7 @@ export function parse(text: string): Fn {
128
128
  slotHomes: undefined,
129
129
  paramEvidence: undefined,
130
130
  localObjects: undefined,
131
+ asmLayout: undefined,
131
132
  };
132
133
  }
133
134
 
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Peer of `pattern/engine.ts`'s `dce`: general, opcode-agnostic, and callable by anything that has
4
4
  // just changed the CFG.
5
- import { Block, Fn, Successor, Value, replaceAllUsesWith } from './core';
5
+ import { Block, Fn, Op, Successor, Value, replaceAllUsesWith } from './core';
6
6
 
7
7
  /**
8
8
  * Remove block params that are really TRIVIAL PHIS — those whose incoming args, across every
@@ -55,12 +55,7 @@ export function simplifyTrivialPhis(fn: Fn, onRemoved?: (param: Value) => void):
55
55
  if (collapsed === null) {
56
56
  continue; // a genuine join, or unreachable (no in-edges at all)
57
57
  }
58
- replaceAllUsesWith(fn, param, collapsed);
59
- onRemoved?.(param);
60
- b.params.splice(i, 1);
61
- for (const s of incoming) {
62
- s.args.splice(i, 1);
63
- }
58
+ retireParam(fn, b, i, collapsed, incoming, onRemoved);
64
59
  removed++;
65
60
  changed = true;
66
61
  }
@@ -77,14 +72,70 @@ export function simplifyTrivialPhis(fn: Fn, onRemoved?: (param: Value) => void):
77
72
  * fixpoint and the non-mutating check below so the two cannot come to disagree about which params
78
73
  * are trivial.
79
74
  *
75
+ * `same` is the equality the args are held to. Identity by default, which is the trivial phi
76
+ * proper; `raise/unpre.ts` passes structural congruence (`ir/entry.ts` `sameAtEntry`) and gets
77
+ * back the FIRST arg as the class's representative, which it recomputes rather than reads. The
78
+ * self-reference waiver is the same for both, and so is its argument above: the first dynamic
79
+ * entry arrives on an edge that hands the representative's class.
80
+ *
80
81
  * `=== null` IS THE ONLY SAFE TEST. A genuine collapse value is never null, but it CAN be
81
82
  * `undefined` where an edge carries fewer args than the block has params — a malformed graph the
82
83
  * verifier catches elsewhere, and one today's callers pass straight through rather than treat as
83
84
  * "not trivial". A truthiness test would silently change that.
84
85
  */
85
- function trivialPhiValue(incoming: readonly Successor[], i: number, param: Value): Value | null {
86
- const distinct = [...new Set(incoming.map((s) => s.args[i]).filter((v) => v !== param))];
87
- return distinct.length === 1 ? distinct[0] : null;
86
+ export function trivialPhiValue(
87
+ incoming: readonly Successor[],
88
+ i: number,
89
+ param: Value,
90
+ same: (a: Value, b: Value) => boolean = (a, b) => a === b,
91
+ ): Value | null {
92
+ const args = incoming.map((s) => s.args[i]).filter((v) => v !== param);
93
+ if (args.length === 0) {
94
+ return null;
95
+ }
96
+ const first = args[0];
97
+ return args.every((v) => v === first || same(v, first)) ? first : null;
98
+ }
99
+
100
+ /** Retire block param `i` of `b` for `value`: every use reads `value`, and the param and each
101
+ * in-edge's arg for it are spliced out. `incoming` is every successor that targets `b` — the
102
+ * SUCCESSOR objects, not their `args`, which `replaceAllUsesWith` replaces. `onRemoved` runs after
103
+ * the uses move and before the splice. */
104
+ export function retireParam(
105
+ fn: Fn,
106
+ b: Block,
107
+ i: number,
108
+ value: Value,
109
+ incoming: readonly Successor[],
110
+ onRemoved?: (param: Value) => void,
111
+ ): void {
112
+ const param = b.params[i];
113
+ replaceAllUsesWith(fn, param, value);
114
+ onRemoved?.(param);
115
+ b.params.splice(i, 1);
116
+ for (const s of incoming) {
117
+ s.args.splice(i, 1);
118
+ }
119
+ }
120
+
121
+ /** Every in-edge, indexed by target: the successor, the block it leaves and the terminator that
122
+ * carries it. ONE pass over the function, for a caller whose CFG does not change while it reads the
123
+ * index (params and args may: the successor objects stay). */
124
+ export function inEdgesOf(fn: Fn): Map<Block, { succ: Successor; from: Block; op: Op }[]> {
125
+ const out = new Map<Block, { succ: Successor; from: Block; op: Op }[]>();
126
+ for (const from of fn.blocks) {
127
+ for (const op of from.ops) {
128
+ for (const succ of op.successors) {
129
+ const list = out.get(succ.block);
130
+ if (list === undefined) {
131
+ out.set(succ.block, [{ succ, from, op }]);
132
+ } else {
133
+ list.push({ succ, from, op });
134
+ }
135
+ }
136
+ }
137
+ }
138
+ return out;
88
139
  }
89
140
 
90
141
  /**
package/src/ir/types.ts CHANGED
@@ -7,6 +7,11 @@ export interface StructField {
7
7
  off: number;
8
8
  type: IrType;
9
9
  name: string;
10
+ /** A cell the function also reads or writes at another width or extension, declared at its
11
+ * widest view because the union of its views would not fit the compiler's layout
12
+ * (raise/structs.ts `buildUnionStruct`). An access of any other type is a cast of the base
13
+ * (structure.ts `memAccess`). */
14
+ castViews?: true;
10
15
  }
11
16
 
12
17
  export type IrType =
@@ -93,6 +98,31 @@ export const T = {
93
98
  void: (): IrType => ({ kind: 'void' }),
94
99
  };
95
100
 
101
+ /** The member name of each view a union holds, by access width. A width with no entry has no view
102
+ * (the deref contract admits no other scalar width either), and its overlap still declines. A
103
+ * narrow width loaded with both extensions has a view per extension, `u`/`s` prefixed. */
104
+ export const UNION_VIEW_NAMES: Readonly<Record<number, string>> = { 1: 'byte', 2: 'half', 4: 'word' };
105
+
106
+ /** The extensions a union declares a view of `width` bytes in, from the accesses of that width:
107
+ * THE one rule raise/structs.ts `buildUnionStruct` and the frame audit (frontend/frame-objects.ts
108
+ * `unionViews`) both declare by, so a struct member's union and a frame object's cannot disagree
109
+ * on their views. A word has one view whatever its loads say (`scalarTypeForAccess`); a narrower
110
+ * width a view per extension it is LOADED with, unsigned first; a width only stored the signed
111
+ * view where a stamped store says signed (raise/storesign.ts) and the unsigned one otherwise. */
112
+ export function unionViewSigns(
113
+ width: number,
114
+ accesses: readonly { readonly isLoad: boolean; readonly signed: boolean; readonly stamped: boolean }[],
115
+ ): boolean[] {
116
+ if (width === 4) {
117
+ return [true];
118
+ }
119
+ const loaded = new Set(accesses.filter((a) => a.isLoad).map((a) => a.signed));
120
+ if (loaded.size === 0) {
121
+ return [accesses.some((a) => a.stamped && a.signed)];
122
+ }
123
+ return [false, true].filter((sg) => loaded.has(sg));
124
+ }
125
+
96
126
  /** The byte width of a union view's element (an array view's element, or the scalar itself). */
97
127
  function viewBytes(t: IrType): number {
98
128
  const e = t.kind === 'array' ? t.elem : t;
@@ -102,6 +132,11 @@ function viewBytes(t: IrType): number {
102
132
  return e.width / 8;
103
133
  }
104
134
 
135
+ /** A struct as its declaration spells it (`declared`), else by its tag; any other type as printed. */
136
+ export function structSpelling(t: IrType): string {
137
+ return t.kind === 'struct' ? (t.declared ?? `struct ${t.name}`) : typeToString(t);
138
+ }
139
+
105
140
  /** The member `name` of an aggregate — a struct's field or a union's view — or undefined when `t`
106
141
  * is not an aggregate or declares no such member. THE one lookup the typing walk and the deref
107
142
  * contract share, so the two cannot disagree on which member access is well-typed. */
@@ -131,8 +166,57 @@ export function unionViewAt(
131
166
  if (member === undefined || member.type.kind !== 'union') {
132
167
  return undefined;
133
168
  }
169
+ return { member, ...unionView(member.type, off - member.off, width, signed, isStore) };
170
+ }
171
+
172
+ /** Does a member declared at signedness `declared` read as EXACTLY the type the cast spelling this
173
+ * replaces would have produced — `scalarTypeForAccess(width, signed)`? That is the whole
174
+ * byte-exactness argument for naming a member instead of casting: same address, same read width,
175
+ * same C type, so every operator downstream compiles identically. A 4-byte access renders s32
176
+ * whatever the load said (the ISA has one word load), so only a SIGNED member may take its place;
177
+ * narrower accesses carry their own signedness and must match it. An undeclared signedness (the
178
+ * member is not a base type — a nested struct, an enum, a pointer) is never assumed to match.
179
+ * THE one copy of "an access names the member": structure.ts's member spellings and `isCastView`
180
+ * below all ask it, so they cannot drift apart. */
181
+ export function spellsAccessType(declared: boolean | undefined, width: number, signed: boolean): boolean {
182
+ if (declared === undefined) {
183
+ return false;
184
+ }
185
+ return typeEquals(T.int(width * 8, declared), scalarTypeForAccess(width, signed));
186
+ }
187
+
188
+ /** Does an access of `width` bytes at byte `off`, inside a `castViews` field of the struct, read it
189
+ * at a view other than the field's own type — another width or offset, or a load whose type the
190
+ * field does not spell (`spellsAccessType`)? A store carries no extension (it truncates to the
191
+ * field's bytes whatever their type), so one of the field's width at its offset is the field. */
192
+ export function isCastView(
193
+ st: Extract<IrType, { kind: 'struct' }>,
194
+ off: number,
195
+ width: number,
196
+ signed: boolean,
197
+ isStore: boolean,
198
+ ): boolean {
199
+ const f = st.fields.find(
200
+ (m) => m.castViews === true && m.off <= off && off + width <= m.off + (intWidth(m.type) ?? 0) / 8,
201
+ );
202
+ if (f === undefined || f.type.kind !== 'int') {
203
+ return false;
204
+ }
205
+ return off !== f.off || f.type.width !== width * 8 || (!isStore && !spellsAccessType(f.type.signed, width, signed));
206
+ }
207
+
208
+ /** The view of a union an access of `width` bytes at byte `at` of it reads — the rule
209
+ * `unionViewAt` states, asked of a union that is not a struct's member (a frame object declared
210
+ * as one, structure.ts `laddrType`). */
211
+ export function unionView(
212
+ u: Extract<IrType, { kind: 'union' }>,
213
+ at: number,
214
+ width: number,
215
+ signed: boolean,
216
+ isStore: boolean,
217
+ ): { view: StructField | undefined; index: number | null } {
134
218
  const elemOf = (t: IrType): IrType => (t.kind === 'array' ? t.elem : t);
135
- const wide = member.type.members.filter((m) => intWidth(elemOf(m.type)) === width * 8);
219
+ const wide = u.members.filter((m) => intWidth(elemOf(m.type)) === width * 8);
136
220
  const signOf = (m: StructField): boolean | undefined => {
137
221
  const e = elemOf(m.type);
138
222
  return e.kind === 'int' ? e.signed : undefined;
@@ -141,7 +225,7 @@ export function unionViewAt(
141
225
  width === 4 || (isStore && wide.length === 1)
142
226
  ? wide[0]
143
227
  : wide.find((m) => signOf(m) === (isStore ? false : signed));
144
- return { member, view, index: view?.type.kind === 'array' ? (off - member.off) / width : null };
228
+ return { view, index: view?.type.kind === 'array' ? at / width : null };
145
229
  }
146
230
 
147
231
  export function typeToString(t: IrType): string {