@scriptc/compiler 0.0.17 → 0.0.19
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 +4 -4
- package/dist/backend/cc.d.ts +5 -0
- package/dist/backend/cc.js +2 -0
- package/dist/backend/cc.js.map +1 -1
- package/dist/backend/emission/emit-async.js +35 -2
- package/dist/backend/emission/emit-async.js.map +1 -1
- package/dist/backend/emission/emit-exprs.js +40 -4
- package/dist/backend/emission/emit-exprs.js.map +1 -1
- package/dist/backend/emission/emitter.js +69 -11
- package/dist/backend/emission/emitter.js.map +1 -1
- package/dist/backend/emission/may-throw.d.ts +3 -3
- package/dist/backend/emission/may-throw.js +15 -4
- package/dist/backend/emission/may-throw.js.map +1 -1
- package/dist/backend/llvm/emitter.js +199 -21
- package/dist/backend/llvm/emitter.js.map +1 -1
- package/dist/frontend/lib-contract.d.ts +1 -0
- package/dist/frontend/lib-contract.js +2 -2
- package/dist/frontend/lib-contract.js.map +1 -1
- package/dist/frontend/lowering/lower-builtins.js +4 -7
- package/dist/frontend/lowering/lower-builtins.js.map +1 -1
- package/dist/frontend/lowering/lower-calls.js +17 -9
- package/dist/frontend/lowering/lower-calls.js.map +1 -1
- package/dist/frontend/lowering/lower-containers.d.ts +22 -17
- package/dist/frontend/lowering/lower-containers.js +628 -51
- package/dist/frontend/lowering/lower-containers.js.map +1 -1
- package/dist/frontend/lowering/lower-exprs.js +19 -1
- package/dist/frontend/lowering/lower-exprs.js.map +1 -1
- package/dist/frontend/lowering/lower-island.js +55 -7
- package/dist/frontend/lowering/lower-island.js.map +1 -1
- package/dist/frontend/lowering/lower-modules.js +144 -10
- package/dist/frontend/lowering/lower-modules.js.map +1 -1
- package/dist/frontend/lowering/lower-stmts.d.ts +8 -0
- package/dist/frontend/lowering/lower-stmts.js +408 -21
- package/dist/frontend/lowering/lower-stmts.js.map +1 -1
- package/dist/frontend/lowering/lowerer.d.ts +38 -0
- package/dist/frontend/lowering/lowerer.js +149 -2
- package/dist/frontend/lowering/lowerer.js.map +1 -1
- package/dist/frontend/lowering/surfaces.js +4 -0
- package/dist/frontend/lowering/surfaces.js.map +1 -1
- package/dist/index.js +97 -21
- package/dist/index.js.map +1 -1
- package/dist/ir/nodes.d.ts +46 -12
- package/dist/ir/nodes.js +41 -0
- package/dist/ir/nodes.js.map +1 -1
- package/dist/ir/serialize.d.ts +1 -1
- package/dist/ir/serialize.js +1 -1
- package/dist/ir/validate.js +98 -39
- package/dist/ir/validate.js.map +1 -1
- package/dist/library/int-infer.d.ts +6 -1
- package/dist/library/int-infer.js +306 -53
- package/dist/library/int-infer.js.map +1 -1
- package/dist/library/int-infer.test.js +271 -1
- package/dist/library/int-infer.test.js.map +1 -1
- package/dist/library/profile.d.ts +6 -5
- package/dist/library/profile.js.map +1 -1
- package/dist/library/sidecar-validate.js +4 -1
- package/dist/library/sidecar-validate.js.map +1 -1
- package/dist/library/sidecar.d.ts +32 -4
- package/dist/library/sidecar.js +264 -47
- package/dist/library/sidecar.js.map +1 -1
- package/package.json +2 -2
- package/surface-manifest.json +25 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IrModule, IrNumBinOp, SrcLoc } from "../ir/nodes.js";
|
|
1
|
+
import type { IrModule, IrNumBinOp, IrType, SrcLoc } from "../ir/nodes.js";
|
|
2
2
|
export declare const SAFE_MAX: number;
|
|
3
3
|
export declare const SAFE_MIN: number;
|
|
4
4
|
/** The set of f64 values a binding may hold at a program point: a closed
|
|
@@ -105,6 +105,11 @@ export interface IntSlotConfig {
|
|
|
105
105
|
path: string;
|
|
106
106
|
}>>;
|
|
107
107
|
}
|
|
108
|
+
/** The IR representations whose PRESENT values can carry one number slot.
|
|
109
|
+
* A bare f64 is plain; a union of exactly one f64 arm and one or more
|
|
110
|
+
* null/undefined arms is optional. The abstract interpreter tracks the
|
|
111
|
+
* possible f64 arm values and treats unit arms as the empty numeric set. */
|
|
112
|
+
export declare function numberCarrierKind(t: IrType, mod: IrModule): "plain" | "optional" | null;
|
|
108
113
|
export declare function hasIntSlots(cfg: IntSlotConfig): boolean;
|
|
109
114
|
/** The class seed for a declared parameter: what the slot's own contract
|
|
110
115
|
* proves about every value that ever arrives through it (internal calls
|
|
@@ -284,6 +284,30 @@ export function checkBoundary(v, path, cls, loc) {
|
|
|
284
284
|
}
|
|
285
285
|
return { path, cls, loc, outcome: "prove", provenLo: v.lo, provenHi: v.hi };
|
|
286
286
|
}
|
|
287
|
+
/** The IR representations whose PRESENT values can carry one number slot.
|
|
288
|
+
* A bare f64 is plain; a union of exactly one f64 arm and one or more
|
|
289
|
+
* null/undefined arms is optional. The abstract interpreter tracks the
|
|
290
|
+
* possible f64 arm values and treats unit arms as the empty numeric set. */
|
|
291
|
+
export function numberCarrierKind(t, mod) {
|
|
292
|
+
if (t.kind === "f64")
|
|
293
|
+
return "plain";
|
|
294
|
+
if (t.kind !== "union")
|
|
295
|
+
return null;
|
|
296
|
+
const def = mod.unions?.find((u) => u.id === t.unionId);
|
|
297
|
+
if (def === undefined)
|
|
298
|
+
return null;
|
|
299
|
+
let numbers = 0;
|
|
300
|
+
let units = 0;
|
|
301
|
+
for (const arm of def.arms) {
|
|
302
|
+
if (arm.kind === "f64")
|
|
303
|
+
numbers++;
|
|
304
|
+
else if (arm.kind === "nullT" || arm.kind === "undefinedT")
|
|
305
|
+
units++;
|
|
306
|
+
else
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return numbers === 1 && units > 0 ? "optional" : null;
|
|
310
|
+
}
|
|
287
311
|
export function hasIntSlots(cfg) {
|
|
288
312
|
if (cfg.records.size > 0)
|
|
289
313
|
return true;
|
|
@@ -307,8 +331,33 @@ export function classSeed(cls) {
|
|
|
307
331
|
default: return { ...TOP };
|
|
308
332
|
}
|
|
309
333
|
}
|
|
334
|
+
/** Static-access facts share Env's join/clone machinery but have their own
|
|
335
|
+
* missing-key value: the declared slot seed. A killed field fact therefore
|
|
336
|
+
* falls back to the same whole-in-class-range assumption a field read had
|
|
337
|
+
* before access-path refinement, preserving SC4023 rather than degrading
|
|
338
|
+
* to a spurious SC4022. */
|
|
339
|
+
const PATH_I64_PREFIX = "%path.i64:";
|
|
340
|
+
const PATH_U64_PREFIX = "%path.u64:";
|
|
341
|
+
function pathClassOfKey(id) {
|
|
342
|
+
if (id.startsWith(PATH_I64_PREFIX))
|
|
343
|
+
return "i64";
|
|
344
|
+
if (id.startsWith(PATH_U64_PREFIX))
|
|
345
|
+
return "u64";
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
function clearPathFacts(env) {
|
|
349
|
+
for (const k of [...env.keys()]) {
|
|
350
|
+
if (pathClassOfKey(k) !== null)
|
|
351
|
+
env.delete(k);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
310
354
|
const isGlobalId = (id) => id.startsWith("%g.");
|
|
311
|
-
const defaultVal = (id) =>
|
|
355
|
+
const defaultVal = (id) => {
|
|
356
|
+
const pathClass = pathClassOfKey(id);
|
|
357
|
+
if (pathClass !== null)
|
|
358
|
+
return classSeed(pathClass);
|
|
359
|
+
return isGlobalId(id) ? TOP : BOTTOM;
|
|
360
|
+
};
|
|
312
361
|
function envGet(env, id) {
|
|
313
362
|
return env.get(id) ?? defaultVal(id);
|
|
314
363
|
}
|
|
@@ -322,6 +371,10 @@ function joinEnv(a, b) {
|
|
|
322
371
|
for (const k of keys) {
|
|
323
372
|
out.set(k, join(a.get(k) ?? defaultVal(k), b.get(k) ?? defaultVal(k)));
|
|
324
373
|
}
|
|
374
|
+
// A real flow join ends the cheap straight-line access-path proof even
|
|
375
|
+
// when both incoming facts happen to agree. A sole reachable edge
|
|
376
|
+
// (early return/throw on the other edge) retains its dominated fact.
|
|
377
|
+
clearPathFacts(out);
|
|
325
378
|
return out;
|
|
326
379
|
}
|
|
327
380
|
function envEquals(a, b) {
|
|
@@ -338,6 +391,7 @@ function widenEnv(prev, next) {
|
|
|
338
391
|
for (const k of keys) {
|
|
339
392
|
out.set(k, widen(prev.get(k) ?? defaultVal(k), next.get(k) ?? defaultVal(k)));
|
|
340
393
|
}
|
|
394
|
+
clearPathFacts(out);
|
|
341
395
|
return out;
|
|
342
396
|
}
|
|
343
397
|
function globalEffectsOf(mod) {
|
|
@@ -553,7 +607,7 @@ class FnAnalyzer {
|
|
|
553
607
|
const env = new Map();
|
|
554
608
|
const slots = this.cfg.fns.get(fn.name);
|
|
555
609
|
fn.params.forEach((p, i) => {
|
|
556
|
-
if (p.
|
|
610
|
+
if (!this.bindingCarriesNumber(p.localId))
|
|
557
611
|
return;
|
|
558
612
|
const declared = slots?.params[i] ?? null;
|
|
559
613
|
const seed = slots?.paramSeeds[i] ?? null;
|
|
@@ -589,13 +643,15 @@ class FnAnalyzer {
|
|
|
589
643
|
if (s.init === null)
|
|
590
644
|
return env;
|
|
591
645
|
const v = this.evalExpr(s.init, env);
|
|
592
|
-
|
|
646
|
+
this.clearPathsRootedAt(env, s.localId);
|
|
647
|
+
if (this.bindingCarriesNumber(s.localId))
|
|
593
648
|
env.set(s.localId, v);
|
|
594
649
|
return env;
|
|
595
650
|
}
|
|
596
651
|
case "assign": {
|
|
597
652
|
const v = this.evalExpr(s.value, env);
|
|
598
|
-
|
|
653
|
+
this.clearPathsRootedAt(env, s.localId);
|
|
654
|
+
if (this.bindingCarriesNumber(s.localId))
|
|
599
655
|
env.set(s.localId, v);
|
|
600
656
|
return env;
|
|
601
657
|
}
|
|
@@ -604,8 +660,9 @@ class FnAnalyzer {
|
|
|
604
660
|
return env;
|
|
605
661
|
case "if": {
|
|
606
662
|
this.evalExpr(s.cond, env);
|
|
607
|
-
const
|
|
608
|
-
const
|
|
663
|
+
const allowPaths = this.stablePathGuard(s.cond);
|
|
664
|
+
const thenEnv = this.refine(cloneEnv(env), s.cond, true, allowPaths);
|
|
665
|
+
const elseEnv = this.refine(cloneEnv(env), s.cond, false, allowPaths);
|
|
609
666
|
const a = thenEnv === null ? null : this.execStmts(s.then, thenEnv);
|
|
610
667
|
const b = s.else_ === null ? elseEnv : elseEnv === null ? null : this.execStmts(s.else_, elseEnv);
|
|
611
668
|
return joinEnv(a, b);
|
|
@@ -629,7 +686,7 @@ class FnAnalyzer {
|
|
|
629
686
|
return this.execLoop(env, { cond: s.cond, body: s.body, labels: s.labels ?? [], doWhile: true });
|
|
630
687
|
case "forOf": {
|
|
631
688
|
this.evalExpr(s.iterable, env);
|
|
632
|
-
const elemF64 = this.
|
|
689
|
+
const elemF64 = this.bindingCarriesNumber(s.localId);
|
|
633
690
|
return this.execLoop(env, {
|
|
634
691
|
body: s.body,
|
|
635
692
|
labels: s.labels ?? [],
|
|
@@ -668,10 +725,12 @@ class FnAnalyzer {
|
|
|
668
725
|
this.evalExpr(s.arr, env);
|
|
669
726
|
this.evalExpr(s.index, env);
|
|
670
727
|
this.evalExpr(s.value, env);
|
|
728
|
+
clearPathFacts(env);
|
|
671
729
|
return env;
|
|
672
730
|
case "fieldSet":
|
|
673
731
|
this.evalExpr(s.obj, env);
|
|
674
732
|
this.evalExpr(s.value, env);
|
|
733
|
+
clearPathFacts(env);
|
|
675
734
|
return env;
|
|
676
735
|
case "recordSet": {
|
|
677
736
|
this.evalExpr(s.obj, env);
|
|
@@ -679,16 +738,33 @@ class FnAnalyzer {
|
|
|
679
738
|
const slot = this.cfg.records.get(s.shapeId)?.get(s.field);
|
|
680
739
|
if (slot !== undefined)
|
|
681
740
|
this.emit(v, slot.path, slot.cls, s.loc);
|
|
741
|
+
// The RHS and its boundary obligation observe the pre-write value;
|
|
742
|
+
// only the completed heap write invalidates paths (JS evaluation
|
|
743
|
+
// order, and what lets `m.count = m.count + 1` prove).
|
|
744
|
+
clearPathFacts(env);
|
|
682
745
|
return env;
|
|
683
746
|
}
|
|
684
|
-
case "recordKeySet":
|
|
747
|
+
case "recordKeySet": {
|
|
685
748
|
this.evalExpr(s.obj, env);
|
|
686
749
|
this.evalExpr(s.key, env);
|
|
687
|
-
this.evalExpr(s.value, env);
|
|
750
|
+
const v = this.evalExpr(s.value, env);
|
|
751
|
+
// A runtime key can dispatch to any declared field of the shape.
|
|
752
|
+
// overflowOnly proves a literal key names no declared field; every
|
|
753
|
+
// other keyed write must therefore discharge every integer slot it
|
|
754
|
+
// could select. Multiple classified fields intentionally emit
|
|
755
|
+
// independent obligations (their classes and paths may differ).
|
|
756
|
+
if (s.overflowOnly !== true) {
|
|
757
|
+
for (const slot of this.cfg.records.get(s.shapeId)?.values() ?? []) {
|
|
758
|
+
this.emit(v, slot.path, slot.cls, s.loc);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
clearPathFacts(env);
|
|
688
762
|
return env;
|
|
763
|
+
}
|
|
689
764
|
case "recordKeyDelete":
|
|
690
765
|
this.evalExpr(s.obj, env);
|
|
691
766
|
this.evalExpr(s.key, env);
|
|
767
|
+
clearPathFacts(env);
|
|
692
768
|
return env;
|
|
693
769
|
case "return": {
|
|
694
770
|
if (s.value !== null) {
|
|
@@ -820,7 +896,7 @@ class FnAnalyzer {
|
|
|
820
896
|
};
|
|
821
897
|
tryBody.forEach(visitStmt);
|
|
822
898
|
for (const id of assigned) {
|
|
823
|
-
if (this.
|
|
899
|
+
if (this.bindingCarriesNumber(id))
|
|
824
900
|
out.set(id, { ...TOP });
|
|
825
901
|
}
|
|
826
902
|
// Globals: any callee may have written before the throw.
|
|
@@ -828,10 +904,14 @@ class FnAnalyzer {
|
|
|
828
904
|
if (isGlobalId(k))
|
|
829
905
|
out.delete(k); // absent global = TOP
|
|
830
906
|
}
|
|
907
|
+
clearPathFacts(out);
|
|
831
908
|
return out;
|
|
832
909
|
}
|
|
833
910
|
/* ── loops: header widening + body-edge refinement ──────────────────── */
|
|
834
911
|
execLoop(env, opts) {
|
|
912
|
+
// Loop headers/backedges are outside the deliberately straight-line
|
|
913
|
+
// access-path scope. Guards inside the body can establish fresh facts.
|
|
914
|
+
clearPathFacts(env);
|
|
835
915
|
const savedCollect = this.collect;
|
|
836
916
|
this.collect = false;
|
|
837
917
|
let head = cloneEnv(env);
|
|
@@ -870,10 +950,10 @@ class FnAnalyzer {
|
|
|
870
950
|
// (`for (;;)`) exits only through breaks.
|
|
871
951
|
let exit = null;
|
|
872
952
|
if (opts.doWhile ?? false) {
|
|
873
|
-
exit = opts.cond !== undefined && final.postBody !== null ? this.refine(final.postBody, opts.cond, false) : null;
|
|
953
|
+
exit = opts.cond !== undefined && final.postBody !== null ? this.refine(final.postBody, opts.cond, false, false) : null;
|
|
874
954
|
}
|
|
875
955
|
else if (opts.cond !== undefined) {
|
|
876
|
-
exit = this.refine(cloneEnv(head), opts.cond, false);
|
|
956
|
+
exit = this.refine(cloneEnv(head), opts.cond, false, false);
|
|
877
957
|
}
|
|
878
958
|
else if (opts.alwaysExits ?? false) {
|
|
879
959
|
exit = cloneEnv(head); // for-of ends when the array runs out
|
|
@@ -894,7 +974,7 @@ class FnAnalyzer {
|
|
|
894
974
|
let bodyIn = cloneEnv(head);
|
|
895
975
|
if (opts.cond !== undefined && !(opts.doWhile ?? false)) {
|
|
896
976
|
this.evalExpr(opts.cond, bodyIn);
|
|
897
|
-
bodyIn = this.refine(bodyIn, opts.cond, true);
|
|
977
|
+
bodyIn = this.refine(bodyIn, opts.cond, true, false);
|
|
898
978
|
}
|
|
899
979
|
if (bodyIn !== null && opts.seedEachIteration !== undefined) {
|
|
900
980
|
bodyIn.set(opts.seedEachIteration, { ...TOP });
|
|
@@ -915,7 +995,7 @@ class FnAnalyzer {
|
|
|
915
995
|
if ((opts.doWhile ?? false) && opts.cond !== undefined && out !== null) {
|
|
916
996
|
this.evalExpr(opts.cond, out);
|
|
917
997
|
postBody = cloneEnv(out);
|
|
918
|
-
out = this.refine(out, opts.cond, true);
|
|
998
|
+
out = this.refine(out, opts.cond, true, false);
|
|
919
999
|
}
|
|
920
1000
|
return { back: out, postBody };
|
|
921
1001
|
}
|
|
@@ -925,7 +1005,7 @@ class FnAnalyzer {
|
|
|
925
1005
|
* false when either side is NaN, so the edge where one HELD proves both
|
|
926
1006
|
* operands NaN-free; on the failed edge NaN survives while the negated
|
|
927
1007
|
* comparison still refines the numeric members. */
|
|
928
|
-
refine(env, cond, branch) {
|
|
1008
|
+
refine(env, cond, branch, allowPaths = false) {
|
|
929
1009
|
if (env === null)
|
|
930
1010
|
return null;
|
|
931
1011
|
switch (cond.kind) {
|
|
@@ -933,19 +1013,39 @@ class FnAnalyzer {
|
|
|
933
1013
|
return cond.value === branch ? env : null;
|
|
934
1014
|
case "unary":
|
|
935
1015
|
if (cond.op === "!")
|
|
936
|
-
return this.refine(env, cond.operand, !branch);
|
|
1016
|
+
return this.refine(env, cond.operand, !branch, allowPaths);
|
|
937
1017
|
return env;
|
|
938
1018
|
case "logical": {
|
|
939
1019
|
const isAnd = cond.op === "&&";
|
|
940
1020
|
if (isAnd === branch) {
|
|
941
1021
|
// (a && b) true — both held; (a || b) false — both failed.
|
|
942
|
-
return this.refine(this.refine(env, cond.left, branch), cond.right, branch);
|
|
1022
|
+
return this.refine(this.refine(env, cond.left, branch, allowPaths), cond.right, branch, allowPaths);
|
|
943
1023
|
}
|
|
944
1024
|
// (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);
|
|
1025
|
+
const viaLeft = this.refine(cloneEnv(env), cond.left, branch, allowPaths);
|
|
1026
|
+
const viaRight = this.refine(this.refine(cloneEnv(env), cond.left, !branch, allowPaths), cond.right, branch, allowPaths);
|
|
947
1027
|
return joinEnv(viaLeft, viaRight);
|
|
948
1028
|
}
|
|
1029
|
+
case "unionIsTag": {
|
|
1030
|
+
const unionType = { kind: "union", unionId: cond.unionId };
|
|
1031
|
+
if (numberCarrierKind(unionType, this.mod) !== "optional")
|
|
1032
|
+
return env;
|
|
1033
|
+
const def = this.mod.unions?.find((u) => u.id === cond.unionId);
|
|
1034
|
+
const arm = def?.arms[cond.tag];
|
|
1035
|
+
const key = this.refinementKey(cond.value, allowPaths);
|
|
1036
|
+
if (arm === undefined || key === null)
|
|
1037
|
+
return env;
|
|
1038
|
+
const tagMatches = cond.negated ? !branch : branch;
|
|
1039
|
+
// The abstract value describes only PRESENT f64 inhabitants. A
|
|
1040
|
+
// branch selecting a unit arm has no numeric inhabitants; likewise
|
|
1041
|
+
// a branch excluding the optional carrier's sole f64 arm.
|
|
1042
|
+
if ((tagMatches && arm.kind !== "f64") || (!tagMatches && arm.kind === "f64")) {
|
|
1043
|
+
const out = cloneEnv(env);
|
|
1044
|
+
out.set(key, { ...BOTTOM });
|
|
1045
|
+
return out;
|
|
1046
|
+
}
|
|
1047
|
+
return env;
|
|
1048
|
+
}
|
|
949
1049
|
case "bin": {
|
|
950
1050
|
if (!CMP_OPS.has(cond.op))
|
|
951
1051
|
return env;
|
|
@@ -961,17 +1061,22 @@ class FnAnalyzer {
|
|
|
961
1061
|
const a = this.evalPure(cond.left, env);
|
|
962
1062
|
const b = this.evalPure(cond.right, env);
|
|
963
1063
|
const out = cloneEnv(env);
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
if (
|
|
967
|
-
out.set(
|
|
1064
|
+
const leftKey = this.refinementKey(cond.left, allowPaths);
|
|
1065
|
+
const rightKey = this.refinementKey(cond.right, allowPaths);
|
|
1066
|
+
if (leftKey !== null)
|
|
1067
|
+
out.set(leftKey, refineLhs(op, a, b, clearNaN));
|
|
1068
|
+
if (rightKey !== null)
|
|
1069
|
+
out.set(rightKey, refineLhs(FLIP[op], b, a, clearNaN));
|
|
968
1070
|
return out;
|
|
969
1071
|
}
|
|
970
1072
|
case "toBool": {
|
|
971
1073
|
const inner = cond.operand;
|
|
972
|
-
if (inner.type.kind !== "f64" ||
|
|
1074
|
+
if (inner.type.kind !== "f64" || !this.isPure(inner))
|
|
1075
|
+
return env;
|
|
1076
|
+
const key = this.refinementKey(inner, allowPaths);
|
|
1077
|
+
if (key === null)
|
|
973
1078
|
return env;
|
|
974
|
-
const v =
|
|
1079
|
+
const v = this.evalPure(inner, env);
|
|
975
1080
|
const out = cloneEnv(env);
|
|
976
1081
|
if (branch) {
|
|
977
1082
|
// Truthy: not NaN, not zero — endpoint exclusion when whole.
|
|
@@ -980,11 +1085,11 @@ class FnAnalyzer {
|
|
|
980
1085
|
r = absVal(1, r.hi, r.whole, false, r.spelling);
|
|
981
1086
|
else if (r.whole && r.hi === 0 && r.lo <= -1)
|
|
982
1087
|
r = absVal(r.lo, -1, r.whole, false, r.spelling);
|
|
983
|
-
out.set(
|
|
1088
|
+
out.set(key, r);
|
|
984
1089
|
}
|
|
985
1090
|
else {
|
|
986
1091
|
// Falsy: 0, -0, or NaN.
|
|
987
|
-
out.set(
|
|
1092
|
+
out.set(key, meetInterval(v, 0, 0, false));
|
|
988
1093
|
}
|
|
989
1094
|
return out;
|
|
990
1095
|
}
|
|
@@ -1019,11 +1124,21 @@ class FnAnalyzer {
|
|
|
1019
1124
|
case "boolLit":
|
|
1020
1125
|
case "varRef":
|
|
1021
1126
|
case "unitLit":
|
|
1127
|
+
case "selfRef":
|
|
1022
1128
|
return true;
|
|
1129
|
+
case "recordGet":
|
|
1130
|
+
case "fieldGet":
|
|
1131
|
+
return this.staticAccessPath(e) !== null;
|
|
1132
|
+
case "unionNarrow":
|
|
1133
|
+
return this.isPure(e.value);
|
|
1023
1134
|
case "bin":
|
|
1024
1135
|
return this.isPure(e.left) && this.isPure(e.right);
|
|
1025
1136
|
case "unary":
|
|
1026
1137
|
return this.isPure(e.operand);
|
|
1138
|
+
case "logical":
|
|
1139
|
+
return this.isPure(e.left) && this.isPure(e.right);
|
|
1140
|
+
case "toBool":
|
|
1141
|
+
return this.isPure(e.operand);
|
|
1027
1142
|
default:
|
|
1028
1143
|
return false;
|
|
1029
1144
|
}
|
|
@@ -1034,7 +1149,18 @@ class FnAnalyzer {
|
|
|
1034
1149
|
case "numLit":
|
|
1035
1150
|
return constVal(e.value, e.spelling);
|
|
1036
1151
|
case "varRef":
|
|
1037
|
-
return e.
|
|
1152
|
+
return this.bindingCarriesNumber(e.localId) ? envGet(env, e.localId) : { ...TOP };
|
|
1153
|
+
case "recordGet": {
|
|
1154
|
+
const slot = this.cfg.records.get(e.shapeId)?.get(e.field);
|
|
1155
|
+
if (slot === undefined || numberCarrierKind(e.type, this.mod) === null)
|
|
1156
|
+
return { ...TOP };
|
|
1157
|
+
const key = this.pathKey(e, slot.cls);
|
|
1158
|
+
return key === null ? classSeed(slot.cls) : envGet(env, key);
|
|
1159
|
+
}
|
|
1160
|
+
case "unionNarrow":
|
|
1161
|
+
return e.type.kind === "f64" && numberCarrierKind(e.value.type, this.mod) === "optional"
|
|
1162
|
+
? this.evalPure(e.value, env)
|
|
1163
|
+
: { ...TOP };
|
|
1038
1164
|
case "unary":
|
|
1039
1165
|
if (e.op === "-")
|
|
1040
1166
|
return transferNeg(this.evalPure(e.operand, env));
|
|
@@ -1049,6 +1175,91 @@ class FnAnalyzer {
|
|
|
1049
1175
|
return { ...TOP };
|
|
1050
1176
|
}
|
|
1051
1177
|
}
|
|
1178
|
+
/** A canonical static data path. Source spellings that lower to the same
|
|
1179
|
+
* IR access (`m.total`, `m["total"]`) intentionally share a key; distinct
|
|
1180
|
+
* receiver bindings never do. Accessors/dynamic keys/computed receivers
|
|
1181
|
+
* have different IR nodes and are excluded. */
|
|
1182
|
+
staticAccessPath(e) {
|
|
1183
|
+
switch (e.kind) {
|
|
1184
|
+
case "varRef":
|
|
1185
|
+
return { rootId: e.localId, steps: [["var", e.localId]] };
|
|
1186
|
+
case "selfRef":
|
|
1187
|
+
return { rootId: null, steps: [["self"]] };
|
|
1188
|
+
case "recordGet": {
|
|
1189
|
+
const base = this.staticAccessPath(e.obj);
|
|
1190
|
+
if (base === null)
|
|
1191
|
+
return null;
|
|
1192
|
+
return { rootId: base.rootId, steps: [...base.steps, ["record", e.shapeId, e.field]] };
|
|
1193
|
+
}
|
|
1194
|
+
case "unionNarrow":
|
|
1195
|
+
return this.staticAccessPath(e.value);
|
|
1196
|
+
case "fieldGet": {
|
|
1197
|
+
const base = this.staticAccessPath(e.obj);
|
|
1198
|
+
if (base === null)
|
|
1199
|
+
return null;
|
|
1200
|
+
return { rootId: base.rootId, steps: [...base.steps, ["field", e.className, e.field]] };
|
|
1201
|
+
}
|
|
1202
|
+
default:
|
|
1203
|
+
return null;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
pathRoots = new Map();
|
|
1207
|
+
pathKey(e, cls) {
|
|
1208
|
+
const path = this.staticAccessPath(e);
|
|
1209
|
+
if (path === null)
|
|
1210
|
+
return null;
|
|
1211
|
+
const prefix = cls === "i64" ? PATH_I64_PREFIX : PATH_U64_PREFIX;
|
|
1212
|
+
const key = `${prefix}${JSON.stringify(path.steps)}`;
|
|
1213
|
+
this.pathRoots.set(key, path.rootId);
|
|
1214
|
+
return key;
|
|
1215
|
+
}
|
|
1216
|
+
refinementKey(e, allowPaths) {
|
|
1217
|
+
if (e.kind === "varRef")
|
|
1218
|
+
return e.localId;
|
|
1219
|
+
if (e.kind === "unionNarrow" && e.type.kind === "f64") {
|
|
1220
|
+
return this.refinementKey(e.value, allowPaths);
|
|
1221
|
+
}
|
|
1222
|
+
if (!allowPaths || e.kind !== "recordGet" || numberCarrierKind(e.type, this.mod) === null)
|
|
1223
|
+
return null;
|
|
1224
|
+
const slot = this.cfg.records.get(e.shapeId)?.get(e.field);
|
|
1225
|
+
return slot === undefined ? null : this.pathKey(e, slot.cls);
|
|
1226
|
+
}
|
|
1227
|
+
/** Path facts are admitted only when the entire guard is synchronous,
|
|
1228
|
+
* call-free, assignment-free static data access. This prevents a later
|
|
1229
|
+
* subexpression from mutating a field and `refine` subsequently
|
|
1230
|
+
* reconstructing a stale fact from the guard syntax. */
|
|
1231
|
+
stablePathGuard(e) {
|
|
1232
|
+
switch (e.kind) {
|
|
1233
|
+
case "numLit":
|
|
1234
|
+
case "strLit":
|
|
1235
|
+
case "boolLit":
|
|
1236
|
+
case "unitLit":
|
|
1237
|
+
case "varRef":
|
|
1238
|
+
case "selfRef":
|
|
1239
|
+
return true;
|
|
1240
|
+
case "recordGet":
|
|
1241
|
+
case "fieldGet":
|
|
1242
|
+
return this.staticAccessPath(e) !== null;
|
|
1243
|
+
case "unionIsTag":
|
|
1244
|
+
return this.stablePathGuard(e.value);
|
|
1245
|
+
case "unionNarrow":
|
|
1246
|
+
return this.stablePathGuard(e.value);
|
|
1247
|
+
case "bin":
|
|
1248
|
+
case "logical":
|
|
1249
|
+
return this.stablePathGuard(e.left) && this.stablePathGuard(e.right);
|
|
1250
|
+
case "unary":
|
|
1251
|
+
case "toBool":
|
|
1252
|
+
return this.stablePathGuard(e.operand);
|
|
1253
|
+
default:
|
|
1254
|
+
return false;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
clearPathsRootedAt(env, localId) {
|
|
1258
|
+
for (const k of [...env.keys()]) {
|
|
1259
|
+
if (pathClassOfKey(k) !== null && this.pathRoots.get(k) === localId)
|
|
1260
|
+
env.delete(k);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1052
1263
|
/* ── expression evaluation (side effects applied to env) ────────────── */
|
|
1053
1264
|
evalMath(e, env, mutate) {
|
|
1054
1265
|
const arg = (i) => (mutate ? this.evalExpr(e.args[i], env) : this.evalPure(e.args[i], env));
|
|
@@ -1064,18 +1275,20 @@ class FnAnalyzer {
|
|
|
1064
1275
|
default: return null;
|
|
1065
1276
|
}
|
|
1066
1277
|
}
|
|
1067
|
-
|
|
1068
|
-
return this.
|
|
1278
|
+
bindingCarriesNumber(id) {
|
|
1279
|
+
return this.numberBindings.has(id);
|
|
1069
1280
|
}
|
|
1070
|
-
|
|
1281
|
+
numberBindings = new Set();
|
|
1071
1282
|
seedBindings(fn, mod) {
|
|
1072
|
-
this.
|
|
1073
|
-
for (const l of fn.locals)
|
|
1074
|
-
if (l.type
|
|
1075
|
-
this.
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1283
|
+
this.numberBindings = new Set();
|
|
1284
|
+
for (const l of fn.locals) {
|
|
1285
|
+
if (numberCarrierKind(l.type, mod) !== null && l.boxed !== true)
|
|
1286
|
+
this.numberBindings.add(l.id);
|
|
1287
|
+
}
|
|
1288
|
+
for (const g of mod.globals ?? []) {
|
|
1289
|
+
if (numberCarrierKind(g.type, mod) !== null)
|
|
1290
|
+
this.numberBindings.add(g.id);
|
|
1291
|
+
}
|
|
1079
1292
|
}
|
|
1080
1293
|
/** Evaluate an expression over the environment, applying the side
|
|
1081
1294
|
* effects of nested writes and calls (a call havocs the globals its
|
|
@@ -1096,7 +1309,7 @@ class FnAnalyzer {
|
|
|
1096
1309
|
case "chainRecv":
|
|
1097
1310
|
return { ...TOP };
|
|
1098
1311
|
case "varRef":
|
|
1099
|
-
return
|
|
1312
|
+
return this.bindingCarriesNumber(e.localId) ? envGet(env, e.localId) : { ...TOP };
|
|
1100
1313
|
case "bin": {
|
|
1101
1314
|
const a = this.evalExpr(e.left, env);
|
|
1102
1315
|
const b = this.evalExpr(e.right, env);
|
|
@@ -1115,22 +1328,24 @@ class FnAnalyzer {
|
|
|
1115
1328
|
return { ...TOP };
|
|
1116
1329
|
}
|
|
1117
1330
|
case "incDec": {
|
|
1118
|
-
const old = this.
|
|
1331
|
+
const old = this.bindingCarriesNumber(e.localId) ? envGet(env, e.localId) : { ...TOP };
|
|
1119
1332
|
const next = transferAdd(old, constVal(e.op === "+" ? 1 : -1));
|
|
1120
|
-
if (this.
|
|
1333
|
+
if (this.bindingCarriesNumber(e.localId))
|
|
1121
1334
|
env.set(e.localId, next);
|
|
1122
1335
|
return e.prefix ? next : old;
|
|
1123
1336
|
}
|
|
1124
1337
|
case "assignExpr": {
|
|
1125
1338
|
const v = this.evalExpr(e.value, env);
|
|
1126
|
-
|
|
1339
|
+
this.clearPathsRootedAt(env, e.localId);
|
|
1340
|
+
if (this.bindingCarriesNumber(e.localId))
|
|
1127
1341
|
env.set(e.localId, v);
|
|
1128
1342
|
return v;
|
|
1129
1343
|
}
|
|
1130
1344
|
case "ternary": {
|
|
1131
1345
|
this.evalExpr(e.cond, env);
|
|
1132
|
-
const
|
|
1133
|
-
const
|
|
1346
|
+
const allowPaths = this.stablePathGuard(e.cond);
|
|
1347
|
+
const thenEnv = this.refine(cloneEnv(env), e.cond, true, allowPaths);
|
|
1348
|
+
const elseEnv = this.refine(cloneEnv(env), e.cond, false, allowPaths);
|
|
1134
1349
|
const a = thenEnv === null ? BOTTOM : this.evalExpr(e.then, thenEnv);
|
|
1135
1350
|
const b = elseEnv === null ? BOTTOM : this.evalExpr(e.else_, elseEnv);
|
|
1136
1351
|
mergeInto(env, joinEnv(thenEnv, elseEnv));
|
|
@@ -1139,18 +1354,18 @@ class FnAnalyzer {
|
|
|
1139
1354
|
case "logical":
|
|
1140
1355
|
case "nullish":
|
|
1141
1356
|
case "orDefault": {
|
|
1142
|
-
this.evalExpr(e.left, env);
|
|
1357
|
+
const left = this.evalExpr(e.left, env);
|
|
1143
1358
|
const rightEnv = cloneEnv(env);
|
|
1144
|
-
this.evalExpr(e.right, rightEnv);
|
|
1359
|
+
const right = this.evalExpr(e.right, rightEnv);
|
|
1145
1360
|
mergeInto(env, joinEnv(env, rightEnv));
|
|
1146
|
-
return { ...TOP };
|
|
1361
|
+
return numberCarrierKind(e.type, this.mod) !== null ? join(left, right) : { ...TOP };
|
|
1147
1362
|
}
|
|
1148
1363
|
case "optChain": {
|
|
1149
1364
|
this.evalExpr(e.receiver, env);
|
|
1150
1365
|
const bodyEnv = cloneEnv(env);
|
|
1151
|
-
this.evalExpr(e.body, bodyEnv);
|
|
1366
|
+
const body = this.evalExpr(e.body, bodyEnv);
|
|
1152
1367
|
mergeInto(env, joinEnv(env, bodyEnv));
|
|
1153
|
-
return { ...TOP };
|
|
1368
|
+
return numberCarrierKind(e.type, this.mod) !== null ? body : { ...TOP };
|
|
1154
1369
|
}
|
|
1155
1370
|
case "seqExpr": {
|
|
1156
1371
|
// Expression-position statements: no jumps can escape them.
|
|
@@ -1185,14 +1400,31 @@ class FnAnalyzer {
|
|
|
1185
1400
|
return classSeed(slots.ret);
|
|
1186
1401
|
return { ...TOP };
|
|
1187
1402
|
}
|
|
1403
|
+
case "unionWrap": {
|
|
1404
|
+
const value = this.evalExpr(e.value, env);
|
|
1405
|
+
if (numberCarrierKind(e.type, this.mod) !== "optional")
|
|
1406
|
+
return { ...TOP };
|
|
1407
|
+
// Unit arms are absence, not an integer crossing. The one f64 arm
|
|
1408
|
+
// contributes its abstract value unchanged.
|
|
1409
|
+
return e.value.type.kind === "f64" ? value : BOTTOM;
|
|
1410
|
+
}
|
|
1411
|
+
case "unionNarrow": {
|
|
1412
|
+
const value = this.evalExpr(e.value, env);
|
|
1413
|
+
return e.type.kind === "f64" && numberCarrierKind(e.value.type, this.mod) === "optional"
|
|
1414
|
+
? value
|
|
1415
|
+
: { ...TOP };
|
|
1416
|
+
}
|
|
1188
1417
|
case "libCall": {
|
|
1189
1418
|
const math = this.evalMath(e, env, true);
|
|
1190
|
-
if (math !== null)
|
|
1419
|
+
if (math !== null) {
|
|
1420
|
+
clearPathFacts(env);
|
|
1191
1421
|
return math;
|
|
1422
|
+
}
|
|
1192
1423
|
for (const a of e.args)
|
|
1193
1424
|
this.evalExpr(a, env);
|
|
1194
1425
|
if (e.args.some((a) => typeContainsFunc(a.type)))
|
|
1195
1426
|
this.havocAllGlobals(env);
|
|
1427
|
+
clearPathFacts(env);
|
|
1196
1428
|
return { ...TOP };
|
|
1197
1429
|
}
|
|
1198
1430
|
case "callValue":
|
|
@@ -1207,13 +1439,23 @@ class FnAnalyzer {
|
|
|
1207
1439
|
this.havocAllGlobals(env);
|
|
1208
1440
|
return { ...TOP };
|
|
1209
1441
|
}
|
|
1442
|
+
case "ffiCall":
|
|
1443
|
+
case "yieldExpr":
|
|
1444
|
+
case "awaitExpr":
|
|
1445
|
+
case "awaitUnionExpr": {
|
|
1446
|
+
for (const v of childExprs(e))
|
|
1447
|
+
this.evalExpr(v, env);
|
|
1448
|
+
clearPathFacts(env);
|
|
1449
|
+
return { ...TOP };
|
|
1450
|
+
}
|
|
1210
1451
|
case "recordLit": {
|
|
1211
1452
|
const slotMap = this.cfg.records.get(e.type.shapeId);
|
|
1212
1453
|
for (const f of e.fields) {
|
|
1213
1454
|
const v = this.evalExpr(f.value, env);
|
|
1214
1455
|
const slot = slotMap?.get(f.name);
|
|
1215
|
-
if (slot !== undefined && f.value.type.
|
|
1456
|
+
if (slot !== undefined && numberCarrierKind(f.value.type, this.mod) !== null) {
|
|
1216
1457
|
this.emit(v, slot.path, slot.cls, f.value.loc);
|
|
1458
|
+
}
|
|
1217
1459
|
}
|
|
1218
1460
|
return { ...TOP };
|
|
1219
1461
|
}
|
|
@@ -1226,18 +1468,28 @@ class FnAnalyzer {
|
|
|
1226
1468
|
// — instead of a spurious may-be-NaN wholeness refusal).
|
|
1227
1469
|
this.evalExpr(e.obj, env);
|
|
1228
1470
|
const slot = this.cfg.records.get(e.shapeId)?.get(e.field);
|
|
1229
|
-
if (slot !== undefined && e.type.
|
|
1230
|
-
|
|
1471
|
+
if (slot !== undefined && numberCarrierKind(e.type, this.mod) !== null) {
|
|
1472
|
+
const key = this.pathKey(e, slot.cls);
|
|
1473
|
+
return key === null ? classSeed(slot.cls) : envGet(env, key);
|
|
1474
|
+
}
|
|
1231
1475
|
return { ...TOP };
|
|
1232
1476
|
}
|
|
1477
|
+
case "fieldGet":
|
|
1478
|
+
this.evalExpr(e.obj, env);
|
|
1479
|
+
return { ...TOP };
|
|
1233
1480
|
default: {
|
|
1234
1481
|
for (const v of childExprs(e))
|
|
1235
1482
|
this.evalExpr(v, env);
|
|
1483
|
+
// Unmodeled expressions do not participate in the cheap
|
|
1484
|
+
// straight-line proof. Some can invoke runtime/user machinery;
|
|
1485
|
+
// refusing to carry a field fact through them is the safe verdict.
|
|
1486
|
+
clearPathFacts(env);
|
|
1236
1487
|
return { ...TOP };
|
|
1237
1488
|
}
|
|
1238
1489
|
}
|
|
1239
1490
|
}
|
|
1240
1491
|
havocCall(callee, env) {
|
|
1492
|
+
clearPathFacts(env);
|
|
1241
1493
|
if (this.effects.havocAll.has(callee) || !this.effects.perFn.has(callee)) {
|
|
1242
1494
|
this.havocAllGlobals(env);
|
|
1243
1495
|
return;
|
|
@@ -1246,6 +1498,7 @@ class FnAnalyzer {
|
|
|
1246
1498
|
env.delete(g); // absent global = TOP
|
|
1247
1499
|
}
|
|
1248
1500
|
havocAllGlobals(env) {
|
|
1501
|
+
clearPathFacts(env);
|
|
1249
1502
|
for (const k of [...env.keys()]) {
|
|
1250
1503
|
if (isGlobalId(k))
|
|
1251
1504
|
env.delete(k);
|