@asmlift/core 0.9.0 → 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 (101) 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 +1 -0
  5. package/src/backend/cfamily.ts +165 -50
  6. package/src/backend/cpp.ts +2 -1
  7. package/src/backend/pascal.ts +31 -12
  8. package/src/contracts.ts +173 -16
  9. package/src/declare.ts +69 -20
  10. package/src/frontend/asmdata.ts +7 -1
  11. package/src/frontend/call-plan.ts +964 -60
  12. package/src/frontend/dead-reads.ts +628 -0
  13. package/src/frontend/device-pins.ts +23 -4
  14. package/src/frontend/emit.ts +4 -3
  15. package/src/frontend/fpu.ts +224 -17
  16. package/src/frontend/frame-objects.ts +1232 -192
  17. package/src/frontend/frontend.ts +64 -3
  18. package/src/frontend/high-half.ts +53 -4
  19. package/src/frontend/jump-table.ts +530 -0
  20. package/src/frontend/local-object.ts +158 -32
  21. package/src/frontend/mips.ts +2076 -215
  22. package/src/frontend/own-struct-return.ts +500 -0
  23. package/src/frontend/ppc.ts +2711 -232
  24. package/src/frontend/register-struct-call.ts +484 -0
  25. package/src/frontend/reloc-symbol.ts +22 -7
  26. package/src/frontend/ssa.ts +1460 -44
  27. package/src/frontend/stackargs.ts +106 -57
  28. package/src/frontend/thumb.ts +2044 -318
  29. package/src/ir/alias.ts +42 -0
  30. package/src/ir/bits.ts +681 -1
  31. package/src/ir/core.ts +100 -5
  32. package/src/ir/discipline.ts +86 -21
  33. package/src/ir/entry.ts +159 -0
  34. package/src/{structure → ir}/loops.ts +6 -2
  35. package/src/ir/opcodes.ts +109 -9
  36. package/src/ir/types.ts +86 -2
  37. package/src/ir/verify.ts +80 -11
  38. package/src/l3/ast.ts +290 -18
  39. package/src/l3/basecse.ts +33 -5
  40. package/src/l3/constinit.ts +382 -0
  41. package/src/l3/dce.ts +2 -2
  42. package/src/l3/initfirst.ts +42 -11
  43. package/src/l3/intcell.ts +2 -1
  44. package/src/l3/narrowdecl.ts +77 -36
  45. package/src/l3/pollguard.ts +8 -2
  46. package/src/l3/ptrcell.ts +31 -34
  47. package/src/l3/ptrfield.ts +123 -8
  48. package/src/l3/regspell.ts +9 -4
  49. package/src/l3/reindex.ts +283 -96
  50. package/src/l3/storage.ts +98 -1
  51. package/src/l3/symbol-refs.ts +84 -6
  52. package/src/l3/tailmerge.ts +4 -2
  53. package/src/l3/typing.ts +108 -15
  54. package/src/l3/unreduce.ts +6 -5
  55. package/src/l3/volatileptr.ts +17 -54
  56. package/src/mangle.ts +30 -0
  57. package/src/pattern/engine.ts +361 -73
  58. package/src/pipeline.ts +541 -24
  59. package/src/proto-context.ts +905 -45
  60. package/src/proto.ts +555 -40
  61. package/src/raise/aggmembers.ts +290 -0
  62. package/src/raise/arrays.ts +5 -1
  63. package/src/raise/declared-volatile.ts +207 -31
  64. package/src/raise/floathelpers.ts +254 -33
  65. package/src/raise/gvn.ts +3 -3
  66. package/src/raise/latch.ts +162 -6
  67. package/src/raise/memberarrays.ts +10 -4
  68. package/src/raise/offsetnames.ts +10 -26
  69. package/src/raise/pairparams.ts +33 -0
  70. package/src/raise/paramwidth.ts +196 -6
  71. package/src/raise/pre-recovery.ts +102 -19
  72. package/src/raise/pre-structure.ts +54 -0
  73. package/src/raise/recover.ts +12 -5
  74. package/src/raise/reroll.ts +694 -0
  75. package/src/raise/shortcircuit.ts +26 -12
  76. package/src/raise/softdiv.ts +1 -1
  77. package/src/raise/storesign.ts +153 -0
  78. package/src/raise/struct-arrays.ts +30 -11
  79. package/src/raise/structs.ts +396 -33
  80. package/src/raise/truncload.ts +4 -3
  81. package/src/raise/unthread.ts +400 -0
  82. package/src/rank-declare.ts +118 -12
  83. package/src/rank-variations.ts +89 -31
  84. package/src/rank.ts +307 -54
  85. package/src/runtime-helpers.ts +140 -22
  86. package/src/structure/analysis.ts +165 -2
  87. package/src/structure/arith-ops.ts +23 -0
  88. package/src/structure/globalaccess.ts +111 -9
  89. package/src/structure/hazards.ts +16 -64
  90. package/src/structure/local-statics.ts +145 -9
  91. package/src/structure/namecoalesce.ts +1 -1
  92. package/src/structure/pointer-spelling.ts +1106 -0
  93. package/src/structure/redundant-test.ts +1 -1
  94. package/src/structure/structure.ts +2180 -1054
  95. package/src/structure/switch-recover.ts +244 -100
  96. package/src/symbols.ts +6 -0
  97. package/src/target.ts +765 -52
  98. package/src/trace.ts +26 -8
  99. package/src/variation-definitions.ts +126 -14
  100. package/src/variation-gates.ts +2 -0
  101. package/src/variation-tokens.ts +5 -1
