@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.
Files changed (111) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +63 -15
  2. package/ambient/scriptc.d.ts +2 -2
  3. package/dist/backend/cc.d.ts +2 -2
  4. package/dist/backend/emission/emit-async.js +1 -1
  5. package/dist/backend/emission/emit-exprs.js +131 -54
  6. package/dist/backend/emission/emit-exprs.js.map +1 -1
  7. package/dist/backend/emission/emit-island.js +1 -1
  8. package/dist/backend/emission/emit-shapes.js +1 -1
  9. package/dist/backend/emission/emit-walkers.d.ts +14 -14
  10. package/dist/backend/emission/emit-walkers.js +44 -44
  11. package/dist/backend/emission/emit-walkers.js.map +1 -1
  12. package/dist/backend/emission/emitter.d.ts +3 -3
  13. package/dist/backend/emission/emitter.js +33 -9
  14. package/dist/backend/emission/emitter.js.map +1 -1
  15. package/dist/backend/emission/may-throw.js +1 -1
  16. package/dist/backend/llvm/dyn.d.ts +10 -10
  17. package/dist/backend/llvm/dyn.js +26 -26
  18. package/dist/backend/llvm/dyn.js.map +1 -1
  19. package/dist/backend/llvm/emitter.js +156 -47
  20. package/dist/backend/llvm/emitter.js.map +1 -1
  21. package/dist/backend/llvm/shapes.js +1 -1
  22. package/dist/backend/llvm/walkers.js +2 -2
  23. package/dist/backend/llvm/walkers.js.map +1 -1
  24. package/dist/coverage/surface-manifest.js +16 -2
  25. package/dist/coverage/surface-manifest.js.map +1 -1
  26. package/dist/diagnostics/diagnostic.d.ts +21 -8
  27. package/dist/diagnostics/diagnostic.js +48 -14
  28. package/dist/diagnostics/diagnostic.js.map +1 -1
  29. package/dist/frontend/lowering/lib-boundary.js +5 -5
  30. package/dist/frontend/lowering/lib-boundary.js.map +1 -1
  31. package/dist/frontend/lowering/lower-assert.js +12 -12
  32. package/dist/frontend/lowering/lower-assert.js.map +1 -1
  33. package/dist/frontend/lowering/lower-builtins.d.ts +63 -7
  34. package/dist/frontend/lowering/lower-builtins.js +327 -35
  35. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  36. package/dist/frontend/lowering/lower-calls.d.ts +4 -4
  37. package/dist/frontend/lowering/lower-calls.js +104 -70
  38. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  39. package/dist/frontend/lowering/lower-classes.d.ts +7 -0
  40. package/dist/frontend/lowering/lower-classes.js +82 -6
  41. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  42. package/dist/frontend/lowering/lower-containers.d.ts +9 -5
  43. package/dist/frontend/lowering/lower-containers.js +88 -26
  44. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  45. package/dist/frontend/lowering/lower-emitter.d.ts +70 -3
  46. package/dist/frontend/lowering/lower-emitter.js +438 -31
  47. package/dist/frontend/lowering/lower-emitter.js.map +1 -1
  48. package/dist/frontend/lowering/lower-exprs.d.ts +87 -12
  49. package/dist/frontend/lowering/lower-exprs.js +630 -125
  50. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  51. package/dist/frontend/lowering/lower-inspect.js +5 -5
  52. package/dist/frontend/lowering/lower-inspect.js.map +1 -1
  53. package/dist/frontend/lowering/lower-island.js +12 -3
  54. package/dist/frontend/lowering/lower-island.js.map +1 -1
  55. package/dist/frontend/lowering/lower-modules.d.ts +8 -5
  56. package/dist/frontend/lowering/lower-modules.js +157 -63
  57. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  58. package/dist/frontend/lowering/lower-namespaces.d.ts +24 -0
  59. package/dist/frontend/lowering/lower-namespaces.js +158 -16
  60. package/dist/frontend/lowering/lower-namespaces.js.map +1 -1
  61. package/dist/frontend/lowering/lower-server.js +11 -11
  62. package/dist/frontend/lowering/lower-server.js.map +1 -1
  63. package/dist/frontend/lowering/lower-stmts.d.ts +1 -1
  64. package/dist/frontend/lowering/lower-stmts.js +271 -36
  65. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  66. package/dist/frontend/lowering/lower-stream.js +171 -20
  67. package/dist/frontend/lowering/lower-stream.js.map +1 -1
  68. package/dist/frontend/lowering/lowerer.d.ts +27 -10
  69. package/dist/frontend/lowering/lowerer.js +156 -54
  70. package/dist/frontend/lowering/lowerer.js.map +1 -1
  71. package/dist/frontend/lowering/surfaces.d.ts +44 -0
  72. package/dist/frontend/lowering/surfaces.js +236 -5
  73. package/dist/frontend/lowering/surfaces.js.map +1 -1
  74. package/dist/frontend/npm.d.ts +12 -0
  75. package/dist/frontend/npm.js +30 -0
  76. package/dist/frontend/npm.js.map +1 -1
  77. package/dist/frontend/program.d.ts +25 -0
  78. package/dist/frontend/program.js +145 -136
  79. package/dist/frontend/program.js.map +1 -1
  80. package/dist/frontend/shared.d.ts +1 -1
  81. package/dist/frontend/shared.js +8 -8
  82. package/dist/frontend/shared.js.map +1 -1
  83. package/dist/frontend/types.d.ts +2 -2
  84. package/dist/frontend/types.js +15 -15
  85. package/dist/frontend/types.js.map +1 -1
  86. package/dist/index.d.ts +2 -1
  87. package/dist/index.js +169 -5
  88. package/dist/index.js.map +1 -1
  89. package/dist/ir/nodes.d.ts +174 -106
  90. package/dist/ir/nodes.js +69 -24
  91. package/dist/ir/nodes.js.map +1 -1
  92. package/dist/ir/validate.js +55 -36
  93. package/dist/ir/validate.js.map +1 -1
  94. package/dist/library/fence-eval.js +12 -2
  95. package/dist/library/fence-eval.js.map +1 -1
  96. package/dist/library/int-infer.d.ts +119 -0
  97. package/dist/library/int-infer.js +1314 -0
  98. package/dist/library/int-infer.js.map +1 -0
  99. package/dist/library/int-infer.test.d.ts +1 -0
  100. package/dist/library/int-infer.test.js +279 -0
  101. package/dist/library/int-infer.test.js.map +1 -0
  102. package/dist/library/profile.d.ts +34 -5
  103. package/dist/library/profile.js +77 -13
  104. package/dist/library/profile.js.map +1 -1
  105. package/dist/library/sidecar-validate.js +1 -1
  106. package/dist/library/sidecar-validate.js.map +1 -1
  107. package/dist/library/sidecar.d.ts +24 -1
  108. package/dist/library/sidecar.js +163 -16
  109. package/dist/library/sidecar.js.map +1 -1
  110. package/package.json +2 -2
  111. 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