@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
package/src/ir/bits.ts CHANGED
@@ -10,12 +10,13 @@
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, type Fn, type Op, type Value, defOpMap, immediateDominators } from './core';
14
+ import { NEGATED_ICMP, OPCODES, type OpSig, SWAPPED_ICMP } 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. */
17
18
  export interface BitsCtx {
18
- defs: Map<Value, Op>;
19
+ defs: ReadonlyMap<Value, Op>;
19
20
  materialize?: ReadonlySet<Op>;
20
21
  /** A bound a CALLER already knows for a def — the escape hatch for facts this layer cannot see,
21
22
  * such as a bitfield read a rendering pass has recognized and can price from the declaration.
@@ -42,9 +43,660 @@ 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
+ /** The function's own parameters. Given beside `incoming`, `mayAlwaysHold` also bounds a block
68
+ * param by the ranges its in-edges pass (`stridedRange`); without it no param is bounded. */
69
+ entry?: ReadonlySet<Value>;
70
+ }
71
+
72
+ /** A value's word, over a lattice: not yet seen (`top`, a loop-carried param before its back
73
+ * edge), one known word, a CONSTANT this evaluator cannot compute (`konst`: a pure op of words
74
+ * it has no rule for, or a register shift by 32 or more, which each ISA reads differently), or a
75
+ * value that varies (`var`). */
76
+ type Word = { k: 'top' } | { k: 'word'; w: number } | { k: 'konst' } | { k: 'var' };
77
+ const TOP: Word = { k: 'top' };
78
+ const KONST: Word = { k: 'konst' };
79
+ const VAR: Word = { k: 'var' };
80
+ const word = (w: number): Word => ({ k: 'word', w: w | 0 });
81
+ const sameWord = (a: Word, b: Word): boolean => a.k === b.k && (a.k !== 'word' || a.w === (b as { w: number }).w);
82
+ const meet = (a: Word, b: Word): Word =>
83
+ a.k === 'top'
84
+ ? b
85
+ : b.k === 'top'
86
+ ? a
87
+ : a.k === 'var' || b.k === 'var'
88
+ ? VAR
89
+ : a.k === 'word' && b.k === 'word'
90
+ ? a.w === b.w
91
+ ? a
92
+ : VAR
93
+ : KONST;
94
+
95
+ const BINARY: Readonly<Record<string, (a: number, c: number) => number>> = {
96
+ add: (a, c) => a + c,
97
+ sub: (a, c) => a - c,
98
+ mul: (a, c) => Math.imul(a, c),
99
+ and: (a, c) => a & c,
100
+ or: (a, c) => a | c,
101
+ xor: (a, c) => a ^ c,
102
+ };
103
+ const SHIFT: Readonly<Record<string, (a: number, c: number) => number>> = {
104
+ shl: (a, c) => a << c,
105
+ shr_u: (a, c) => a >>> c,
106
+ shr_s: (a, c) => a >> c,
107
+ };
108
+
109
+ /** One op's word, given its operands'. The annihilators come first because they hold whatever
110
+ * the other operand is: `x & 0`, `x | -1`, `x * 0`, `x - x`, `x ^ x`. */
111
+ function opWord(d: Op, of: (v: Value) => Word): Word {
112
+ if (d.opcode === 'const') {
113
+ const value = d.attrs.value;
114
+ return typeof value === 'number'
115
+ ? word(value)
116
+ : typeof value === 'bigint'
117
+ ? word(Number(BigInt.asIntN(32, value)))
118
+ : KONST;
119
+ }
120
+ const sig = (OPCODES as Record<string, OpSig | undefined>)[d.opcode];
121
+ if (!sig || sig.effects || sig.reads || sig.terminator || d.results.length !== 1 || d.operands.length === 0) {
122
+ return VAR; // a load, a call, an address (`gaddr`: the linker's, not a word a compiler folds)
123
+ }
124
+ const imm = typeof d.attrs.imm === 'number' ? word(d.attrs.imm as number) : null;
125
+ const ws = d.operands.map(of);
126
+ const [a, c] = d.operands.length === 1 && imm !== null ? [ws[0], imm] : ws;
127
+ const is = (x: Word | undefined, n: number) => x?.k === 'word' && x.w === n;
128
+ if (
129
+ (d.opcode === 'and' && (is(a, 0) || is(c, 0))) ||
130
+ (d.opcode === 'mul' && (is(a, 0) || is(c, 0))) ||
131
+ ((d.opcode === 'sub' || d.opcode === 'xor') && d.operands.length === 2 && d.operands[0] === d.operands[1])
132
+ ) {
133
+ return word(0);
134
+ }
135
+ if (d.opcode === 'or' && (is(a, -1) || is(c, -1))) {
136
+ return word(-1);
137
+ }
138
+ const all = d.operands.length === 1 && imm !== null ? [a, c] : ws;
139
+ // An operand not yet seen comes BEFORE one that varies. Once it settles it may be the word an
140
+ // annihilator above needs (`q & x` with q a param that settles to 0), and an answer of VAR now
141
+ // would drop a param that reads it for good: the solver below would then reach whichever
142
+ // fixpoint the param order made, and the imprecise one answers `mayAlwaysHold` false.
143
+ if (all.some((x) => x.k === 'top')) {
144
+ return TOP;
145
+ }
146
+ if (all.some((x) => x.k === 'var')) {
147
+ return VAR;
148
+ }
149
+ if (all.some((x) => x.k !== 'word')) {
150
+ return KONST;
151
+ }
152
+ const n = all.map((x) => (x as { w: number }).w);
153
+ if ((d.opcode === 'neg' || d.opcode === 'not') && n.length === 1) {
154
+ return word(d.opcode === 'neg' ? -n[0] : ~n[0]);
155
+ }
156
+ // The mathematical negation is the wrapping one at every word but INT_MIN, whose negation
157
+ // (+2^31) is a constant and no word.
158
+ if (d.opcode === 'neg_s' && n.length === 1) {
159
+ return n[0] === -0x80000000 ? KONST : word(-n[0]);
160
+ }
161
+ if (n.length === 2 && d.opcode in BINARY) {
162
+ return word(BINARY[d.opcode](n[0], n[1]));
163
+ }
164
+ if (n.length === 2 && d.opcode in SHIFT) {
165
+ return n[1] >= 0 && n[1] < 32 ? word(SHIFT[d.opcode](n[0], n[1])) : KONST;
166
+ }
167
+ return KONST;
168
+ }
169
+
170
+ /** Solve every block param `ctx.incoming` names, optimistically: each starts unseen and drops to
171
+ * the meet of what its in-edges pass, until nothing moves — so a loop-carried param that only
172
+ * ever passes itself back keeps the word it entered with, and a counter (`0`, then `i + 1`)
173
+ * varies. A param's new state is met with its old one, so it only ever drops, and drops at most
174
+ * three times; with `opWord` monotone (an unseen operand is TOP before a varying one is VAR)
175
+ * that meet changes nothing, and the fixpoint is the same in every param order.
176
+ *
177
+ * A forwarded load is solved the same way, its forwarded stores as its in-edges: memory carried
178
+ * round a loop is a param the frontend did not name, and a loop that stores a reload back
179
+ * (`ldr r2, [r3]; …; str r2, [r3]`) passes it itself, which must keep the word it entered with. */
180
+ function solveParams(ctx: WordCtx): Map<Value, Word> {
181
+ const state = new Map<Value, Word>();
182
+ const incoming = new Map<Value, readonly (Value | undefined)[]>(ctx.incoming ?? []);
183
+ for (const [load, vals] of ctx.forwarded ?? []) {
184
+ if (load.results.length === 1) {
185
+ incoming.set(load.results[0], vals);
186
+ }
187
+ }
188
+ for (const p of incoming.keys()) {
189
+ state.set(p, TOP);
190
+ }
191
+ for (let round = 0; ; round++) {
192
+ if (round > 3 * state.size + 1) {
193
+ throw new Error('constWord: block params did not settle');
194
+ }
195
+ const memo = new Map<Value, Word>();
196
+ let moved = false;
197
+ for (const [p, args] of incoming) {
198
+ let w: Word = TOP;
199
+ for (const x of args) {
200
+ w = meet(w, x === undefined ? VAR : x === p ? TOP : evalWord(ctx, state, memo, x));
201
+ }
202
+ w = meet(state.get(p)!, w);
203
+ if (!sameWord(w, state.get(p)!)) {
204
+ state.set(p, w);
205
+ moved = true;
206
+ }
207
+ }
208
+ if (!moved) {
209
+ return state;
210
+ }
211
+ }
212
+ }
213
+
214
+ function evalWord(ctx: WordCtx, params: ReadonlyMap<Value, Word>, memo: Map<Value, Word>, v: Value): Word {
215
+ if (ctx.opaque?.(v)) {
216
+ return VAR;
217
+ }
218
+ const p = params.get(v);
219
+ if (p !== undefined) {
220
+ return p;
221
+ }
222
+ const seen = memo.get(v);
223
+ if (seen !== undefined) {
224
+ return seen;
225
+ }
226
+ const d = ctx.defs.get(v);
227
+ // In progress reads VAR: SSA has no cycle but through a param, and a forwarded load is one
228
+ // (`solveParams`), so nothing reaches this mark but malformed IR.
229
+ memo.set(v, VAR);
230
+ const w = d === undefined ? VAR : ctx.overwritten?.has(d) ? KONST : opWord(d, (x) => evalWord(ctx, params, memo, x));
231
+ memo.set(v, w);
232
+ return w;
233
+ }
234
+
235
+ const solved = new WeakMap<WordCtx, Map<Value, Word>>();
236
+ function wordOf(ctx: WordCtx, v: Value): Word {
237
+ let params = solved.get(ctx);
238
+ if (params === undefined) {
239
+ params = solveParams(ctx);
240
+ solved.set(ctx, params);
241
+ }
242
+ return evalWord(ctx, params, new Map(), v);
243
+ }
244
+
245
+ /** THE WORD A VALUE ALWAYS HOLDS — what the machine computes, when every input to it is a constant
246
+ * (a pool word, a `mov` and a shift of it, any integer arithmetic over those, an operand an
247
+ * annihilator makes irrelevant, a block param every in-edge passes the same word), or null.
248
+ * Unlike `constMask` it reads THROUGH a materialized def: it answers what the machine holds, not
249
+ * whether a spelling may fold it. Its readers: the structurer's `neg_s`, switch recovery's case
250
+ * constants, the float helpers' literal words. An opcode it has no rule for is no word, so a new
251
+ * op whose value is not the plain machine word fails closed here; `neg_s` has its own rule. */
252
+ export function constWord(ctx: WordCtx, v: Value): number | null {
253
+ const w = wordOf(ctx, v);
254
+ return w.k === 'word' ? w.w : null;
255
+ }
256
+
257
+ /** Whether a value MAY hold `w` on every execution — it is that constant, a constant this
258
+ * evaluator cannot compute, or a value the solver never settled (a param no executed edge
259
+ * feeds). The fail-closed reading of `constWord`, for a caller to whom a constant it cannot name
260
+ * is as bad as the one it is looking for. A value that varies, or that is `w` on some paths only,
261
+ * answers false — and so does one that cannot hold `w` at all by what its ops leave
262
+ * (`stridedRange`'s bounds, which read widths and arithmetic, never memory): a halfword reload of
263
+ * memory this function overwrote is a constant this cannot compute, but never INT_MIN, and nor is
264
+ * a param both of whose arms pass such a reload. */
265
+ export function mayAlwaysHold(ctx: WordCtx, v: Value, w: number): boolean {
266
+ const rc = rangeCtxOf(ctx);
267
+ for (const wordFirst of [false, true]) {
268
+ const r = integerRange(rc, v, wordFirst);
269
+ if (r !== undefined && !rangeMayBe(r, w)) {
270
+ return false;
271
+ }
272
+ }
273
+ const x = wordOf(ctx, v);
274
+ return x.k === 'konst' || x.k === 'top' || (x.k === 'word' && x.w === (w | 0));
275
+ }
276
+
277
+ /** The range question's reading of a word context. A block param is read through its in-edges
278
+ * only when the caller names the function's own params (`entry`), which arrive from a caller and
279
+ * hold anything even where a back edge passes them a bounded value. */
280
+ function rangeCtxOf(ctx: WordCtx): RangeCtx {
281
+ const entry = ctx.entry;
282
+ return {
283
+ defs: ctx.defs,
284
+ constOf: (x) => {
285
+ const d = ctx.defs.get(x);
286
+ return d?.opcode === 'const' && typeof d.attrs.value === 'number' ? d.attrs.value : undefined;
287
+ },
288
+ incoming:
289
+ entry === undefined || ctx.incoming === undefined
290
+ ? new Map()
291
+ : new Map([...ctx.incoming].map(([p, args]) => [p, args.map((arg) => ({ arg }))])),
292
+ entry: entry ?? new Set(),
293
+ };
294
+ }
295
+
296
+ /** Where an access lands, normalised: a ROOT (one SSA value, one symbol, or the absolute address
297
+ * space) and a byte offset from it. `add`/`sub` of a constant move along the root, so a base held
298
+ * in another register (`add r2, r3, #4`), an addend in the pool word (`.word gX+4`) and the same
299
+ * offset carried by the access all name one cell. Two keys on one root overlap exactly when their
300
+ * byte ranges do; keys on two roots are two pointers nothing here can tell apart. */
301
+ interface Cell {
302
+ root: Value | string;
303
+ byte: number;
304
+ width: number;
305
+ }
306
+
307
+ /** The blocks a path from the entry (`blocks[0]`) reaches. A block nothing reaches, and every
308
+ * edge out of one, is no path in: a dead block falling into a read must not pass for a way in
309
+ * that wrote nothing. */
310
+ function reachable(blocks: readonly Block[]): Set<Block> {
311
+ const seen = new Set<Block>(blocks.slice(0, 1));
312
+ const work = blocks.slice(0, 1);
313
+ while (work.length > 0) {
314
+ for (const o of work.pop()!.ops) {
315
+ for (const s of o.successors) {
316
+ if (!seen.has(s.block)) {
317
+ seen.add(s.block);
318
+ work.push(s.block);
319
+ }
320
+ }
321
+ }
322
+ }
323
+ return seen;
324
+ }
325
+
326
+ /** Two cells may be the same memory: on one root when their bytes overlap, and always on two. */
327
+ const overlaps = (x: Cell, y: Cell): boolean =>
328
+ x.root !== y.root || (x.byte < y.byte + y.width && y.byte < x.byte + x.width);
329
+
330
+ function cellOf(defs: ReadonlyMap<Value, Op>, addr: Value, off: number, width: number): Cell {
331
+ let root: Value | string = addr;
332
+ let byte = off;
333
+ for (let hops = 0; hops < 64 && typeof root !== 'string'; hops++) {
334
+ const d = defs.get(root);
335
+ if (d === undefined) {
336
+ break;
337
+ }
338
+ if (d.opcode === 'gaddr') {
339
+ root = `sym:${d.attrs.sym as string}`;
340
+ break;
341
+ }
342
+ if (d.opcode === 'const' && typeof d.attrs.value === 'number') {
343
+ root = 'abs';
344
+ byte += d.attrs.value;
345
+ break;
346
+ }
347
+ const imm = typeof d.attrs.imm === 'number' ? d.attrs.imm : null;
348
+ if ((d.opcode === 'add' || d.opcode === 'sub') && d.operands.length === 1 && imm !== null) {
349
+ root = d.operands[0];
350
+ byte += d.opcode === 'add' ? imm : -imm;
351
+ continue;
352
+ }
353
+ if ((d.opcode === 'add' || d.opcode === 'sub') && d.operands.length === 2) {
354
+ const k = (v: Value): number | null => {
355
+ const c = defs.get(v);
356
+ return c?.opcode === 'const' && typeof c.attrs.value === 'number' ? c.attrs.value : null;
357
+ };
358
+ const [a, b] = d.operands;
359
+ if (k(b) !== null) {
360
+ root = a;
361
+ byte += d.opcode === 'add' ? k(b)! : -k(b)!;
362
+ continue;
363
+ }
364
+ if (d.opcode === 'add' && k(a) !== null) {
365
+ root = b;
366
+ byte += k(a)!;
367
+ continue;
368
+ }
369
+ }
370
+ break;
371
+ }
372
+ return { root, byte: byte | 0, width };
373
+ }
374
+
375
+ /** Whole-word load -> every value it may read, for `WordCtx.forwarded`: a forward MUST-analysis of
376
+ * the words this function's own stores leave in memory. Each block starts from the meet of what
377
+ * its predecessors leave — a cell survives a join only if every in-edge knows it, and then carries
378
+ * each in-edge's stored value — and a predecessor not yet reached (a loop's back edge, on the
379
+ * first pass) is optimistic and adds nothing, so a store before a loop that nothing in the loop
380
+ * overwrites reaches a reload in its header. A store sets its cell and forgets every cell it may
381
+ * overlap (any cell on another root, which may be the same memory); any other op with effects (a
382
+ * call, an indexed store) forgets everything. A load is forwarded when every path into it knows
383
+ * its cell, as a whole word; the entry block knows nothing, and a block the entry does not reach
384
+ * forwards nothing and is no path into the blocks it jumps to. */
385
+ export function storeForwards(blocks: readonly Block[]): Map<Op, Value[]> {
386
+ const defs = new Map<Value, Op>();
387
+ const preds = new Map<Block, Block[]>();
388
+ const live = reachable(blocks);
389
+ for (const b of blocks) {
390
+ for (const op of b.ops) {
391
+ for (const r of op.results) {
392
+ defs.set(r, op);
393
+ }
394
+ for (const s of op.successors) {
395
+ if (live.has(b)) {
396
+ preds.set(s.block, [...(preds.get(s.block) ?? []), b]);
397
+ }
398
+ }
399
+ }
400
+ }
401
+ type Mem = Map<string, { cell: Cell; vals: Value[] }>;
402
+ const ids = new Map<Value, number>();
403
+ const keyOf = (c: Cell): string =>
404
+ `${typeof c.root === 'string' ? c.root : `v${ids.get(c.root) ?? ids.set(c.root, ids.size).get(c.root)!}`}@${c.byte}:${c.width}`;
405
+ const cells = new Map<Op, Cell>();
406
+ for (const b of blocks) {
407
+ for (const op of b.ops) {
408
+ if ((op.opcode === 'load' || op.opcode === 'store') && typeof op.attrs.width === 'number') {
409
+ cells.set(op, cellOf(defs, op.operands[0], (op.attrs.off as number | undefined) ?? 0, op.attrs.width));
410
+ }
411
+ }
412
+ }
413
+ const out = new Map<Op, Value[]>();
414
+ // One pass over a block: from its entry state, record each whole-word load's forward (when
415
+ // `record`) and return its exit state.
416
+ const run = (b: Block, mem: Mem, record: boolean): Mem => {
417
+ const m: Mem = new Map(mem);
418
+ for (const op of b.ops) {
419
+ const cell = cells.get(op);
420
+ if (op.opcode === 'load') {
421
+ const known = cell === undefined || cell.width !== 4 ? undefined : m.get(keyOf(cell));
422
+ if (record && known !== undefined) {
423
+ out.set(op, known.vals);
424
+ }
425
+ continue;
426
+ }
427
+ if (op.opcode === 'store' && cell !== undefined) {
428
+ for (const [k, e] of m) {
429
+ if (overlaps(e.cell, cell)) {
430
+ m.delete(k);
431
+ }
432
+ }
433
+ m.set(keyOf(cell), { cell, vals: [op.operands[1]] });
434
+ continue;
435
+ }
436
+ const sig = (OPCODES as Record<string, OpSig | undefined>)[op.opcode];
437
+ if ((sig === undefined || sig.effects) && !sig?.terminator) {
438
+ m.clear();
439
+ }
440
+ }
441
+ return m;
442
+ };
443
+ const meetMem = (a: Mem, b: Mem): Mem => {
444
+ const m: Mem = new Map();
445
+ for (const [k, x] of a) {
446
+ const y = b.get(k);
447
+ if (y !== undefined) {
448
+ m.set(k, { cell: x.cell, vals: [...new Set([...x.vals, ...y.vals])] });
449
+ }
450
+ }
451
+ return m;
452
+ };
453
+ const sameMem = (a: Mem, b: Mem): boolean =>
454
+ a.size === b.size &&
455
+ [...a].every(([k, x]) => {
456
+ const y = b.get(k);
457
+ return y !== undefined && y.vals.length === x.vals.length && x.vals.every((v) => y.vals.includes(v));
458
+ });
459
+ // Exit states; a block absent from it is not reached yet (TOP). Cells only ever leave a state
460
+ // and values only ever join one, both finite, so this settles.
461
+ const exit = new Map<Block, Mem>();
462
+ const entryOf = (b: Block): Mem | undefined => {
463
+ if (!live.has(b)) {
464
+ return undefined; // dead: it records nothing and adds nothing to a block it falls into
465
+ }
466
+ const from = preds.get(b) ?? [];
467
+ if (b === blocks[0]) {
468
+ return new Map();
469
+ }
470
+ let m: Mem | undefined;
471
+ for (const p of from) {
472
+ const e = exit.get(p);
473
+ if (e !== undefined) {
474
+ m = m === undefined ? e : meetMem(m, e);
475
+ }
476
+ }
477
+ return m;
478
+ };
479
+ for (let moved = true, round = 0; moved; round++) {
480
+ if (round > 4 * blocks.length + 64) {
481
+ throw new Error('storeForwards: memory states did not settle');
482
+ }
483
+ moved = false;
484
+ for (const b of blocks) {
485
+ const at = entryOf(b);
486
+ if (at === undefined) {
487
+ continue;
488
+ }
489
+ const e = run(b, at, false);
490
+ const old = exit.get(b);
491
+ if (old === undefined || !sameMem(old, e)) {
492
+ exit.set(b, e);
493
+ moved = true;
494
+ }
495
+ }
496
+ }
497
+ for (const b of blocks) {
498
+ const at = entryOf(b);
499
+ if (at !== undefined) {
500
+ run(b, at, true);
501
+ }
502
+ }
503
+ return out;
504
+ }
505
+
506
+ /** Every read (`load`, `aload`) that each path from the function's entry reaches only past a write
507
+ * that may land on its cell, for `WordCtx.overwritten`: a store to an overlapping cell or to
508
+ * another root (which may be the same memory), and any other op with effects. What such a read
509
+ * sees is what this function wrote, on every path, and when `storeForwards` cannot say which word
510
+ * that was — a register-indexed address, two spellings of one sum, narrow stores that compose a
511
+ * word, a write through another pointer — the read must not pass for one that varies. A read with
512
+ * one write-free path in from the entry sees the caller's memory on it, which varies; a block the
513
+ * entry does not reach is no such path. An `aload`'s cell is never known, so every write may land
514
+ * on it. */
515
+ export function overwrittenReads(blocks: readonly Block[]): Set<Op> {
516
+ const defs = new Map<Value, Op>();
517
+ for (const b of blocks) {
518
+ for (const op of b.ops) {
519
+ for (const r of op.results) {
520
+ defs.set(r, op);
521
+ }
522
+ }
523
+ }
524
+ const cellAt = (op: Op): Cell | undefined =>
525
+ (op.opcode === 'load' || op.opcode === 'store') && typeof op.attrs.width === 'number'
526
+ ? cellOf(defs, op.operands[0], (op.attrs.off as number | undefined) ?? 0, op.attrs.width)
527
+ : undefined;
528
+ const writesOver = (op: Op, cell: Cell | undefined): boolean => {
529
+ if (op.opcode === 'load' || op.opcode === 'aload') {
530
+ return false;
531
+ }
532
+ if (op.opcode === 'store') {
533
+ const at = cellAt(op);
534
+ return at === undefined || cell === undefined || overlaps(at, cell);
535
+ }
536
+ const sig = (OPCODES as Record<string, OpSig | undefined>)[op.opcode];
537
+ return (sig === undefined || sig.effects === true) && !sig?.terminator;
538
+ };
539
+ const roots = blocks.slice(0, 1);
540
+ const out = new Set<Op>();
541
+ for (const b of blocks) {
542
+ b.ops.forEach((op, i) => {
543
+ if (op.opcode !== 'load' && op.opcode !== 'aload') {
544
+ return;
545
+ }
546
+ const cell = cellAt(op);
547
+ // The blocks whose entry a write-free path reaches.
548
+ const clean = new Set<Block>(roots);
549
+ const work = [...roots];
550
+ while (work.length > 0) {
551
+ const x = work.pop()!;
552
+ if (x.ops.some((o) => writesOver(o, cell))) {
553
+ continue;
554
+ }
555
+ for (const o of x.ops) {
556
+ for (const s of o.successors) {
557
+ if (!clean.has(s.block)) {
558
+ clean.add(s.block);
559
+ work.push(s.block);
560
+ }
561
+ }
562
+ }
563
+ }
564
+ if (!clean.has(b) || b.ops.slice(0, i).some((o) => writesOver(o, cell))) {
565
+ out.add(op);
566
+ }
567
+ });
568
+ }
569
+ return out;
570
+ }
571
+
572
+ /** Block param -> the values its in-edges pass, for `WordCtx.incoming`: the edges alone, without
573
+ * what their branches prove (`edgeFacts`), which a word context does not read. */
574
+ export function incomingArgs(blocks: readonly Block[]): Map<Value, (Value | undefined)[]> {
575
+ const out = new Map<Value, (Value | undefined)[]>();
576
+ for (const b of blocks) {
577
+ for (const op of b.ops) {
578
+ for (const s of op.successors) {
579
+ s.block.params.forEach((p, i) => {
580
+ if (!out.has(p)) {
581
+ out.set(p, []);
582
+ }
583
+ out.get(p)!.push(s.args[i]);
584
+ });
585
+ }
586
+ }
587
+ }
588
+ return out;
589
+ }
590
+
591
+ /** A comparison an edge proves: `lhs <opcode> rhs` holds whenever control takes it. */
592
+ export interface EdgeTest {
593
+ readonly opcode: string;
594
+ readonly lhs: Value;
595
+ readonly rhs: Value;
596
+ }
597
+
598
+ /** One in-edge of a block param: the value it passes, and what the branches on the way prove. */
599
+ export interface InEdge {
600
+ readonly arg: Value | undefined;
601
+ readonly holds?: readonly EdgeTest[];
602
+ }
603
+
604
+ /** An edge: its branch `op` in block `from`, to successor `si`. */
605
+ interface EdgeOut {
606
+ readonly from: Block;
607
+ readonly op: Op;
608
+ readonly si: number;
609
+ }
610
+
611
+ /** The most compares one edge or one block carries, the nearest first; an older one is dropped. */
612
+ const EDGE_TESTS = 8;
613
+
614
+ /** What the branches above a point prove there: for each block, the compares that hold whenever
615
+ * control is in it, and for each edge, the compares that hold whenever it is taken. A `cond_br` on
616
+ * an `icmp_*` proves the compare on its first successor and the negation (`NEGATED_ICMP`) on its
617
+ * second; an edge out of a `cond_br` whose two successors are one block proves nothing — the same
618
+ * edge is taken either way — and nor does any other branch. A block holds what the one edge into
619
+ * each block that DOMINATES it proves (itself included; a block entered by more than one edge, or
620
+ * the entry, proves nothing of its way in), so a test at the top of a loop body still holds past
621
+ * an if/else that joins below it, and agbcc's far latch `bhi .LCB; b .Lhead @long jump` carries
622
+ * the test it made in one block to the jump back from the next. An edge holds its own compare and
623
+ * what its source block holds. Sound because a compare's operands are values: each was defined in
624
+ * a block that dominates the test, and control that passes a dominating edge and reaches the point
625
+ * without passing it again cannot have redefined one (that block would be reachable around the
626
+ * edge), so the instance each test read is the one the point reads. */
627
+ export interface EdgeFacts {
628
+ /** block param -> its in-edges, each with what holds whenever it is taken */
629
+ readonly incoming: Map<Value, InEdge[]>;
630
+ /** the compares that hold in a block, the nearest first */
631
+ holdsAt(b: Block): readonly EdgeTest[];
632
+ }
633
+
634
+ /** `EdgeFacts` over a function's blocks, `blocks[0]` the entry. */
635
+ export function edgeFacts(blocks: readonly Block[]): EdgeFacts {
636
+ const defs = new Map<Value, Op>();
637
+ /** a block -> the one edge into it, or null where there are more, or the caller enters it */
638
+ const only = new Map<Block, EdgeOut | null>();
639
+ if (blocks[0] !== undefined) {
640
+ only.set(blocks[0], null);
641
+ }
642
+ for (const b of blocks) {
643
+ for (const op of b.ops) {
644
+ op.results.forEach((r) => defs.set(r, op));
645
+ op.successors.forEach((s, si) => only.set(s.block, only.has(s.block) ? null : { from: b, op, si }));
646
+ }
647
+ }
648
+ const idom = immediateDominators({ blocks });
649
+ const tested = (op: Op, si: number): EdgeTest | undefined => {
650
+ const c = op.opcode === 'cond_br' ? defs.get(op.operands[0]) : undefined;
651
+ return c !== undefined &&
652
+ c.operands.length === 2 &&
653
+ NEGATED_ICMP[c.opcode] !== undefined &&
654
+ op.successors.length === 2 &&
655
+ op.successors[0].block !== op.successors[1].block
656
+ ? { opcode: si === 0 ? c.opcode : NEGATED_ICMP[c.opcode], lhs: c.operands[0], rhs: c.operands[1] }
657
+ : undefined;
658
+ };
659
+ const at = new Map<Block, readonly EdgeTest[]>();
660
+ const holdsAt = (b: Block): readonly EdgeTest[] => {
661
+ const had = at.get(b);
662
+ if (had !== undefined) {
663
+ return had;
664
+ }
665
+ const out: EdgeTest[] = [];
666
+ for (let x: Block | undefined = b; x !== undefined && out.length < EDGE_TESTS; x = idom.get(x)) {
667
+ const e = only.get(x);
668
+ const t = e === null || e === undefined ? undefined : tested(e.op, e.si);
669
+ if (t !== undefined) {
670
+ out.push(t);
671
+ }
672
+ }
673
+ at.set(b, out);
674
+ return out;
675
+ };
676
+ const incoming = new Map<Value, InEdge[]>();
677
+ for (const b of blocks) {
678
+ for (const op of b.ops) {
679
+ op.successors.forEach((s, si) => {
680
+ if (s.block.params.length === 0) {
681
+ return;
682
+ }
683
+ const own = tested(op, si);
684
+ const holds = [...(own === undefined ? [] : [own]), ...holdsAt(b)].slice(0, EDGE_TESTS);
685
+ s.block.params.forEach((p, i) => {
686
+ if (!incoming.has(p)) {
687
+ incoming.set(p, []);
688
+ }
689
+ incoming.get(p)!.push(holds.length === 0 ? { arg: s.args[i] } : { arg: s.args[i], holds });
690
+ });
691
+ });
692
+ }
693
+ }
694
+ return { incoming, holdsAt };
695
+ }
696
+
45
697
  /** An UPPER BOUND on the significant bits of a value, or 32 when nothing bounds it. Every answer
46
698
  * below 32 comes from an instruction or from a caller's declaration-backed `bound`:
47
- * · a zero-fill shift right by n leaves 32 - n;
699
+ * · a zero-fill shift right by n leaves n fewer bits than its operand has (32 - n of a word);
48
700
  * · a load leaves its width in bits — UNLESS it sign-extends, which leaves all 32;
49
701
  * · an `and` with a non-negative constant leaves that constant's top set bit.
50
702
  * A SIGNED anything is 32: sign extension sets the high bits, and a bound that ignores it turns a
@@ -59,7 +711,7 @@ export function provableBits(ctx: BitsCtx, v: Value): number {
59
711
  return given;
60
712
  }
61
713
  if (d.opcode === 'shr_u' && d.operands.length === 1 && typeof d.attrs.imm === 'number') {
62
- return 32 - (d.attrs.imm as number);
714
+ return Math.max(0, provableBits(ctx, d.operands[0]) - (d.attrs.imm as number));
63
715
  }
64
716
  if (d.opcode === 'load') {
65
717
  return d.attrs.signed === true ? 32 : (d.attrs.width as number) * 8;
@@ -73,3 +725,1046 @@ export function provableBits(ctx: BitsCtx, v: Value): number {
73
725
  : null;
74
726
  return m !== null && m >= 0 ? 32 - Math.clz32(m) : 32;
75
727
  }
728
+
729
+ // LINEAR FORMS AND STRIDED RANGES — "which words may this value be", for a caller that asks
730
+ // whether a computed address can land on one register (frontend/frame-objects.ts: a store after a
731
+ // DMA arming that may hit the channel's control halfword, a word store that hands an address to a
732
+ // channel's source register). One decomposition answers both, so they cannot disagree on a store.
733
+
734
+ /** What a linear form may read: each value's def and the constant a value is. */
735
+ export interface LinearCtx {
736
+ readonly defs: ReadonlyMap<Value, Op>;
737
+ constOf(v: Value): number | undefined;
738
+ }
739
+
740
+ /** `v` as a sum of runtime values times constants, plus a constant (the `1` key): agbcc's `ch *
741
+ * 12` is `((ch << 1) + ch) << 2`, which is 12 times `ch` only once the two `ch` terms are added.
742
+ * Every rule is a ring operation, so the form equals the machine's word modulo 2^32 whatever
743
+ * wraps on the way. A value this cannot read further is its own term. A coefficient is a JS
744
+ * number, exact only up to 2^53: one past that rounds, and a later `sub` could cancel the rounded
745
+ * coefficient to 0 and drop a term the word still holds, so a form whose coefficient leaves the
746
+ * safe integers is no form (undefined). A value in `opaque` is read no further: it is a term. */
747
+ export function linearTerms(
748
+ ctx: LinearCtx,
749
+ v: Value,
750
+ depth = 0,
751
+ opaque?: { has(v: Value): boolean },
752
+ ): Map<Value | 1, number> | undefined {
753
+ const c = ctx.constOf(v);
754
+ if (c !== undefined) {
755
+ return new Map([[1, c]]);
756
+ }
757
+ const d = opaque?.has(v) === true ? undefined : ctx.defs.get(v);
758
+ if (d === undefined || depth > 8) {
759
+ return new Map([[v, 1]]);
760
+ }
761
+ const scaled = (x: Value, by: number) => {
762
+ const l = linearTerms(ctx, x, depth + 1, opaque);
763
+ const out = l && new Map([...l].map(([t, k]) => [t, k * by] as const));
764
+ return out && [...out.values()].every(Number.isSafeInteger) ? out : undefined;
765
+ };
766
+ const k = d.operands.length === 1 ? (d.attrs.imm as number | undefined) : ctx.constOf(d.operands[1]);
767
+ if (d.opcode === 'shl' && k !== undefined && k >= 0 && k < 31) {
768
+ return scaled(d.operands[0], 2 ** k);
769
+ }
770
+ if (d.opcode === 'mul' && d.operands.length === 2 && k !== undefined) {
771
+ return scaled(d.operands[0], k);
772
+ }
773
+ // the wrapping `0 - x`, a ring operation too: agbcc's `mov rN, #2; neg rN, rN` is the constant -2
774
+ if (d.opcode === 'neg' && d.operands.length === 1) {
775
+ return scaled(d.operands[0], -1);
776
+ }
777
+ if ((d.opcode === 'add' || d.opcode === 'sub') && d.operands.length === 2) {
778
+ const x = linearTerms(ctx, d.operands[0], depth + 1, opaque);
779
+ const y = scaled(d.operands[1], d.opcode === 'add' ? 1 : -1);
780
+ if (x === undefined || y === undefined) {
781
+ return undefined;
782
+ }
783
+ y.forEach((n, t) => x.set(t, (x.get(t) ?? 0) + n));
784
+ return [...x.values()].every(Number.isSafeInteger) ? x : undefined;
785
+ }
786
+ return new Map([[v, 1]]);
787
+ }
788
+
789
+ /** A number every value of `v` is a multiple of, or 0 when this cannot name one. */
790
+ export function linearFactor(ctx: LinearCtx, v: Value): number {
791
+ const l = linearTerms(ctx, v);
792
+ return l === undefined ? 0 : [...l.values()].reduce(gcdOf, 0);
793
+ }
794
+
795
+ /** Every word a value may hold is `lo + stride * n` for some `n >= 0`, and lies in `[lo, hi]`, an
796
+ * unsigned closed interval. `stride` is 0 exactly when `lo === hi`. (Inside this module the same
797
+ * shape also carries an integer range before its move into one word, which may start below 0.) */
798
+ export interface StridedRange {
799
+ readonly lo: number;
800
+ readonly hi: number;
801
+ readonly stride: number;
802
+ }
803
+
804
+ /** What a strided range may read beyond a linear form. */
805
+ export interface RangeCtx extends LinearCtx {
806
+ /** block param -> its in-edges, each with what its branch proves where that is known
807
+ * (`edgeFacts`) */
808
+ readonly incoming: ReadonlyMap<Value, readonly InEdge[]>;
809
+ /** the function's own parameters, which arrive from a caller and hold anything */
810
+ readonly entry: ReadonlySet<Value>;
811
+ /** the one word a term the form cannot read is known to be — a name a symbol map places */
812
+ pointOf?(v: Value): number | undefined;
813
+ /** the compares that hold in a block (`edgeFacts`), for a question asked at one */
814
+ holdsAt?(b: Block): readonly EdgeTest[];
815
+ }
816
+
817
+ const WORD = 2 ** 32;
818
+ /** The most terms one range question bounds, across every own question it opens (`question`), before
819
+ * what is left answers nothing. */
820
+ const RANGE_BUDGET = 256;
821
+
822
+ /** Every word `v` may hold, or undefined when this cannot bound it. The form is `linearTerms`; a
823
+ * term is bounded by the word a caller places it at (`pointOf`), by `provableBits` (a zero-fill
824
+ * shift right, an unsigned narrow load, a non-negative mask) or a `zext`'s width, by the range a
825
+ * narrow read or `sext` leaves (`narrowLeaf`: signed ones included), by what a shift or a mask
826
+ * leaves of its operand's range (`shiftMaskRange`), a remainder by its divisor (`remainderRange`:
827
+ * `smod`, `umod`), or, a block param, by the union of what its in-edges pass, each read under what
828
+ * its branch proves, or by a range every in-edge provably keeps it in (`integerRange`'s induction,
829
+ * for a loop counter whose latch bounds one side). A signed remainder by a power of two spelled
830
+ * with shifts, `x - (t >> k << k)` with `t` a biased `x` and `x` any linear form (`(i + 1) % 8`),
831
+ * is one bounded term (`remainderPair`).
832
+ * Anything else — a function parameter, a whole word loaded from memory, a name nothing places, a
833
+ * param whose range depends on itself through a loop that no branch bounds — bounds nothing, and
834
+ * nothing is the answer. The form equals the word modulo 2^32, so the words are the sum's integer
835
+ * range moved into one word, when it fits one; a sum whose range crosses a multiple of 2^32 holds
836
+ * words on both sides of zero, which no interval names (a lone signed narrow value among them:
837
+ * `(s8)i` is [-128, 127]). Exact is not the point: an answer must hold every word the machine can
838
+ * compute there, and a wider one only proves less. Asked `at` a block (where `v` is read, or any
839
+ * block its definition dominates), what the branches above it prove there (`holdsAt`) bounds the
840
+ * terms they test too: `if (i < 8) a[i]` reads `i` in [0, 7] though `i` is a parameter. The
841
+ * `narrowest` window answers the signed integers those words are where that interval is narrower,
842
+ * `lo` then below 0 — an index straddling zero, IDO's `i % 32` in [-32, 31], that no unsigned
843
+ * interval names — for a reader that adds it to a base and moves the sum into words itself. */
844
+ export function stridedRange(
845
+ ctx: RangeCtx,
846
+ v: Value,
847
+ at?: Block,
848
+ window: 'word' | 'narrowest' = 'word',
849
+ ): StridedRange | undefined {
850
+ const holds = at === undefined ? undefined : ctx.holdsAt?.(at);
851
+ const r = integerRange(ctx, v, false, holds);
852
+ let words = r === undefined ? undefined : intoWord(r);
853
+ let signed = r === undefined || window === 'word' ? undefined : intoSigned(r);
854
+ // the reading that moves each join into one word first, only for a window the first one missed
855
+ if (r !== undefined && (words === undefined || (window === 'narrowest' && signed === undefined))) {
856
+ const moved = integerRange(ctx, v, true, holds);
857
+ words ??= moved === undefined ? undefined : intoWord(moved);
858
+ signed ??= moved === undefined || window === 'word' ? undefined : intoSigned(moved);
859
+ }
860
+ return signed !== undefined && (words === undefined || signed.hi - signed.lo < words.hi - words.lo) ? signed : words;
861
+ }
862
+
863
+ /** An integer range moved into one unsigned word, or undefined when it crosses a multiple of
864
+ * 2^32. Every integer in the answer is congruent modulo 2^32 to the one it moved. */
865
+ function intoWord(r: StridedRange): StridedRange | undefined {
866
+ const base = Math.floor(r.lo / WORD) * WORD;
867
+ return r.hi - base >= WORD ? undefined : { lo: r.lo - base, hi: r.hi - base, stride: r.stride };
868
+ }
869
+
870
+ /** Whether an integer range holds an integer congruent to `w` modulo 2^32 — a word the machine
871
+ * may compute there. Fails open (true) past a range too wide to walk. */
872
+ function rangeMayBe(r: StridedRange, w: number): boolean {
873
+ let x = r.lo + mod(mod(w) - r.lo);
874
+ for (let n = 0; x <= r.hi; x += WORD, n++) {
875
+ if (n > 64 || (r.stride === 0 ? x === r.lo : (x - r.lo) % r.stride === 0)) {
876
+ return true;
877
+ }
878
+ }
879
+ return false;
880
+ }
881
+
882
+ /** The range a narrow read or a `sext` leaves whatever it reads, where `provableBits` names none:
883
+ * a sign-extending `load`, a `sext` (its `width` in bits), and a flat array's `aload` (whose
884
+ * `elemSize` is the element it reads; a struct-array `aload`'s, with a `fieldOff`, is the stride,
885
+ * not the field, and bounds nothing). An integer range: a signed one straddles zero. */
886
+ function narrowLeaf(d: Op): StridedRange | undefined {
887
+ const bits =
888
+ d.opcode === 'sext'
889
+ ? d.attrs.width
890
+ : d.opcode === 'load' && d.attrs.signed === true && typeof d.attrs.width === 'number'
891
+ ? d.attrs.width * 8
892
+ : d.opcode === 'aload' && d.attrs.fieldOff === undefined && typeof d.attrs.elemSize === 'number'
893
+ ? d.attrs.elemSize * 8
894
+ : undefined;
895
+ if (typeof bits !== 'number' || !(bits > 0 && bits < 32)) {
896
+ return undefined;
897
+ }
898
+ return d.opcode === 'sext' || d.attrs.signed === true
899
+ ? { lo: -(2 ** (bits - 1)), hi: 2 ** (bits - 1) - 1, stride: 1 }
900
+ : { lo: 0, hi: 2 ** bits - 1, stride: 1 };
901
+ }
902
+
903
+ /** The words a `zext`, an `and` with a constant or a zero-fill shift right by a constant leaves,
904
+ * from the words its operand may hold (`wordsOf`, undefined where nothing bounds them): a `zext`
905
+ * or a mask of `2^n - 1` keeps every word that already fits it, `x & m` is no greater than `x` or
906
+ * `m`, and `x >>> s` is each word divided by `2^s`, rounded down. A shift left is a product,
907
+ * which `linearTerms` reads, so agbcc's `(u8)k * 2`, `lsl #24; lsr #23`, is `k`'s range doubled
908
+ * where that fits a word. Undefined where it names nothing, and the width bound
909
+ * (`provableBits`) is all there is. */
910
+ function shiftMaskRange(
911
+ ctx: LinearCtx,
912
+ d: Op,
913
+ wordsOf: (x: Value) => StridedRange | undefined,
914
+ ): StridedRange | undefined {
915
+ const amount =
916
+ d.operands.length === 1
917
+ ? typeof d.attrs.imm === 'number'
918
+ ? d.attrs.imm
919
+ : undefined
920
+ : d.operands.length === 2
921
+ ? ctx.constOf(d.operands[1])
922
+ : undefined;
923
+ if (d.opcode === 'zext' && typeof d.attrs.width === 'number' && d.attrs.width > 0 && d.attrs.width < 32) {
924
+ const w = wordsOf(d.operands[0]);
925
+ return w !== undefined && w.hi < 2 ** d.attrs.width ? w : undefined;
926
+ }
927
+ if (d.opcode === 'and') {
928
+ const [x, c] =
929
+ amount !== undefined
930
+ ? [d.operands[0], amount]
931
+ : d.operands.length === 2
932
+ ? [d.operands[1], ctx.constOf(d.operands[0])]
933
+ : [undefined, undefined];
934
+ if (x === undefined || c === undefined) {
935
+ return undefined;
936
+ }
937
+ const m = mod(c);
938
+ const w = wordsOf(x);
939
+ if (w !== undefined && w.hi <= m && Number.isInteger(Math.log2(m + 1))) {
940
+ return w;
941
+ }
942
+ const hi = Math.min(m, w?.hi ?? m);
943
+ return { lo: 0, hi, stride: hi === 0 ? 0 : 1 };
944
+ }
945
+ if (d.opcode === 'shr_u' && amount !== undefined && amount > 0 && amount < 32) {
946
+ const w = wordsOf(d.operands[0]);
947
+ if (w === undefined) {
948
+ return undefined;
949
+ }
950
+ const by = 2 ** amount;
951
+ const [lo, hi] = [Math.floor(w.lo / by), Math.floor(w.hi / by)];
952
+ return { lo, hi, stride: lo === hi ? 0 : w.stride % by === 0 ? w.stride / by : 1 };
953
+ }
954
+ return undefined;
955
+ }
956
+
957
+ /** The integers a remainder `x % y` leaves (`smod`, C's truncating one, or `umod`), from the
958
+ * integers its operands may hold (`rangeOf`, undefined where nothing bounds them): the divisor
959
+ * bounds it whatever the dividend holds, `umod` in [0, y - 1] and `smod` in [-(|y| - 1), |y| - 1],
960
+ * and the dividend narrows that, its sign (`smod` of an `x` proven non-negative is in [0, |y| -
961
+ * 1]) or itself where it is already smaller than every `y` (`x % y` is `x`). IDO's `div; mfhi`,
962
+ * which the MIPS frontend reads as `smod`/`umod`, is `i % 30` in [-29, 29] by this alone. A
963
+ * divisor that may be zero bounds nothing: the remainder is then whatever the machine leaves.
964
+ * The shift spelling of `x % 2^k` (agbcc, gcc, kmc: no `smod` at all) is the same bound read off
965
+ * its own ops (`remainderPair`). */
966
+ function remainderRange(d: Op, rangeOf: (x: Value) => StridedRange | undefined): StridedRange | undefined {
967
+ if ((d.opcode !== 'smod' && d.opcode !== 'umod') || d.operands.length !== 2) {
968
+ return undefined;
969
+ }
970
+ const point = (lo: number, hi: number): StridedRange => ({ lo, hi, stride: lo === hi ? 0 : 1 });
971
+ const [rx, ry] = [rangeOf(d.operands[0]), rangeOf(d.operands[1])];
972
+ if (d.opcode === 'umod') {
973
+ const y = ry === undefined ? undefined : intoWord(ry);
974
+ if (y === undefined || y.lo < 1) {
975
+ return undefined;
976
+ }
977
+ const x = rx === undefined ? undefined : intoWord(rx);
978
+ return x !== undefined && x.hi < y.lo ? x : point(0, Math.min(y.hi - 1, x?.hi ?? y.hi - 1));
979
+ }
980
+ const y = ry === undefined ? undefined : intoSigned(ry);
981
+ if (y === undefined || (y.lo <= 0 && y.hi >= 0)) {
982
+ return undefined;
983
+ }
984
+ const [least, most] = y.lo > 0 ? [y.lo, y.hi] : [-y.hi, -y.lo];
985
+ const x = rx === undefined ? undefined : intoSigned(rx);
986
+ if (x !== undefined && -x.lo < least && x.hi < least) {
987
+ return x;
988
+ }
989
+ const m = most - 1;
990
+ return point(
991
+ x === undefined || x.lo < 0 ? Math.max(-m, x?.lo ?? -m) : 0,
992
+ x === undefined || x.hi > 0 ? Math.min(m, x?.hi ?? m) : 0,
993
+ );
994
+ }
995
+
996
+ /** `stridedRange` before the move into one word: integers, every one of which is congruent modulo
997
+ * 2^32 to a word `v` may hold, and which cover them all. A block param is the union of what its
998
+ * in-edges pass, read one of two ways, each a sound cover on its own: as the integers they are
999
+ * (`wordFirst` false: a constant -1 beside a halfword reload is [-1, 65535], which no word
1000
+ * interval names but which excludes INT_MIN), or each moved into one word first where it fits one
1001
+ * (`wordFirst` true: a -1 beside a 0 is {0, 2^32 - 1}, one word interval where [-1, 0] is none).
1002
+ * The two never mix in one union. Where the integer union fits one word, both readings name the
1003
+ * same words (every edge then shares one multiple of 2^32), so `stridedRange` asks the integer
1004
+ * reading first and the moved one only where that crosses a multiple; `mayAlwaysHold` asks both,
1005
+ * and a word either excludes cannot be held. Where an in-edge names what its branch proves,
1006
+ * that edge's argument is read under it (`edgeRange`): a loop's latch `k + 1 <= 7` bounds the
1007
+ * `k + 1` its back edge passes, which no reading of `k` alone could, `k` depending on itself. */
1008
+ function integerRange(
1009
+ ctx: RangeCtx,
1010
+ v: Value,
1011
+ wordFirst: boolean,
1012
+ holds?: readonly EdgeTest[],
1013
+ ): StridedRange | undefined {
1014
+ if (question?.ctx === ctx) {
1015
+ return rangeIn(ctx, v, wordFirst, holds);
1016
+ }
1017
+ const outer = question; // a question of another context asked from inside one: its own
1018
+ question = { ctx, own: [new Map(), new Map()], left: RANGE_BUDGET, depth: 0 };
1019
+ try {
1020
+ return rangeIn(ctx, v, wordFirst, holds);
1021
+ } finally {
1022
+ question = outer;
1023
+ }
1024
+ }
1025
+
1026
+ /** `integerRange` inside the outermost question under way (`question`), whose budget it spends. */
1027
+ function rangeIn(ctx: RangeCtx, v: Value, wordFirst: boolean, holds?: readonly EdgeTest[]): StridedRange | undefined {
1028
+ const q = question!;
1029
+ const open = new Set<Value>();
1030
+ /** one more term bounded: false once its question's budget is spent */
1031
+ const spend = (): boolean => --q.left >= 0;
1032
+ /** params met again while their own in-edges were being read */
1033
+ const cyclic = new Set<Value>();
1034
+ /** the induction hypotheses under test: each param holds only this range */
1035
+ const assumed = new Map<Value, StridedRange>();
1036
+ const bits: BitsCtx = {
1037
+ defs: ctx.defs,
1038
+ bound: (d) => (d.opcode === 'zext' ? (d.attrs.width as number) : null),
1039
+ };
1040
+ /** `facts`: what the in-edge being read proves, by the value it bounds; each is read as a term.
1041
+ * `null` (here and from `rangeOf`) is NEVER: a fact no value of its term can meet, so the edge
1042
+ * that proves it is never taken and passes nothing. */
1043
+ const termRange = (t: Value, facts: Facts): StridedRange | undefined | null => {
1044
+ const fact = facts.get(t);
1045
+ if (fact !== undefined) {
1046
+ const own = rangeOf(t, new Map([...facts].filter(([x]) => x !== t)));
1047
+ return own === null ? null : meetBound(own, fact);
1048
+ }
1049
+ const at = ctx.pointOf?.(t);
1050
+ if (at !== undefined) {
1051
+ return { lo: mod(at), hi: mod(at), stride: 0 };
1052
+ }
1053
+ const d = ctx.defs.get(t);
1054
+ if (d !== undefined) {
1055
+ const leaf = narrowLeaf(d);
1056
+ if (leaf !== undefined) {
1057
+ return leaf;
1058
+ }
1059
+ // what a shift or a mask leaves of its operand's range: agbcc's `(u8)k` of a counter that
1060
+ // stays in [0, 7] is [0, 7], not the [0, 255] the width alone leaves
1061
+ const moved = shiftMaskRange(ctx, d, (x) => {
1062
+ const r = spend() ? rangeOf(x, facts) : undefined;
1063
+ return r === undefined || r === null ? undefined : intoWord(r);
1064
+ });
1065
+ const n = provableBits(bits, t);
1066
+ const width = n < 32 ? { lo: 0, hi: 2 ** n - 1, stride: n === 0 ? 0 : 1 } : undefined;
1067
+ // a remainder is bounded by its divisor whatever its dividend holds: IDO's `i % 30`
1068
+ const rem = remainderRange(d, (x) => {
1069
+ const r = spend() ? rangeOf(x, facts) : undefined;
1070
+ return r === null ? undefined : r;
1071
+ });
1072
+ if (rem !== undefined) {
1073
+ return width !== undefined && width.hi - width.lo < rem.hi - rem.lo ? width : rem;
1074
+ }
1075
+ return moved !== undefined && (width === undefined || moved.hi - moved.lo <= width.hi - width.lo) ? moved : width;
1076
+ }
1077
+ const hypothesis = assumed.get(t);
1078
+ if (hypothesis !== undefined) {
1079
+ return hypothesis;
1080
+ }
1081
+ const ins = ctx.entry.has(t) ? undefined : ctx.incoming.get(t);
1082
+ if (open.has(t)) {
1083
+ cyclic.add(t);
1084
+ return undefined;
1085
+ }
1086
+ if (ins === undefined || ins.length === 0 || !spend()) {
1087
+ return undefined;
1088
+ }
1089
+ open.add(t);
1090
+ try {
1091
+ // each in-edge as it reads, and apart those that read without the param: an induction's start;
1092
+ // an edge that is never taken (`null`) passes nothing, and the param is what the others pass
1093
+ let readsItself = false;
1094
+ const seeds: (StridedRange | undefined)[] = [];
1095
+ const direct = ins.flatMap((e) => {
1096
+ const { range } = edgeRange(e);
1097
+ const self = cyclic.delete(t);
1098
+ readsItself ||= self;
1099
+ seeds.push(self || range === null ? undefined : range);
1100
+ return range === null ? [] : [range];
1101
+ });
1102
+ if (direct.length === 0) {
1103
+ return null; // no in-edge is ever taken: nor is any edge that reads the param
1104
+ }
1105
+ let out = direct.every((r) => r !== undefined)
1106
+ ? hullOf(direct.map((r) => (wordFirst ? (intoWord(r) ?? r) : r)))
1107
+ : undefined;
1108
+ // a param read through itself may still hold a range every in-edge keeps it in
1109
+ const proven = readsItself ? inductive(t, ins, seeds) : undefined;
1110
+ if (proven !== undefined && (out === undefined || proven.hi - proven.lo < out.hi - out.lo)) {
1111
+ out = wordFirst ? (intoWord(proven) ?? proven) : proven;
1112
+ }
1113
+ // met inside a loop through a param of its cycle that was asked first, the reading above saw
1114
+ // that param as nothing, and the induction that bounds the loop never ran: `k` read from
1115
+ // `k2 = phi(k + 1, k + 2)`, the value a signed latch tests, is only the latch's one side.
1116
+ // Its own question runs it from its own in-edges (`ownRange`); each answer covers every
1117
+ // value, and the narrower is kept.
1118
+ if ([...cyclic].some((x) => x !== t)) {
1119
+ const own = ownRange(ctx, t, wordFirst);
1120
+ if (own !== undefined && (out === undefined || own.hi - own.lo < out.hi - out.lo)) {
1121
+ out = own;
1122
+ }
1123
+ }
1124
+ return out;
1125
+ } finally {
1126
+ open.delete(t);
1127
+ }
1128
+ };
1129
+ /** A range `r` for a param that reads itself, proven by induction: every in-edge, read with the
1130
+ * param anywhere in `r` and under what its branch proves, passes a value inside `r`, so no run
1131
+ * of the loop can leave it. A signed latch bounds its counter on one side only (`k + 1 <= 7`
1132
+ * holds of every negative k), and only the counter's start bounds the other. The hypothesis
1133
+ * starts at the in-edges that read without the param, takes in what they pass, and once the
1134
+ * bounds the branches name on values computed from the param; every sum is exact and a bound
1135
+ * read outside its window holds alone, so a counter that can wrap, an update on some paths only
1136
+ * or an unbounded start never settles and proves nothing. */
1137
+ const inductive = (
1138
+ t: Value,
1139
+ ins: readonly InEdge[],
1140
+ seeds: readonly (StridedRange | undefined)[],
1141
+ ): StridedRange | undefined => {
1142
+ let r = hullOf(seeds.filter((x): x is StridedRange => x !== undefined));
1143
+ for (let round = 0; r !== undefined && round < INDUCTION_ROUNDS; round++) {
1144
+ const passed: StridedRange[] = [];
1145
+ const named: StridedRange[] = [];
1146
+ assumed.set(t, r);
1147
+ try {
1148
+ for (const e of ins) {
1149
+ const { range, facts } = edgeRange(e);
1150
+ if (range === null) {
1151
+ continue; // never taken while the param is in `r`: it passes nothing
1152
+ }
1153
+ if (range === undefined) {
1154
+ return undefined;
1155
+ }
1156
+ passed.push(range);
1157
+ // only a bound on a value computed from the param can be where it settles: an edge also
1158
+ // carries what the tests above it prove of a loaded global or a dispatch value
1159
+ facts.forEach((b, x) => {
1160
+ if (computedFrom(x, t)) {
1161
+ named.push(...boundSides(b));
1162
+ }
1163
+ });
1164
+ }
1165
+ } finally {
1166
+ assumed.delete(t);
1167
+ }
1168
+ const next = hullOf([r, ...passed])!;
1169
+ if (next.lo === r.lo && next.hi === r.hi && next.stride === r.stride) {
1170
+ return r;
1171
+ }
1172
+ r = round === 0 ? hullOf([next, ...named]) : next;
1173
+ }
1174
+ return undefined;
1175
+ };
1176
+ /** Whether `x` is computed from the param `t` by arithmetic: through the operands of what defines
1177
+ * it, and through what a block param's in-edges pass (a join of `k + 1` and `k + 2` is computed
1178
+ * from `k`), but never through memory, whose word a load reads whatever address it took. */
1179
+ const computedFrom = (x: Value, t: Value): boolean => {
1180
+ const seen = new Set<Value>();
1181
+ const work = [x];
1182
+ while (work.length > 0 && seen.size < RANGE_BUDGET) {
1183
+ const y = work.pop()!;
1184
+ if (y === t) {
1185
+ return true;
1186
+ }
1187
+ if (seen.has(y)) {
1188
+ continue;
1189
+ }
1190
+ seen.add(y);
1191
+ const d = ctx.defs.get(y);
1192
+ if (d !== undefined) {
1193
+ if (!READS_MEMORY.has(d.opcode)) {
1194
+ work.push(...d.operands);
1195
+ }
1196
+ } else {
1197
+ ctx.incoming.get(y)?.forEach((e) => e.arg !== undefined && work.push(e.arg));
1198
+ }
1199
+ }
1200
+ return false;
1201
+ };
1202
+ /** What one in-edge passes, read under what its branch proves (`facts`, returned beside it).
1203
+ * Every value the argument reads is the instance the branch tested, so a bound on it holds
1204
+ * wherever the argument reads it; a param read on the way is read under its own in-edges'
1205
+ * facts, never these. */
1206
+ const edgeRange = (e: InEdge): { readonly range: StridedRange | undefined | null; readonly facts: Facts } => {
1207
+ const facts = e.holds === undefined ? NO_FACTS : factsOf(e.holds);
1208
+ return { range: e.arg === undefined ? undefined : rangeOf(e.arg, facts), facts };
1209
+ };
1210
+ /** The bound each compare puts on each side, from the other side's range (read with no facts);
1211
+ * of two bounds on one value, each holds, and the narrower is kept. */
1212
+ const factsOf = (hs: readonly EdgeTest[]): Facts => {
1213
+ const out = new Map<Value, EdgeBound>();
1214
+ for (const h of hs) {
1215
+ if (h.lhs === h.rhs) {
1216
+ continue;
1217
+ }
1218
+ for (const [x, y, opcode] of [
1219
+ [h.lhs, h.rhs, h.opcode],
1220
+ [h.rhs, h.lhs, SWAPPED_ICMP[h.opcode]],
1221
+ ] as const) {
1222
+ if (opcode === undefined || ctx.constOf(x) !== undefined) {
1223
+ continue;
1224
+ }
1225
+ const other = rangeOf(y, NO_FACTS);
1226
+ const b = other === undefined || other === null ? undefined : testBound(opcode, other);
1227
+ const had = out.get(x);
1228
+ if (b !== undefined && (had === undefined || b.r.hi - b.r.lo < had.r.hi - had.r.lo)) {
1229
+ out.set(x, b);
1230
+ }
1231
+ }
1232
+ }
1233
+ return out;
1234
+ };
1235
+ /** Two sets of facts on the same values, both holding: of two bounds in one window, their meet
1236
+ * (`null` where they meet nowhere, so no value passes both), else the narrower. */
1237
+ const bothFacts = (a: Facts, b: Facts): Facts | null => {
1238
+ if (a.size === 0 || b.size === 0) {
1239
+ return a.size === 0 ? b : a;
1240
+ }
1241
+ const out = new Map(a);
1242
+ for (const [x, bb] of b) {
1243
+ const had = out.get(x);
1244
+ if (had === undefined) {
1245
+ out.set(x, bb);
1246
+ } else if (had.window === bb.window) {
1247
+ const r = meetIn(had.r, bb.r);
1248
+ if (r === null) {
1249
+ return null;
1250
+ }
1251
+ out.set(x, { r, window: bb.window });
1252
+ } else if (bb.r.hi - bb.r.lo < had.r.hi - had.r.lo) {
1253
+ out.set(x, bb);
1254
+ }
1255
+ }
1256
+ return out;
1257
+ };
1258
+ /** A signed remainder by `2^k` the form holds as the pair `c * X - c * 2^k * (p >> k)`, `>>` the
1259
+ * sign-filling shift and `p` the operand `X + B` with a bias `B` in [0, 2^k), or a block param
1260
+ * whose every in-edge passes `X + B` for its own `B`: agbcc's (and gcc's) `X % 8` is `if (X < 0)
1261
+ * t = X + 7; else t = X; X - (t >> 3 << 3)`, and agbcc's `X % 2` is `X - ((X + (X >>> 31)) >> 1
1262
+ * << 1)`, its bias a term. The dividend `X` is any linear form: `(i + 1) % 8`
1263
+ * passes `i + 1` and `i + 8`, `(i * 2) % 8` passes `2 * i` and `2 * i + 7`. `X` is the terms the
1264
+ * outer form holds `c` times of the in-edge argument with the least constant, with that
1265
+ * argument's constant (moved out of the pair); every edge passes each of `X`'s terms as often,
1266
+ * so each is an SSA value defined above the join, the same word at the edge and at the use.
1267
+ *
1268
+ * Modulo 2^32, `X - 2^k * ((X + B) >> k)` is `(W mod 2^k) - B` of the signed word `W` the shift
1269
+ * reads, whether or not `X + B` wrapped to it: each edge's part is in [-B_hi, 2^k - 1 - B_lo]
1270
+ * with nothing known of `X`. Where every bias is a constant `b` and `X + b` stays inside the
1271
+ * signed window, the part is `X - 2^k * floor((X + b) / 2^k)`, exactly `X - 2^k * q` where the
1272
+ * floor `q` is one constant over `X`'s range: `X` is read under each in-edge's branch and under
1273
+ * `facts` (what holds where the form is read: `X`'s terms are the same words there), and the
1274
+ * parts hulled, an edge never taken passing nothing (`null` when none is taken). Undefined,
1275
+ * leaving the pair as terms, where `k` is outside [1, 30], an in-edge passes no linear form or
1276
+ * not each of `X`'s terms as often, a bias is out of range or holds the shift, or the outer form
1277
+ * holds none of the argument's terms `c` times.
1278
+ *
1279
+ * raise/divpow2.ts folds the same bias diamond into `sdiv`, but only in runPreRecovery, after the
1280
+ * frontends' frame audit has asked these ranges; and it is identity-or-decline about the shape
1281
+ * where this is a bound, which holds for any bias in range. */
1282
+ const remainderPair = (
1283
+ s: Value,
1284
+ cs: number,
1285
+ form: ReadonlyMap<Value | 1, number>,
1286
+ facts: Facts,
1287
+ ):
1288
+ | { readonly terms: readonly Value[]; readonly c: number; readonly moved: number; readonly r: StridedRange | null }
1289
+ | undefined => {
1290
+ const d = ctx.defs.get(s);
1291
+ if (d === undefined || d.opcode !== 'shr_s') {
1292
+ return undefined;
1293
+ }
1294
+ const k = d.operands.length === 1 ? (d.attrs.imm as number | undefined) : ctx.constOf(d.operands[1]);
1295
+ if (k === undefined || !(k > 0 && k < 31)) {
1296
+ return undefined;
1297
+ }
1298
+ const by = 2 ** k;
1299
+ const c = -cs / by;
1300
+ if (!Number.isSafeInteger(c) || c === 0) {
1301
+ return undefined;
1302
+ }
1303
+ const p = d.operands[0];
1304
+ const ins = ctx.defs.has(p) || ctx.entry.has(p) ? undefined : ctx.incoming.get(p);
1305
+ const edges = ins !== undefined && ins.length > 0 ? ins : [{ arg: p } as InEdge];
1306
+ const forms = edges.map((e) => (e.arg === undefined ? undefined : linearTerms(ctx, e.arg)));
1307
+ if (forms.some((f) => f === undefined)) {
1308
+ return undefined;
1309
+ }
1310
+ const constOf = (f: ReadonlyMap<Value | 1, number>) => f.get(1) ?? 0;
1311
+ const at = forms.reduce((m, f, i) => (constOf(f!) < constOf(forms[m]!) ? i : m), 0);
1312
+ const fx = forms[at]!;
1313
+ // X: the terms of that argument the outer form holds `c` times; X is not computed from the shift
1314
+ const terms = [...fx].filter(([t, n]) => t !== 1 && n !== 0 && form.get(t) === c * n) as [Value, number][];
1315
+ if (terms.length === 0 || terms.some(([t]) => t === s)) {
1316
+ return undefined;
1317
+ }
1318
+ // each edge's bias B: its form less X's, holding none of X's terms and not the shift
1319
+ const biases: Map<Value | 1, number>[] = [];
1320
+ for (const f of forms) {
1321
+ const b = new Map<Value | 1, number>();
1322
+ for (const [t, n] of f!) {
1323
+ if (n !== 0) {
1324
+ b.set(t, n);
1325
+ }
1326
+ }
1327
+ for (const [t, n] of terms) {
1328
+ if (b.get(t) !== n) {
1329
+ return undefined;
1330
+ }
1331
+ b.delete(t);
1332
+ }
1333
+ b.set(1, constOf(f!) - constOf(fx));
1334
+ if (b.has(s)) {
1335
+ return undefined;
1336
+ }
1337
+ biases.push(b);
1338
+ }
1339
+ // where every bias is a constant, X is the least argument itself
1340
+ const exact = biases.every((b) => b.size === 1);
1341
+ const x = edges[at].arg!;
1342
+ const parts: StridedRange[] = [];
1343
+ for (const [i, e] of edges.entries()) {
1344
+ const bias = biases[i];
1345
+ const own = e.holds === undefined ? NO_FACTS : factsOf(e.holds);
1346
+ // B is read where the edge leaves, under its branch alone: its terms need not be above the join
1347
+ const br =
1348
+ bias.size === 1
1349
+ ? { lo: bias.get(1)!, hi: bias.get(1)!, stride: 0 }
1350
+ : spend()
1351
+ ? formRange(bias, own)
1352
+ : undefined;
1353
+ if (br === null) {
1354
+ continue; // never taken
1355
+ }
1356
+ if (br === undefined || !(br.lo >= 0 && br.hi < by)) {
1357
+ return undefined;
1358
+ }
1359
+ const any = { lo: -br.hi, hi: by - 1 - br.lo, stride: 1 };
1360
+ if (!exact) {
1361
+ parts.push(any);
1362
+ continue;
1363
+ }
1364
+ const under = bothFacts(facts, own);
1365
+ if (under === null) {
1366
+ continue; // never taken where the form is read
1367
+ }
1368
+ const read = spend() ? rangeOf(x, under) : undefined;
1369
+ if (read === null) {
1370
+ continue; // never taken
1371
+ }
1372
+ const xs = read === undefined ? undefined : intoSigned(read);
1373
+ const b = br.lo;
1374
+ if (xs === undefined || xs.hi + b > S_MAX) {
1375
+ parts.push(any);
1376
+ continue;
1377
+ }
1378
+ const [q0, q1] = [Math.floor((xs.lo + b) / by), Math.floor((xs.hi + b) / by)];
1379
+ parts.push(q0 === q1 ? { lo: xs.lo - q0 * by, hi: xs.hi - q0 * by, stride: xs.stride } : any);
1380
+ }
1381
+ const moved = c * constOf(fx);
1382
+ return Number.isSafeInteger(moved)
1383
+ ? { terms: terms.map(([t]) => t), c, moved, r: hullOf(parts) ?? null }
1384
+ : undefined;
1385
+ };
1386
+ const rangeOf = (x: Value, facts: Facts): StridedRange | undefined | null => {
1387
+ const form = linearTerms(ctx, x, 0, facts);
1388
+ return form === undefined ? undefined : formRange(form, facts);
1389
+ };
1390
+ /** The integers a linear form holds, its terms read under `facts`. */
1391
+ const formRange = (form: ReadonlyMap<Value | 1, number>, facts: Facts): StridedRange | undefined | null => {
1392
+ let lo = 0;
1393
+ let hi = 0;
1394
+ let stride = 0;
1395
+ // each remainder pair first, as one bounded term; the rest of the form after it
1396
+ const paired = new Set<Value | 1>();
1397
+ for (const [s, cs] of form) {
1398
+ const pair = s === 1 || cs === 0 || paired.has(s) ? undefined : remainderPair(s, cs, form, facts);
1399
+ if (pair === undefined || pair.terms.some((t) => paired.has(t))) {
1400
+ continue;
1401
+ }
1402
+ if (pair.r === null) {
1403
+ return null;
1404
+ }
1405
+ paired.add(s);
1406
+ pair.terms.forEach((t) => paired.add(t));
1407
+ // the pair is `c * X - c * 2^k * q`, and X's constant is already in the form's own constant
1408
+ const parts = [pair.c * pair.r.lo, pair.c * pair.r.hi, pair.c * pair.r.stride];
1409
+ lo += Math.min(parts[0], parts[1]) - pair.moved;
1410
+ hi += Math.max(parts[0], parts[1]) - pair.moved;
1411
+ if (![...parts, lo, hi].every(Number.isSafeInteger)) {
1412
+ return undefined;
1413
+ }
1414
+ stride = gcdOf(stride, parts[2]);
1415
+ }
1416
+ for (const [t, c] of form) {
1417
+ if (c === 0 || paired.has(t)) {
1418
+ continue;
1419
+ }
1420
+ const r = t === 1 ? { lo: 1, hi: 1, stride: 0 } : termRange(t, facts);
1421
+ if (r === undefined || r === null) {
1422
+ return r;
1423
+ }
1424
+ const parts = [c * r.lo, c * r.hi, c * r.stride];
1425
+ lo += c > 0 ? parts[0] : parts[1];
1426
+ hi += c > 0 ? parts[1] : parts[0];
1427
+ // every product and partial sum stays exact, or a rounded one could cancel to a false range
1428
+ if (![...parts, lo, hi].every(Number.isSafeInteger)) {
1429
+ return undefined;
1430
+ }
1431
+ stride = gcdOf(stride, parts[2]);
1432
+ }
1433
+ return Number.isSafeInteger(lo) && Number.isSafeInteger(hi) ? { lo, hi, stride } : undefined;
1434
+ };
1435
+ // asked at a block, `v` is read under what holds there, as an in-edge's argument is under what
1436
+ // its edge proves; a point no value can reach (`null`) is answered as nothing proven
1437
+ return rangeOf(v, holds === undefined || holds.length === 0 ? NO_FACTS : factsOf(holds)) ?? undefined;
1438
+ }
1439
+
1440
+ /** The outermost range question under way, which every question it opens shares: each block
1441
+ * param's range as its own question (`ownRange`), by reading (index 1: `wordFirst`), the terms
1442
+ * all its readings may still bound between them, and how many own questions are under way, one
1443
+ * inside another. Nothing outlives the question, so what it answers never depends on what was
1444
+ * asked before it. */
1445
+ let question:
1446
+ | {
1447
+ readonly ctx: RangeCtx;
1448
+ readonly own: [Map<Value, StridedRange | undefined>, Map<Value, StridedRange | undefined>];
1449
+ left: number;
1450
+ depth: number;
1451
+ }
1452
+ | undefined;
1453
+ /** The most `ownRange` questions under way at once, one inside another. */
1454
+ const OWN_DEPTH = 8;
1455
+
1456
+ /** A block param's range as its own question, asked from it with nothing open, holding nothing
1457
+ * assumed and read under no fact (`integerRange`), kept for the rest of the outermost question.
1458
+ * It covers every value the param takes, so the answer serves every reading that meets the
1459
+ * param, and one met inside a reading that opened another param of the same loop reads it here.
1460
+ * A param whose own question is already under way answers nothing to it, which only proves less. */
1461
+ function ownRange(ctx: RangeCtx, p: Value, wordFirst: boolean): StridedRange | undefined {
1462
+ const q = question!;
1463
+ const known = q.own[wordFirst ? 1 : 0];
1464
+ if (known.has(p)) {
1465
+ return known.get(p);
1466
+ }
1467
+ if (q.depth >= OWN_DEPTH) {
1468
+ known.set(p, undefined); // past the nesting it may run to: nothing, kept as any answer is
1469
+ return undefined;
1470
+ }
1471
+ known.set(p, undefined); // under way: met again, it answers nothing
1472
+ q.depth++;
1473
+ try {
1474
+ const r = rangeIn(ctx, p, wordFirst);
1475
+ known.set(p, r);
1476
+ return r;
1477
+ } finally {
1478
+ q.depth--;
1479
+ }
1480
+ }
1481
+
1482
+ /** The integers every range in a list holds, and the stride all of them sit on. */
1483
+ function hullOf(rs: readonly StridedRange[]): StridedRange | undefined {
1484
+ let out: StridedRange | undefined;
1485
+ for (const r of rs) {
1486
+ out =
1487
+ out === undefined
1488
+ ? r
1489
+ : {
1490
+ lo: Math.min(out.lo, r.lo),
1491
+ hi: Math.max(out.hi, r.hi),
1492
+ stride: gcdOf(gcdOf(out.stride, r.stride), r.lo - out.lo),
1493
+ };
1494
+ }
1495
+ return out;
1496
+ }
1497
+
1498
+ /** What a branch proves of a value: the integers of one window it may be, unsigned words (`u`,
1499
+ * from `icmp_u*` and `icmp_eq`) or the signed ones (`s`, from `icmp_s*`). */
1500
+ interface EdgeBound {
1501
+ readonly r: StridedRange;
1502
+ readonly window: 'u' | 's';
1503
+ }
1504
+
1505
+ /** What an in-edge's branch proves, by the value each bound is on. */
1506
+ type Facts = ReadonlyMap<Value, EdgeBound>;
1507
+ const NO_FACTS: Facts = new Map();
1508
+
1509
+ const S_MIN = -(2 ** 31);
1510
+ const S_MAX = 2 ** 31 - 1;
1511
+
1512
+ /** The ops whose result is a word read from memory or handed back by a callee (the registry's
1513
+ * `reads` and `effects`), not computed from their operands: `integerRange`'s `computedFrom` reads
1514
+ * no further. */
1515
+ const READS_MEMORY: ReadonlySet<string> = new Set(
1516
+ Object.entries(OPCODES as Record<string, OpSig>)
1517
+ .filter(([, sig]) => sig.reads === true || sig.effects === true)
1518
+ .map(([op]) => op),
1519
+ );
1520
+
1521
+ /** The rounds an induction hypothesis may grow before it proves nothing. */
1522
+ const INDUCTION_ROUNDS = 4;
1523
+
1524
+ /** The sides of a branch's bound that are not its window's edge, each as a one-value range. */
1525
+ function boundSides(b: EdgeBound): StridedRange[] {
1526
+ const [floor, ceil] = b.window === 'u' ? [0, WORD - 1] : [S_MIN, S_MAX];
1527
+ return [b.r.lo === floor ? undefined : b.r.lo, b.r.hi === ceil ? undefined : b.r.hi]
1528
+ .filter((x): x is number => x !== undefined)
1529
+ .map((x) => ({ lo: x, hi: x, stride: 0 }));
1530
+ }
1531
+
1532
+ /** An integer range as signed words, or undefined when it holds words on both sides of the sign. */
1533
+ function intoSigned(r: StridedRange): StridedRange | undefined {
1534
+ if (r.lo >= S_MIN && r.hi <= S_MAX) {
1535
+ return r;
1536
+ }
1537
+ const w = intoWord(r);
1538
+ if (w === undefined || w.hi <= S_MAX) {
1539
+ return w;
1540
+ }
1541
+ return w.lo > S_MAX ? { lo: w.lo - WORD, hi: w.hi - WORD, stride: w.stride } : undefined;
1542
+ }
1543
+
1544
+ /** What `x <opcode> y` proves of `x`, `y` being somewhere in `other`; undefined for `icmp_ne`, and
1545
+ * for a compare no `x` can pass (an edge that is never taken bounds nothing here). */
1546
+ function testBound(opcode: string, other: StridedRange): EdgeBound | undefined {
1547
+ const span = (lo: number, hi: number, window: 'u' | 's'): EdgeBound | undefined =>
1548
+ lo > hi ? undefined : { r: { lo, hi, stride: lo === hi ? 0 : 1 }, window };
1549
+ const w = intoWord(other);
1550
+ const s = intoSigned(other);
1551
+ switch (opcode) {
1552
+ case 'icmp_eq':
1553
+ return w === undefined ? undefined : { r: w, window: 'u' };
1554
+ case 'icmp_ult':
1555
+ return w && span(0, w.hi - 1, 'u');
1556
+ case 'icmp_ule':
1557
+ return w && span(0, w.hi, 'u');
1558
+ case 'icmp_ugt':
1559
+ return w && span(w.lo + 1, WORD - 1, 'u');
1560
+ case 'icmp_uge':
1561
+ return w && span(w.lo, WORD - 1, 'u');
1562
+ case 'icmp_slt':
1563
+ return s && span(S_MIN, s.hi - 1, 's');
1564
+ case 'icmp_sle':
1565
+ return s && span(S_MIN, s.hi, 's');
1566
+ case 'icmp_sgt':
1567
+ return s && span(s.lo + 1, S_MAX, 's');
1568
+ case 'icmp_sge':
1569
+ return s && span(s.lo, S_MAX, 's');
1570
+ default:
1571
+ return undefined;
1572
+ }
1573
+ }
1574
+
1575
+ /** A value's range where a branch also bounds it: the two met in the branch's window. A range with
1576
+ * words on both sides of that window's edge is two pieces there, each met with the bound: `d =
1577
+ * k - 1` of a k in [0, 10] is [-1, 9], which `d <=u 10` meets as [0, 9], the piece at 2^32 - 1
1578
+ * taking no branch. Where both pieces stay, their hull or the range itself, whichever is
1579
+ * narrower, the hull on a tie (it is a range of the window). A meet that holds nothing is `null`:
1580
+ * no word the value may hold takes the branch, so the edge is never taken — `if (k < 0) k += 7`
1581
+ * on a counter from 0. */
1582
+ function meetBound(r: StridedRange | undefined, b: EdgeBound): StridedRange | null {
1583
+ if (r === undefined) {
1584
+ return b.r;
1585
+ }
1586
+ const m = b.window === 'u' ? intoWord(r) : intoSigned(r);
1587
+ if (m !== undefined) {
1588
+ return meetIn(m, b.r);
1589
+ }
1590
+ if (r.hi - r.lo >= WORD || r.stride === 0) {
1591
+ return b.r; // every word: only the bound is left (one point always fits a window)
1592
+ }
1593
+ // the integer where the window ends inside `r`: the pieces below it and from it, each moved
1594
+ // into the window by its own multiple of 2^32
1595
+ const floor = b.window === 'u' ? 0 : S_MIN;
1596
+ const shift = Math.floor((r.lo - floor) / WORD) * WORD;
1597
+ const [below, from] = splitAt(r, floor + shift + WORD);
1598
+ const met = [
1599
+ below && meetIn({ ...below, lo: below.lo - shift, hi: below.hi - shift }, b.r),
1600
+ from && meetIn({ ...from, lo: from.lo - shift - WORD, hi: from.hi - shift - WORD }, b.r),
1601
+ ].filter((x): x is StridedRange => x !== undefined && x !== null);
1602
+ const hull = hullOf(met);
1603
+ if (hull === undefined) {
1604
+ return null;
1605
+ }
1606
+ return hull.hi - hull.lo <= r.hi - r.lo ? hull : r;
1607
+ }
1608
+
1609
+ /** The points of a strided range (stride above 0) below `cut` and from it, each keeping the
1610
+ * stride; a side with no point is undefined. */
1611
+ function splitAt(r: StridedRange, cut: number): [StridedRange | undefined, StridedRange | undefined] {
1612
+ const firstUp = r.lo + Math.ceil((cut - r.lo) / r.stride) * r.stride;
1613
+ const piece = (lo: number, hi: number): StridedRange | undefined =>
1614
+ lo > hi ? undefined : { lo, hi, stride: lo === hi ? 0 : r.stride };
1615
+ return [piece(r.lo, Math.min(firstUp - r.stride, r.hi)), piece(Math.max(firstUp, r.lo), r.hi)];
1616
+ }
1617
+
1618
+ /** A range inside one window met with a bound of that window; `null` when they share no point. */
1619
+ function meetIn(m: StridedRange, br: StridedRange): StridedRange | null {
1620
+ const lo = Math.max(m.lo, br.lo);
1621
+ const hi = Math.min(m.hi, br.hi);
1622
+ if (m.stride === 0) {
1623
+ return lo <= hi ? m : null;
1624
+ }
1625
+ const first = m.lo + Math.ceil((lo - m.lo) / m.stride) * m.stride;
1626
+ const last = m.lo + Math.floor((hi - m.lo) / m.stride) * m.stride;
1627
+ return first > last ? null : { lo: first, hi: last, stride: first === last ? 0 : m.stride };
1628
+ }
1629
+
1630
+ const mod = (w: number): number => ((w % WORD) + WORD) % WORD;
1631
+
1632
+ function gcdOf(a: number, b: number): number {
1633
+ return b === 0 ? Math.abs(a) : gcdOf(b, a % b);
1634
+ }
1635
+
1636
+ const FULL = 0xffffffff;
1637
+
1638
+ /** The low `bits` bits, as a mask. */
1639
+ const lowBits = (bits: number): number => (bits >= 32 ? FULL : 2 ** bits - 1);
1640
+
1641
+ /** What `demandedBits` walks, built from the whole function: each value's defining op, the ops that
1642
+ * read it (each once), and the block parameters an edge hands it to. A caller asking about many
1643
+ * values of one unchanged function builds it once and hands it to each ask; each ask still walks
1644
+ * from its own root with its own memo, so the answer is the one a fresh index gives. */
1645
+ export interface DemandedBitsIndex {
1646
+ readonly defs: ReadonlyMap<Value, Op>;
1647
+ readonly readers: ReadonlyMap<Value, readonly Op[]>;
1648
+ readonly params: ReadonlyMap<Value, readonly Value[]>;
1649
+ }
1650
+
1651
+ export function demandedBitsIndex(fn: Fn): DemandedBitsIndex {
1652
+ const defs = defOpMap(fn);
1653
+ const readers = new Map<Value, Op[]>();
1654
+ const params = new Map<Value, Value[]>();
1655
+ const add = <T>(m: Map<Value, T[]>, v: Value, x: T) => (m.get(v) ?? m.set(v, []).get(v)!).push(x);
1656
+ for (const b of fn.blocks) {
1657
+ for (const op of b.ops) {
1658
+ for (const v of new Set(op.operands)) {
1659
+ add(readers, v, op);
1660
+ }
1661
+ for (const e of op.successors) {
1662
+ e.args.forEach((a, i) => add(params, a, e.block.params[i]));
1663
+ }
1664
+ }
1665
+ }
1666
+ return { defs, readers, params };
1667
+ }
1668
+
1669
+ /** THE BITS OF `root` ANY READER NEEDS, as a mask: a store of W bytes needs the low 8W bits of the
1670
+ * value it stores, a `zext` of W bits its operand's low W bits its own readers need, a `sext` those
1671
+ * and its sign bit where they need any bit above, a constant mask and the bitwise ops pass their
1672
+ * readers' bits through, a shift by a constant moves them, an add, a subtract or a multiply needs
1673
+ * every bit up to its readers' highest, since a carry only travels up, and a join needs what its
1674
+ * parameter's readers need. Every other reader — a compare, a call, an address, a shift's count —
1675
+ * needs all 32, and so does a join that reaches itself again. A `ret` needs none where the
1676
+ * function's own declaration returns nothing (`voidOwn`), all 32 otherwise. A value nothing reads
1677
+ * needs none: a caller for which an unseen reader may exist checks for a use itself.
1678
+ *
1679
+ * Asked whether a caller reads a register-returned struct's word past the struct
1680
+ * (frontend/register-struct-call.ts), and whether any use of a narrow load reads the bits its
1681
+ * extension fills (structure.ts `extensionImmaterial`). */
1682
+ export function demandedBits(
1683
+ fn: Fn,
1684
+ root: Value,
1685
+ voidOwn: boolean,
1686
+ index: DemandedBitsIndex = demandedBitsIndex(fn),
1687
+ ): number {
1688
+ const { defs, readers, params } = index;
1689
+ const memo = new Map<Value, number>();
1690
+ const constOf = (x: Value | undefined): number | undefined => {
1691
+ const d = x === undefined ? undefined : defs.get(x);
1692
+ return d?.opcode === 'const' && typeof d.attrs.value === 'number' ? d.attrs.value >>> 0 : undefined;
1693
+ };
1694
+ const upTo = (d: number): number => (d === 0 ? 0 : d >= 0x80000000 ? FULL : lowBits(32 - Math.clz32(d)));
1695
+ const of = (v: Value): number => {
1696
+ const known = memo.get(v);
1697
+ if (known !== undefined) {
1698
+ return known;
1699
+ }
1700
+ memo.set(v, FULL); // a reader reached again through a join needs everything
1701
+ let need = 0;
1702
+ for (const p of params.get(v) ?? []) {
1703
+ need |= of(p);
1704
+ }
1705
+ for (const op of readers.get(v) ?? []) {
1706
+ const out = () => (op.results.length === 1 ? of(op.results[0]) : FULL);
1707
+ const imm = typeof op.attrs.imm === 'number' && op.operands.length === 1 ? op.attrs.imm : undefined;
1708
+ const other = op.operands.length === 2 ? op.operands[op.operands[0] === v ? 1 : 0] : undefined;
1709
+ // a shift's constant count, where `v` is only what it shifts
1710
+ const count =
1711
+ other === v || (op.operands.length === 2 && op.operands[1] === v) ? undefined : (imm ?? constOf(other));
1712
+ const width = typeof op.attrs.width === 'number' ? op.attrs.width : undefined;
1713
+ switch (op.opcode) {
1714
+ case 'store':
1715
+ need |= op.operands[0] === v || width === undefined || width >= 4 ? FULL : lowBits(8 * width);
1716
+ break;
1717
+ case 'ret':
1718
+ need |= voidOwn ? 0 : FULL;
1719
+ break;
1720
+ case 'zext':
1721
+ need |= width === undefined ? FULL : out() & lowBits(width);
1722
+ break;
1723
+ case 'sext': {
1724
+ const d = out();
1725
+ need |=
1726
+ width === undefined || width < 1
1727
+ ? FULL
1728
+ : (d & lowBits(width)) | ((d & ~lowBits(width)) >>> 0 !== 0 ? 2 ** (width - 1) : 0);
1729
+ break;
1730
+ }
1731
+ case 'and': {
1732
+ const c = other === v ? undefined : (imm ?? constOf(other));
1733
+ need |= c === undefined ? out() : out() & c;
1734
+ break;
1735
+ }
1736
+ case 'or':
1737
+ case 'xor':
1738
+ case 'not':
1739
+ need |= other === v ? FULL : out();
1740
+ break;
1741
+ case 'add':
1742
+ case 'sub':
1743
+ case 'mul':
1744
+ case 'neg':
1745
+ need |= other === v ? FULL : upTo(out());
1746
+ break;
1747
+ case 'shl':
1748
+ need |= count === undefined || count >= 32 ? FULL : out() >>> count;
1749
+ break;
1750
+ case 'shr_u':
1751
+ need |= count === undefined || count >= 32 ? FULL : (out() << count) >>> 0;
1752
+ break;
1753
+ case 'shr_s': {
1754
+ const d = out();
1755
+ need |=
1756
+ count === undefined || count >= 32
1757
+ ? FULL
1758
+ : ((d << count) | (count > 0 && d >>> (32 - count) !== 0 ? 0x80000000 : 0)) >>> 0;
1759
+ break;
1760
+ }
1761
+ default:
1762
+ need |= FULL;
1763
+ }
1764
+ need >>>= 0;
1765
+ }
1766
+ memo.set(v, need);
1767
+ return need;
1768
+ };
1769
+ return of(root);
1770
+ }