@scriptc/compiler 0.0.10 → 0.0.12
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/ambient/scriptc-node-fallback.d.ts +63 -15
- package/ambient/scriptc.d.ts +2 -2
- package/dist/backend/cc.d.ts +2 -2
- package/dist/backend/emission/emit-async.js +1 -1
- package/dist/backend/emission/emit-exprs.js +131 -54
- package/dist/backend/emission/emit-exprs.js.map +1 -1
- package/dist/backend/emission/emit-island.js +1 -1
- package/dist/backend/emission/emit-shapes.js +1 -1
- package/dist/backend/emission/emit-walkers.d.ts +14 -14
- package/dist/backend/emission/emit-walkers.js +44 -44
- package/dist/backend/emission/emit-walkers.js.map +1 -1
- package/dist/backend/emission/emitter.d.ts +3 -3
- package/dist/backend/emission/emitter.js +33 -9
- package/dist/backend/emission/emitter.js.map +1 -1
- package/dist/backend/emission/may-throw.js +1 -1
- package/dist/backend/llvm/dyn.d.ts +10 -10
- package/dist/backend/llvm/dyn.js +26 -26
- package/dist/backend/llvm/dyn.js.map +1 -1
- package/dist/backend/llvm/emitter.js +156 -47
- package/dist/backend/llvm/emitter.js.map +1 -1
- package/dist/backend/llvm/shapes.js +1 -1
- package/dist/backend/llvm/walkers.js +2 -2
- package/dist/backend/llvm/walkers.js.map +1 -1
- package/dist/coverage/surface-manifest.js +16 -2
- package/dist/coverage/surface-manifest.js.map +1 -1
- package/dist/diagnostics/diagnostic.d.ts +21 -8
- package/dist/diagnostics/diagnostic.js +48 -14
- package/dist/diagnostics/diagnostic.js.map +1 -1
- package/dist/frontend/lowering/lib-boundary.js +5 -5
- package/dist/frontend/lowering/lib-boundary.js.map +1 -1
- package/dist/frontend/lowering/lower-assert.js +12 -12
- package/dist/frontend/lowering/lower-assert.js.map +1 -1
- package/dist/frontend/lowering/lower-builtins.d.ts +63 -7
- package/dist/frontend/lowering/lower-builtins.js +327 -35
- package/dist/frontend/lowering/lower-builtins.js.map +1 -1
- package/dist/frontend/lowering/lower-calls.d.ts +4 -4
- package/dist/frontend/lowering/lower-calls.js +104 -70
- package/dist/frontend/lowering/lower-calls.js.map +1 -1
- package/dist/frontend/lowering/lower-classes.d.ts +7 -0
- package/dist/frontend/lowering/lower-classes.js +82 -6
- package/dist/frontend/lowering/lower-classes.js.map +1 -1
- package/dist/frontend/lowering/lower-containers.d.ts +9 -5
- package/dist/frontend/lowering/lower-containers.js +88 -26
- package/dist/frontend/lowering/lower-containers.js.map +1 -1
- package/dist/frontend/lowering/lower-emitter.d.ts +70 -3
- package/dist/frontend/lowering/lower-emitter.js +438 -31
- package/dist/frontend/lowering/lower-emitter.js.map +1 -1
- package/dist/frontend/lowering/lower-exprs.d.ts +87 -12
- package/dist/frontend/lowering/lower-exprs.js +630 -125
- package/dist/frontend/lowering/lower-exprs.js.map +1 -1
- package/dist/frontend/lowering/lower-inspect.js +5 -5
- package/dist/frontend/lowering/lower-inspect.js.map +1 -1
- package/dist/frontend/lowering/lower-island.js +12 -3
- package/dist/frontend/lowering/lower-island.js.map +1 -1
- package/dist/frontend/lowering/lower-modules.d.ts +8 -5
- package/dist/frontend/lowering/lower-modules.js +157 -63
- package/dist/frontend/lowering/lower-modules.js.map +1 -1
- package/dist/frontend/lowering/lower-namespaces.d.ts +24 -0
- package/dist/frontend/lowering/lower-namespaces.js +158 -16
- package/dist/frontend/lowering/lower-namespaces.js.map +1 -1
- package/dist/frontend/lowering/lower-server.js +11 -11
- package/dist/frontend/lowering/lower-server.js.map +1 -1
- package/dist/frontend/lowering/lower-stmts.d.ts +1 -1
- package/dist/frontend/lowering/lower-stmts.js +271 -36
- package/dist/frontend/lowering/lower-stmts.js.map +1 -1
- package/dist/frontend/lowering/lower-stream.js +171 -20
- package/dist/frontend/lowering/lower-stream.js.map +1 -1
- package/dist/frontend/lowering/lowerer.d.ts +27 -10
- package/dist/frontend/lowering/lowerer.js +156 -54
- package/dist/frontend/lowering/lowerer.js.map +1 -1
- package/dist/frontend/lowering/surfaces.d.ts +44 -0
- package/dist/frontend/lowering/surfaces.js +236 -5
- package/dist/frontend/lowering/surfaces.js.map +1 -1
- package/dist/frontend/npm.d.ts +12 -0
- package/dist/frontend/npm.js +30 -0
- package/dist/frontend/npm.js.map +1 -1
- package/dist/frontend/program.d.ts +25 -0
- package/dist/frontend/program.js +145 -136
- package/dist/frontend/program.js.map +1 -1
- package/dist/frontend/shared.d.ts +1 -1
- package/dist/frontend/shared.js +8 -8
- package/dist/frontend/shared.js.map +1 -1
- package/dist/frontend/types.d.ts +2 -2
- package/dist/frontend/types.js +15 -15
- package/dist/frontend/types.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +169 -5
- package/dist/index.js.map +1 -1
- package/dist/ir/nodes.d.ts +174 -106
- package/dist/ir/nodes.js +69 -24
- package/dist/ir/nodes.js.map +1 -1
- package/dist/ir/validate.js +55 -36
- package/dist/ir/validate.js.map +1 -1
- package/dist/library/fence-eval.js +12 -2
- package/dist/library/fence-eval.js.map +1 -1
- package/dist/library/int-infer.d.ts +119 -0
- package/dist/library/int-infer.js +1314 -0
- package/dist/library/int-infer.js.map +1 -0
- package/dist/library/int-infer.test.d.ts +1 -0
- package/dist/library/int-infer.test.js +279 -0
- package/dist/library/int-infer.test.js.map +1 -0
- package/dist/library/profile.d.ts +34 -5
- package/dist/library/profile.js +77 -13
- package/dist/library/profile.js.map +1 -1
- package/dist/library/sidecar-validate.js +1 -1
- package/dist/library/sidecar-validate.js.map +1 -1
- package/dist/library/sidecar.d.ts +24 -1
- package/dist/library/sidecar.js +163 -16
- package/dist/library/sidecar.js.map +1 -1
- package/package.json +2 -2
- package/surface-manifest.json +210 -12
|
@@ -0,0 +1,1314 @@
|
|
|
1
|
+
/* ── the abstract domain ────────────────────────────────────────────────── */
|
|
2
|
+
export const SAFE_MAX = 2 ** 53 - 1; // Number.MAX_SAFE_INTEGER
|
|
3
|
+
export const SAFE_MIN = -SAFE_MAX;
|
|
4
|
+
const normZero = (x) => (Object.is(x, -0) ? 0 : x);
|
|
5
|
+
export function absVal(lo, hi, whole, maybeNaN, spelling) {
|
|
6
|
+
lo = normZero(lo);
|
|
7
|
+
hi = normZero(hi);
|
|
8
|
+
// Infinities are not integers: a set with a non-finite bound may
|
|
9
|
+
// contain them, so the wholeness claim drops.
|
|
10
|
+
if (whole && !(Number.isFinite(lo) && Number.isFinite(hi)))
|
|
11
|
+
whole = false;
|
|
12
|
+
const v = { lo, hi, whole, maybeNaN };
|
|
13
|
+
if (spelling !== undefined)
|
|
14
|
+
v.spelling = spelling;
|
|
15
|
+
return v;
|
|
16
|
+
}
|
|
17
|
+
/** Any f64 a caller could pass — unbounded, not whole, NaN included. */
|
|
18
|
+
export const TOP = { lo: -Infinity, hi: Infinity, whole: false, maybeNaN: true };
|
|
19
|
+
/** The empty set (unreachable): lo > hi and no NaN. */
|
|
20
|
+
export const BOTTOM = { lo: Infinity, hi: -Infinity, whole: true, maybeNaN: false };
|
|
21
|
+
export const isBottom = (v) => v.lo > v.hi && !v.maybeNaN;
|
|
22
|
+
export const isSingleton = (v) => v.lo === v.hi && !v.maybeNaN;
|
|
23
|
+
const hasNumeric = (v) => v.lo <= v.hi;
|
|
24
|
+
export function constVal(value, spelling) {
|
|
25
|
+
if (Number.isNaN(value))
|
|
26
|
+
return { lo: Infinity, hi: -Infinity, whole: false, maybeNaN: true };
|
|
27
|
+
return absVal(value, value, Number.isInteger(value), false, spelling);
|
|
28
|
+
}
|
|
29
|
+
/** Least upper bound: interval hull, wholeness/NaN pessimism, spelling
|
|
30
|
+
* kept only when both sides carry the same one. */
|
|
31
|
+
export function join(a, b) {
|
|
32
|
+
if (isBottom(a))
|
|
33
|
+
return b;
|
|
34
|
+
if (isBottom(b))
|
|
35
|
+
return a;
|
|
36
|
+
const numA = hasNumeric(a);
|
|
37
|
+
const numB = hasNumeric(b);
|
|
38
|
+
const lo = numA && numB ? Math.min(a.lo, b.lo) : numA ? a.lo : b.lo;
|
|
39
|
+
const hi = numA && numB ? Math.max(a.hi, b.hi) : numA ? a.hi : b.hi;
|
|
40
|
+
const whole = (numA ? a.whole : true) && (numB ? b.whole : true);
|
|
41
|
+
const spelling = a.spelling !== undefined && a.spelling === b.spelling ? a.spelling : undefined;
|
|
42
|
+
return absVal(lo, hi, whole, a.maybeNaN || b.maybeNaN, spelling);
|
|
43
|
+
}
|
|
44
|
+
export function sameVal(a, b) {
|
|
45
|
+
return a.lo === b.lo && a.hi === b.hi && a.whole === b.whole && a.maybeNaN === b.maybeNaN && a.spelling === b.spelling;
|
|
46
|
+
}
|
|
47
|
+
/* Threshold widening: when a loop-header join keeps growing, jump each
|
|
48
|
+
* still-moving bound to the next threshold instead of crawling one
|
|
49
|
+
* iteration at a time. The thresholds are the ranges the boundary check
|
|
50
|
+
* cares about, so precision is lost only past the points where the
|
|
51
|
+
* verdict would change anyway. */
|
|
52
|
+
const WIDEN_THRESHOLDS = [0, 2 ** 31 - 1, 2 ** 32 - 1, SAFE_MAX, Infinity];
|
|
53
|
+
const WIDEN_THRESHOLDS_NEG = [0, -(2 ** 31), SAFE_MIN, -Infinity];
|
|
54
|
+
export function widen(prev, next) {
|
|
55
|
+
if (isBottom(prev))
|
|
56
|
+
return next;
|
|
57
|
+
let lo = next.lo;
|
|
58
|
+
let hi = next.hi;
|
|
59
|
+
if (next.lo < prev.lo)
|
|
60
|
+
lo = WIDEN_THRESHOLDS_NEG.find((t) => t <= next.lo) ?? -Infinity;
|
|
61
|
+
if (next.hi > prev.hi)
|
|
62
|
+
hi = WIDEN_THRESHOLDS.find((t) => t >= next.hi) ?? Infinity;
|
|
63
|
+
return absVal(lo, hi, next.whole, next.maybeNaN, next.spelling);
|
|
64
|
+
}
|
|
65
|
+
/* ── transfer functions (JS semantics, never idealized math) ───────────── */
|
|
66
|
+
/* Endpoint product with the JS wrinkle 0 * Infinity = NaN handled for
|
|
67
|
+
* BOUND purposes: as an interval bound the correct limit is 0 (the NaN is
|
|
68
|
+
* the maybeNaN flag's business). */
|
|
69
|
+
function boundMul(a, b) {
|
|
70
|
+
if (a === 0 || b === 0)
|
|
71
|
+
return 0;
|
|
72
|
+
return a * b;
|
|
73
|
+
}
|
|
74
|
+
function transferAdd(a, b) {
|
|
75
|
+
let maybeNaN = a.maybeNaN || b.maybeNaN;
|
|
76
|
+
if (!hasNumeric(a) || !hasNumeric(b))
|
|
77
|
+
return { ...BOTTOM, maybeNaN };
|
|
78
|
+
// Infinity + -Infinity = NaN: possible when opposite infinities meet.
|
|
79
|
+
if ((a.hi === Infinity && b.lo === -Infinity) || (a.lo === -Infinity && b.hi === Infinity))
|
|
80
|
+
maybeNaN = true;
|
|
81
|
+
return absVal(a.lo + b.lo, a.hi + b.hi, a.whole && b.whole, maybeNaN);
|
|
82
|
+
}
|
|
83
|
+
function transferSub(a, b) {
|
|
84
|
+
let maybeNaN = a.maybeNaN || b.maybeNaN;
|
|
85
|
+
if (!hasNumeric(a) || !hasNumeric(b))
|
|
86
|
+
return { ...BOTTOM, maybeNaN };
|
|
87
|
+
if ((a.hi === Infinity && b.hi === Infinity) || (a.lo === -Infinity && b.lo === -Infinity))
|
|
88
|
+
maybeNaN = true;
|
|
89
|
+
return absVal(a.lo - b.hi, a.hi - b.lo, a.whole && b.whole, maybeNaN);
|
|
90
|
+
}
|
|
91
|
+
function transferMul(a, b) {
|
|
92
|
+
let maybeNaN = a.maybeNaN || b.maybeNaN;
|
|
93
|
+
if (!hasNumeric(a) || !hasNumeric(b))
|
|
94
|
+
return { ...BOTTOM, maybeNaN };
|
|
95
|
+
// 0 * Infinity = NaN: possible when one side may be 0 and the other infinite.
|
|
96
|
+
const aHasZero = a.lo <= 0 && a.hi >= 0;
|
|
97
|
+
const bHasZero = b.lo <= 0 && b.hi >= 0;
|
|
98
|
+
const aInf = a.lo === -Infinity || a.hi === Infinity;
|
|
99
|
+
const bInf = b.lo === -Infinity || b.hi === Infinity;
|
|
100
|
+
if ((aHasZero && bInf) || (bHasZero && aInf))
|
|
101
|
+
maybeNaN = true;
|
|
102
|
+
const p = [boundMul(a.lo, b.lo), boundMul(a.lo, b.hi), boundMul(a.hi, b.lo), boundMul(a.hi, b.hi)];
|
|
103
|
+
return absVal(Math.min(...p), Math.max(...p), a.whole && b.whole, maybeNaN);
|
|
104
|
+
}
|
|
105
|
+
function transferDiv(a, b) {
|
|
106
|
+
const maybeNaN = a.maybeNaN || b.maybeNaN;
|
|
107
|
+
if (!hasNumeric(a) || !hasNumeric(b))
|
|
108
|
+
return { ...BOTTOM, maybeNaN };
|
|
109
|
+
// Divisor exactly 0: x/0 is ±Infinity (sign by the dividend), 0/0 NaN.
|
|
110
|
+
if (isSingleton(b) && b.lo === 0) {
|
|
111
|
+
const nanPossible = maybeNaN || (a.lo <= 0 && a.hi >= 0);
|
|
112
|
+
const lo = a.lo < 0 ? -Infinity : Infinity; // -Infinity reachable iff some dividend < 0
|
|
113
|
+
const hi = a.hi > 0 ? Infinity : -Infinity; // +Infinity reachable iff some dividend > 0
|
|
114
|
+
if (lo > hi)
|
|
115
|
+
return { ...BOTTOM, maybeNaN: nanPossible }; // only 0/0: no numeric members
|
|
116
|
+
return absVal(lo, hi, false, nanPossible);
|
|
117
|
+
}
|
|
118
|
+
// Divisor may be 0 among other values: give up on precision.
|
|
119
|
+
if (b.lo <= 0 && b.hi >= 0)
|
|
120
|
+
return { ...TOP };
|
|
121
|
+
const q = [a.lo / b.lo, a.lo / b.hi, a.hi / b.lo, a.hi / b.hi];
|
|
122
|
+
const lo = Math.min(...q);
|
|
123
|
+
const hi = Math.max(...q);
|
|
124
|
+
// Division does not preserve wholeness (7 / 2 = 3.5); the one provable
|
|
125
|
+
// case is a singleton landing on an integer.
|
|
126
|
+
return absVal(lo, hi, lo === hi && Number.isInteger(lo), maybeNaN);
|
|
127
|
+
}
|
|
128
|
+
function transferMod(a, b) {
|
|
129
|
+
let maybeNaN = a.maybeNaN || b.maybeNaN;
|
|
130
|
+
if (!hasNumeric(a) || !hasNumeric(b))
|
|
131
|
+
return { ...BOTTOM, maybeNaN };
|
|
132
|
+
// x % 0 = NaN; Infinity % y = NaN.
|
|
133
|
+
if (b.lo <= 0 && b.hi >= 0)
|
|
134
|
+
maybeNaN = true;
|
|
135
|
+
if (a.lo === -Infinity || a.hi === Infinity)
|
|
136
|
+
maybeNaN = true;
|
|
137
|
+
if (isSingleton(a) && isSingleton(b) && !maybeNaN)
|
|
138
|
+
return constVal(a.lo % b.lo);
|
|
139
|
+
// JS remainder: the sign follows the DIVIDEND; |r| < |divisor|.
|
|
140
|
+
const dMax = Math.max(Math.abs(b.lo), Math.abs(b.hi));
|
|
141
|
+
const bound = a.whole && b.whole && Number.isFinite(dMax) ? dMax - 1 : dMax;
|
|
142
|
+
const lo = a.lo < 0 ? -bound : 0;
|
|
143
|
+
const hi = a.hi > 0 ? bound : 0;
|
|
144
|
+
return absVal(lo, hi, a.whole && b.whole, maybeNaN);
|
|
145
|
+
}
|
|
146
|
+
function transferPow(a, b) {
|
|
147
|
+
const maybeNaN = a.maybeNaN || b.maybeNaN;
|
|
148
|
+
if (!hasNumeric(a) || !hasNumeric(b))
|
|
149
|
+
return { ...BOTTOM, maybeNaN };
|
|
150
|
+
if (isSingleton(a) && isSingleton(b) && !maybeNaN)
|
|
151
|
+
return constVal(a.lo ** b.lo);
|
|
152
|
+
// Provable without folding: whole base ≥ 1 with whole non-negative
|
|
153
|
+
// exponent (pow is monotone in both arguments on that region).
|
|
154
|
+
if (a.whole && b.whole && b.lo >= 0 && a.lo >= 1) {
|
|
155
|
+
return absVal(a.lo ** b.lo, a.hi ** b.hi, true, maybeNaN);
|
|
156
|
+
}
|
|
157
|
+
// Anything else (negative bases, fractional exponents): NaN is
|
|
158
|
+
// reachable ((-1) ** 0.5), so give up rather than enumerate corners.
|
|
159
|
+
return { ...TOP };
|
|
160
|
+
}
|
|
161
|
+
/* The bitwise coercion contract, exactly: each operand passes through
|
|
162
|
+
* ToInt32 (ToUint32 for >>>), which maps NaN and the infinities to 0 and
|
|
163
|
+
* truncates-and-wraps everything else; shift counts mask to 5 bits. The
|
|
164
|
+
* RESULT is therefore always whole and in int32 (uint32 for >>>) range
|
|
165
|
+
* no matter what the inputs were — `x | 0` is a proof, not a hint. */
|
|
166
|
+
function transferBitwise(op, a, b) {
|
|
167
|
+
if (isSingleton(a) && isSingleton(b)) {
|
|
168
|
+
const x = a.lo;
|
|
169
|
+
const y = b.lo;
|
|
170
|
+
const r = op === "&" ? x & y :
|
|
171
|
+
op === "|" ? x | y :
|
|
172
|
+
op === "^" ? x ^ y :
|
|
173
|
+
op === "<<" ? x << y :
|
|
174
|
+
op === ">>" ? x >> y :
|
|
175
|
+
x >>> y;
|
|
176
|
+
return constVal(r);
|
|
177
|
+
}
|
|
178
|
+
if (op === ">>>")
|
|
179
|
+
return absVal(0, 2 ** 32 - 1, true, false);
|
|
180
|
+
return absVal(-(2 ** 31), 2 ** 31 - 1, true, false);
|
|
181
|
+
}
|
|
182
|
+
/** JS `~`: ToInt32, complement — whole, int32 range, whatever the input. */
|
|
183
|
+
export function transferBitNot(a) {
|
|
184
|
+
if (isSingleton(a))
|
|
185
|
+
return constVal(~a.lo);
|
|
186
|
+
return absVal(-(2 ** 31), 2 ** 31 - 1, true, false);
|
|
187
|
+
}
|
|
188
|
+
export function transferNeg(a) {
|
|
189
|
+
if (!hasNumeric(a))
|
|
190
|
+
return { ...BOTTOM, maybeNaN: a.maybeNaN };
|
|
191
|
+
return absVal(-a.hi, -a.lo, a.whole, a.maybeNaN);
|
|
192
|
+
}
|
|
193
|
+
export function transferBin(op, a, b) {
|
|
194
|
+
switch (op) {
|
|
195
|
+
case "+": return transferAdd(a, b);
|
|
196
|
+
case "-": return transferSub(a, b);
|
|
197
|
+
case "*": return transferMul(a, b);
|
|
198
|
+
case "/": return transferDiv(a, b);
|
|
199
|
+
case "%": return transferMod(a, b);
|
|
200
|
+
case "**": return transferPow(a, b);
|
|
201
|
+
case "&":
|
|
202
|
+
case "|":
|
|
203
|
+
case "^":
|
|
204
|
+
case "<<":
|
|
205
|
+
case ">>":
|
|
206
|
+
case ">>>":
|
|
207
|
+
return transferBitwise(op, a, b);
|
|
208
|
+
default:
|
|
209
|
+
// Comparisons produce bool, not a numeric abstract value.
|
|
210
|
+
return { ...TOP };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** The Math rounding family is the author's stated intent: every finite
|
|
214
|
+
* input maps to a whole output, so these discharge the WHOLENESS
|
|
215
|
+
* obligation. They do not discharge range (an unbounded input stays
|
|
216
|
+
* unbounded) or NaN (Math.trunc(NaN) is NaN — maybeNaN propagates). */
|
|
217
|
+
export function transferMathRound(fn, a) {
|
|
218
|
+
if (!hasNumeric(a))
|
|
219
|
+
return { ...BOTTOM, maybeNaN: a.maybeNaN };
|
|
220
|
+
const f = fn === "trunc" ? Math.trunc : fn === "floor" ? Math.floor : fn === "ceil" ? Math.ceil : Math.round;
|
|
221
|
+
return absVal(f(a.lo), f(a.hi), true, a.maybeNaN);
|
|
222
|
+
}
|
|
223
|
+
export function transferAbs(a) {
|
|
224
|
+
if (!hasNumeric(a))
|
|
225
|
+
return { ...BOTTOM, maybeNaN: a.maybeNaN };
|
|
226
|
+
const lo = a.lo <= 0 && a.hi >= 0 ? 0 : Math.min(Math.abs(a.lo), Math.abs(a.hi));
|
|
227
|
+
return absVal(lo, Math.max(Math.abs(a.lo), Math.abs(a.hi)), a.whole, a.maybeNaN);
|
|
228
|
+
}
|
|
229
|
+
/** Math.min/max propagate NaN from ANY argument, exactly as JS does. */
|
|
230
|
+
export function transferMinMax(fn, args) {
|
|
231
|
+
const maybeNaN = args.some((v) => v.maybeNaN);
|
|
232
|
+
if (args.some((v) => !hasNumeric(v)))
|
|
233
|
+
return { ...BOTTOM, maybeNaN };
|
|
234
|
+
const lo = fn === "min" ? Math.min(...args.map((v) => v.lo)) : Math.max(...args.map((v) => v.lo));
|
|
235
|
+
const hi = fn === "min" ? Math.min(...args.map((v) => v.hi)) : Math.max(...args.map((v) => v.hi));
|
|
236
|
+
return absVal(lo, hi, args.every((v) => v.whole), maybeNaN);
|
|
237
|
+
}
|
|
238
|
+
/** Does an integer literal's source spelling survive the trip through
|
|
239
|
+
* f64? Parse, convert, format back, compare (numeric separators were
|
|
240
|
+
* stripped by the frontend — they are spelling sugar, not value). */
|
|
241
|
+
export function spellingRoundTrips(spelling) {
|
|
242
|
+
return String(Number(spelling)) === spelling;
|
|
243
|
+
}
|
|
244
|
+
/** Check a converged abstract value against a slot's obligations, in the
|
|
245
|
+
* teaching-quality order: representability, then NaN, then range, then
|
|
246
|
+
* fractional wholeness — the FIRST failure names the refusal (a value
|
|
247
|
+
* both fractional and out of range hears about the more fundamental
|
|
248
|
+
* problem). */
|
|
249
|
+
export function checkBoundary(v, path, cls, loc) {
|
|
250
|
+
// Representability first: the author wrote a number the program never held.
|
|
251
|
+
if (v.spelling !== undefined && !spellingRoundTrips(v.spelling)) {
|
|
252
|
+
return {
|
|
253
|
+
path, cls, loc, outcome: "refuse", obligation: "representability",
|
|
254
|
+
detail: `the literal ${v.spelling} is not representable as f64 — it reads back as ${Number(v.spelling)}`,
|
|
255
|
+
fix: "write the nearest representable integer explicitly, or restructure so the value stays within ±(2^53 − 1)",
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (isBottom(v)) {
|
|
259
|
+
// The slot is unreachable on every path; vacuously proven.
|
|
260
|
+
return { path, cls, loc, outcome: "prove", provenLo: NaN, provenHi: NaN };
|
|
261
|
+
}
|
|
262
|
+
if (v.maybeNaN) {
|
|
263
|
+
return {
|
|
264
|
+
path, cls, loc, outcome: "refuse", obligation: "wholeness",
|
|
265
|
+
detail: "the value may be NaN, which is not a whole number",
|
|
266
|
+
fix: "guard the value with a comparison before the boundary (any ordered comparison excludes NaN), then state intent with Math.trunc/Math.floor/Math.ceil/Math.round if it may be fractional",
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const min = cls === "u64" ? 0 : SAFE_MIN;
|
|
270
|
+
if (!(v.lo >= min && v.hi <= SAFE_MAX)) {
|
|
271
|
+
return {
|
|
272
|
+
path, cls, loc, outcome: "refuse", obligation: "range",
|
|
273
|
+
detail: `the proven range [${v.lo}, ${v.hi}] does not fit ${cls === "u64" ? `[0, ${SAFE_MAX}]` : `[${SAFE_MIN}, ${SAFE_MAX}]`} — integrality is provable only within ±(2^53 − 1)` +
|
|
274
|
+
(cls === "u64" ? ", and a u64 slot additionally requires a non-negative proven range" : ""),
|
|
275
|
+
fix: "bound the value before the boundary (clamp, compare, or restructure the computation to stay in range)",
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (!v.whole) {
|
|
279
|
+
return {
|
|
280
|
+
path, cls, loc, outcome: "refuse", obligation: "wholeness",
|
|
281
|
+
detail: `the value is not provably whole — the proven range [${v.lo}, ${v.hi}] may contain non-integers`,
|
|
282
|
+
fix: "state intent at the boundary with Math.trunc, Math.floor, Math.ceil, or Math.round",
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return { path, cls, loc, outcome: "prove", provenLo: v.lo, provenHi: v.hi };
|
|
286
|
+
}
|
|
287
|
+
export function hasIntSlots(cfg) {
|
|
288
|
+
if (cfg.records.size > 0)
|
|
289
|
+
return true;
|
|
290
|
+
for (const f of cfg.fns.values()) {
|
|
291
|
+
if (f.ret !== null || f.params.some((p) => p !== null))
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
/** The class seed for a declared parameter: what the slot's own contract
|
|
297
|
+
* proves about every value that ever arrives through it (internal calls
|
|
298
|
+
* are checked at their call sites; external calls range-check in the
|
|
299
|
+
* marshalling wrapper). */
|
|
300
|
+
export function classSeed(cls) {
|
|
301
|
+
switch (cls) {
|
|
302
|
+
case "i64": return absVal(SAFE_MIN, SAFE_MAX, true, false);
|
|
303
|
+
case "u64": return absVal(0, SAFE_MAX, true, false);
|
|
304
|
+
case "u8": return absVal(0, 255, true, false);
|
|
305
|
+
case "u32": return absVal(0, 2 ** 32 - 1, true, false);
|
|
306
|
+
case "i32": return absVal(-(2 ** 31), 2 ** 31 - 1, true, false);
|
|
307
|
+
default: return { ...TOP };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const isGlobalId = (id) => id.startsWith("%g.");
|
|
311
|
+
const defaultVal = (id) => (isGlobalId(id) ? TOP : BOTTOM);
|
|
312
|
+
function envGet(env, id) {
|
|
313
|
+
return env.get(id) ?? defaultVal(id);
|
|
314
|
+
}
|
|
315
|
+
function joinEnv(a, b) {
|
|
316
|
+
if (a === null)
|
|
317
|
+
return b === null ? null : new Map(b);
|
|
318
|
+
if (b === null)
|
|
319
|
+
return new Map(a);
|
|
320
|
+
const out = new Map();
|
|
321
|
+
const keys = new Set([...a.keys(), ...b.keys()]);
|
|
322
|
+
for (const k of keys) {
|
|
323
|
+
out.set(k, join(a.get(k) ?? defaultVal(k), b.get(k) ?? defaultVal(k)));
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
function envEquals(a, b) {
|
|
328
|
+
const keys = new Set([...a.keys(), ...b.keys()]);
|
|
329
|
+
for (const k of keys) {
|
|
330
|
+
if (!sameVal(a.get(k) ?? defaultVal(k), b.get(k) ?? defaultVal(k)))
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
function widenEnv(prev, next) {
|
|
336
|
+
const out = new Map();
|
|
337
|
+
const keys = new Set([...prev.keys(), ...next.keys()]);
|
|
338
|
+
for (const k of keys) {
|
|
339
|
+
out.set(k, widen(prev.get(k) ?? defaultVal(k), next.get(k) ?? defaultVal(k)));
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
function globalEffectsOf(mod) {
|
|
344
|
+
const writes = new Map();
|
|
345
|
+
const calls = new Map();
|
|
346
|
+
const unknown = new Set();
|
|
347
|
+
const typeHasFunc = (t) => JSON.stringify(t).includes('"func"');
|
|
348
|
+
for (const fn of mod.functions) {
|
|
349
|
+
const w = new Set();
|
|
350
|
+
const c = new Set();
|
|
351
|
+
let u = false;
|
|
352
|
+
const visitExpr = (e) => {
|
|
353
|
+
switch (e.kind) {
|
|
354
|
+
case "assignExpr":
|
|
355
|
+
if (isGlobalId(e.localId))
|
|
356
|
+
w.add(e.localId);
|
|
357
|
+
visitExpr(e.value);
|
|
358
|
+
return;
|
|
359
|
+
case "incDec":
|
|
360
|
+
if (isGlobalId(e.localId))
|
|
361
|
+
w.add(e.localId);
|
|
362
|
+
return;
|
|
363
|
+
case "call":
|
|
364
|
+
c.add(e.callee);
|
|
365
|
+
e.args.forEach(visitExpr);
|
|
366
|
+
return;
|
|
367
|
+
case "callValue":
|
|
368
|
+
case "newValue":
|
|
369
|
+
case "dynCall":
|
|
370
|
+
case "dynInvoke":
|
|
371
|
+
case "virtualCall":
|
|
372
|
+
case "new":
|
|
373
|
+
case "intrinsic":
|
|
374
|
+
u = true;
|
|
375
|
+
break;
|
|
376
|
+
case "libCall":
|
|
377
|
+
if (e.args.some((a) => typeHasFunc(a.type)))
|
|
378
|
+
u = true;
|
|
379
|
+
break;
|
|
380
|
+
case "seqExpr":
|
|
381
|
+
e.stmts.forEach(visitStmt);
|
|
382
|
+
visitExpr(e.result);
|
|
383
|
+
return;
|
|
384
|
+
default:
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
for (const key of Object.keys(e)) {
|
|
388
|
+
const v = e[key];
|
|
389
|
+
if (Array.isArray(v)) {
|
|
390
|
+
for (const item of v) {
|
|
391
|
+
if (item !== null && typeof item === "object" && "kind" in item) {
|
|
392
|
+
const it = item;
|
|
393
|
+
if (typeof it.kind === "string")
|
|
394
|
+
visitExpr(item);
|
|
395
|
+
}
|
|
396
|
+
else if (item !== null && typeof item === "object") {
|
|
397
|
+
// field-shaped entries ({ name, value } etc.)
|
|
398
|
+
for (const sub of Object.values(item)) {
|
|
399
|
+
if (sub !== null && typeof sub === "object" && typeof sub.kind === "string") {
|
|
400
|
+
visitExpr(sub);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else if (v !== null && typeof v === "object" && typeof v.kind === "string" && key !== "type") {
|
|
407
|
+
visitExpr(v);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
const visitStmt = (s) => {
|
|
412
|
+
switch (s.kind) {
|
|
413
|
+
case "assign":
|
|
414
|
+
if (isGlobalId(s.localId))
|
|
415
|
+
w.add(s.localId);
|
|
416
|
+
visitExpr(s.value);
|
|
417
|
+
return;
|
|
418
|
+
case "varDecl":
|
|
419
|
+
if (s.init !== null)
|
|
420
|
+
visitExpr(s.init);
|
|
421
|
+
return;
|
|
422
|
+
default:
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
for (const v of Object.values(s)) {
|
|
426
|
+
if (Array.isArray(v)) {
|
|
427
|
+
for (const item of v) {
|
|
428
|
+
if (item !== null && typeof item === "object" && typeof item.kind === "string") {
|
|
429
|
+
const node = item;
|
|
430
|
+
if (isStmtKind(node.kind))
|
|
431
|
+
visitStmt(item);
|
|
432
|
+
else
|
|
433
|
+
visitExpr(item);
|
|
434
|
+
}
|
|
435
|
+
else if (item !== null && typeof item === "object") {
|
|
436
|
+
// switch cases: { test, body }
|
|
437
|
+
const cs = item;
|
|
438
|
+
if (cs.test !== undefined && cs.test !== null)
|
|
439
|
+
visitExpr(cs.test);
|
|
440
|
+
if (Array.isArray(cs.body))
|
|
441
|
+
cs.body.forEach(visitStmt);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
else if (v !== null && typeof v === "object" && typeof v.kind === "string") {
|
|
446
|
+
const node = v;
|
|
447
|
+
if (isStmtKind(node.kind))
|
|
448
|
+
visitStmt(v);
|
|
449
|
+
else
|
|
450
|
+
visitExpr(v);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
fn.body.forEach(visitStmt);
|
|
455
|
+
writes.set(fn.name, w);
|
|
456
|
+
calls.set(fn.name, c);
|
|
457
|
+
if (u)
|
|
458
|
+
unknown.add(fn.name);
|
|
459
|
+
}
|
|
460
|
+
// Transitive closure over the direct-call graph.
|
|
461
|
+
let changed = true;
|
|
462
|
+
while (changed) {
|
|
463
|
+
changed = false;
|
|
464
|
+
for (const [name, callees] of calls) {
|
|
465
|
+
const w = writes.get(name);
|
|
466
|
+
for (const callee of callees) {
|
|
467
|
+
if (unknown.has(callee) && !unknown.has(name)) {
|
|
468
|
+
unknown.add(name);
|
|
469
|
+
changed = true;
|
|
470
|
+
}
|
|
471
|
+
for (const g of writes.get(callee) ?? []) {
|
|
472
|
+
if (!w.has(g)) {
|
|
473
|
+
w.add(g);
|
|
474
|
+
changed = true;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return { perFn: writes, havocAll: unknown };
|
|
481
|
+
}
|
|
482
|
+
const STMT_KINDS = new Set([
|
|
483
|
+
"varDecl", "assign", "exprStmt", "if", "while", "doWhile", "switch", "for",
|
|
484
|
+
"arraySet", "bytesSet", "forOf", "return", "fieldSet", "recordSet",
|
|
485
|
+
"recordKeySet", "recordKeyDelete", "break", "continue", "block", "throw",
|
|
486
|
+
"runtimeFence", "rethrow", "tryCatch",
|
|
487
|
+
]);
|
|
488
|
+
const isStmtKind = (k) => STMT_KINDS.has(k);
|
|
489
|
+
/* ── the analyzer ──────────────────────────────────────────────────────── */
|
|
490
|
+
const WIDEN_AFTER = 3; // plain joins at a loop header before widening
|
|
491
|
+
const LOOP_CAP = 64; // hard bound; thresholds converge far earlier
|
|
492
|
+
const NEGATE = { "<": ">=", "<=": ">", ">": "<=", ">=": "<", "===": "!==", "!==": "===" };
|
|
493
|
+
const FLIP = { "<": ">", "<=": ">=", ">": "<", ">=": "<=", "===": "===", "!==": "!==" };
|
|
494
|
+
const CMP_OPS = new Set(["<", "<=", ">", ">=", "===", "!=="]);
|
|
495
|
+
/** Meet a value with an interval; `clearNaN` when the comparison's truth
|
|
496
|
+
* on this edge excludes NaN operands. */
|
|
497
|
+
function meetInterval(v, lo, hi, clearNaN) {
|
|
498
|
+
const newLo = Math.max(v.lo, lo);
|
|
499
|
+
const newHi = Math.min(v.hi, hi);
|
|
500
|
+
if (newLo > newHi) {
|
|
501
|
+
// No numeric member survives this edge; NaN may still flow through.
|
|
502
|
+
return { ...BOTTOM, maybeNaN: clearNaN ? false : v.maybeNaN };
|
|
503
|
+
}
|
|
504
|
+
return absVal(newLo, newHi, v.whole, clearNaN ? false : v.maybeNaN, v.spelling);
|
|
505
|
+
}
|
|
506
|
+
/** Refine `a` under the assumption `a OP b` held. Wholeness sharpens the
|
|
507
|
+
* strict bounds: whole x < b ⇒ x ≤ ⌈b.hi⌉ − 1 — the rule that lets the
|
|
508
|
+
* ordinary counter loop prove a precise bound. */
|
|
509
|
+
function refineLhs(op, a, b, clearNaN) {
|
|
510
|
+
if (!hasNumeric(b) || !hasNumeric(a))
|
|
511
|
+
return clearNaN ? { ...a, maybeNaN: false } : a;
|
|
512
|
+
switch (op) {
|
|
513
|
+
case "<":
|
|
514
|
+
return meetInterval(a, -Infinity, a.whole ? Math.ceil(b.hi) - 1 : b.hi, clearNaN);
|
|
515
|
+
case "<=":
|
|
516
|
+
return meetInterval(a, -Infinity, a.whole ? Math.floor(b.hi) : b.hi, clearNaN);
|
|
517
|
+
case ">":
|
|
518
|
+
return meetInterval(a, a.whole ? Math.floor(b.lo) + 1 : b.lo, Infinity, clearNaN);
|
|
519
|
+
case ">=":
|
|
520
|
+
return meetInterval(a, a.whole ? Math.ceil(b.lo) : b.lo, Infinity, clearNaN);
|
|
521
|
+
case "===":
|
|
522
|
+
return meetInterval(a, b.lo, b.hi, clearNaN);
|
|
523
|
+
case "!==": {
|
|
524
|
+
// Only endpoint exclusion is useful: whole x !== integer singleton k
|
|
525
|
+
// sitting on an endpoint of x's interval.
|
|
526
|
+
if (isSingleton(b) && a.whole && Number.isInteger(b.lo)) {
|
|
527
|
+
if (a.lo === b.lo)
|
|
528
|
+
return meetInterval(a, a.lo + 1, a.hi, clearNaN);
|
|
529
|
+
if (a.hi === b.lo)
|
|
530
|
+
return meetInterval(a, a.lo, a.hi - 1, clearNaN);
|
|
531
|
+
}
|
|
532
|
+
return clearNaN ? { ...a, maybeNaN: false } : a;
|
|
533
|
+
}
|
|
534
|
+
default:
|
|
535
|
+
return a;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
class FnAnalyzer {
|
|
539
|
+
mod;
|
|
540
|
+
cfg;
|
|
541
|
+
effects;
|
|
542
|
+
verdicts;
|
|
543
|
+
frames = [];
|
|
544
|
+
collect = false;
|
|
545
|
+
/** Return-site abstract values (for the declared-return check). */
|
|
546
|
+
constructor(mod, cfg, effects, verdicts) {
|
|
547
|
+
this.mod = mod;
|
|
548
|
+
this.cfg = cfg;
|
|
549
|
+
this.effects = effects;
|
|
550
|
+
this.verdicts = verdicts;
|
|
551
|
+
}
|
|
552
|
+
analyze(fn) {
|
|
553
|
+
const env = new Map();
|
|
554
|
+
const slots = this.cfg.fns.get(fn.name);
|
|
555
|
+
fn.params.forEach((p, i) => {
|
|
556
|
+
if (p.type.kind !== "f64")
|
|
557
|
+
return;
|
|
558
|
+
const declared = slots?.params[i] ?? null;
|
|
559
|
+
const seed = slots?.paramSeeds[i] ?? null;
|
|
560
|
+
if (declared !== null)
|
|
561
|
+
env.set(p.localId, classSeed(declared));
|
|
562
|
+
else if (seed !== null)
|
|
563
|
+
env.set(p.localId, { ...seed });
|
|
564
|
+
else
|
|
565
|
+
env.set(p.localId, { ...TOP });
|
|
566
|
+
});
|
|
567
|
+
this.collect = true;
|
|
568
|
+
this.retSlot = slots !== undefined && slots.ret !== null ? { cls: slots.ret, path: slots.retPath } : null;
|
|
569
|
+
this.execStmts(fn.body, env);
|
|
570
|
+
}
|
|
571
|
+
retSlot = null;
|
|
572
|
+
emit(v, path, cls, loc) {
|
|
573
|
+
if (!this.collect)
|
|
574
|
+
return;
|
|
575
|
+
this.verdicts.push(checkBoundary(v, path, cls, loc));
|
|
576
|
+
}
|
|
577
|
+
/* ── statements ─────────────────────────────────────────────────────── */
|
|
578
|
+
execStmts(stmts, env) {
|
|
579
|
+
for (const s of stmts) {
|
|
580
|
+
if (env === null)
|
|
581
|
+
return null; // unreachable remainder
|
|
582
|
+
env = this.execStmt(s, env);
|
|
583
|
+
}
|
|
584
|
+
return env;
|
|
585
|
+
}
|
|
586
|
+
execStmt(s, env) {
|
|
587
|
+
switch (s.kind) {
|
|
588
|
+
case "varDecl": {
|
|
589
|
+
if (s.init === null)
|
|
590
|
+
return env;
|
|
591
|
+
const v = this.evalExpr(s.init, env);
|
|
592
|
+
if (this.bindingIsF64(s.localId))
|
|
593
|
+
env.set(s.localId, v);
|
|
594
|
+
return env;
|
|
595
|
+
}
|
|
596
|
+
case "assign": {
|
|
597
|
+
const v = this.evalExpr(s.value, env);
|
|
598
|
+
if (this.bindingIsF64(s.localId))
|
|
599
|
+
env.set(s.localId, v);
|
|
600
|
+
return env;
|
|
601
|
+
}
|
|
602
|
+
case "exprStmt":
|
|
603
|
+
this.evalExpr(s.expr, env);
|
|
604
|
+
return env;
|
|
605
|
+
case "if": {
|
|
606
|
+
this.evalExpr(s.cond, env);
|
|
607
|
+
const thenEnv = this.refine(cloneEnv(env), s.cond, true);
|
|
608
|
+
const elseEnv = this.refine(cloneEnv(env), s.cond, false);
|
|
609
|
+
const a = thenEnv === null ? null : this.execStmts(s.then, thenEnv);
|
|
610
|
+
const b = s.else_ === null ? elseEnv : elseEnv === null ? null : this.execStmts(s.else_, elseEnv);
|
|
611
|
+
return joinEnv(a, b);
|
|
612
|
+
}
|
|
613
|
+
case "while":
|
|
614
|
+
return this.execLoop(env, { cond: s.cond, body: s.body, labels: s.labels ?? [] });
|
|
615
|
+
case "for": {
|
|
616
|
+
let e = env;
|
|
617
|
+
if (s.init !== null)
|
|
618
|
+
e = this.execStmt(s.init, e);
|
|
619
|
+
if (e === null)
|
|
620
|
+
return null;
|
|
621
|
+
return this.execLoop(e, {
|
|
622
|
+
...(s.cond !== null ? { cond: s.cond } : {}),
|
|
623
|
+
body: s.body,
|
|
624
|
+
...(s.update !== null ? { update: s.update } : {}),
|
|
625
|
+
labels: s.labels ?? [],
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
case "doWhile":
|
|
629
|
+
return this.execLoop(env, { cond: s.cond, body: s.body, labels: s.labels ?? [], doWhile: true });
|
|
630
|
+
case "forOf": {
|
|
631
|
+
this.evalExpr(s.iterable, env);
|
|
632
|
+
const elemF64 = this.bindingIsF64(s.localId);
|
|
633
|
+
return this.execLoop(env, {
|
|
634
|
+
body: s.body,
|
|
635
|
+
labels: s.labels ?? [],
|
|
636
|
+
alwaysExits: true, // the iteration ends when the array runs out
|
|
637
|
+
...(elemF64 ? { seedEachIteration: s.localId } : {}),
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
case "switch": {
|
|
641
|
+
this.evalExpr(s.disc, env);
|
|
642
|
+
const frame = { kind: "switch", labels: s.labels ?? [], breaks: [], continues: [] };
|
|
643
|
+
this.frames.push(frame);
|
|
644
|
+
let running = null; // the fallthrough path
|
|
645
|
+
let hasDefault = false;
|
|
646
|
+
try {
|
|
647
|
+
for (const c of s.cases) {
|
|
648
|
+
if (c.test === null)
|
|
649
|
+
hasDefault = true;
|
|
650
|
+
else
|
|
651
|
+
this.evalExpr(c.test, env);
|
|
652
|
+
running = this.execStmts(c.body, joinEnv(running, cloneEnv(env)));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
finally {
|
|
656
|
+
this.frames.pop();
|
|
657
|
+
}
|
|
658
|
+
let out = running;
|
|
659
|
+
for (const b of frame.breaks)
|
|
660
|
+
out = joinEnv(out, b);
|
|
661
|
+
// Without a default (or with tests that all miss) control skips past.
|
|
662
|
+
if (!hasDefault)
|
|
663
|
+
out = joinEnv(out, env);
|
|
664
|
+
return out;
|
|
665
|
+
}
|
|
666
|
+
case "arraySet":
|
|
667
|
+
case "bytesSet":
|
|
668
|
+
this.evalExpr(s.arr, env);
|
|
669
|
+
this.evalExpr(s.index, env);
|
|
670
|
+
this.evalExpr(s.value, env);
|
|
671
|
+
return env;
|
|
672
|
+
case "fieldSet":
|
|
673
|
+
this.evalExpr(s.obj, env);
|
|
674
|
+
this.evalExpr(s.value, env);
|
|
675
|
+
return env;
|
|
676
|
+
case "recordSet": {
|
|
677
|
+
this.evalExpr(s.obj, env);
|
|
678
|
+
const v = this.evalExpr(s.value, env);
|
|
679
|
+
const slot = this.cfg.records.get(s.shapeId)?.get(s.field);
|
|
680
|
+
if (slot !== undefined)
|
|
681
|
+
this.emit(v, slot.path, slot.cls, s.loc);
|
|
682
|
+
return env;
|
|
683
|
+
}
|
|
684
|
+
case "recordKeySet":
|
|
685
|
+
this.evalExpr(s.obj, env);
|
|
686
|
+
this.evalExpr(s.key, env);
|
|
687
|
+
this.evalExpr(s.value, env);
|
|
688
|
+
return env;
|
|
689
|
+
case "recordKeyDelete":
|
|
690
|
+
this.evalExpr(s.obj, env);
|
|
691
|
+
this.evalExpr(s.key, env);
|
|
692
|
+
return env;
|
|
693
|
+
case "return": {
|
|
694
|
+
if (s.value !== null) {
|
|
695
|
+
const v = this.evalExpr(s.value, env);
|
|
696
|
+
if (this.retSlot !== null)
|
|
697
|
+
this.emit(v, this.retSlot.path, this.retSlot.cls, s.loc);
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
case "throw":
|
|
702
|
+
this.evalExpr(s.value, env);
|
|
703
|
+
return null;
|
|
704
|
+
case "rethrow":
|
|
705
|
+
case "runtimeFence":
|
|
706
|
+
return null;
|
|
707
|
+
case "break": {
|
|
708
|
+
const frame = this.jumpTarget(s.label, "break");
|
|
709
|
+
if (frame !== null)
|
|
710
|
+
frame.breaks.push(cloneEnv(env));
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
case "continue": {
|
|
714
|
+
const frame = this.jumpTarget(s.label, "continue");
|
|
715
|
+
if (frame !== null)
|
|
716
|
+
frame.continues.push(cloneEnv(env));
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
case "block": {
|
|
720
|
+
if ((s.labels ?? []).length === 0)
|
|
721
|
+
return this.execStmts(s.body, env);
|
|
722
|
+
const frame = { kind: "block", labels: s.labels, breaks: [], continues: [] };
|
|
723
|
+
this.frames.push(frame);
|
|
724
|
+
let out;
|
|
725
|
+
try {
|
|
726
|
+
out = this.execStmts(s.body, env);
|
|
727
|
+
}
|
|
728
|
+
finally {
|
|
729
|
+
this.frames.pop();
|
|
730
|
+
}
|
|
731
|
+
for (const b of frame.breaks)
|
|
732
|
+
out = joinEnv(out, b);
|
|
733
|
+
return out;
|
|
734
|
+
}
|
|
735
|
+
case "tryCatch": {
|
|
736
|
+
const entry = cloneEnv(env);
|
|
737
|
+
const afterTry = this.execStmts(s.tryBody, env);
|
|
738
|
+
let afterCatch = null;
|
|
739
|
+
if (s.catchBody !== null) {
|
|
740
|
+
// The exception may have unwound from ANY point of the try
|
|
741
|
+
// body: every binding the try can write (and every global —
|
|
742
|
+
// a callee may have mutated some before throwing) is unknown.
|
|
743
|
+
afterCatch = this.execStmts(s.catchBody, this.havocForCatch(entry, s.tryBody));
|
|
744
|
+
}
|
|
745
|
+
let out = joinEnv(afterTry, afterCatch);
|
|
746
|
+
if (s.finallyBody !== null) {
|
|
747
|
+
// The finally also runs on the pending-exception and
|
|
748
|
+
// pending-return paths; obligations inside it must hold there
|
|
749
|
+
// too, so it executes over the havoc-joined state.
|
|
750
|
+
out = this.execStmts(s.finallyBody, joinEnv(out, this.havocForCatch(entry, s.tryBody)) ?? this.havocForCatch(entry, s.tryBody));
|
|
751
|
+
}
|
|
752
|
+
return out;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
jumpTarget(label, kind) {
|
|
757
|
+
for (let i = this.frames.length - 1; i >= 0; i--) {
|
|
758
|
+
const f = this.frames[i];
|
|
759
|
+
if (label !== undefined) {
|
|
760
|
+
if (f.labels.includes(label) && (kind === "break" || f.kind === "loop"))
|
|
761
|
+
return f;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (kind === "break" && (f.kind === "loop" || f.kind === "switch"))
|
|
765
|
+
return f;
|
|
766
|
+
if (kind === "continue" && f.kind === "loop")
|
|
767
|
+
return f;
|
|
768
|
+
}
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
havocForCatch(entry, tryBody) {
|
|
772
|
+
const out = cloneEnv(entry);
|
|
773
|
+
const assigned = new Set();
|
|
774
|
+
const visitStmt = (s) => {
|
|
775
|
+
if (s.kind === "assign" || s.kind === "varDecl")
|
|
776
|
+
assigned.add(s.localId);
|
|
777
|
+
for (const v of Object.values(s)) {
|
|
778
|
+
if (Array.isArray(v)) {
|
|
779
|
+
for (const item of v) {
|
|
780
|
+
if (item !== null && typeof item === "object" && typeof item.kind === "string") {
|
|
781
|
+
const node = item;
|
|
782
|
+
if (isStmtKind(node.kind))
|
|
783
|
+
visitStmt(item);
|
|
784
|
+
else
|
|
785
|
+
visitExpr(item);
|
|
786
|
+
}
|
|
787
|
+
else if (item !== null && typeof item === "object") {
|
|
788
|
+
const cs = item;
|
|
789
|
+
if (Array.isArray(cs.body))
|
|
790
|
+
cs.body.forEach(visitStmt);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
else if (v !== null && typeof v === "object" && typeof v.kind === "string") {
|
|
795
|
+
const node = v;
|
|
796
|
+
if (isStmtKind(node.kind))
|
|
797
|
+
visitStmt(v);
|
|
798
|
+
else
|
|
799
|
+
visitExpr(v);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
const visitExpr = (e) => {
|
|
804
|
+
if (e.kind === "assignExpr" || e.kind === "incDec")
|
|
805
|
+
assigned.add(e.localId);
|
|
806
|
+
if (e.kind === "seqExpr")
|
|
807
|
+
e.stmts.forEach(visitStmt);
|
|
808
|
+
for (const v of Object.values(e)) {
|
|
809
|
+
if (Array.isArray(v)) {
|
|
810
|
+
for (const item of v) {
|
|
811
|
+
if (item !== null && typeof item === "object" && typeof item.kind === "string") {
|
|
812
|
+
visitExpr(item);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
else if (v !== null && typeof v === "object" && typeof v.kind === "string") {
|
|
817
|
+
visitExpr(v);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
tryBody.forEach(visitStmt);
|
|
822
|
+
for (const id of assigned) {
|
|
823
|
+
if (this.bindingIsF64(id))
|
|
824
|
+
out.set(id, { ...TOP });
|
|
825
|
+
}
|
|
826
|
+
// Globals: any callee may have written before the throw.
|
|
827
|
+
for (const k of [...out.keys()]) {
|
|
828
|
+
if (isGlobalId(k))
|
|
829
|
+
out.delete(k); // absent global = TOP
|
|
830
|
+
}
|
|
831
|
+
return out;
|
|
832
|
+
}
|
|
833
|
+
/* ── loops: header widening + body-edge refinement ──────────────────── */
|
|
834
|
+
execLoop(env, opts) {
|
|
835
|
+
const savedCollect = this.collect;
|
|
836
|
+
this.collect = false;
|
|
837
|
+
let head = cloneEnv(env);
|
|
838
|
+
let joins = 0;
|
|
839
|
+
// Phase 1: fixpoint (collect off). `head` is the state at the loop
|
|
840
|
+
// header (the condition's evaluation point; for do-while, the body's
|
|
841
|
+
// entry). Widening applies ONLY here — an acyclic join converges by
|
|
842
|
+
// itself and keeps its precision; precision the header loses is
|
|
843
|
+
// recovered by the body-edge refinement below.
|
|
844
|
+
for (let iter = 0; iter < LOOP_CAP; iter++) {
|
|
845
|
+
const trip = this.runLoopBodyOnce(head, opts);
|
|
846
|
+
let next = joinEnv(head, trip.back);
|
|
847
|
+
joins++;
|
|
848
|
+
if (joins > WIDEN_AFTER)
|
|
849
|
+
next = widenEnv(head, next);
|
|
850
|
+
if (envEquals(head, next))
|
|
851
|
+
break;
|
|
852
|
+
head = next;
|
|
853
|
+
if (iter === LOOP_CAP - 1) {
|
|
854
|
+
// Safety net (the thresholds converge long before this): give up
|
|
855
|
+
// on precision rather than loop.
|
|
856
|
+
for (const k of [...head.keys()])
|
|
857
|
+
head.set(k, { ...TOP });
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
// Phase 2: one collect pass over the stabilized header state — the
|
|
861
|
+
// pass that emits verdicts sees the loop-body edge's refined values
|
|
862
|
+
// (the header may have widened to [0, 2^31−1]; the body sees n < 10
|
|
863
|
+
// and proves [0, 9]).
|
|
864
|
+
this.collect = savedCollect;
|
|
865
|
+
const frame = { kind: "loop", labels: opts.labels, breaks: [], continues: [] };
|
|
866
|
+
const final = this.runLoopBodyOnce(head, opts, frame);
|
|
867
|
+
// The exit state: the condition's false edge (evaluated at the
|
|
868
|
+
// header for while/for, after the body for do-while), joined with
|
|
869
|
+
// every break's state. A condition-free, non-iterating loop
|
|
870
|
+
// (`for (;;)`) exits only through breaks.
|
|
871
|
+
let exit = null;
|
|
872
|
+
if (opts.doWhile ?? false) {
|
|
873
|
+
exit = opts.cond !== undefined && final.postBody !== null ? this.refine(final.postBody, opts.cond, false) : null;
|
|
874
|
+
}
|
|
875
|
+
else if (opts.cond !== undefined) {
|
|
876
|
+
exit = this.refine(cloneEnv(head), opts.cond, false);
|
|
877
|
+
}
|
|
878
|
+
else if (opts.alwaysExits ?? false) {
|
|
879
|
+
exit = cloneEnv(head); // for-of ends when the array runs out
|
|
880
|
+
}
|
|
881
|
+
for (const b of frame.breaks)
|
|
882
|
+
exit = joinEnv(exit, b);
|
|
883
|
+
return exit;
|
|
884
|
+
}
|
|
885
|
+
/** One trip around the loop from the header state: refine by the
|
|
886
|
+
* condition's true edge (unless do-while, whose condition sits after
|
|
887
|
+
* the body), execute the body, fold continues, run the update, and for
|
|
888
|
+
* do-while apply the condition's true edge to form the back edge.
|
|
889
|
+
* `back` is the back-edge environment (null when the body never reaches
|
|
890
|
+
* it); `postBody` is the do-while exit candidate — the state at the
|
|
891
|
+
* condition, before its verdict. */
|
|
892
|
+
runLoopBodyOnce(head, opts, frame) {
|
|
893
|
+
const f = frame ?? { kind: "loop", labels: opts.labels, breaks: [], continues: [] };
|
|
894
|
+
let bodyIn = cloneEnv(head);
|
|
895
|
+
if (opts.cond !== undefined && !(opts.doWhile ?? false)) {
|
|
896
|
+
this.evalExpr(opts.cond, bodyIn);
|
|
897
|
+
bodyIn = this.refine(bodyIn, opts.cond, true);
|
|
898
|
+
}
|
|
899
|
+
if (bodyIn !== null && opts.seedEachIteration !== undefined) {
|
|
900
|
+
bodyIn.set(opts.seedEachIteration, { ...TOP });
|
|
901
|
+
}
|
|
902
|
+
this.frames.push(f);
|
|
903
|
+
let out;
|
|
904
|
+
try {
|
|
905
|
+
out = bodyIn === null ? null : this.execStmts(opts.body, bodyIn);
|
|
906
|
+
}
|
|
907
|
+
finally {
|
|
908
|
+
this.frames.pop();
|
|
909
|
+
}
|
|
910
|
+
for (const c of f.continues)
|
|
911
|
+
out = joinEnv(out, c);
|
|
912
|
+
if (opts.update !== undefined && out !== null)
|
|
913
|
+
out = this.execStmt(opts.update, out);
|
|
914
|
+
let postBody = null;
|
|
915
|
+
if ((opts.doWhile ?? false) && opts.cond !== undefined && out !== null) {
|
|
916
|
+
this.evalExpr(opts.cond, out);
|
|
917
|
+
postBody = cloneEnv(out);
|
|
918
|
+
out = this.refine(out, opts.cond, true);
|
|
919
|
+
}
|
|
920
|
+
return { back: out, postBody };
|
|
921
|
+
}
|
|
922
|
+
/* ── branch refinement ──────────────────────────────────────────────── */
|
|
923
|
+
/** Refine an environment under `cond === branch`. Returns null when the
|
|
924
|
+
* edge is impossible. Every ordered comparison (and ===) evaluates
|
|
925
|
+
* false when either side is NaN, so the edge where one HELD proves both
|
|
926
|
+
* operands NaN-free; on the failed edge NaN survives while the negated
|
|
927
|
+
* comparison still refines the numeric members. */
|
|
928
|
+
refine(env, cond, branch) {
|
|
929
|
+
if (env === null)
|
|
930
|
+
return null;
|
|
931
|
+
switch (cond.kind) {
|
|
932
|
+
case "boolLit":
|
|
933
|
+
return cond.value === branch ? env : null;
|
|
934
|
+
case "unary":
|
|
935
|
+
if (cond.op === "!")
|
|
936
|
+
return this.refine(env, cond.operand, !branch);
|
|
937
|
+
return env;
|
|
938
|
+
case "logical": {
|
|
939
|
+
const isAnd = cond.op === "&&";
|
|
940
|
+
if (isAnd === branch) {
|
|
941
|
+
// (a && b) true — both held; (a || b) false — both failed.
|
|
942
|
+
return this.refine(this.refine(env, cond.left, branch), cond.right, branch);
|
|
943
|
+
}
|
|
944
|
+
// (a && b) false — a failed, or a held and b failed; dually for ||.
|
|
945
|
+
const viaLeft = this.refine(cloneEnv(env), cond.left, branch);
|
|
946
|
+
const viaRight = this.refine(this.refine(cloneEnv(env), cond.left, !branch), cond.right, branch);
|
|
947
|
+
return joinEnv(viaLeft, viaRight);
|
|
948
|
+
}
|
|
949
|
+
case "bin": {
|
|
950
|
+
if (!CMP_OPS.has(cond.op))
|
|
951
|
+
return env;
|
|
952
|
+
if (cond.left.type.kind !== "f64" || cond.right.type.kind !== "f64")
|
|
953
|
+
return env;
|
|
954
|
+
if (!this.isPure(cond.left) || !this.isPure(cond.right))
|
|
955
|
+
return env;
|
|
956
|
+
const op = branch ? cond.op : NEGATE[cond.op];
|
|
957
|
+
// NaN makes < <= > >= === evaluate false, so the edge where one of
|
|
958
|
+
// those was TRUE proves both operands NaN-free (!== held excludes
|
|
959
|
+
// nothing — NaN !== x is true).
|
|
960
|
+
const clearNaN = op !== "!==";
|
|
961
|
+
const a = this.evalPure(cond.left, env);
|
|
962
|
+
const b = this.evalPure(cond.right, env);
|
|
963
|
+
const out = cloneEnv(env);
|
|
964
|
+
if (cond.left.kind === "varRef")
|
|
965
|
+
out.set(cond.left.localId, refineLhs(op, a, b, clearNaN));
|
|
966
|
+
if (cond.right.kind === "varRef")
|
|
967
|
+
out.set(cond.right.localId, refineLhs(FLIP[op], b, a, clearNaN));
|
|
968
|
+
return out;
|
|
969
|
+
}
|
|
970
|
+
case "toBool": {
|
|
971
|
+
const inner = cond.operand;
|
|
972
|
+
if (inner.type.kind !== "f64" || inner.kind !== "varRef" || !this.isPure(inner))
|
|
973
|
+
return env;
|
|
974
|
+
const v = envGet(env, inner.localId);
|
|
975
|
+
const out = cloneEnv(env);
|
|
976
|
+
if (branch) {
|
|
977
|
+
// Truthy: not NaN, not zero — endpoint exclusion when whole.
|
|
978
|
+
let r = { ...v, maybeNaN: false };
|
|
979
|
+
if (r.whole && r.lo === 0 && r.hi >= 1)
|
|
980
|
+
r = absVal(1, r.hi, r.whole, false, r.spelling);
|
|
981
|
+
else if (r.whole && r.hi === 0 && r.lo <= -1)
|
|
982
|
+
r = absVal(r.lo, -1, r.whole, false, r.spelling);
|
|
983
|
+
out.set(inner.localId, r);
|
|
984
|
+
}
|
|
985
|
+
else {
|
|
986
|
+
// Falsy: 0, -0, or NaN.
|
|
987
|
+
out.set(inner.localId, meetInterval(v, 0, 0, false));
|
|
988
|
+
}
|
|
989
|
+
return out;
|
|
990
|
+
}
|
|
991
|
+
default:
|
|
992
|
+
return env;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
/** No side effects anywhere in the tree: safe to (re-)evaluate during
|
|
996
|
+
* refinement. */
|
|
997
|
+
isPure(e) {
|
|
998
|
+
switch (e.kind) {
|
|
999
|
+
case "incDec":
|
|
1000
|
+
case "assignExpr":
|
|
1001
|
+
case "seqExpr":
|
|
1002
|
+
case "call":
|
|
1003
|
+
case "callValue":
|
|
1004
|
+
case "new":
|
|
1005
|
+
case "newValue":
|
|
1006
|
+
case "virtualCall":
|
|
1007
|
+
case "dynCall":
|
|
1008
|
+
case "dynInvoke":
|
|
1009
|
+
case "intrinsic":
|
|
1010
|
+
case "yieldExpr":
|
|
1011
|
+
case "awaitExpr":
|
|
1012
|
+
case "awaitUnionExpr":
|
|
1013
|
+
return false;
|
|
1014
|
+
case "libCall":
|
|
1015
|
+
return e.fn.startsWith("math.") && e.args.every((a) => this.isPure(a));
|
|
1016
|
+
case "numLit":
|
|
1017
|
+
case "strLit":
|
|
1018
|
+
case "boolLit":
|
|
1019
|
+
case "varRef":
|
|
1020
|
+
case "unitLit":
|
|
1021
|
+
return true;
|
|
1022
|
+
case "bin":
|
|
1023
|
+
return this.isPure(e.left) && this.isPure(e.right);
|
|
1024
|
+
case "unary":
|
|
1025
|
+
return this.isPure(e.operand);
|
|
1026
|
+
default:
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
/** The abstract value of a PURE f64 expression, no env writes. */
|
|
1031
|
+
evalPure(e, env) {
|
|
1032
|
+
switch (e.kind) {
|
|
1033
|
+
case "numLit":
|
|
1034
|
+
return constVal(e.value, e.spelling);
|
|
1035
|
+
case "varRef":
|
|
1036
|
+
return e.type.kind === "f64" ? envGet(env, e.localId) : { ...TOP };
|
|
1037
|
+
case "unary":
|
|
1038
|
+
if (e.op === "-")
|
|
1039
|
+
return transferNeg(this.evalPure(e.operand, env));
|
|
1040
|
+
if (e.op === "~")
|
|
1041
|
+
return transferBitNot(this.evalPure(e.operand, env));
|
|
1042
|
+
return { ...TOP };
|
|
1043
|
+
case "bin":
|
|
1044
|
+
return transferBin(e.op, this.evalPure(e.left, env), this.evalPure(e.right, env));
|
|
1045
|
+
case "libCall":
|
|
1046
|
+
return this.evalMath(e, env, false) ?? { ...TOP };
|
|
1047
|
+
default:
|
|
1048
|
+
return { ...TOP };
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/* ── expression evaluation (side effects applied to env) ────────────── */
|
|
1052
|
+
evalMath(e, env, mutate) {
|
|
1053
|
+
const arg = (i) => (mutate ? this.evalExpr(e.args[i], env) : this.evalPure(e.args[i], env));
|
|
1054
|
+
switch (e.fn) {
|
|
1055
|
+
case "math.trunc": return transferMathRound("trunc", arg(0));
|
|
1056
|
+
case "math.floor": return transferMathRound("floor", arg(0));
|
|
1057
|
+
case "math.ceil": return transferMathRound("ceil", arg(0));
|
|
1058
|
+
case "math.round": return transferMathRound("round", arg(0));
|
|
1059
|
+
case "math.abs": return transferAbs(arg(0));
|
|
1060
|
+
case "math.min": return transferMinMax("min", [arg(0), arg(1)]);
|
|
1061
|
+
case "math.max": return transferMinMax("max", [arg(0), arg(1)]);
|
|
1062
|
+
case "math.random": return absVal(0, 1, false, false); // [0, 1): never NaN, never whole beyond 0
|
|
1063
|
+
default: return null;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
bindingIsF64(id) {
|
|
1067
|
+
return this.f64Bindings.has(id);
|
|
1068
|
+
}
|
|
1069
|
+
f64Bindings = new Set();
|
|
1070
|
+
seedBindings(fn, mod) {
|
|
1071
|
+
this.f64Bindings = new Set();
|
|
1072
|
+
for (const l of fn.locals)
|
|
1073
|
+
if (l.type.kind === "f64" && l.boxed !== true)
|
|
1074
|
+
this.f64Bindings.add(l.id);
|
|
1075
|
+
for (const g of mod.globals ?? [])
|
|
1076
|
+
if (g.type.kind === "f64")
|
|
1077
|
+
this.f64Bindings.add(g.id);
|
|
1078
|
+
}
|
|
1079
|
+
/** Evaluate an expression over the environment, applying the side
|
|
1080
|
+
* effects of nested writes and calls (a call havocs the globals its
|
|
1081
|
+
* transitive callee set can write; an indirect call havocs them all).
|
|
1082
|
+
* The returned value is meaningful for f64-typed expressions; anything
|
|
1083
|
+
* without a modeled transfer is TOP. */
|
|
1084
|
+
evalExpr(e, env) {
|
|
1085
|
+
switch (e.kind) {
|
|
1086
|
+
case "numLit":
|
|
1087
|
+
return constVal(e.value, e.spelling);
|
|
1088
|
+
case "strLit":
|
|
1089
|
+
case "boolLit":
|
|
1090
|
+
case "unitLit":
|
|
1091
|
+
case "regexLit":
|
|
1092
|
+
case "templateStrings":
|
|
1093
|
+
case "classRef":
|
|
1094
|
+
case "selfRef":
|
|
1095
|
+
case "chainRecv":
|
|
1096
|
+
return { ...TOP };
|
|
1097
|
+
case "varRef":
|
|
1098
|
+
return e.type.kind === "f64" && this.bindingIsF64(e.localId) ? envGet(env, e.localId) : { ...TOP };
|
|
1099
|
+
case "bin": {
|
|
1100
|
+
const a = this.evalExpr(e.left, env);
|
|
1101
|
+
const b = this.evalExpr(e.right, env);
|
|
1102
|
+
if (CMP_OPS.has(e.op))
|
|
1103
|
+
return { ...TOP };
|
|
1104
|
+
if (e.left.type.kind !== "f64" || e.right.type.kind !== "f64")
|
|
1105
|
+
return { ...TOP };
|
|
1106
|
+
return transferBin(e.op, a, b);
|
|
1107
|
+
}
|
|
1108
|
+
case "unary": {
|
|
1109
|
+
const v = this.evalExpr(e.operand, env);
|
|
1110
|
+
if (e.op === "-")
|
|
1111
|
+
return transferNeg(v);
|
|
1112
|
+
if (e.op === "~")
|
|
1113
|
+
return transferBitNot(v);
|
|
1114
|
+
return { ...TOP };
|
|
1115
|
+
}
|
|
1116
|
+
case "incDec": {
|
|
1117
|
+
const old = this.bindingIsF64(e.localId) ? envGet(env, e.localId) : { ...TOP };
|
|
1118
|
+
const next = transferAdd(old, constVal(e.op === "+" ? 1 : -1));
|
|
1119
|
+
if (this.bindingIsF64(e.localId))
|
|
1120
|
+
env.set(e.localId, next);
|
|
1121
|
+
return e.prefix ? next : old;
|
|
1122
|
+
}
|
|
1123
|
+
case "assignExpr": {
|
|
1124
|
+
const v = this.evalExpr(e.value, env);
|
|
1125
|
+
if (this.bindingIsF64(e.localId))
|
|
1126
|
+
env.set(e.localId, v);
|
|
1127
|
+
return v;
|
|
1128
|
+
}
|
|
1129
|
+
case "ternary": {
|
|
1130
|
+
this.evalExpr(e.cond, env);
|
|
1131
|
+
const thenEnv = this.refine(cloneEnv(env), e.cond, true);
|
|
1132
|
+
const elseEnv = this.refine(cloneEnv(env), e.cond, false);
|
|
1133
|
+
const a = thenEnv === null ? BOTTOM : this.evalExpr(e.then, thenEnv);
|
|
1134
|
+
const b = elseEnv === null ? BOTTOM : this.evalExpr(e.else_, elseEnv);
|
|
1135
|
+
mergeInto(env, joinEnv(thenEnv, elseEnv));
|
|
1136
|
+
return join(a, b);
|
|
1137
|
+
}
|
|
1138
|
+
case "logical":
|
|
1139
|
+
case "nullish":
|
|
1140
|
+
case "orDefault": {
|
|
1141
|
+
this.evalExpr(e.left, env);
|
|
1142
|
+
const rightEnv = cloneEnv(env);
|
|
1143
|
+
this.evalExpr(e.right, rightEnv);
|
|
1144
|
+
mergeInto(env, joinEnv(env, rightEnv));
|
|
1145
|
+
return { ...TOP };
|
|
1146
|
+
}
|
|
1147
|
+
case "optChain": {
|
|
1148
|
+
this.evalExpr(e.receiver, env);
|
|
1149
|
+
const bodyEnv = cloneEnv(env);
|
|
1150
|
+
this.evalExpr(e.body, bodyEnv);
|
|
1151
|
+
mergeInto(env, joinEnv(env, bodyEnv));
|
|
1152
|
+
return { ...TOP };
|
|
1153
|
+
}
|
|
1154
|
+
case "seqExpr": {
|
|
1155
|
+
// Expression-position statements: no jumps can escape them.
|
|
1156
|
+
let running = env;
|
|
1157
|
+
for (const s of e.stmts) {
|
|
1158
|
+
if (running === null)
|
|
1159
|
+
break;
|
|
1160
|
+
running = this.execStmt(s, running);
|
|
1161
|
+
}
|
|
1162
|
+
if (running === null)
|
|
1163
|
+
return BOTTOM;
|
|
1164
|
+
if (running !== env)
|
|
1165
|
+
mergeInto(env, running);
|
|
1166
|
+
return this.evalExpr(e.result, env);
|
|
1167
|
+
}
|
|
1168
|
+
case "call": {
|
|
1169
|
+
// Arguments evaluate left to right and the VALUES captured here
|
|
1170
|
+
// are exactly what the call passes (a later argument's side
|
|
1171
|
+
// effect cannot reach an earlier argument's already-read value).
|
|
1172
|
+
const vals = e.args.map((a) => this.evalExpr(a, env));
|
|
1173
|
+
const slots = this.cfg.fns.get(e.callee);
|
|
1174
|
+
if (slots !== undefined) {
|
|
1175
|
+
e.args.forEach((arg, i) => {
|
|
1176
|
+
const cls = slots.params[i] ?? null;
|
|
1177
|
+
if (cls === null)
|
|
1178
|
+
return;
|
|
1179
|
+
this.emit(vals[i], slots.paramPaths[i], cls, arg.loc);
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
this.havocCall(e.callee, env);
|
|
1183
|
+
if (slots?.ret != null)
|
|
1184
|
+
return classSeed(slots.ret);
|
|
1185
|
+
return { ...TOP };
|
|
1186
|
+
}
|
|
1187
|
+
case "libCall": {
|
|
1188
|
+
const math = this.evalMath(e, env, true);
|
|
1189
|
+
if (math !== null)
|
|
1190
|
+
return math;
|
|
1191
|
+
for (const a of e.args)
|
|
1192
|
+
this.evalExpr(a, env);
|
|
1193
|
+
if (e.args.some((a) => typeContainsFunc(a.type)))
|
|
1194
|
+
this.havocAllGlobals(env);
|
|
1195
|
+
return { ...TOP };
|
|
1196
|
+
}
|
|
1197
|
+
case "callValue":
|
|
1198
|
+
case "newValue":
|
|
1199
|
+
case "dynCall":
|
|
1200
|
+
case "dynInvoke":
|
|
1201
|
+
case "virtualCall":
|
|
1202
|
+
case "new":
|
|
1203
|
+
case "intrinsic": {
|
|
1204
|
+
for (const v of childExprs(e))
|
|
1205
|
+
this.evalExpr(v, env);
|
|
1206
|
+
this.havocAllGlobals(env);
|
|
1207
|
+
return { ...TOP };
|
|
1208
|
+
}
|
|
1209
|
+
case "recordLit": {
|
|
1210
|
+
const slotMap = this.cfg.records.get(e.type.shapeId);
|
|
1211
|
+
for (const f of e.fields) {
|
|
1212
|
+
const v = this.evalExpr(f.value, env);
|
|
1213
|
+
const slot = slotMap?.get(f.name);
|
|
1214
|
+
if (slot !== undefined && f.value.type.kind === "f64")
|
|
1215
|
+
this.emit(v, slot.path, slot.cls, f.value.loc);
|
|
1216
|
+
}
|
|
1217
|
+
return { ...TOP };
|
|
1218
|
+
}
|
|
1219
|
+
case "recordGet": {
|
|
1220
|
+
// A declared record-field slot is an assumption on the read side,
|
|
1221
|
+
// exactly like a declared parameter inside its callee: every write
|
|
1222
|
+
// into the field discharged the class's obligations, so the value
|
|
1223
|
+
// read back is whole and in class range (the read that lets
|
|
1224
|
+
// `model.count + 1` refuse RANGE — the unbounded-counter teaching
|
|
1225
|
+
// — instead of a spurious may-be-NaN wholeness refusal).
|
|
1226
|
+
this.evalExpr(e.obj, env);
|
|
1227
|
+
const slot = this.cfg.records.get(e.shapeId)?.get(e.field);
|
|
1228
|
+
if (slot !== undefined && e.type.kind === "f64")
|
|
1229
|
+
return classSeed(slot.cls);
|
|
1230
|
+
return { ...TOP };
|
|
1231
|
+
}
|
|
1232
|
+
default: {
|
|
1233
|
+
for (const v of childExprs(e))
|
|
1234
|
+
this.evalExpr(v, env);
|
|
1235
|
+
return { ...TOP };
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
havocCall(callee, env) {
|
|
1240
|
+
if (this.effects.havocAll.has(callee) || !this.effects.perFn.has(callee)) {
|
|
1241
|
+
this.havocAllGlobals(env);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
for (const g of this.effects.perFn.get(callee))
|
|
1245
|
+
env.delete(g); // absent global = TOP
|
|
1246
|
+
}
|
|
1247
|
+
havocAllGlobals(env) {
|
|
1248
|
+
for (const k of [...env.keys()]) {
|
|
1249
|
+
if (isGlobalId(k))
|
|
1250
|
+
env.delete(k);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
function cloneEnv(env) {
|
|
1255
|
+
return new Map(env);
|
|
1256
|
+
}
|
|
1257
|
+
function mergeInto(dst, src) {
|
|
1258
|
+
if (src === null || src === dst)
|
|
1259
|
+
return;
|
|
1260
|
+
dst.clear();
|
|
1261
|
+
for (const [k, v] of src)
|
|
1262
|
+
dst.set(k, v);
|
|
1263
|
+
}
|
|
1264
|
+
function typeContainsFunc(t) {
|
|
1265
|
+
return JSON.stringify(t).includes('"func"');
|
|
1266
|
+
}
|
|
1267
|
+
/** Every direct IrExpr child of a node, order-preserving (evaluation
|
|
1268
|
+
* order for the shapes we don't model precisely). */
|
|
1269
|
+
function childExprs(e) {
|
|
1270
|
+
const out = [];
|
|
1271
|
+
for (const [key, v] of Object.entries(e)) {
|
|
1272
|
+
if (key === "type")
|
|
1273
|
+
continue;
|
|
1274
|
+
if (Array.isArray(v)) {
|
|
1275
|
+
for (const item of v) {
|
|
1276
|
+
if (item !== null && typeof item === "object") {
|
|
1277
|
+
if (typeof item.kind === "string" && !isStmtKind(item.kind)) {
|
|
1278
|
+
out.push(item);
|
|
1279
|
+
}
|
|
1280
|
+
else {
|
|
1281
|
+
for (const sub of Object.values(item)) {
|
|
1282
|
+
if (sub !== null && typeof sub === "object" && typeof sub.kind === "string" && !isStmtKind(sub.kind)) {
|
|
1283
|
+
out.push(sub);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
else if (v !== null && typeof v === "object" && typeof v.kind === "string" && !isStmtKind(v.kind)) {
|
|
1291
|
+
out.push(v);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return out;
|
|
1295
|
+
}
|
|
1296
|
+
/* ── the entry point ───────────────────────────────────────────────────── */
|
|
1297
|
+
/** Run the inference over every function of a lowered library module and
|
|
1298
|
+
* return one verdict per (obligation site) — internal call arguments into
|
|
1299
|
+
* declared integer parameters, returns of declared integer returns, and
|
|
1300
|
+
* writes into declared record-field slots. Callers turn REFUSE verdicts
|
|
1301
|
+
* into diagnostics; PROVE verdicts carry the proven crossing range. */
|
|
1302
|
+
export function checkLibraryIntegerSlots(mod, cfg) {
|
|
1303
|
+
const verdicts = [];
|
|
1304
|
+
if (!hasIntSlots(cfg))
|
|
1305
|
+
return verdicts;
|
|
1306
|
+
const effects = globalEffectsOf(mod);
|
|
1307
|
+
for (const fn of mod.functions) {
|
|
1308
|
+
const analyzer = new FnAnalyzer(mod, cfg, effects, verdicts);
|
|
1309
|
+
analyzer.seedBindings(fn, mod);
|
|
1310
|
+
analyzer.analyze(fn);
|
|
1311
|
+
}
|
|
1312
|
+
return verdicts;
|
|
1313
|
+
}
|
|
1314
|
+
//# sourceMappingURL=int-infer.js.map
|