package/src/ir/bits.ts CHANGED
@@ -10,7 +10,8 @@
10
10
  // It is a function over `Fn` rather than a closure inside a rendering pass so that a test can ask
11
11
  // it directly: reachable only through emitted C, a wrong answer for one opcode reads as a silent
12
12
  // wrong address in a row nobody is looking at.
13
- import type { Op, Value } from './core';
13
+ import type { Block, Op, Value } from './core';
14
+ import { OPCODES, type OpSig } from './opcodes';
14
15
 
15
16
  /** A materialized def emits its own named temp, so its value is a VARIABLE at every use — never a
16
17
  * literal this analysis may fold through. Callers that have no materialization model pass none. */
@@ -42,6 +43,518 @@ export function constMask(ctx: BitsCtx, v: Value): number | null {
42
43
  return null;
43
44
  }
44
45
 
46
+ /** What the word evaluator below may read. */
47
+ export interface WordCtx {
48
+ defs: ReadonlyMap<Value, Op>;
49
+ /** Block param -> the value each in-edge passes for it (undefined: an edge that passes none). A
50
+ * param absent from it (an entry param, or every param when the caller gives none) holds no
51
+ * known word. */
52
+ incoming?: ReadonlyMap<Value, readonly (Value | undefined)[]>;
53
+ /** A value the caller says is no constant, whatever computes it: switch recovery's named
54
+ * variables, whose every use reads the variable rather than the expression that set it. */
55
+ opaque?: (v: Value) => boolean;
56
+ /** A whole-word load -> every value a store this function made may have left there, one per
57
+ * path in (`storeForwards`), met like a block param's in-edges. The machine
58
+ * reads that word back unless the address is I/O, so only a reader that fails closed passes it:
59
+ * the structurer's `neg_s`, to whom a store and a reload of INT_MIN is INT_MIN as surely as a
60
+ * compiler that forwards the store makes it. Without it every load varies. */
61
+ forwarded?: ReadonlyMap<Op, readonly Value[]>;
62
+ /** A read every path into which passes a write this function made that may land on its cell
63
+ * (`overwrittenReads`). Unless `forwarded` names what that write left, it reads a constant this
64
+ * evaluator cannot compute: the memory the function wrote, through an address it cannot match
65
+ * to the read's, may be every word that read sees. Passed only beside `forwarded`. */
66
+ overwritten?: ReadonlySet<Op>;
67
+ }
68
+
69
+ /** A value's word, over a lattice: not yet seen (`top`, a loop-carried param before its back
70
+ * edge), one known word, a CONSTANT this evaluator cannot compute (`konst`: a pure op of words
71
+ * it has no rule for, or a register shift by 32 or more, which each ISA reads differently), or a
72
+ * value that varies (`var`). */
73
+ type Word = { k: 'top' } | { k: 'word'; w: number } | { k: 'konst' } | { k: 'var' };
74
+ const TOP: Word = { k: 'top' };
75
+ const KONST: Word = { k: 'konst' };
76
+ const VAR: Word = { k: 'var' };
77
+ const word = (w: number): Word => ({ k: 'word', w: w | 0 });
78
+ const sameWord = (a: Word, b: Word): boolean => a.k === b.k && (a.k !== 'word' || a.w === (b as { w: number }).w);
79
+ const meet = (a: Word, b: Word): Word =>
80
+ a.k === 'top'
81
+ ? b
82
+ : b.k === 'top'
83
+ ? a
84
+ : a.k === 'var' || b.k === 'var'
85
+ ? VAR
86
+ : a.k === 'word' && b.k === 'word'
87
+ ? a.w === b.w
88
+ ? a
89
+ : VAR
90
+ : KONST;
91
+
92
+ const BINARY: Readonly<Record<string, (a: number, c: number) => number>> = {
93
+ add: (a, c) => a + c,
94
+ sub: (a, c) => a - c,
95
+ mul: (a, c) => Math.imul(a, c),
96
+ and: (a, c) => a & c,
97
+ or: (a, c) => a | c,
98
+ xor: (a, c) => a ^ c,
99
+ };
100
+ const SHIFT: Readonly<Record<string, (a: number, c: number) => number>> = {
101
+ shl: (a, c) => a << c,
102
+ shr_u: (a, c) => a >>> c,
103
+ shr_s: (a, c) => a >> c,
104
+ };
105
+
106
+ /** One op's word, given its operands'. The annihilators come first because they hold whatever
107
+ * the other operand is: `x & 0`, `x | -1`, `x * 0`, `x - x`, `x ^ x`. */
108
+ function opWord(d: Op, of: (v: Value) => Word): Word {
109
+ if (d.opcode === 'const') {
110
+ const value = d.attrs.value;
111
+ return typeof value === 'number'
112
+ ? word(value)
113
+ : typeof value === 'bigint'
114
+ ? word(Number(BigInt.asIntN(32, value)))
115
+ : KONST;
116
+ }
117
+ const sig = (OPCODES as Record<string, OpSig | undefined>)[d.opcode];
118
+ if (!sig || sig.effects || sig.reads || sig.terminator || d.results.length !== 1 || d.operands.length === 0) {
119
+ return VAR; // a load, a call, an address (`gaddr`: the linker's, not a word a compiler folds)
120
+ }
121
+ const imm = typeof d.attrs.imm === 'number' ? word(d.attrs.imm as number) : null;
122
+ const ws = d.operands.map(of);
123
+ const [a, c] = d.operands.length === 1 && imm !== null ? [ws[0], imm] : ws;
124
+ const is = (x: Word | undefined, n: number) => x?.k === 'word' && x.w === n;
125
+ if (
126
+ (d.opcode === 'and' && (is(a, 0) || is(c, 0))) ||
127
+ (d.opcode === 'mul' && (is(a, 0) || is(c, 0))) ||
128
+ ((d.opcode === 'sub' || d.opcode === 'xor') && d.operands.length === 2 && d.operands[0] === d.operands[1])
129
+ ) {
130
+ return word(0);
131
+ }
132
+ if (d.opcode === 'or' && (is(a, -1) || is(c, -1))) {
133
+ return word(-1);
134
+ }
135
+ const all = d.operands.length === 1 && imm !== null ? [a, c] : ws;
136
+ // An operand not yet seen comes BEFORE one that varies. Once it settles it may be the word an
137
+ // annihilator above needs (`q & x` with q a param that settles to 0), and an answer of VAR now
138
+ // would drop a param that reads it for good: the solver below would then reach whichever
139
+ // fixpoint the param order made, and the imprecise one answers `mayAlwaysHold` false.
140
+ if (all.some((x) => x.k === 'top')) {
141
+ return TOP;
142
+ }
143
+ if (all.some((x) => x.k === 'var')) {
144
+ return VAR;
145
+ }
146
+ if (all.some((x) => x.k !== 'word')) {
147
+ return KONST;
148
+ }
149
+ const n = all.map((x) => (x as { w: number }).w);
150
+ if ((d.opcode === 'neg' || d.opcode === 'not') && n.length === 1) {
151
+ return word(d.opcode === 'neg' ? -n[0] : ~n[0]);
152
+ }
153
+ // The mathematical negation is the wrapping one at every word but INT_MIN, whose negation
154
+ // (+2^31) is a constant and no word.
155
+ if (d.opcode === 'neg_s' && n.length === 1) {
156
+ return n[0] === -0x80000000 ? KONST : word(-n[0]);
157
+ }
158
+ if (n.length === 2 && d.opcode in BINARY) {
159
+ return word(BINARY[d.opcode](n[0], n[1]));
160
+ }
161
+ if (n.length === 2 && d.opcode in SHIFT) {
162
+ return n[1] >= 0 && n[1] < 32 ? word(SHIFT[d.opcode](n[0], n[1])) : KONST;
163
+ }
164
+ return KONST;
165
+ }
166
+
167
+ /** Solve every block param `ctx.incoming` names, optimistically: each starts unseen and drops to
168
+ * the meet of what its in-edges pass, until nothing moves — so a loop-carried param that only
169
+ * ever passes itself back keeps the word it entered with, and a counter (`0`, then `i + 1`)
170
+ * varies. A param's new state is met with its old one, so it only ever drops, and drops at most
171
+ * three times; with `opWord` monotone (an unseen operand is TOP before a varying one is VAR)
172
+ * that meet changes nothing, and the fixpoint is the same in every param order.
173
+ *
174
+ * A forwarded load is solved the same way, its forwarded stores as its in-edges: memory carried
175
+ * round a loop is a param the frontend did not name, and a loop that stores a reload back
176
+ * (`ldr r2, [r3]; …; str r2, [r3]`) passes it itself, which must keep the word it entered with. */
177
+ function solveParams(ctx: WordCtx): Map<Value, Word> {
178
+ const state = new Map<Value, Word>();
179
+ const incoming = new Map<Value, readonly (Value | undefined)[]>(ctx.incoming ?? []);
180
+ for (const [load, vals] of ctx.forwarded ?? []) {
181
+ if (load.results.length === 1) {
182
+ incoming.set(load.results[0], vals);
183
+ }
184
+ }
185
+ for (const p of incoming.keys()) {
186
+ state.set(p, TOP);
187
+ }
188
+ for (let round = 0; ; round++) {
189
+ if (round > 3 * state.size + 1) {
190
+ throw new Error('constWord: block params did not settle');
191
+ }
192
+ const memo = new Map<Value, Word>();
193
+ let moved = false;
194
+ for (const [p, args] of incoming) {
195
+ let w: Word = TOP;
196
+ for (const x of args) {
197
+ w = meet(w, x === undefined ? VAR : x === p ? TOP : evalWord(ctx, state, memo, x));
198
+ }
199
+ w = meet(state.get(p)!, w);
200
+ if (!sameWord(w, state.get(p)!)) {
201
+ state.set(p, w);
202
+ moved = true;
203
+ }
204
+ }
205
+ if (!moved) {
206
+ return state;
207
+ }
208
+ }
209
+ }
210
+
211
+ function evalWord(ctx: WordCtx, params: ReadonlyMap<Value, Word>, memo: Map<Value, Word>, v: Value): Word {
212
+ if (ctx.opaque?.(v)) {
213
+ return VAR;
214
+ }
215
+ const p = params.get(v);
216
+ if (p !== undefined) {
217
+ return p;
218
+ }
219
+ const seen = memo.get(v);
220
+ if (seen !== undefined) {
221
+ return seen;
222
+ }
223
+ const d = ctx.defs.get(v);
224
+ // In progress reads VAR: SSA has no cycle but through a param, and a forwarded load is one
225
+ // (`solveParams`), so nothing reaches this mark but malformed IR.
226
+ memo.set(v, VAR);
227
+ const w = d === undefined ? VAR : ctx.overwritten?.has(d) ? KONST : opWord(d, (x) => evalWord(ctx, params, memo, x));
228
+ memo.set(v, w);
229
+ return w;
230
+ }
231
+
232
+ const solved = new WeakMap<WordCtx, Map<Value, Word>>();
233
+ function wordOf(ctx: WordCtx, v: Value): Word {
234
+ let params = solved.get(ctx);
235
+ if (params === undefined) {
236
+ params = solveParams(ctx);
237
+ solved.set(ctx, params);
238
+ }
239
+ return evalWord(ctx, params, new Map(), v);
240
+ }
241
+
242
+ /** THE WORD A VALUE ALWAYS HOLDS — what the machine computes, when every input to it is a constant
243
+ * (a pool word, a `mov` and a shift of it, any integer arithmetic over those, an operand an
244
+ * annihilator makes irrelevant, a block param every in-edge passes the same word), or null.
245
+ * Unlike `constMask` it reads THROUGH a materialized def: it answers what the machine holds, not
246
+ * whether a spelling may fold it. Its readers: the structurer's `neg_s`, switch recovery's case
247
+ * constants, the float helpers' literal words. An opcode it has no rule for is no word, so a new
248
+ * op whose value is not the plain machine word fails closed here; `neg_s` has its own rule. */
249
+ export function constWord(ctx: WordCtx, v: Value): number | null {
250
+ const w = wordOf(ctx, v);
251
+ return w.k === 'word' ? w.w : null;
252
+ }
253
+
254
+ /** Whether a value MAY hold `w` on every execution — it is that constant, a constant this
255
+ * evaluator cannot compute, or a value the solver never settled (a param no executed edge
256
+ * feeds). The fail-closed reading of `constWord`, for a caller to whom a constant it cannot name
257
+ * is as bad as the one it is looking for. A value that varies, or that is `w` on some paths only,
258
+ * answers false. */
259
+ export function mayAlwaysHold(ctx: WordCtx, v: Value, w: number): boolean {
260
+ const x = wordOf(ctx, v);
261
+ return x.k === 'konst' || x.k === 'top' || (x.k === 'word' && x.w === (w | 0));
262
+ }
263
+
264
+ /** Where an access lands, normalised: a ROOT (one SSA value, one symbol, or the absolute address
265
+ * space) and a byte offset from it. `add`/`sub` of a constant move along the root, so a base held
266
+ * in another register (`add r2, r3, #4`), an addend in the pool word (`.word gX+4`) and the same
267
+ * offset carried by the access all name one cell. Two keys on one root overlap exactly when their
268
+ * byte ranges do; keys on two roots are two pointers nothing here can tell apart. */
269
+ interface Cell {
270
+ root: Value | string;
271
+ byte: number;
272
+ width: number;
273
+ }
274
+
275
+ /** The blocks a path from the entry (`blocks[0]`) reaches. A block nothing reaches, and every
276
+ * edge out of one, is no path in: a dead block falling into a read must not pass for a way in
277
+ * that wrote nothing. */
278
+ function reachable(blocks: readonly Block[]): Set<Block> {
279
+ const seen = new Set<Block>(blocks.slice(0, 1));
280
+ const work = blocks.slice(0, 1);
281
+ while (work.length > 0) {
282
+ for (const o of work.pop()!.ops) {
283
+ for (const s of o.successors) {
284
+ if (!seen.has(s.block)) {
285
+ seen.add(s.block);
286
+ work.push(s.block);
287
+ }
288
+ }
289
+ }
290
+ }
291
+ return seen;
292
+ }
293
+
294
+ /** Two cells may be the same memory: on one root when their bytes overlap, and always on two. */
295
+ const overlaps = (x: Cell, y: Cell): boolean =>
296
+ x.root !== y.root || (x.byte < y.byte + y.width && y.byte < x.byte + x.width);
297
+
298
+ function cellOf(defs: ReadonlyMap<Value, Op>, addr: Value, off: number, width: number): Cell {
299
+ let root: Value | string = addr;
300
+ let byte = off;
301
+ for (let hops = 0; hops < 64 && typeof root !== 'string'; hops++) {
302
+ const d = defs.get(root);
303
+ if (d === undefined) {
304
+ break;
305
+ }
306
+ if (d.opcode === 'gaddr') {
307
+ root = `sym:${d.attrs.sym as string}`;
308
+ break;
309
+ }
310
+ if (d.opcode === 'const' && typeof d.attrs.value === 'number') {
311
+ root = 'abs';
312
+ byte += d.attrs.value;
313
+ break;
314
+ }
315
+ const imm = typeof d.attrs.imm === 'number' ? d.attrs.imm : null;
316
+ if ((d.opcode === 'add' || d.opcode === 'sub') && d.operands.length === 1 && imm !== null) {
317
+ root = d.operands[0];
318
+ byte += d.opcode === 'add' ? imm : -imm;
319
+ continue;
320
+ }
321
+ if ((d.opcode === 'add' || d.opcode === 'sub') && d.operands.length === 2) {
322
+ const k = (v: Value): number | null => {
323
+ const c = defs.get(v);
324
+ return c?.opcode === 'const' && typeof c.attrs.value === 'number' ? c.attrs.value : null;
325
+ };
326
+ const [a, b] = d.operands;
327
+ if (k(b) !== null) {
328
+ root = a;
329
+ byte += d.opcode === 'add' ? k(b)! : -k(b)!;
330
+ continue;
331
+ }
332
+ if (d.opcode === 'add' && k(a) !== null) {
333
+ root = b;
334
+ byte += k(a)!;
335
+ continue;
336
+ }
337
+ }
338
+ break;
339
+ }
340
+ return { root, byte: byte | 0, width };
341
+ }
342
+
343
+ /** Whole-word load -> every value it may read, for `WordCtx.forwarded`: a forward MUST-analysis of
344
+ * the words this function's own stores leave in memory. Each block starts from the meet of what
345
+ * its predecessors leave — a cell survives a join only if every in-edge knows it, and then carries
346
+ * each in-edge's stored value — and a predecessor not yet reached (a loop's back edge, on the
347
+ * first pass) is optimistic and adds nothing, so a store before a loop that nothing in the loop
348
+ * overwrites reaches a reload in its header. A store sets its cell and forgets every cell it may
349
+ * overlap (any cell on another root, which may be the same memory); any other op with effects (a
350
+ * call, an indexed store) forgets everything. A load is forwarded when every path into it knows
351
+ * its cell, as a whole word; the entry block knows nothing, and a block the entry does not reach
352
+ * forwards nothing and is no path into the blocks it jumps to. */
353
+ export function storeForwards(blocks: readonly Block[]): Map<Op, Value[]> {
354
+ const defs = new Map<Value, Op>();
355
+ const preds = new Map<Block, Block[]>();
356
+ const live = reachable(blocks);
357
+ for (const b of blocks) {
358
+ for (const op of b.ops) {
359
+ for (const r of op.results) {
360
+ defs.set(r, op);
361
+ }
362
+ for (const s of op.successors) {
363
+ if (live.has(b)) {
364
+ preds.set(s.block, [...(preds.get(s.block) ?? []), b]);
365
+ }
366
+ }
367
+ }
368
+ }
369
+ type Mem = Map<string, { cell: Cell; vals: Value[] }>;
370
+ const ids = new Map<Value, number>();
371
+ const keyOf = (c: Cell): string =>
372
+ `${typeof c.root === 'string' ? c.root : `v${ids.get(c.root) ?? ids.set(c.root, ids.size).get(c.root)!}`}@${c.byte}:${c.width}`;
373
+ const cells = new Map<Op, Cell>();
374
+ for (const b of blocks) {
375
+ for (const op of b.ops) {
376
+ if ((op.opcode === 'load' || op.opcode === 'store') && typeof op.attrs.width === 'number') {
377
+ cells.set(op, cellOf(defs, op.operands[0], (op.attrs.off as number | undefined) ?? 0, op.attrs.width));
378
+ }
379
+ }
380
+ }
381
+ const out = new Map<Op, Value[]>();
382
+ // One pass over a block: from its entry state, record each whole-word load's forward (when
383
+ // `record`) and return its exit state.
384
+ const run = (b: Block, mem: Mem, record: boolean): Mem => {
385
+ const m: Mem = new Map(mem);
386
+ for (const op of b.ops) {
387
+ const cell = cells.get(op);
388
+ if (op.opcode === 'load') {
389
+ const known = cell === undefined || cell.width !== 4 ? undefined : m.get(keyOf(cell));
390
+ if (record && known !== undefined) {
391
+ out.set(op, known.vals);
392
+ }
393
+ continue;
394
+ }
395
+ if (op.opcode === 'store' && cell !== undefined) {
396
+ for (const [k, e] of m) {
397
+ if (overlaps(e.cell, cell)) {
398
+ m.delete(k);
399
+ }
400
+ }
401
+ m.set(keyOf(cell), { cell, vals: [op.operands[1]] });
402
+ continue;
403
+ }
404
+ const sig = (OPCODES as Record<string, OpSig | undefined>)[op.opcode];
405
+ if ((sig === undefined || sig.effects) && !sig?.terminator) {
406
+ m.clear();
407
+ }
408
+ }
409
+ return m;
410
+ };
411
+ const meetMem = (a: Mem, b: Mem): Mem => {
412
+ const m: Mem = new Map();
413
+ for (const [k, x] of a) {
414
+ const y = b.get(k);
415
+ if (y !== undefined) {
416
+ m.set(k, { cell: x.cell, vals: [...new Set([...x.vals, ...y.vals])] });
417
+ }
418
+ }
419
+ return m;
420
+ };
421
+ const sameMem = (a: Mem, b: Mem): boolean =>
422
+ a.size === b.size &&
423
+ [...a].every(([k, x]) => {
424
+ const y = b.get(k);
425
+ return y !== undefined && y.vals.length === x.vals.length && x.vals.every((v) => y.vals.includes(v));
426
+ });
427
+ // Exit states; a block absent from it is not reached yet (TOP). Cells only ever leave a state
428
+ // and values only ever join one, both finite, so this settles.
429
+ const exit = new Map<Block, Mem>();
430
+ const entryOf = (b: Block): Mem | undefined => {
431
+ if (!live.has(b)) {
432
+ return undefined; // dead: it records nothing and adds nothing to a block it falls into
433
+ }
434
+ const from = preds.get(b) ?? [];
435
+ if (b === blocks[0]) {
436
+ return new Map();
437
+ }
438
+ let m: Mem | undefined;
439
+ for (const p of from) {
440
+ const e = exit.get(p);
441
+ if (e !== undefined) {
442
+ m = m === undefined ? e : meetMem(m, e);
443
+ }
444
+ }
445
+ return m;
446
+ };
447
+ for (let moved = true, round = 0; moved; round++) {
448
+ if (round > 4 * blocks.length + 64) {
449
+ throw new Error('storeForwards: memory states did not settle');
450
+ }
451
+ moved = false;
452
+ for (const b of blocks) {
453
+ const at = entryOf(b);
454
+ if (at === undefined) {
455
+ continue;
456
+ }
457
+ const e = run(b, at, false);
458
+ const old = exit.get(b);
459
+ if (old === undefined || !sameMem(old, e)) {
460
+ exit.set(b, e);
461
+ moved = true;
462
+ }
463
+ }
464
+ }
465
+ for (const b of blocks) {
466
+ const at = entryOf(b);
467
+ if (at !== undefined) {
468
+ run(b, at, true);
469
+ }
470
+ }
471
+ return out;
472
+ }
473
+
474
+ /** Every read (`load`, `aload`) that each path from the function's entry reaches only past a write
475
+ * that may land on its cell, for `WordCtx.overwritten`: a store to an overlapping cell or to
476
+ * another root (which may be the same memory), and any other op with effects. What such a read
477
+ * sees is what this function wrote, on every path, and when `storeForwards` cannot say which word
478
+ * that was — a register-indexed address, two spellings of one sum, narrow stores that compose a
479
+ * word, a write through another pointer — the read must not pass for one that varies. A read with
480
+ * one write-free path in from the entry sees the caller's memory on it, which varies; a block the
481
+ * entry does not reach is no such path. An `aload`'s cell is never known, so every write may land
482
+ * on it. */
483
+ export function overwrittenReads(blocks: readonly Block[]): Set<Op> {
484
+ const defs = new Map<Value, Op>();
485
+ for (const b of blocks) {
486
+ for (const op of b.ops) {
487
+ for (const r of op.results) {
488
+ defs.set(r, op);
489
+ }
490
+ }
491
+ }
492
+ const cellAt = (op: Op): Cell | undefined =>
493
+ (op.opcode === 'load' || op.opcode === 'store') && typeof op.attrs.width === 'number'
494
+ ? cellOf(defs, op.operands[0], (op.attrs.off as number | undefined) ?? 0, op.attrs.width)
495
+ : undefined;
496
+ const writesOver = (op: Op, cell: Cell | undefined): boolean => {
497
+ if (op.opcode === 'load' || op.opcode === 'aload') {
498
+ return false;
499
+ }
500
+ if (op.opcode === 'store') {
501
+ const at = cellAt(op);
502
+ return at === undefined || cell === undefined || overlaps(at, cell);
503
+ }
504
+ const sig = (OPCODES as Record<string, OpSig | undefined>)[op.opcode];
505
+ return (sig === undefined || sig.effects === true) && !sig?.terminator;
506
+ };
507
+ const roots = blocks.slice(0, 1);
508
+ const out = new Set<Op>();
509
+ for (const b of blocks) {
510
+ b.ops.forEach((op, i) => {
511
+ if (op.opcode !== 'load' && op.opcode !== 'aload') {
512
+ return;
513
+ }
514
+ const cell = cellAt(op);
515
+ // The blocks whose entry a write-free path reaches.
516
+ const clean = new Set<Block>(roots);
517
+ const work = [...roots];
518
+ while (work.length > 0) {
519
+ const x = work.pop()!;
520
+ if (x.ops.some((o) => writesOver(o, cell))) {
521
+ continue;
522
+ }
523
+ for (const o of x.ops) {
524
+ for (const s of o.successors) {
525
+ if (!clean.has(s.block)) {
526
+ clean.add(s.block);
527
+ work.push(s.block);
528
+ }
529
+ }
530
+ }
531
+ }
532
+ if (!clean.has(b) || b.ops.slice(0, i).some((o) => writesOver(o, cell))) {
533
+ out.add(op);
534
+ }
535
+ });
536
+ }
537
+ return out;
538
+ }
539
+
540
+ /** Block param -> the values its in-edges pass, for `WordCtx.incoming`. */
541
+ export function incomingArgs(blocks: readonly Block[]): Map<Value, (Value | undefined)[]> {
542
+ const out = new Map<Value, (Value | undefined)[]>();
543
+ for (const b of blocks) {
544
+ for (const op of b.ops) {
545
+ for (const s of op.successors) {
546
+ s.block.params.forEach((p, i) => {
547
+ if (!out.has(p)) {
548
+ out.set(p, []);
549
+ }
550
+ out.get(p)!.push(s.args[i]);
551
+ });
552
+ }
553
+ }
554
+ }
555
+ return out;
556
+ }
557
+
45
558
  /** An UPPER BOUND on the significant bits of a value, or 32 when nothing bounds it. Every answer
46
559
  * below 32 comes from an instruction or from a caller's declaration-backed `bound`:
47
560
  * · a zero-fill shift right by n leaves 32 - n;
@@ -73,3 +586,170 @@ export function provableBits(ctx: BitsCtx, v: Value): number {
73
586
  : null;
74
587
  return m !== null && m >= 0 ? 32 - Math.clz32(m) : 32;
75
588
  }
589
+
590
+ // LINEAR FORMS AND STRIDED RANGES — "which words may this value be", for a caller that asks
591
+ // whether a computed address can land on one register (frontend/frame-objects.ts: a store after a
592
+ // DMA arming that may hit the channel's control halfword, a word store that hands an address to a
593
+ // channel's source register). One decomposition answers both, so they cannot disagree on a store.
594
+
595
+ /** What a linear form may read: each value's def and the constant a value is. */
596
+ export interface LinearCtx {
597
+ readonly defs: ReadonlyMap<Value, Op>;
598
+ constOf(v: Value): number | undefined;
599
+ }
600
+
601
+ /** `v` as a sum of runtime values times constants, plus a constant (the `1` key): agbcc's `ch *
602
+ * 12` is `((ch << 1) + ch) << 2`, which is 12 times `ch` only once the two `ch` terms are added.
603
+ * Every rule is a ring operation, so the form equals the machine's word modulo 2^32 whatever
604
+ * wraps on the way. A value this cannot read further is its own term. */
605
+ export function linearTerms(ctx: LinearCtx, v: Value, depth = 0): Map<Value | 1, number> | undefined {
606
+ const c = ctx.constOf(v);
607
+ if (c !== undefined) {
608
+ return new Map([[1, c]]);
609
+ }
610
+ const d = ctx.defs.get(v);
611
+ if (d === undefined || depth > 8) {
612
+ return new Map([[v, 1]]);
613
+ }
614
+ const scaled = (x: Value, by: number) => {
615
+ const l = linearTerms(ctx, x, depth + 1);
616
+ return l && new Map([...l].map(([t, k]) => [t, k * by] as const));
617
+ };
618
+ const k = d.operands.length === 1 ? (d.attrs.imm as number | undefined) : ctx.constOf(d.operands[1]);
619
+ if (d.opcode === 'shl' && k !== undefined && k >= 0 && k < 31) {
620
+ return scaled(d.operands[0], 2 ** k);
621
+ }
622
+ if (d.opcode === 'mul' && d.operands.length === 2 && k !== undefined) {
623
+ return scaled(d.operands[0], k);
624
+ }
625
+ if ((d.opcode === 'add' || d.opcode === 'sub') && d.operands.length === 2) {
626
+ const x = linearTerms(ctx, d.operands[0], depth + 1);
627
+ const y = scaled(d.operands[1], d.opcode === 'add' ? 1 : -1);
628
+ if (x === undefined || y === undefined) {
629
+ return undefined;
630
+ }
631
+ y.forEach((n, t) => x.set(t, (x.get(t) ?? 0) + n));
632
+ return x;
633
+ }
634
+ return new Map([[v, 1]]);
635
+ }
636
+
637
+ /** A number every value of `v` is a multiple of, or 0 when this cannot name one. */
638
+ export function linearFactor(ctx: LinearCtx, v: Value): number {
639
+ const l = linearTerms(ctx, v);
640
+ return l === undefined ? 0 : [...l.values()].reduce(gcdOf, 0);
641
+ }
642
+
643
+ /** Every word a value may hold is `lo + stride * n` for some `n >= 0`, and lies in `[lo, hi]`, an
644
+ * unsigned closed interval. `stride` is 0 exactly when `lo === hi`. */
645
+ export interface StridedRange {
646
+ readonly lo: number;
647
+ readonly hi: number;
648
+ readonly stride: number;
649
+ }
650
+
651
+ /** What a strided range may read beyond a linear form. */
652
+ export interface RangeCtx extends LinearCtx {
653
+ /** block param -> the value each in-edge passes (`incomingArgs`) */
654
+ readonly incoming: ReadonlyMap<Value, readonly (Value | undefined)[]>;
655
+ /** the function's own parameters, which arrive from a caller and hold anything */
656
+ readonly entry: ReadonlySet<Value>;
657
+ /** the one word a term the form cannot read is known to be — a name a symbol map places */
658
+ pointOf?(v: Value): number | undefined;
659
+ }
660
+
661
+ const WORD = 2 ** 32;
662
+ /** The most terms one `stridedRange` question bounds before it answers nothing. */
663
+ const RANGE_BUDGET = 256;
664
+
665
+ /** Every word `v` may hold, or undefined when this cannot bound it. The form is `linearTerms`; a
666
+ * term is bounded by the word a caller places it at (`pointOf`), by `provableBits` (a zero-fill
667
+ * shift right, an unsigned narrow load, a non-negative mask) or a `zext`'s width, or, a block
668
+ * param, by the union of what its in-edges pass. Anything else — a function parameter, a whole
669
+ * word loaded from memory, a name nothing places, a param whose range depends on itself through a
670
+ * loop — bounds nothing, and nothing is the answer. The form equals the word modulo 2^32, so the
671
+ * words are the sum's integer range moved into one word, when it fits one; a sum whose range
672
+ * crosses a multiple of 2^32 holds words on both sides of zero, which no interval names. Exact is
673
+ * not the point: an answer must hold every word the machine can compute there, and a wider one
674
+ * only proves less.
675
+ *
676
+ * KNOWN GAP: a signed narrow index bounds nothing. Every leaf is unsigned (`provableBits` reads a
677
+ * signed value as 32 bits), and each in-edge of a param is moved into one word before the union,
678
+ * so `8 * (s8)i` — [-1024, 1016], across zero — names no interval even with a signed leaf. Both
679
+ * would have to change; no real GBA DMA function measured yet needs either. */
680
+ export function stridedRange(ctx: RangeCtx, v: Value): StridedRange | undefined {
681
+ const open = new Set<Value>();
682
+ let left = RANGE_BUDGET;
683
+ const bits: BitsCtx = {
684
+ defs: ctx.defs as Map<Value, Op>,
685
+ bound: (d) => (d.opcode === 'zext' ? (d.attrs.width as number) : null),
686
+ };
687
+ const termRange = (t: Value): StridedRange | undefined => {
688
+ const at = ctx.pointOf?.(t);
689
+ if (at !== undefined) {
690
+ return { lo: mod(at), hi: mod(at), stride: 0 };
691
+ }
692
+ if (ctx.defs.has(t)) {
693
+ const n = provableBits(bits, t);
694
+ return n < 32 ? { lo: 0, hi: 2 ** n - 1, stride: n === 0 ? 0 : 1 } : undefined;
695
+ }
696
+ const ins = ctx.entry.has(t) ? undefined : ctx.incoming.get(t);
697
+ if (ins === undefined || ins.length === 0 || open.has(t) || --left < 0) {
698
+ return undefined;
699
+ }
700
+ open.add(t);
701
+ try {
702
+ let out: StridedRange | undefined;
703
+ for (const x of ins) {
704
+ const r = x === undefined ? undefined : rangeOf(x);
705
+ if (r === undefined) {
706
+ return undefined;
707
+ }
708
+ out =
709
+ out === undefined
710
+ ? r
711
+ : {
712
+ lo: Math.min(out.lo, r.lo),
713
+ hi: Math.max(out.hi, r.hi),
714
+ stride: gcdOf(gcdOf(out.stride, r.stride), r.lo - out.lo),
715
+ };
716
+ }
717
+ return out;
718
+ } finally {
719
+ open.delete(t);
720
+ }
721
+ };
722
+ const rangeOf = (x: Value): StridedRange | undefined => {
723
+ const form = linearTerms(ctx, x);
724
+ if (form === undefined) {
725
+ return undefined;
726
+ }
727
+ let lo = 0;
728
+ let hi = 0;
729
+ let stride = 0;
730
+ for (const [t, c] of form) {
731
+ if (c === 0) {
732
+ continue;
733
+ }
734
+ const r = t === 1 ? { lo: 1, hi: 1, stride: 0 } : termRange(t);
735
+ if (r === undefined) {
736
+ return undefined;
737
+ }
738
+ lo += c > 0 ? c * r.lo : c * r.hi;
739
+ hi += c > 0 ? c * r.hi : c * r.lo;
740
+ stride = gcdOf(stride, c * r.stride);
741
+ }
742
+ if (!Number.isSafeInteger(lo) || !Number.isSafeInteger(hi)) {
743
+ return undefined;
744
+ }
745
+ const base = Math.floor(lo / WORD) * WORD;
746
+ return hi - base >= WORD ? undefined : { lo: lo - base, hi: hi - base, stride };
747
+ };
748
+ return rangeOf(v);
749
+ }
750
+
751
+ const mod = (w: number): number => ((w % WORD) + WORD) % WORD;
752
+
753
+ function gcdOf(a: number, b: number): number {
754
+ return b === 0 ? Math.abs(a) : gcdOf(b, a % b);
755
+ }