@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.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- 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
|
|
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:
|
|
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
|
|
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
|
+
}
|