@nudojs/core 1.15.2 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-WKIRPUHZ.js → chunk-OM6SAY2M.js} +29846 -20390
- package/dist/{chunk-NXZSRLCT.js → chunk-POGFCYV2.js} +3999 -999
- package/dist/exec.d.ts +384 -38
- package/dist/exec.js +3 -1
- package/dist/index.d.ts +389 -23
- package/dist/index.js +1250 -336
- package/dist/internal.d.ts +72 -8
- package/dist/internal.js +147 -115
- package/dist/{projection-budget-07VnMktV.d.ts → projection-budget-DW-svmIf.d.ts} +178 -42
- package/dist/{run-DfloWX6i.d.ts → run-T9HWTziZ.d.ts} +65 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,10 +2,16 @@ import "./chunk-US532PZA.js";
|
|
|
2
2
|
import {
|
|
3
3
|
INTERFACE_SOURCE_ABBR,
|
|
4
4
|
NudoSidecarError,
|
|
5
|
+
absWithStableTermIds,
|
|
5
6
|
actionsForIssue,
|
|
6
7
|
buildArgsFromAssume,
|
|
7
8
|
canSkipLiteralCallScan,
|
|
9
|
+
checkArg,
|
|
10
|
+
checkCall,
|
|
8
11
|
collectAbsFreeVars,
|
|
12
|
+
contractSegmentLocs,
|
|
13
|
+
ctorParamCountOf,
|
|
14
|
+
deferredExecutionSpans,
|
|
9
15
|
effectiveInterface,
|
|
10
16
|
evictGeneralizeMemoForPaths,
|
|
11
17
|
execNudoModule,
|
|
@@ -27,6 +33,7 @@ import {
|
|
|
27
33
|
fnTopLevelLocalNames,
|
|
28
34
|
formatCheckReport,
|
|
29
35
|
formatConstraint,
|
|
36
|
+
formatDiagnostics,
|
|
30
37
|
formatEffectiveInterfaceDisplay,
|
|
31
38
|
formatGithubAnnotations,
|
|
32
39
|
formatGitlabCodeQuality,
|
|
@@ -35,20 +42,24 @@ import {
|
|
|
35
42
|
generalizeFromAst,
|
|
36
43
|
generatedExportNames,
|
|
37
44
|
getGeneralizeMemoSize,
|
|
45
|
+
guardLoadModule,
|
|
38
46
|
hashSource,
|
|
39
47
|
interfaceDiagCount,
|
|
40
48
|
interfaceSourceOf,
|
|
41
49
|
interfaceTierOf,
|
|
42
50
|
isNodeModulesPath,
|
|
51
|
+
listClassEntryNames,
|
|
43
52
|
listFnDirectiveScopes,
|
|
44
53
|
listFunctionNames,
|
|
45
54
|
listTopFunctions,
|
|
46
55
|
loadModuleDepsFingerprint,
|
|
47
56
|
localNamedExports,
|
|
57
|
+
nodeSpan,
|
|
48
58
|
normPath,
|
|
49
59
|
parseEnvPayload,
|
|
50
60
|
parseMockModulePayload,
|
|
51
61
|
parseNudoImportPayload,
|
|
62
|
+
reachableTopBindings,
|
|
52
63
|
refineDiagCount,
|
|
53
64
|
refineToIndexedFull,
|
|
54
65
|
resetGeneralizeMemo,
|
|
@@ -56,6 +67,7 @@ import {
|
|
|
56
67
|
resetNudoModuleExecCache,
|
|
57
68
|
resetSidecarLoadFailureCache,
|
|
58
69
|
resolveDepPath,
|
|
70
|
+
resolveExportSlotCall,
|
|
59
71
|
scanBudgetDecl,
|
|
60
72
|
scanCaseArgSpans,
|
|
61
73
|
scanCaseTags,
|
|
@@ -66,6 +78,7 @@ import {
|
|
|
66
78
|
serializeCheckJsonMulti,
|
|
67
79
|
setInterfaceDiagCollector,
|
|
68
80
|
setRefineDiagCollector,
|
|
81
|
+
sidecarAutoBindAllowed,
|
|
69
82
|
sidecarClosureFingerprint,
|
|
70
83
|
sidecarPathOf,
|
|
71
84
|
stableAnalyzeKeySource,
|
|
@@ -74,7 +87,7 @@ import {
|
|
|
74
87
|
takeInterfaceDiagsSince,
|
|
75
88
|
takeRefineDiags,
|
|
76
89
|
takeRefineDiagsSince
|
|
77
|
-
} from "./chunk-
|
|
90
|
+
} from "./chunk-POGFCYV2.js";
|
|
78
91
|
import {
|
|
79
92
|
$add,
|
|
80
93
|
$arguments,
|
|
@@ -193,6 +206,7 @@ import {
|
|
|
193
206
|
DIAG,
|
|
194
207
|
FORK_TRUNCATION_LABEL,
|
|
195
208
|
HOST_EFFECT_LABEL_PREFIX,
|
|
209
|
+
LAZY_TEMPLATE_DEPTH,
|
|
196
210
|
LOOP_TRUNCATION_LABEL,
|
|
197
211
|
MATH_FOLD_ERROR_LABEL,
|
|
198
212
|
MAX_EVAL_CALL_DEPTH,
|
|
@@ -213,6 +227,7 @@ import {
|
|
|
213
227
|
absOnly,
|
|
214
228
|
absShapeKey,
|
|
215
229
|
absToString,
|
|
230
|
+
accessorReadDomain,
|
|
216
231
|
add,
|
|
217
232
|
alphaOf,
|
|
218
233
|
and,
|
|
@@ -227,6 +242,7 @@ import {
|
|
|
227
242
|
asAbs,
|
|
228
243
|
asAbsVal,
|
|
229
244
|
assignSourceSlots,
|
|
245
|
+
assumeConsumed,
|
|
230
246
|
assumeFinite,
|
|
231
247
|
attachContractFaces,
|
|
232
248
|
attachFnImpl,
|
|
@@ -362,6 +378,7 @@ import {
|
|
|
362
378
|
isSetAbs,
|
|
363
379
|
isStrPrim,
|
|
364
380
|
isSymbolAbs,
|
|
381
|
+
isThrowsIgnored,
|
|
365
382
|
joinAbs,
|
|
366
383
|
joinFunctions,
|
|
367
384
|
joinObjects,
|
|
@@ -415,6 +432,7 @@ import {
|
|
|
415
432
|
never,
|
|
416
433
|
not,
|
|
417
434
|
notAbs,
|
|
435
|
+
noteAssumeConsumed,
|
|
418
436
|
noteCollectionWrite,
|
|
419
437
|
noteEvalCallRecord,
|
|
420
438
|
noteEvalFallback,
|
|
@@ -460,6 +478,7 @@ import {
|
|
|
460
478
|
relationFn,
|
|
461
479
|
requiredFnArity,
|
|
462
480
|
resetAbsCallBudget,
|
|
481
|
+
resetAssumeConsumed,
|
|
463
482
|
resetEvalCallBudget,
|
|
464
483
|
resetParseSourceCache,
|
|
465
484
|
runTranspiled,
|
|
@@ -526,6 +545,7 @@ import {
|
|
|
526
545
|
tryPromoteHofCallback,
|
|
527
546
|
tryPromoteReceiverAsArr,
|
|
528
547
|
tryRunTranspiled,
|
|
548
|
+
tryRunTranspiledDetailed,
|
|
529
549
|
typeofAbs,
|
|
530
550
|
undefAbs,
|
|
531
551
|
union,
|
|
@@ -536,7 +556,7 @@ import {
|
|
|
536
556
|
withExecPhi,
|
|
537
557
|
withInvariantSpecs,
|
|
538
558
|
withVar
|
|
539
|
-
} from "./chunk-
|
|
559
|
+
} from "./chunk-OM6SAY2M.js";
|
|
540
560
|
|
|
541
561
|
// src/environment.ts
|
|
542
562
|
function createEnvironment(parent, bindings = /* @__PURE__ */ new Map()) {
|
|
@@ -652,123 +672,6 @@ function describePhi() {
|
|
|
652
672
|
return p.op === "true" ? "\u22A4" : "(phi)";
|
|
653
673
|
}
|
|
654
674
|
|
|
655
|
-
// src/algebra/diagnostics.ts
|
|
656
|
-
function checkArg(arg, expect, phi = pTrue) {
|
|
657
|
-
if (!expect || expect.op === "true") return void 0;
|
|
658
|
-
if (arg.shape.k === "unknown" && !arg.term) {
|
|
659
|
-
return {
|
|
660
|
-
severity: "warning",
|
|
661
|
-
code: DIAG.argOpaque,
|
|
662
|
-
message: `argument type is unknown; cannot verify constraint ${predToString(expect)}`,
|
|
663
|
-
suggestion: "add a call site or @nudo:case, or provide preconditions via --assume"
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
|
-
const combined = arg.pred && arg.pred.op !== "true" ? phi.op === "true" ? arg.pred : { op: "and", args: [phi, arg.pred] } : phi;
|
|
667
|
-
if (implies(combined, expect)) return void 0;
|
|
668
|
-
const lvR = litValue(arg);
|
|
669
|
-
if (lvR.ok && (expect.op === "gt" || expect.op === "ge" || expect.op === "lt" || expect.op === "le")) {
|
|
670
|
-
const ok = decideLitCmp(lvR.value, expect);
|
|
671
|
-
if (ok === true) return void 0;
|
|
672
|
-
if (ok === false) {
|
|
673
|
-
return {
|
|
674
|
-
severity: "error",
|
|
675
|
-
code: DIAG.constraintViolated,
|
|
676
|
-
message: `argument ${JSON.stringify(lvR.value)} does not satisfy ${predToString(expect)}`,
|
|
677
|
-
suggestion: `use a value satisfying the constraint, or relax the function precondition`
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
return {
|
|
682
|
-
severity: "warning",
|
|
683
|
-
code: DIAG.constraintUnproven,
|
|
684
|
-
message: `cannot prove the argument satisfies ${predToString(expect)}`,
|
|
685
|
-
suggestion: `not enough information; use --assume or add a case`
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
function decideLitCmp(lv, p) {
|
|
689
|
-
if (typeof lv !== "number" || p.b.op !== "lit" || typeof p.b.value !== "number") {
|
|
690
|
-
return void 0;
|
|
691
|
-
}
|
|
692
|
-
const n = p.b.value;
|
|
693
|
-
switch (p.op) {
|
|
694
|
-
case "gt":
|
|
695
|
-
return lv > n;
|
|
696
|
-
case "ge":
|
|
697
|
-
return lv >= n;
|
|
698
|
-
case "lt":
|
|
699
|
-
return lv < n;
|
|
700
|
-
case "le":
|
|
701
|
-
return lv <= n;
|
|
702
|
-
default:
|
|
703
|
-
return void 0;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
function checkCall(source, fnName, args, phi = pTrue) {
|
|
707
|
-
const diags = [];
|
|
708
|
-
const g = generalizeFromAst(fnName, source);
|
|
709
|
-
if (!g) {
|
|
710
|
-
diags.push({
|
|
711
|
-
severity: "error",
|
|
712
|
-
code: DIAG.fnNotFound,
|
|
713
|
-
message: `function ${fnName} not found`
|
|
714
|
-
});
|
|
715
|
-
return diags;
|
|
716
|
-
}
|
|
717
|
-
if (args.length !== g.params.length) {
|
|
718
|
-
diags.push({
|
|
719
|
-
severity: "error",
|
|
720
|
-
code: DIAG.argCount,
|
|
721
|
-
message: `${fnName} expects ${g.params.length} argument(s), got ${args.length}`,
|
|
722
|
-
fn: fnName
|
|
723
|
-
});
|
|
724
|
-
return diags;
|
|
725
|
-
}
|
|
726
|
-
const required = extractParamRequirements(g);
|
|
727
|
-
args.forEach((arg, i) => {
|
|
728
|
-
const req = required.get(i);
|
|
729
|
-
const d = checkArg(arg, req, phi);
|
|
730
|
-
if (d) diags.push({ ...d, fn: fnName, argIndex: i });
|
|
731
|
-
});
|
|
732
|
-
if (g.symbolic.conf === "partial" || g.symbolic.conf === "opaque") {
|
|
733
|
-
diags.push({
|
|
734
|
-
severity: "info",
|
|
735
|
-
code: DIAG.partialResult,
|
|
736
|
-
message: `${fnName}(...) result confidence ${g.symbolic.conf}`,
|
|
737
|
-
suggestion: "add a call site or constraint to improve precision",
|
|
738
|
-
fn: fnName
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
return diags;
|
|
742
|
-
}
|
|
743
|
-
function extractParamRequirements(g) {
|
|
744
|
-
const map = /* @__PURE__ */ new Map();
|
|
745
|
-
const pred = g.symbolic.pred;
|
|
746
|
-
if (!pred || pred.op === "true") return map;
|
|
747
|
-
const paramIndex = /* @__PURE__ */ new Map();
|
|
748
|
-
g.typeParams.forEach((t, i) => paramIndex.set(t.id, i));
|
|
749
|
-
const consider = (p) => {
|
|
750
|
-
if (p.op === "and") {
|
|
751
|
-
p.args.forEach(consider);
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
if ((p.op === "gt" || p.op === "ge" || p.op === "lt" || p.op === "le") && p.a.op === "var" && paramIndex.has(p.a.id) && p.b.op === "lit") {
|
|
755
|
-
map.set(paramIndex.get(p.a.id), p);
|
|
756
|
-
}
|
|
757
|
-
};
|
|
758
|
-
consider(pred);
|
|
759
|
-
return map;
|
|
760
|
-
}
|
|
761
|
-
function formatDiagnostics(diags) {
|
|
762
|
-
if (diags.length === 0) return "No issues found.";
|
|
763
|
-
return diags.map((d) => {
|
|
764
|
-
const loc = d.fn ? `${d.fn}${d.argIndex !== void 0 ? `[${d.argIndex}]` : ""}: ` : "";
|
|
765
|
-
let s = `[${d.severity}] ${loc}${d.message} (${d.code})`;
|
|
766
|
-
if (d.suggestion) s += `
|
|
767
|
-
\u2192 ${d.suggestion}`;
|
|
768
|
-
return s;
|
|
769
|
-
}).join("\n");
|
|
770
|
-
}
|
|
771
|
-
|
|
772
675
|
// src/algebra/check.ts
|
|
773
676
|
import { isBuiltin } from "module";
|
|
774
677
|
|
|
@@ -794,12 +697,43 @@ function isValueSetUnknown(ret) {
|
|
|
794
697
|
if (ret.shape.k === "any") return true;
|
|
795
698
|
return false;
|
|
796
699
|
}
|
|
797
|
-
function isBoundedEvidenceMissing(ret, constraint) {
|
|
700
|
+
function isBoundedEvidenceMissing(ret, constraint, phi) {
|
|
798
701
|
if (constraint.preds.length === 0 && !constraint.members) return false;
|
|
799
702
|
if (ret.term?.op === "lit") return false;
|
|
800
703
|
if (ret.pred && ret.pred.op !== "true") return false;
|
|
704
|
+
if (phiSuppliesConstraint(phi, constraint, ret)) return false;
|
|
801
705
|
return true;
|
|
802
706
|
}
|
|
707
|
+
function phiSuppliesConstraint(phi, constraint, ret) {
|
|
708
|
+
if (!phi || phi.op === "true") return false;
|
|
709
|
+
const item = retAnchorTerm(ret);
|
|
710
|
+
if (!hasSameItemBoundEvidence(phi, item)) return false;
|
|
711
|
+
return implies(phi, instantiateOnTerm(constraint, item));
|
|
712
|
+
}
|
|
713
|
+
function hasSameItemBoundEvidence(p, item) {
|
|
714
|
+
if (!p) return false;
|
|
715
|
+
switch (p.op) {
|
|
716
|
+
case "gt":
|
|
717
|
+
case "ge":
|
|
718
|
+
case "lt":
|
|
719
|
+
case "le":
|
|
720
|
+
return sameItemTerm(item, p.a);
|
|
721
|
+
case "and":
|
|
722
|
+
case "or":
|
|
723
|
+
return p.args.some((a) => hasSameItemBoundEvidence(a, item));
|
|
724
|
+
case "not":
|
|
725
|
+
return hasSameItemBoundEvidence(p.arg, item);
|
|
726
|
+
default:
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function sameItemTerm(item, left) {
|
|
731
|
+
if (termEquals(item, left)) return true;
|
|
732
|
+
if (left.op === "app" && left.fn === "length" && left.args.length === 1) {
|
|
733
|
+
return termEquals(item, left.args[0]);
|
|
734
|
+
}
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
803
737
|
function flattenSumArms(ret) {
|
|
804
738
|
if (ret.shape.k !== "sum") return [ret];
|
|
805
739
|
const out = [];
|
|
@@ -956,7 +890,11 @@ function assertImpliesSingle(arm, constraint, opts, phi) {
|
|
|
956
890
|
}
|
|
957
891
|
return r.status === "unprovable" ? unprovable(`array element: ${r.reason}`) : proved();
|
|
958
892
|
}
|
|
959
|
-
if (constraint.fn)
|
|
893
|
+
if (constraint.fn) {
|
|
894
|
+
return unprovable(
|
|
895
|
+
"fn-shaped return contract cannot be enforced (declare the returned value as the fn's returns constraint)"
|
|
896
|
+
);
|
|
897
|
+
}
|
|
960
898
|
return assertScalar(arm, constraint, phi);
|
|
961
899
|
}
|
|
962
900
|
function isAnyConstraint(c) {
|
|
@@ -981,7 +919,7 @@ function assertShapeFields(ret, constraint, opts) {
|
|
|
981
919
|
}
|
|
982
920
|
continue;
|
|
983
921
|
}
|
|
984
|
-
const r = assertImplies(slot.value, field.constraint, opts);
|
|
922
|
+
const r = assertImplies(accessorReadDomain(ret, key) ?? slot.value, field.constraint, opts);
|
|
985
923
|
if (r.status === "disproved") {
|
|
986
924
|
return disproved(`field ${key}: ${r.reason}`);
|
|
987
925
|
}
|
|
@@ -1022,10 +960,14 @@ function assertScalar(ret, constraint, phi) {
|
|
|
1022
960
|
}
|
|
1023
961
|
if (constraintPred.op !== "true") {
|
|
1024
962
|
if (implies(retPred, constraintPred)) return proved();
|
|
963
|
+
if (phi.op !== "true" && ret.pred && ret.pred.op !== "true" && implies(ret.pred, constraintPred)) {
|
|
964
|
+
return proved();
|
|
965
|
+
}
|
|
1025
966
|
if (ret.pred && ret.pred.op !== "true" && numericBoundsImplyLocal(ret.pred, constraintPred)) {
|
|
1026
967
|
return proved();
|
|
1027
968
|
}
|
|
1028
|
-
if (
|
|
969
|
+
if (phiSuppliesConstraint(phi, constraint, ret)) return proved();
|
|
970
|
+
if (isBoundedEvidenceMissing(ret, constraint, phi)) {
|
|
1029
971
|
return unprovable(
|
|
1030
972
|
`cannot prove return satisfies ${formatConstraint(constraint)} (no bound evidence on the return value)`
|
|
1031
973
|
);
|
|
@@ -1044,7 +986,7 @@ function assertScalar(ret, constraint, phi) {
|
|
|
1044
986
|
}
|
|
1045
987
|
}
|
|
1046
988
|
if (constraintPred.op !== "true" || constraint.preds.length > 0) {
|
|
1047
|
-
if (isBoundedEvidenceMissing(ret, constraint)) {
|
|
989
|
+
if (isBoundedEvidenceMissing(ret, constraint, phi)) {
|
|
1048
990
|
return unprovable(
|
|
1049
991
|
`cannot prove return satisfies ${formatConstraint(constraint)} (no bound evidence on the return value)`
|
|
1050
992
|
);
|
|
@@ -1127,6 +1069,104 @@ function isLengthSelfTerm(t) {
|
|
|
1127
1069
|
return t.op === "app" && t.fn === "length" && t.args.length === 1 && t.args[0].op === "var" && t.args[0].id === SELF;
|
|
1128
1070
|
}
|
|
1129
1071
|
|
|
1072
|
+
// src/algebra/check-return.ts
|
|
1073
|
+
function checkReturnConstraint(fnName, cName, constraint, ret, audit = {}) {
|
|
1074
|
+
const {
|
|
1075
|
+
loc,
|
|
1076
|
+
source = "directive",
|
|
1077
|
+
assumeDisplay,
|
|
1078
|
+
invariantDisplay,
|
|
1079
|
+
consumedAssume,
|
|
1080
|
+
phi,
|
|
1081
|
+
phiPremises
|
|
1082
|
+
} = audit;
|
|
1083
|
+
const proof = assertImplies(ret, constraint, phi ? { phi } : void 0);
|
|
1084
|
+
const phiUsedIdx = /* @__PURE__ */ new Set();
|
|
1085
|
+
if (phi && proof.status === "proved" && phiPremises && phiPremises.length > 0) {
|
|
1086
|
+
const withoutPhi = assertImplies(ret, constraint);
|
|
1087
|
+
if (withoutPhi.status !== "proved") {
|
|
1088
|
+
const solo = phiPremises.filter(
|
|
1089
|
+
(p) => phiSuppliesConstraint(p, constraint, ret)
|
|
1090
|
+
);
|
|
1091
|
+
const used = solo.length > 0 ? solo : [...phiPremises];
|
|
1092
|
+
for (const p of used) noteAssumeConsumed(p);
|
|
1093
|
+
phiPremises.forEach((p, i) => {
|
|
1094
|
+
if (used.includes(p)) phiUsedIdx.add(i);
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
const where = () => {
|
|
1099
|
+
if (loc?.line === void 0) return {};
|
|
1100
|
+
return loc.column === void 0 ? { line: loc.line } : { line: loc.line, column: loc.column };
|
|
1101
|
+
};
|
|
1102
|
+
const origin = source === "sidecar" ? `sidecar fn() return slot (${cName})` : `@nudo:contract return ${cName}`;
|
|
1103
|
+
if (proof.status === "proved") {
|
|
1104
|
+
const ledgerUsedIdx = /* @__PURE__ */ new Set();
|
|
1105
|
+
if (assumeDisplay && phiPremises) {
|
|
1106
|
+
const consumed = assumeConsumed();
|
|
1107
|
+
for (let i = 0; i < assumeDisplay.length; i++) {
|
|
1108
|
+
const premise = phiPremises[i];
|
|
1109
|
+
if (premise !== void 0 && consumed.includes(premise)) ledgerUsedIdx.add(i);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
const relied = [];
|
|
1113
|
+
const usedAssume = (assumeDisplay ?? []).filter(
|
|
1114
|
+
(d, i) => consumedAssume?.has(d) === true || ledgerUsedIdx.has(i) || phiUsedIdx.has(i)
|
|
1115
|
+
);
|
|
1116
|
+
if (usedAssume.length > 0) {
|
|
1117
|
+
relied.push({
|
|
1118
|
+
severity: "info",
|
|
1119
|
+
code: DIAG.assumeRelied,
|
|
1120
|
+
message: `${fnName}: return contract proved via @nudo:assume ${usedAssume.join("; ")}`,
|
|
1121
|
+
actual: `proof relies on user lemma${usedAssume.length > 1 ? "s" : ""}`,
|
|
1122
|
+
expected: "engine verifies the obligation, not the lemma itself",
|
|
1123
|
+
suggestion: "@nudo:assume is a user-supplied axiom \u2014 falsify it with concrete @nudo:case runs (nudo test)",
|
|
1124
|
+
fn: fnName,
|
|
1125
|
+
...where()
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
if (invariantDisplay && invariantDisplay.length > 0) {
|
|
1129
|
+
relied.push({
|
|
1130
|
+
severity: "info",
|
|
1131
|
+
code: DIAG.invariantRelied,
|
|
1132
|
+
message: `${fnName}: return contract proved via @nudo:invariant ${invariantDisplay.join("; ")}`,
|
|
1133
|
+
actual: `proof relies on user invariant${invariantDisplay.length > 1 ? "s" : ""}`,
|
|
1134
|
+
expected: "engine verifies the obligation, not the loop invariant itself",
|
|
1135
|
+
suggestion: "@nudo:invariant is a user-supplied axiom \u2014 falsify it with concrete @nudo:case runs (nudo test)",
|
|
1136
|
+
fn: fnName,
|
|
1137
|
+
...where()
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
return relied;
|
|
1141
|
+
}
|
|
1142
|
+
const push = (severity, actual, expected, suggestion) => [
|
|
1143
|
+
{
|
|
1144
|
+
severity,
|
|
1145
|
+
code: severity === "error" ? DIAG.constraintViolated : DIAG.unprovenReturn,
|
|
1146
|
+
message: `${fnName}: return value \u22AD ${origin}`,
|
|
1147
|
+
actual,
|
|
1148
|
+
expected,
|
|
1149
|
+
suggestion,
|
|
1150
|
+
fn: fnName,
|
|
1151
|
+
...where()
|
|
1152
|
+
}
|
|
1153
|
+
];
|
|
1154
|
+
if (proof.status === "disproved") {
|
|
1155
|
+
return push(
|
|
1156
|
+
"error",
|
|
1157
|
+
formatAbs(ret),
|
|
1158
|
+
proof.reason,
|
|
1159
|
+
proof.reason.includes("nullish") ? `use nullable(${formatConstraint(constraint)}) or union(..., lit(null)) to allow nullish returns` : `return a value satisfying ${formatConstraint(constraint)}`
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
return push(
|
|
1163
|
+
"warning",
|
|
1164
|
+
formatAbs(ret),
|
|
1165
|
+
formatConstraint(constraint),
|
|
1166
|
+
`${proof.reason}; add a precondition, narrow the return expression, or relax the return contract`
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1130
1170
|
// src/algebra/projection.ts
|
|
1131
1171
|
var PROJECTABLE_CONF = /* @__PURE__ */ new Set(["exact", "path"]);
|
|
1132
1172
|
var ARR_FALLBACK_CONF = /* @__PURE__ */ new Set([
|
|
@@ -1134,6 +1174,9 @@ var ARR_FALLBACK_CONF = /* @__PURE__ */ new Set([
|
|
|
1134
1174
|
"path",
|
|
1135
1175
|
"widened"
|
|
1136
1176
|
]);
|
|
1177
|
+
function isHiddenInternalSlot(k, slot) {
|
|
1178
|
+
return slot.internal === true && canonicalArrayIndex(k) === void 0;
|
|
1179
|
+
}
|
|
1137
1180
|
function absToConstraint(a, budget = new ProjectionBudget()) {
|
|
1138
1181
|
if (!PROJECTABLE_CONF.has(a.conf)) return void 0;
|
|
1139
1182
|
const stop = budget.enter(a);
|
|
@@ -1162,7 +1205,7 @@ function absToConstraintInner(a, budget) {
|
|
|
1162
1205
|
case "any":
|
|
1163
1206
|
return a.pred?.op === "or" ? projectOrLiterals(a) : void 0;
|
|
1164
1207
|
case "obj":
|
|
1165
|
-
return projectObj(
|
|
1208
|
+
return projectObj(a, s, budget);
|
|
1166
1209
|
case "arr":
|
|
1167
1210
|
return projectArr(s.element, budget);
|
|
1168
1211
|
case "sum":
|
|
@@ -1347,11 +1390,12 @@ function projectOrLiterals(a) {
|
|
|
1347
1390
|
}
|
|
1348
1391
|
return lits.length > 0 ? union(...lits) : void 0;
|
|
1349
1392
|
}
|
|
1350
|
-
function projectObj(
|
|
1351
|
-
if (index || open) return void 0;
|
|
1352
|
-
const fields =
|
|
1353
|
-
for (const [k, slot] of Object.entries(slots)) {
|
|
1354
|
-
|
|
1393
|
+
function projectObj(a, s, budget) {
|
|
1394
|
+
if (s.index || s.open) return void 0;
|
|
1395
|
+
const fields = /* @__PURE__ */ Object.create(null);
|
|
1396
|
+
for (const [k, slot] of Object.entries(s.slots)) {
|
|
1397
|
+
if (isHiddenInternalSlot(k, slot)) continue;
|
|
1398
|
+
const fc = absToConstraint(accessorReadDomain(a, k) ?? slot.value, budget);
|
|
1355
1399
|
if (!fc) return void 0;
|
|
1356
1400
|
fields[k] = { constraint: fc, ...slot.optional ? { optional: true } : {} };
|
|
1357
1401
|
}
|
|
@@ -1590,11 +1634,21 @@ function isDefaultExportName(source, fnName) {
|
|
|
1590
1634
|
);
|
|
1591
1635
|
return re.test(source);
|
|
1592
1636
|
}
|
|
1637
|
+
function formatEntryParamShape(t) {
|
|
1638
|
+
if (t.shape.k === "unknown") {
|
|
1639
|
+
const term = t.term;
|
|
1640
|
+
if (term?.op === "lit" && (term.value === null || term.value === void 0)) {
|
|
1641
|
+
return formatShape(t);
|
|
1642
|
+
}
|
|
1643
|
+
return "unknown";
|
|
1644
|
+
}
|
|
1645
|
+
return formatShape(t, { showLits: true });
|
|
1646
|
+
}
|
|
1593
1647
|
function formatEntrySigLine(name, g, throws) {
|
|
1594
1648
|
const names = g.formals && g.formals.length > 0 ? formalParamSignatureNames(g.formals) : g.params;
|
|
1595
1649
|
const ps = names.map((p, i) => {
|
|
1596
1650
|
const t = g.typeParams[i]?.value;
|
|
1597
|
-
const shown = !t ? "any" :
|
|
1651
|
+
const shown = !t ? "any" : formatEntryParamShape(t);
|
|
1598
1652
|
return `${p}: ${shown}`;
|
|
1599
1653
|
}).join(", ");
|
|
1600
1654
|
return `${name}(${ps}) => ${formatShape(g.symbolic)} throws ${throws}`;
|
|
@@ -1633,14 +1687,25 @@ function findFnLoc(file, name) {
|
|
|
1633
1687
|
}
|
|
1634
1688
|
return {};
|
|
1635
1689
|
}
|
|
1690
|
+
function formatSigDetail(absVal, name) {
|
|
1691
|
+
return formatAbsMultiline(absWithStableTermIds(absVal), name);
|
|
1692
|
+
}
|
|
1636
1693
|
function formatSigCached(absVal, name) {
|
|
1637
|
-
|
|
1694
|
+
let byName = sigFormatCache.get(absVal);
|
|
1695
|
+
if (!byName) {
|
|
1696
|
+
byName = /* @__PURE__ */ new Map();
|
|
1697
|
+
sigFormatCache.set(absVal, byName);
|
|
1698
|
+
}
|
|
1699
|
+
const hit = byName.get(name);
|
|
1638
1700
|
if (hit) return hit;
|
|
1639
1701
|
const out = {
|
|
1640
|
-
|
|
1641
|
-
|
|
1702
|
+
// **人类面口径**(`formatShape`):与 `formatCheckReport` 的签名行**逐字
|
|
1703
|
+
// 同源**(Bug 413:机器面此前用 `formatAbs` ⇒ 把 term / pred 段渲进
|
|
1704
|
+
// 对外载荷,其中混着引擎内部计数器 id `len#N` ⇒ 载荷随参数列表位置漂移)。
|
|
1705
|
+
display: formatShape(absVal),
|
|
1706
|
+
detail: formatSigDetail(absVal, name)
|
|
1642
1707
|
};
|
|
1643
|
-
|
|
1708
|
+
byName.set(name, out);
|
|
1644
1709
|
return out;
|
|
1645
1710
|
}
|
|
1646
1711
|
|
|
@@ -1667,12 +1732,6 @@ function loadModuleId(fn2) {
|
|
|
1667
1732
|
}
|
|
1668
1733
|
return id;
|
|
1669
1734
|
}
|
|
1670
|
-
function checkDepsFingerprint(source, opts) {
|
|
1671
|
-
if (!opts.loadModule || !opts.fromFile) {
|
|
1672
|
-
return { fp: "-", paths: [], contents: [], truncated: false, readError: false };
|
|
1673
|
-
}
|
|
1674
|
-
return loadModuleDepsFingerprint(source, opts.loadModule, opts.fromFile);
|
|
1675
|
-
}
|
|
1676
1735
|
function unindexCheckKey(key) {
|
|
1677
1736
|
const deps = checkKeyDeps.get(key);
|
|
1678
1737
|
if (!deps) return;
|
|
@@ -1741,13 +1800,23 @@ function checkMemoKey(filePath, source, identityOpts, deps, sidecarFp) {
|
|
|
1741
1800
|
runTranspiledOptionsMemoKey(identityOpts.inject),
|
|
1742
1801
|
// skips 必须进键:同 source 不同 skip 表(host 解析差异 / 测试注入)
|
|
1743
1802
|
// 不得回放另一档报告。
|
|
1744
|
-
skipsKey(identityOpts.skips)
|
|
1803
|
+
skipsKey(identityOpts.skips),
|
|
1804
|
+
// Bug 366:`from`(--from 的跨文件调用点证据)必须进键——同 source 不同
|
|
1805
|
+
// 证据不得互为陈旧命中:无证据那档会把有证据的 drift 吞掉,有证据那档
|
|
1806
|
+
// 会把 drift 回放进无证据的检查(假阳)。
|
|
1807
|
+
fromKey(identityOpts.from)
|
|
1745
1808
|
].join("|");
|
|
1746
1809
|
}
|
|
1747
1810
|
function skipsKey(skips) {
|
|
1748
1811
|
if (!skips || skips.size === 0) return "-";
|
|
1749
1812
|
return [...skips.entries()].map(([name, abs2]) => `${name}:${abs2 ? formatAbs(abs2) : "-"}`).sort().join(",");
|
|
1750
1813
|
}
|
|
1814
|
+
function fromKey(from) {
|
|
1815
|
+
if (!from || from.length === 0) return "-";
|
|
1816
|
+
return from.map(
|
|
1817
|
+
(r) => `${r.fnName}:${r.args.map((a) => formatAbs(a)).join(",")}:${formatAbs(r.result)}`
|
|
1818
|
+
).sort().join(";");
|
|
1819
|
+
}
|
|
1751
1820
|
function checkMemoGet(key) {
|
|
1752
1821
|
if (!checkReportMemo.has(key)) return null;
|
|
1753
1822
|
const v2 = checkReportMemo.get(key);
|
|
@@ -1785,8 +1854,17 @@ function collectEntryMayThrows(source, fnName, g, file, phi, opts = {}) {
|
|
|
1785
1854
|
return runWithMayThrowSession(() => {
|
|
1786
1855
|
setMayThrowCollector((e) => effects.push(e));
|
|
1787
1856
|
try {
|
|
1788
|
-
const
|
|
1789
|
-
if (
|
|
1857
|
+
const outcome = evalThrowsOutcome(source, fnName, entryArgs, opts, phi, file);
|
|
1858
|
+
if (outcome.kind === "unanalyzable") {
|
|
1859
|
+
if (!unsupportedFallsInside(file, fnName, outcome.reason, outcome.loc)) return effects;
|
|
1860
|
+
effects.push({
|
|
1861
|
+
kind: "Error",
|
|
1862
|
+
cause: `entry not analyzed (${outcome.reason}) \u21D2 may-throw undeterminable`
|
|
1863
|
+
});
|
|
1864
|
+
return effects;
|
|
1865
|
+
}
|
|
1866
|
+
if (outcome.kind !== "analyzed") return effects;
|
|
1867
|
+
const full = outcome.result;
|
|
1790
1868
|
if (full.throws && full.throws.shape.k !== "never") {
|
|
1791
1869
|
for (const tName of throwAbsToKinds(full.throws)) {
|
|
1792
1870
|
if (!effects.some((e) => e.kind === tName && e.cause.startsWith("throw"))) {
|
|
@@ -1828,7 +1906,13 @@ function invokeAsThrows(fn2, phi = pTrue) {
|
|
|
1828
1906
|
throw e;
|
|
1829
1907
|
}
|
|
1830
1908
|
}
|
|
1831
|
-
function
|
|
1909
|
+
function declaredThrowsForEntry(source, fnName) {
|
|
1910
|
+
const own = extractDeclaredThrows(source, fnName);
|
|
1911
|
+
if (own !== void 0) return own;
|
|
1912
|
+
if (fnName.includes(".")) return void 0;
|
|
1913
|
+
return extractDeclaredThrows(source, `${fnName}.constructor`);
|
|
1914
|
+
}
|
|
1915
|
+
function evalThrowsOutcome(source, fnName, args, opts = {}, phi = pTrue, file) {
|
|
1832
1916
|
const runKey = `${source}|${runTranspiledOptionsMemoKey(opts.inject)}|${runTranspiledOptionsMemoKey(opts.modules ? { modules: opts.modules } : void 0)}`;
|
|
1833
1917
|
if (evalRunMemo.size >= MAX_CHECK_MEMO) {
|
|
1834
1918
|
const oldest = evalRunMemo.keys().next().value;
|
|
@@ -1836,9 +1920,16 @@ function evalThrowsOf(source, fnName, args, opts = {}, phi = pTrue) {
|
|
|
1836
1920
|
}
|
|
1837
1921
|
let exports = evalRunMemo.get(runKey);
|
|
1838
1922
|
if (exports === void 0) {
|
|
1839
|
-
const
|
|
1840
|
-
if (run === void 0)
|
|
1841
|
-
|
|
1923
|
+
const attempt = tryRunTranspiledDetailed(source, evalAnalyzeOpts(opts));
|
|
1924
|
+
if (attempt.run === void 0) {
|
|
1925
|
+
const reason = attempt.fallback?.reason ?? "transpile failed";
|
|
1926
|
+
if (reason.startsWith("unsupported:")) {
|
|
1927
|
+
const loc = attempt.fallback?.loc;
|
|
1928
|
+
return loc ? { kind: "unanalyzable", reason, loc } : { kind: "unanalyzable", reason };
|
|
1929
|
+
}
|
|
1930
|
+
return { kind: "absent" };
|
|
1931
|
+
}
|
|
1932
|
+
exports = attempt.run;
|
|
1842
1933
|
evalRunMemo.set(runKey, exports);
|
|
1843
1934
|
}
|
|
1844
1935
|
const isAbsVal2 = (v2) => !!v2 && typeof v2 === "object" && "shape" in v2 && "conf" in v2;
|
|
@@ -1850,29 +1941,336 @@ function evalThrowsOf(source, fnName, args, opts = {}, phi = pTrue) {
|
|
|
1850
1941
|
}
|
|
1851
1942
|
};
|
|
1852
1943
|
try {
|
|
1853
|
-
if (
|
|
1944
|
+
if (!fnName.includes(".")) {
|
|
1945
|
+
const clsAbs = classAbsOf(exports, file, fnName);
|
|
1946
|
+
if (isAbsVal2(clsAbs) && isClassValueAbs(clsAbs)) {
|
|
1947
|
+
return {
|
|
1948
|
+
kind: "analyzed",
|
|
1949
|
+
result: classEntryThrows(clsAbs, file, fnName, phi)
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
if (Object.hasOwn(exports, fnName)) {
|
|
1954
|
+
const r = call(fnName, args);
|
|
1955
|
+
return r ? { kind: "analyzed", result: r } : { kind: "absent" };
|
|
1956
|
+
}
|
|
1854
1957
|
if (fnName.includes(".")) {
|
|
1855
1958
|
const [clsName, methodName] = fnName.split(".", 2);
|
|
1856
|
-
const clsAbs = exports
|
|
1959
|
+
const clsAbs = classAbsOf(exports, file, clsName ?? "");
|
|
1857
1960
|
if (isAbsVal2(clsAbs)) {
|
|
1858
|
-
|
|
1961
|
+
if (classMethodIsStatic(file, clsName ?? "", methodName ?? "") === false) {
|
|
1962
|
+
return {
|
|
1963
|
+
kind: "analyzed",
|
|
1964
|
+
result: invokeAsThrows(() => {
|
|
1965
|
+
const inst = instanceForMethodBridge(
|
|
1966
|
+
clsAbs,
|
|
1967
|
+
clsName ?? "",
|
|
1968
|
+
ctorAnyArgs(file, clsName ?? "")
|
|
1969
|
+
);
|
|
1970
|
+
return $invoke(inst, methodName ?? "", args);
|
|
1971
|
+
}, phi)
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
return {
|
|
1975
|
+
kind: "analyzed",
|
|
1976
|
+
result: invokeAsThrows(() => $staticInvoke(clsAbs, methodName ?? "", args), phi)
|
|
1977
|
+
};
|
|
1859
1978
|
}
|
|
1860
|
-
return
|
|
1979
|
+
return { kind: "absent" };
|
|
1861
1980
|
}
|
|
1862
1981
|
if ("default" in exports) {
|
|
1863
1982
|
const d = exports["default"];
|
|
1864
|
-
if (typeof d === "function")
|
|
1983
|
+
if (typeof d === "function") {
|
|
1984
|
+
const r = call("default", args);
|
|
1985
|
+
return r ? { kind: "analyzed", result: r } : { kind: "absent" };
|
|
1986
|
+
}
|
|
1865
1987
|
if (isAbsVal2(d)) {
|
|
1866
|
-
if (d.shape.k === "fn")
|
|
1988
|
+
if (d.shape.k === "fn") {
|
|
1989
|
+
const r = call("default", args);
|
|
1990
|
+
return r ? { kind: "analyzed", result: r } : { kind: "absent" };
|
|
1991
|
+
}
|
|
1867
1992
|
if (d.shape.k === "obj") {
|
|
1868
|
-
return
|
|
1993
|
+
return {
|
|
1994
|
+
kind: "analyzed",
|
|
1995
|
+
result: invokeAsThrows(() => $invoke(d, fnName, args), phi)
|
|
1996
|
+
};
|
|
1869
1997
|
}
|
|
1870
1998
|
}
|
|
1871
1999
|
}
|
|
2000
|
+
return { kind: "absent" };
|
|
2001
|
+
} catch {
|
|
2002
|
+
return { kind: "absent" };
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
function classAbsOf(exports, file, clsName) {
|
|
2006
|
+
const slot = file ? resolveExportSlotCall(file, clsName, exports) : void 0;
|
|
2007
|
+
if (slot && slot.kind !== "cjs-member") return exports[slot.key];
|
|
2008
|
+
return exports[clsName];
|
|
2009
|
+
}
|
|
2010
|
+
function isClassValueAbs(v2) {
|
|
2011
|
+
return v2.shape.k === "brand" && v2.shape.ctor === true;
|
|
2012
|
+
}
|
|
2013
|
+
function classEntryThrows(clsAbs, file, clsName, phi) {
|
|
2014
|
+
return invokeAsThrows(
|
|
2015
|
+
() => runWithMayThrowSession(() => $new(clsAbs, ctorAnyArgs(file, clsName))),
|
|
2016
|
+
phi
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
function classMethodIsStatic(file, clsName, methodName) {
|
|
2020
|
+
if (!file) return void 0;
|
|
2021
|
+
for (const stmt of file.program.body) {
|
|
2022
|
+
const s = stmt;
|
|
2023
|
+
let decl = stmt;
|
|
2024
|
+
if (s.type === "ExportNamedDeclaration" || s.type === "ExportDefaultDeclaration") {
|
|
2025
|
+
decl = s.declaration;
|
|
2026
|
+
}
|
|
2027
|
+
const c = decl;
|
|
2028
|
+
if (!c || c.type !== "ClassDeclaration") continue;
|
|
2029
|
+
if (c.id?.name !== clsName) continue;
|
|
2030
|
+
for (const m of c.body?.body ?? []) {
|
|
2031
|
+
const mem = m;
|
|
2032
|
+
if (mem.kind === "constructor") continue;
|
|
2033
|
+
if (classMemberName(mem) !== methodName) continue;
|
|
2034
|
+
return mem.static === true;
|
|
2035
|
+
}
|
|
1872
2036
|
return void 0;
|
|
2037
|
+
}
|
|
2038
|
+
return void 0;
|
|
2039
|
+
}
|
|
2040
|
+
function ctorAnyArgs(file, clsName) {
|
|
2041
|
+
const n = file ? ctorParamCountOf(file, clsName) : 0;
|
|
2042
|
+
return Array.from({ length: n }, () => abs({ k: "any" }, void 0, pTrue, "path"));
|
|
2043
|
+
}
|
|
2044
|
+
function instanceForMethodBridge(clsAbs, clsName, ctorArgs) {
|
|
2045
|
+
try {
|
|
2046
|
+
return runWithMayThrowSession(() => $new(clsAbs, ctorArgs));
|
|
1873
2047
|
} catch {
|
|
2048
|
+
return bareInstanceOf(clsAbs, clsName);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
function bareInstanceOf(clsAbs, clsName) {
|
|
2052
|
+
const name = clsAbs.shape.k === "brand" && clsAbs.shape.name.length > 0 ? clsAbs.shape.name : clsName;
|
|
2053
|
+
return abs(
|
|
2054
|
+
{
|
|
2055
|
+
k: "brand",
|
|
2056
|
+
name,
|
|
2057
|
+
shape: abs({ k: "unknown" }, void 0, void 0, "partial")
|
|
2058
|
+
},
|
|
2059
|
+
void 0,
|
|
2060
|
+
void 0,
|
|
2061
|
+
"path"
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
2064
|
+
function classMemberName(m) {
|
|
2065
|
+
const key = m.key;
|
|
2066
|
+
if (!key) return void 0;
|
|
2067
|
+
return key.name ?? (typeof key.value === "string" ? key.value : void 0);
|
|
2068
|
+
}
|
|
2069
|
+
function isExportsTarget(node) {
|
|
2070
|
+
const n = node;
|
|
2071
|
+
if (!n) return false;
|
|
2072
|
+
if (n.type === "Identifier") return n.name === "exports";
|
|
2073
|
+
if (n.type !== "MemberExpression") return false;
|
|
2074
|
+
const obj2 = n.object;
|
|
2075
|
+
const prop = n.property;
|
|
2076
|
+
return obj2?.type === "Identifier" && obj2.name === "module" && prop?.name === "exports";
|
|
2077
|
+
}
|
|
2078
|
+
function cjsExportName(left) {
|
|
2079
|
+
const l = left;
|
|
2080
|
+
if (!l || l.type !== "MemberExpression" || l.computed === true) return void 0;
|
|
2081
|
+
if (!isExportsTarget(l.object)) return void 0;
|
|
2082
|
+
const prop = l.property;
|
|
2083
|
+
return prop?.name;
|
|
2084
|
+
}
|
|
2085
|
+
function keyNameOf(node) {
|
|
2086
|
+
const n = node;
|
|
2087
|
+
if (!n) return void 0;
|
|
2088
|
+
if (n.type === "Identifier" && typeof n.name === "string") return n.name;
|
|
2089
|
+
if (n.type === "StringLiteral" && typeof n.value === "string") return n.value;
|
|
2090
|
+
return void 0;
|
|
2091
|
+
}
|
|
2092
|
+
function entryValueNode(node) {
|
|
2093
|
+
const n = node;
|
|
2094
|
+
if (!n) return void 0;
|
|
2095
|
+
return n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression" || n.type === "ClassDeclaration" || n.type === "ClassExpression" ? node : void 0;
|
|
2096
|
+
}
|
|
2097
|
+
function classMethodEntryNodes(cls, methodName) {
|
|
2098
|
+
const members = cls.body?.body ?? [];
|
|
2099
|
+
let method;
|
|
2100
|
+
for (const m of members) {
|
|
2101
|
+
if (m.kind === "constructor") continue;
|
|
2102
|
+
if (classMemberName(m) !== methodName) continue;
|
|
2103
|
+
method = m;
|
|
2104
|
+
break;
|
|
2105
|
+
}
|
|
2106
|
+
if (!method) return void 0;
|
|
2107
|
+
const isStatic = method.static === true;
|
|
2108
|
+
const nodes = [method];
|
|
2109
|
+
for (const m of members) {
|
|
2110
|
+
if (m === method) continue;
|
|
2111
|
+
if (m.kind === "constructor") {
|
|
2112
|
+
if (!isStatic) nodes.push(m);
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
if (m.type === "StaticBlock") {
|
|
2116
|
+
if (isStatic) nodes.push(m);
|
|
2117
|
+
continue;
|
|
2118
|
+
}
|
|
2119
|
+
if (m.type === "ClassProperty" || m.type === "ClassPrivateProperty") {
|
|
2120
|
+
if (m.static === true !== isStatic) continue;
|
|
2121
|
+
nodes.push(m.value ?? m);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
return nodes;
|
|
2125
|
+
}
|
|
2126
|
+
function cjsEntryNodes(file, expression, fnName, depth) {
|
|
2127
|
+
const e = expression;
|
|
2128
|
+
if (!e || e.type !== "AssignmentExpression" || e.operator !== "=") return void 0;
|
|
2129
|
+
const right = e.right;
|
|
2130
|
+
if (!right) return void 0;
|
|
2131
|
+
const fromValue = (node) => {
|
|
2132
|
+
const value = entryValueNode(node);
|
|
2133
|
+
if (value) return [value];
|
|
2134
|
+
const ident = node;
|
|
2135
|
+
if (ident?.type === "Identifier" && ident.name) {
|
|
2136
|
+
return entryNodesOf(file, ident.name, depth + 1);
|
|
2137
|
+
}
|
|
1874
2138
|
return void 0;
|
|
2139
|
+
};
|
|
2140
|
+
if (cjsExportName(e.left) === fnName) return fromValue(right);
|
|
2141
|
+
if (!isExportsTarget(e.left)) return void 0;
|
|
2142
|
+
if (right.type === "ObjectExpression") {
|
|
2143
|
+
for (const p of right.properties ?? []) {
|
|
2144
|
+
const prop = p;
|
|
2145
|
+
if (keyNameOf(prop.key) !== fnName) continue;
|
|
2146
|
+
if (prop.type === "ObjectMethod") return [prop];
|
|
2147
|
+
return fromValue(prop.value);
|
|
2148
|
+
}
|
|
2149
|
+
return void 0;
|
|
2150
|
+
}
|
|
2151
|
+
if (right.type === "Identifier" && right.name === fnName) {
|
|
2152
|
+
return entryNodesOf(file, fnName, depth + 1);
|
|
1875
2153
|
}
|
|
2154
|
+
return void 0;
|
|
2155
|
+
}
|
|
2156
|
+
function entryNodesOf(file, fnName, depth = 0) {
|
|
2157
|
+
if (!file || depth > 3) return void 0;
|
|
2158
|
+
for (const stmt of file.program.body) {
|
|
2159
|
+
const s = stmt;
|
|
2160
|
+
if (s.type === "ExpressionStatement") {
|
|
2161
|
+
const hit = cjsEntryNodes(file, s.expression, fnName, depth);
|
|
2162
|
+
if (hit) return hit;
|
|
2163
|
+
continue;
|
|
2164
|
+
}
|
|
2165
|
+
let decl = stmt;
|
|
2166
|
+
if (s.type === "ExportNamedDeclaration" || s.type === "ExportDefaultDeclaration") {
|
|
2167
|
+
decl = s.declaration;
|
|
2168
|
+
}
|
|
2169
|
+
const d = decl;
|
|
2170
|
+
if (!d || typeof d !== "object") continue;
|
|
2171
|
+
if (fnName.includes(".")) {
|
|
2172
|
+
const [clsName, methodName] = fnName.split(".", 2);
|
|
2173
|
+
if (d.type !== "ClassDeclaration" || d.id?.name !== clsName) continue;
|
|
2174
|
+
return classMethodEntryNodes(d, methodName ?? "");
|
|
2175
|
+
}
|
|
2176
|
+
if (d.type === "FunctionDeclaration" && d.id?.name === fnName) return [d];
|
|
2177
|
+
if (fnName === "default" && s.type === "ExportDefaultDeclaration") {
|
|
2178
|
+
const t = d.type;
|
|
2179
|
+
if (t === "FunctionDeclaration" || t === "ArrowFunctionExpression" || t === "FunctionExpression" || t === "ClassDeclaration") {
|
|
2180
|
+
return [d];
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
if (d.type === "ClassDeclaration" && d.id?.name === fnName) return [d];
|
|
2184
|
+
if (d.type === "VariableDeclaration") {
|
|
2185
|
+
for (const dec of d.declarations ?? []) {
|
|
2186
|
+
const id = dec.id;
|
|
2187
|
+
if (id?.name === fnName) return [dec];
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
return void 0;
|
|
2192
|
+
}
|
|
2193
|
+
function locInSpan(loc, span) {
|
|
2194
|
+
if (!loc || !span) return false;
|
|
2195
|
+
if (loc.line < span.startLine || loc.line > span.endLine) return false;
|
|
2196
|
+
if (loc.line === span.startLine && loc.column < span.startColumn) return false;
|
|
2197
|
+
if (loc.line === span.endLine && loc.column > span.endColumn) return false;
|
|
2198
|
+
return true;
|
|
2199
|
+
}
|
|
2200
|
+
function isModuleInitLoc(file, loc) {
|
|
2201
|
+
if (!loc) return false;
|
|
2202
|
+
if (!locInSpan(loc, nodeSpan(file.program))) return false;
|
|
2203
|
+
for (const span of deferredExecutionSpans(file)) {
|
|
2204
|
+
if (locInSpan(loc, span)) return false;
|
|
2205
|
+
}
|
|
2206
|
+
return true;
|
|
2207
|
+
}
|
|
2208
|
+
function definiteModuleThrowOf(file) {
|
|
2209
|
+
return file ? scanDefiniteThrowSeq(file.program.body) : void 0;
|
|
2210
|
+
}
|
|
2211
|
+
function scanDefiniteThrowSeq(stmts) {
|
|
2212
|
+
for (const stmt of stmts) {
|
|
2213
|
+
const s = stmt;
|
|
2214
|
+
switch (s.type) {
|
|
2215
|
+
case "ThrowStatement": {
|
|
2216
|
+
const at = s.loc?.start;
|
|
2217
|
+
if (at?.line === void 0) return void 0;
|
|
2218
|
+
const kind = thrownKindOf(s.argument);
|
|
2219
|
+
return {
|
|
2220
|
+
line: at.line,
|
|
2221
|
+
column: at.column ?? 0,
|
|
2222
|
+
...kind !== void 0 ? { kind } : {}
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
// 裸块 = 无条件的语句序列 ⇒ 继续下钻
|
|
2226
|
+
case "BlockStatement": {
|
|
2227
|
+
const hit = scanDefiniteThrowSeq(s.body ?? []);
|
|
2228
|
+
if (hit) return hit;
|
|
2229
|
+
continue;
|
|
2230
|
+
}
|
|
2231
|
+
case "ReturnStatement":
|
|
2232
|
+
return void 0;
|
|
2233
|
+
case "IfStatement":
|
|
2234
|
+
case "SwitchStatement":
|
|
2235
|
+
case "TryStatement":
|
|
2236
|
+
case "ForStatement":
|
|
2237
|
+
case "ForInStatement":
|
|
2238
|
+
case "ForOfStatement":
|
|
2239
|
+
case "WhileStatement":
|
|
2240
|
+
case "DoWhileStatement":
|
|
2241
|
+
case "LabeledStatement":
|
|
2242
|
+
case "WithStatement":
|
|
2243
|
+
return void 0;
|
|
2244
|
+
default:
|
|
2245
|
+
continue;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
return void 0;
|
|
2249
|
+
}
|
|
2250
|
+
function thrownKindOf(arg) {
|
|
2251
|
+
const a = arg;
|
|
2252
|
+
const callee = a?.callee;
|
|
2253
|
+
if (callee?.type !== "Identifier" || typeof callee.name !== "string") return void 0;
|
|
2254
|
+
if (a?.type === "NewExpression") return callee.name;
|
|
2255
|
+
return a?.type === "CallExpression" && callee.name.endsWith("Error") ? callee.name : void 0;
|
|
2256
|
+
}
|
|
2257
|
+
var TS_ONLY_STATEMENT_REASON_PREFIX = "unsupported:statement:TS";
|
|
2258
|
+
function unsupportedFallsInside(file, fnName, reason, loc) {
|
|
2259
|
+
if (!file || !loc) return false;
|
|
2260
|
+
if (!reason.startsWith(TS_ONLY_STATEMENT_REASON_PREFIX) && isModuleInitLoc(file, loc)) {
|
|
2261
|
+
return true;
|
|
2262
|
+
}
|
|
2263
|
+
const entryNodes = entryNodesOf(file, fnName);
|
|
2264
|
+
if (!entryNodes || entryNodes.length === 0) return false;
|
|
2265
|
+
for (const node of entryNodes) {
|
|
2266
|
+
if (locInSpan(loc, nodeSpan(node))) return true;
|
|
2267
|
+
}
|
|
2268
|
+
for (const node of entryNodes) {
|
|
2269
|
+
for (const reach of reachableTopBindings(file, node, { includeClasses: true })) {
|
|
2270
|
+
if (locInSpan(loc, nodeSpan(reach.node))) return true;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
return false;
|
|
1876
2274
|
}
|
|
1877
2275
|
|
|
1878
2276
|
// src/algebra/check-assign.ts
|
|
@@ -1884,6 +2282,8 @@ function widenForAssign(a) {
|
|
|
1884
2282
|
const el = els.reduce((x, y) => joinAbs(x, y));
|
|
1885
2283
|
return abs({ k: "arr", element: el }, void 0, void 0, a.conf);
|
|
1886
2284
|
}
|
|
2285
|
+
const prim = samePrimOfSum(s.members);
|
|
2286
|
+
if (prim) return abs({ k: "prim", type: prim }, void 0, void 0, a.conf);
|
|
1887
2287
|
return a;
|
|
1888
2288
|
}
|
|
1889
2289
|
if (s.k === "tuple") {
|
|
@@ -1935,14 +2335,40 @@ function widenForAssign(a) {
|
|
|
1935
2335
|
}
|
|
1936
2336
|
return a;
|
|
1937
2337
|
}
|
|
2338
|
+
function samePrimOfSum(members) {
|
|
2339
|
+
let out;
|
|
2340
|
+
for (const m of members) {
|
|
2341
|
+
const t = primNameOf(m);
|
|
2342
|
+
if (!t) return void 0;
|
|
2343
|
+
if (out === void 0) out = t;
|
|
2344
|
+
else if (out !== t) return void 0;
|
|
2345
|
+
}
|
|
2346
|
+
return out;
|
|
2347
|
+
}
|
|
2348
|
+
function primNameOf(a) {
|
|
2349
|
+
if (a.shape.k === "prim") return a.shape.type;
|
|
2350
|
+
const lv = a.term?.op === "lit" ? a.term.value : void 0;
|
|
2351
|
+
if (typeof lv === "string") return "string";
|
|
2352
|
+
if (typeof lv === "number") return "number";
|
|
2353
|
+
if (typeof lv === "boolean") return "boolean";
|
|
2354
|
+
if (typeof lv === "bigint") return "bigint";
|
|
2355
|
+
return void 0;
|
|
2356
|
+
}
|
|
2357
|
+
function isAbsLike(v2) {
|
|
2358
|
+
return !!v2 && typeof v2 === "object" && "shape" in v2 && "conf" in v2;
|
|
2359
|
+
}
|
|
2360
|
+
function shapeOnly(a) {
|
|
2361
|
+
return abs(a.shape, void 0, void 0, a.conf);
|
|
2362
|
+
}
|
|
1938
2363
|
function structuralAssignIssues(records) {
|
|
1939
2364
|
const out = [];
|
|
1940
2365
|
for (const r of records) {
|
|
1941
2366
|
if (!r.prev) continue;
|
|
2367
|
+
if (!isAbsLike(r.prev) || !isAbsLike(r.next)) continue;
|
|
1942
2368
|
if (r.conditional) continue;
|
|
1943
2369
|
if (r.next.shape.k === "unknown" && !r.next.term) continue;
|
|
1944
2370
|
if (r.prev.shape.k === "unknown" && !r.prev.term) continue;
|
|
1945
|
-
const leq = leqAbs(widenForAssign(r.next), widenForAssign(r.prev));
|
|
2371
|
+
const leq = leqAbs(widenForAssign(r.next), shapeOnly(widenForAssign(r.prev)));
|
|
1946
2372
|
if (!leq.ok) {
|
|
1947
2373
|
out.push({
|
|
1948
2374
|
severity: "error",
|
|
@@ -1961,6 +2387,339 @@ function structuralAssignIssues(records) {
|
|
|
1961
2387
|
}
|
|
1962
2388
|
|
|
1963
2389
|
// src/algebra/check-case-scan.ts
|
|
2390
|
+
function parseNumericLiteral(text) {
|
|
2391
|
+
const s = text.trim();
|
|
2392
|
+
if (s === "") return void 0;
|
|
2393
|
+
let sign = 1;
|
|
2394
|
+
let body = s;
|
|
2395
|
+
if (body.startsWith("-")) {
|
|
2396
|
+
sign = -1;
|
|
2397
|
+
body = body.slice(1).trimStart();
|
|
2398
|
+
} else if (body.startsWith("+")) {
|
|
2399
|
+
body = body.slice(1).trimStart();
|
|
2400
|
+
}
|
|
2401
|
+
if (body === "") return void 0;
|
|
2402
|
+
if (body.endsWith("n")) {
|
|
2403
|
+
const d = body.slice(0, -1);
|
|
2404
|
+
if (d.includes("_")) return void 0;
|
|
2405
|
+
if (/^0[xX][0-9a-fA-F]+$/.test(d)) return BigInt(d) * BigInt(sign);
|
|
2406
|
+
if (/^0[bB][01]+$/.test(d)) return BigInt(d) * BigInt(sign);
|
|
2407
|
+
if (/^0[oO][0-7]+$/.test(d)) return BigInt(d) * BigInt(sign);
|
|
2408
|
+
if (/^[0-9]+$/.test(d)) return BigInt(d) * BigInt(sign);
|
|
2409
|
+
return void 0;
|
|
2410
|
+
}
|
|
2411
|
+
if (body.includes("__")) return void 0;
|
|
2412
|
+
const clean = body.replace(/_/g, "");
|
|
2413
|
+
if (clean === "") return void 0;
|
|
2414
|
+
if (/^0[xX][0-9a-fA-F]+$/.test(clean)) return sign * Number(BigInt(clean));
|
|
2415
|
+
if (/^0[bB][01]+$/.test(clean)) return sign * Number(BigInt(clean));
|
|
2416
|
+
if (/^0[oO][0-7]+$/.test(clean)) return sign * Number(BigInt(clean));
|
|
2417
|
+
if (clean === "Infinity") return sign * Infinity;
|
|
2418
|
+
if (clean === "NaN") return NaN;
|
|
2419
|
+
if (/^0[0-7]+$/.test(clean) && !body.includes("_")) return sign * parseInt(clean.slice(1), 8);
|
|
2420
|
+
if (/^(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/.test(clean)) return sign * Number(clean);
|
|
2421
|
+
return void 0;
|
|
2422
|
+
}
|
|
2423
|
+
function parseStringLiteralValue(text) {
|
|
2424
|
+
const t = text.trim();
|
|
2425
|
+
if (t.length < 2) return void 0;
|
|
2426
|
+
const q = t[0];
|
|
2427
|
+
if (q !== '"' && q !== "'") return void 0;
|
|
2428
|
+
if (t[t.length - 1] !== q) return void 0;
|
|
2429
|
+
const inner = t.slice(1, -1);
|
|
2430
|
+
for (let i = 0; i < inner.length; i++) {
|
|
2431
|
+
const ch = inner[i];
|
|
2432
|
+
if (ch === "\\") {
|
|
2433
|
+
i++;
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
if (ch === q) return void 0;
|
|
2437
|
+
}
|
|
2438
|
+
let out = "";
|
|
2439
|
+
for (let i = 0; i < inner.length; i++) {
|
|
2440
|
+
const ch = inner[i];
|
|
2441
|
+
if (ch !== "\\") {
|
|
2442
|
+
out += ch;
|
|
2443
|
+
continue;
|
|
2444
|
+
}
|
|
2445
|
+
const n = inner[i + 1];
|
|
2446
|
+
if (n === void 0) return void 0;
|
|
2447
|
+
i++;
|
|
2448
|
+
switch (n) {
|
|
2449
|
+
case "n":
|
|
2450
|
+
out += "\n";
|
|
2451
|
+
break;
|
|
2452
|
+
case "t":
|
|
2453
|
+
out += " ";
|
|
2454
|
+
break;
|
|
2455
|
+
case "r":
|
|
2456
|
+
out += "\r";
|
|
2457
|
+
break;
|
|
2458
|
+
case "b":
|
|
2459
|
+
out += "\b";
|
|
2460
|
+
break;
|
|
2461
|
+
case "f":
|
|
2462
|
+
out += "\f";
|
|
2463
|
+
break;
|
|
2464
|
+
case "v":
|
|
2465
|
+
out += "\v";
|
|
2466
|
+
break;
|
|
2467
|
+
case "0":
|
|
2468
|
+
out += "\0";
|
|
2469
|
+
break;
|
|
2470
|
+
case "x": {
|
|
2471
|
+
const hex = inner.slice(i + 1, i + 3);
|
|
2472
|
+
if (!/^[0-9a-fA-F]{2}$/.test(hex)) return void 0;
|
|
2473
|
+
out += String.fromCharCode(parseInt(hex, 16));
|
|
2474
|
+
i += 2;
|
|
2475
|
+
break;
|
|
2476
|
+
}
|
|
2477
|
+
case "u": {
|
|
2478
|
+
if (inner[i + 1] === "{") {
|
|
2479
|
+
const end = inner.indexOf("}", i + 1);
|
|
2480
|
+
if (end < 0) return void 0;
|
|
2481
|
+
const hex = inner.slice(i + 2, end);
|
|
2482
|
+
if (!/^[0-9a-fA-F]{1,6}$/.test(hex)) return void 0;
|
|
2483
|
+
out += String.fromCodePoint(parseInt(hex, 16));
|
|
2484
|
+
i = end;
|
|
2485
|
+
} else {
|
|
2486
|
+
const hex = inner.slice(i + 1, i + 5);
|
|
2487
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return void 0;
|
|
2488
|
+
out += String.fromCharCode(parseInt(hex, 16));
|
|
2489
|
+
i += 4;
|
|
2490
|
+
}
|
|
2491
|
+
break;
|
|
2492
|
+
}
|
|
2493
|
+
case "\n":
|
|
2494
|
+
break;
|
|
2495
|
+
// 行连接
|
|
2496
|
+
case "\r":
|
|
2497
|
+
if (inner[i + 1] === "\n") i++;
|
|
2498
|
+
break;
|
|
2499
|
+
default:
|
|
2500
|
+
out += n;
|
|
2501
|
+
break;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
return out;
|
|
2505
|
+
}
|
|
2506
|
+
function parseScalarArgAbs(s) {
|
|
2507
|
+
const t = s.trim();
|
|
2508
|
+
if (t === "") return void 0;
|
|
2509
|
+
if (t === "true") return { shape: { k: "prim", type: "boolean" }, term: { op: "lit", value: true }, conf: "exact" };
|
|
2510
|
+
if (t === "false") return { shape: { k: "prim", type: "boolean" }, term: { op: "lit", value: false }, conf: "exact" };
|
|
2511
|
+
if (t === "null") return { shape: { k: "unknown" }, term: { op: "lit", value: null }, conf: "exact" };
|
|
2512
|
+
if (t === "undefined") return { shape: { k: "unknown" }, term: { op: "lit", value: void 0 }, conf: "exact" };
|
|
2513
|
+
const num2 = parseNumericLiteral(t);
|
|
2514
|
+
if (num2 !== void 0) {
|
|
2515
|
+
return typeof num2 === "bigint" ? bigintLit(num2) : numLit(num2);
|
|
2516
|
+
}
|
|
2517
|
+
const str2 = parseStringLiteralValue(t);
|
|
2518
|
+
if (str2 !== void 0) {
|
|
2519
|
+
return {
|
|
2520
|
+
shape: { k: "prim", type: "string" },
|
|
2521
|
+
term: { op: "lit", value: str2 },
|
|
2522
|
+
conf: "exact"
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
return void 0;
|
|
2526
|
+
}
|
|
2527
|
+
function splitCaseArgsText(text) {
|
|
2528
|
+
const result = [];
|
|
2529
|
+
let current = "";
|
|
2530
|
+
let depth = 0;
|
|
2531
|
+
let inString = null;
|
|
2532
|
+
for (let i = 0; i < text.length; i++) {
|
|
2533
|
+
const ch = text[i];
|
|
2534
|
+
if (inString) {
|
|
2535
|
+
current += ch;
|
|
2536
|
+
if (ch === inString) inString = null;
|
|
2537
|
+
continue;
|
|
2538
|
+
}
|
|
2539
|
+
if (ch === '"' || ch === "'") {
|
|
2540
|
+
inString = ch;
|
|
2541
|
+
current += ch;
|
|
2542
|
+
continue;
|
|
2543
|
+
}
|
|
2544
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
2545
|
+
if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
2546
|
+
if (ch === "," && depth === 0) {
|
|
2547
|
+
result.push(current.trim());
|
|
2548
|
+
current = "";
|
|
2549
|
+
continue;
|
|
2550
|
+
}
|
|
2551
|
+
current += ch;
|
|
2552
|
+
}
|
|
2553
|
+
if (current.trim()) result.push(current.trim());
|
|
2554
|
+
return result;
|
|
2555
|
+
}
|
|
2556
|
+
function stripCaseContinuation(text) {
|
|
2557
|
+
return text.split("\n").map((line) => line.replace(/^\s*\*\s?/, "")).join("\n");
|
|
2558
|
+
}
|
|
2559
|
+
function findTopLevelColon(text) {
|
|
2560
|
+
let found = -1;
|
|
2561
|
+
let depth = 0;
|
|
2562
|
+
let inString = null;
|
|
2563
|
+
for (let i = 0; i < text.length && found < 0; i++) {
|
|
2564
|
+
const ch = text[i];
|
|
2565
|
+
if (inString) {
|
|
2566
|
+
if (ch === inString) inString = null;
|
|
2567
|
+
continue;
|
|
2568
|
+
}
|
|
2569
|
+
if (ch === '"' || ch === "'") {
|
|
2570
|
+
inString = ch;
|
|
2571
|
+
continue;
|
|
2572
|
+
}
|
|
2573
|
+
if (ch === "(" || ch === "[" || ch === "{") {
|
|
2574
|
+
depth++;
|
|
2575
|
+
continue;
|
|
2576
|
+
}
|
|
2577
|
+
if (ch === ")" || ch === "]" || ch === "}") {
|
|
2578
|
+
depth--;
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
if (ch === ":" && depth === 0) found = i;
|
|
2582
|
+
}
|
|
2583
|
+
return found;
|
|
2584
|
+
}
|
|
2585
|
+
function parseStructureArgAbs(text) {
|
|
2586
|
+
const t = text.trim();
|
|
2587
|
+
if (t.startsWith("{") && t.endsWith("}")) {
|
|
2588
|
+
const body = t.slice(1, -1).trim();
|
|
2589
|
+
const slots = {};
|
|
2590
|
+
if (body !== "") {
|
|
2591
|
+
for (const entry of splitCaseArgsText(body)) {
|
|
2592
|
+
if (entry === "") continue;
|
|
2593
|
+
const colon = findTopLevelColon(entry);
|
|
2594
|
+
if (colon < 0) return void 0;
|
|
2595
|
+
const keyText = entry.slice(0, colon).trim();
|
|
2596
|
+
if (keyText === "" || keyText.includes("[")) return void 0;
|
|
2597
|
+
const key = keyText.replace(/^["']|["']$/g, "");
|
|
2598
|
+
if (key === "") return void 0;
|
|
2599
|
+
const value = parseCaseArgAbs(entry.slice(colon + 1));
|
|
2600
|
+
if (!value) return void 0;
|
|
2601
|
+
slots[key] = { value };
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
return abs({ k: "obj", slots }, void 0, void 0, "exact");
|
|
2605
|
+
}
|
|
2606
|
+
if (t.startsWith("[") && t.endsWith("]")) {
|
|
2607
|
+
const body = t.slice(1, -1).trim();
|
|
2608
|
+
const elements = [];
|
|
2609
|
+
if (body !== "") {
|
|
2610
|
+
for (const el of splitCaseArgsText(body)) {
|
|
2611
|
+
if (el === "") continue;
|
|
2612
|
+
const value = parseCaseArgAbs(el);
|
|
2613
|
+
if (!value) return void 0;
|
|
2614
|
+
elements.push(value);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
return abs({ k: "tuple", elements }, void 0, void 0, "exact");
|
|
2618
|
+
}
|
|
2619
|
+
return void 0;
|
|
2620
|
+
}
|
|
2621
|
+
function parseCaseArgAbs(text) {
|
|
2622
|
+
return parseScalarArgAbs(text) ?? parseStructureArgAbs(text);
|
|
2623
|
+
}
|
|
2624
|
+
function isConstraintBuilderArg(text) {
|
|
2625
|
+
return CONSTRAINT_EXPR_RE.test(text.trim());
|
|
2626
|
+
}
|
|
2627
|
+
function derefDeepConstraint(c) {
|
|
2628
|
+
let cur = c;
|
|
2629
|
+
for (let i = 0; i < LAZY_TEMPLATE_DEPTH && cur.lazy; i++) {
|
|
2630
|
+
cur = derefConstraint(cur);
|
|
2631
|
+
}
|
|
2632
|
+
return cur;
|
|
2633
|
+
}
|
|
2634
|
+
function witnessNoInfo(w) {
|
|
2635
|
+
const k = w.shape.k;
|
|
2636
|
+
return k === "never" || (k === "unknown" || k === "any") && !w.term;
|
|
2637
|
+
}
|
|
2638
|
+
function witnessIsStructure(w) {
|
|
2639
|
+
const k = w.shape.k;
|
|
2640
|
+
return k === "obj" || k === "arr" || k === "tuple";
|
|
2641
|
+
}
|
|
2642
|
+
function witnessPrimType(w) {
|
|
2643
|
+
const shape2 = w.shape.k === "brand" ? w.shape.shape.shape : w.shape;
|
|
2644
|
+
if (shape2.k === "prim") return shape2.type;
|
|
2645
|
+
return void 0;
|
|
2646
|
+
}
|
|
2647
|
+
function constraintIsAnyLike(c) {
|
|
2648
|
+
return c.prim === void 0 && c.preds.length === 0 && (c.members?.length ?? 0) === 0 && !c.fields && !c.element && !c.fn;
|
|
2649
|
+
}
|
|
2650
|
+
function witnessViolations(c, w, path, depth) {
|
|
2651
|
+
if (depth > 6) return [];
|
|
2652
|
+
const cc = derefDeepConstraint(c);
|
|
2653
|
+
if (cc.members && cc.members.length > 0) {
|
|
2654
|
+
let first = [];
|
|
2655
|
+
for (const m of cc.members) {
|
|
2656
|
+
const vs = witnessViolations(m, w, path, depth + 1);
|
|
2657
|
+
if (vs.length === 0) return [];
|
|
2658
|
+
if (first.length === 0) first = vs;
|
|
2659
|
+
}
|
|
2660
|
+
return first;
|
|
2661
|
+
}
|
|
2662
|
+
if (cc.fields) {
|
|
2663
|
+
const shape2 = w.shape.k === "brand" ? w.shape.shape.shape : w.shape;
|
|
2664
|
+
if (shape2.k !== "obj") {
|
|
2665
|
+
if (witnessNoInfo(w)) return [];
|
|
2666
|
+
return [{ actual: w, expected: formatConstraint(cc), path, missing: false }];
|
|
2667
|
+
}
|
|
2668
|
+
const out = [];
|
|
2669
|
+
for (const [key, field] of Object.entries(cc.fields)) {
|
|
2670
|
+
const slot = getSlot(shape2.slots, key);
|
|
2671
|
+
const fieldPath = path === "" ? key : `${path}.${key}`;
|
|
2672
|
+
if (!slot) {
|
|
2673
|
+
if (!field.optional && !field.constraint.isOptional) {
|
|
2674
|
+
out.push({
|
|
2675
|
+
actual: w,
|
|
2676
|
+
expected: `missing field ${fieldPath}`,
|
|
2677
|
+
path: fieldPath,
|
|
2678
|
+
missing: true
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
continue;
|
|
2682
|
+
}
|
|
2683
|
+
out.push(...witnessViolations(field.constraint, slot.value, fieldPath, depth + 1));
|
|
2684
|
+
}
|
|
2685
|
+
return out;
|
|
2686
|
+
}
|
|
2687
|
+
if (cc.element) {
|
|
2688
|
+
const shape2 = w.shape.k === "brand" ? w.shape.shape.shape : w.shape;
|
|
2689
|
+
const elements = shape2.k === "tuple" ? shape2.elements : shape2.k === "arr" ? [shape2.element] : void 0;
|
|
2690
|
+
if (!elements) {
|
|
2691
|
+
if (witnessNoInfo(w)) return [];
|
|
2692
|
+
return [{ actual: w, expected: formatConstraint(cc), path, missing: false }];
|
|
2693
|
+
}
|
|
2694
|
+
const out = [];
|
|
2695
|
+
elements.forEach((el, i) => {
|
|
2696
|
+
out.push(...witnessViolations(cc.element, el, `${path}[${i}]`, depth + 1));
|
|
2697
|
+
});
|
|
2698
|
+
return out;
|
|
2699
|
+
}
|
|
2700
|
+
if (cc.fn) return [];
|
|
2701
|
+
if (witnessIsStructure(w)) {
|
|
2702
|
+
if (constraintIsAnyLike(cc)) return [];
|
|
2703
|
+
return [{ actual: w, expected: formatConstraint(cc), path, missing: false }];
|
|
2704
|
+
}
|
|
2705
|
+
const argLit = w.term?.op === "lit" ? w.term.value : void 0;
|
|
2706
|
+
if (w.term?.op !== "lit") return [];
|
|
2707
|
+
if (typeof argLit !== "number" && typeof argLit !== "string" && typeof argLit !== "boolean" && argLit !== null && argLit !== void 0) {
|
|
2708
|
+
return [];
|
|
2709
|
+
}
|
|
2710
|
+
if (cc.prim) {
|
|
2711
|
+
const prim = witnessPrimType(w);
|
|
2712
|
+
if (prim !== void 0 && prim !== cc.prim) {
|
|
2713
|
+
return [
|
|
2714
|
+
{ actual: w, expected: `typeof ${path} = "${cc.prim}"`, path, missing: false }
|
|
2715
|
+
];
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
if (!literalMeetsConstraint(argLit, cc)) {
|
|
2719
|
+
return [{ actual: w, expected: formatConstraint(cc), path, missing: false }];
|
|
2720
|
+
}
|
|
2721
|
+
return [];
|
|
2722
|
+
}
|
|
1964
2723
|
function projectCaseArgField(arg, fieldPath) {
|
|
1965
2724
|
let cur = arg;
|
|
1966
2725
|
for (const seg of fieldPath) {
|
|
@@ -1980,57 +2739,6 @@ function scanCaseInconsistency(source, knownFns, opts) {
|
|
|
1980
2739
|
const hasContractOrigin = directiveSrc.includes("@nudo:contract") || opts.sidecarPresent === true;
|
|
1981
2740
|
if (!hasContractOrigin) return out;
|
|
1982
2741
|
const file = opts.file ?? parseSource(source);
|
|
1983
|
-
const parseLitArg = (s) => {
|
|
1984
|
-
const t = s.trim();
|
|
1985
|
-
if (t === "") return void 0;
|
|
1986
|
-
if (t === "true") return { shape: { k: "prim", type: "boolean" }, term: { op: "lit", value: true }, conf: "exact" };
|
|
1987
|
-
if (t === "false") return { shape: { k: "prim", type: "boolean" }, term: { op: "lit", value: false }, conf: "exact" };
|
|
1988
|
-
if (t === "null") return { shape: { k: "unknown" }, term: { op: "lit", value: null }, conf: "exact" };
|
|
1989
|
-
if (t === "undefined") return { shape: { k: "unknown" }, term: { op: "lit", value: void 0 }, conf: "exact" };
|
|
1990
|
-
if (/^-?\d+(\.\d+)?$/.test(t)) return numLit(Number(t));
|
|
1991
|
-
const str2 = t.match(/^(['"])([\s\S]*)\1$/);
|
|
1992
|
-
if (str2) {
|
|
1993
|
-
return {
|
|
1994
|
-
shape: { k: "prim", type: "string" },
|
|
1995
|
-
term: { op: "lit", value: str2[2] },
|
|
1996
|
-
conf: "exact"
|
|
1997
|
-
};
|
|
1998
|
-
}
|
|
1999
|
-
return void 0;
|
|
2000
|
-
};
|
|
2001
|
-
const parseCaseArgs = (raw) => {
|
|
2002
|
-
const m = raw.match(/@nudo:case\s+"[^"]+"\s*\(([\s\S]*)\)/);
|
|
2003
|
-
if (!m) return void 0;
|
|
2004
|
-
const inner = m[1].trim();
|
|
2005
|
-
if (inner === "") return [];
|
|
2006
|
-
const parts = [];
|
|
2007
|
-
let depth = 0;
|
|
2008
|
-
let cur = "";
|
|
2009
|
-
let quote = null;
|
|
2010
|
-
for (let i = 0; i < inner.length; i++) {
|
|
2011
|
-
const ch = inner[i];
|
|
2012
|
-
if (quote) {
|
|
2013
|
-
cur += ch;
|
|
2014
|
-
if (ch === quote && inner[i - 1] !== "\\") quote = null;
|
|
2015
|
-
continue;
|
|
2016
|
-
}
|
|
2017
|
-
if (ch === '"' || ch === "'") {
|
|
2018
|
-
quote = ch;
|
|
2019
|
-
cur += ch;
|
|
2020
|
-
continue;
|
|
2021
|
-
}
|
|
2022
|
-
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
2023
|
-
if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
2024
|
-
if (ch === "," && depth === 0) {
|
|
2025
|
-
parts.push(cur);
|
|
2026
|
-
cur = "";
|
|
2027
|
-
continue;
|
|
2028
|
-
}
|
|
2029
|
-
cur += ch;
|
|
2030
|
-
}
|
|
2031
|
-
if (cur.trim()) parts.push(cur);
|
|
2032
|
-
return parts;
|
|
2033
|
-
};
|
|
2034
2742
|
const checkCaseAgainstReqs = (fnName, caseName, args, line) => {
|
|
2035
2743
|
const g = generalizeFromAst(fnName, source, {
|
|
2036
2744
|
...file ? { file } : {},
|
|
@@ -2072,20 +2780,45 @@ function scanCaseInconsistency(source, knownFns, opts) {
|
|
|
2072
2780
|
]);
|
|
2073
2781
|
}
|
|
2074
2782
|
if (reqs.length === 0) return;
|
|
2075
|
-
const absArgs = args.map((a) =>
|
|
2783
|
+
const absArgs = args.map((a) => parseCaseArgAbs(a));
|
|
2076
2784
|
for (const req of reqs) {
|
|
2077
2785
|
const idx = req[0];
|
|
2078
2786
|
const entry = req[1];
|
|
2079
2787
|
const fieldPath = req[2];
|
|
2080
|
-
|
|
2788
|
+
const paramName = entry.param || paramNames[idx] || `arg${idx}`;
|
|
2789
|
+
if (idx >= args.length) {
|
|
2790
|
+
out.push({
|
|
2791
|
+
severity: "warning",
|
|
2792
|
+
code: DIAG.directiveSyntax,
|
|
2793
|
+
message: `${fnName} case "${caseName}": no witness argument for contract slot ${paramName} (${args.length} argument(s) given)`,
|
|
2794
|
+
actual: `${args.length} argument(s)`,
|
|
2795
|
+
expected: `${idx + 1} argument(s)`,
|
|
2796
|
+
suggestion: `add a witness argument for ${paramName}, or drop the contract on ${paramName}`,
|
|
2797
|
+
fn: fnName,
|
|
2798
|
+
line
|
|
2799
|
+
});
|
|
2800
|
+
continue;
|
|
2801
|
+
}
|
|
2802
|
+
if (absArgs[idx] === void 0) {
|
|
2803
|
+
if (isConstraintBuilderArg(args[idx])) continue;
|
|
2804
|
+
out.push({
|
|
2805
|
+
severity: "warning",
|
|
2806
|
+
code: DIAG.directiveSyntax,
|
|
2807
|
+
message: `${fnName} case "${caseName}": witness argument ${idx + 1} could not be parsed \u2014 contract slot ${paramName} was not checked`,
|
|
2808
|
+
actual: args[idx],
|
|
2809
|
+
expected: "a literal witness (number / string / boolean / null / undefined / object literal / array literal)",
|
|
2810
|
+
suggestion: `use a literal witness for ${paramName}, or relax the refine on ${paramName}`,
|
|
2811
|
+
fn: fnName,
|
|
2812
|
+
line
|
|
2813
|
+
});
|
|
2814
|
+
continue;
|
|
2815
|
+
}
|
|
2081
2816
|
let arg = absArgs[idx];
|
|
2082
|
-
if (!arg) continue;
|
|
2083
2817
|
if (fieldPath) {
|
|
2084
2818
|
const projected = projectCaseArgField(arg, fieldPath);
|
|
2085
2819
|
if (!projected) {
|
|
2086
2820
|
const k = arg.shape.k;
|
|
2087
2821
|
if (k !== "unknown" && k !== "any") {
|
|
2088
|
-
const paramName = entry.param || paramNames[idx] || `arg${idx}`;
|
|
2089
2822
|
const field = fieldPath.join(".");
|
|
2090
2823
|
out.push({
|
|
2091
2824
|
severity: "error",
|
|
@@ -2102,6 +2835,22 @@ function scanCaseInconsistency(source, knownFns, opts) {
|
|
|
2102
2835
|
}
|
|
2103
2836
|
arg = projected;
|
|
2104
2837
|
}
|
|
2838
|
+
const cc = derefDeepConstraint(entry.constraint);
|
|
2839
|
+
if (cc.fields || witnessIsStructure(arg)) {
|
|
2840
|
+
for (const v2 of witnessViolations(cc, arg, "", 0)) {
|
|
2841
|
+
out.push({
|
|
2842
|
+
severity: "error",
|
|
2843
|
+
code: DIAG.caseInconsistency,
|
|
2844
|
+
message: `${fnName} case "${caseName}": witness \u22AD contract`,
|
|
2845
|
+
actual: formatAbs(v2.actual),
|
|
2846
|
+
expected: v2.expected,
|
|
2847
|
+
suggestion: v2.missing ? `add the missing field ${v2.path} to the case argument (contract slot ${paramName})` : `change the case argument, or relax the refine on ${paramName}`,
|
|
2848
|
+
fn: fnName,
|
|
2849
|
+
line
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
continue;
|
|
2853
|
+
}
|
|
2105
2854
|
const argLit = arg.term?.op === "lit" ? arg.term.value : void 0;
|
|
2106
2855
|
const argIsLit = arg.term?.op === "lit";
|
|
2107
2856
|
if (!argIsLit || typeof argLit !== "number" && typeof argLit !== "string" && typeof argLit !== "boolean" && argLit !== null && argLit !== void 0) {
|
|
@@ -2110,7 +2859,6 @@ function scanCaseInconsistency(source, knownFns, opts) {
|
|
|
2110
2859
|
const hasEqOr = (entry.constraint.members?.length ?? 0) > 0 || entry.constraint.preds.some((p) => p.op === "eq");
|
|
2111
2860
|
if (hasEqOr) {
|
|
2112
2861
|
if (!literalMeetsConstraint(argLit, entry.constraint)) {
|
|
2113
|
-
const paramName = entry.param || paramNames[idx] || `arg${idx}`;
|
|
2114
2862
|
out.push({
|
|
2115
2863
|
severity: "error",
|
|
2116
2864
|
code: DIAG.caseInconsistency,
|
|
@@ -2126,7 +2874,6 @@ function scanCaseInconsistency(source, knownFns, opts) {
|
|
|
2126
2874
|
}
|
|
2127
2875
|
if (typeof argLit !== "number") continue;
|
|
2128
2876
|
if (isIntFlag(entry.constraint) && !Number.isInteger(argLit)) {
|
|
2129
|
-
const paramName = entry.param || paramNames[idx] || `arg${idx}`;
|
|
2130
2877
|
out.push({
|
|
2131
2878
|
severity: "error",
|
|
2132
2879
|
code: DIAG.caseInconsistency,
|
|
@@ -2148,7 +2895,6 @@ function scanCaseInconsistency(source, knownFns, opts) {
|
|
|
2148
2895
|
if (p.op === "lt") ok = argLit < n;
|
|
2149
2896
|
if (p.op === "le") ok = argLit <= n;
|
|
2150
2897
|
if (!ok) {
|
|
2151
|
-
const paramName = entry.param || paramNames[idx] || `arg${idx}`;
|
|
2152
2898
|
out.push({
|
|
2153
2899
|
severity: "error",
|
|
2154
2900
|
code: DIAG.caseInconsistency,
|
|
@@ -2170,8 +2916,7 @@ function scanCaseInconsistency(source, knownFns, opts) {
|
|
|
2170
2916
|
const text = scope.commentTexts[ci];
|
|
2171
2917
|
const startLine = scope.commentStartLines[ci] ?? 0;
|
|
2172
2918
|
for (const tag of scanCaseTags(text)) {
|
|
2173
|
-
const caseArgs =
|
|
2174
|
-
if (!caseArgs) continue;
|
|
2919
|
+
const caseArgs = splitCaseArgsText(stripCaseContinuation(tag.argsText));
|
|
2175
2920
|
checkCaseAgainstReqs(scope.name, tag.name, caseArgs, startLine);
|
|
2176
2921
|
}
|
|
2177
2922
|
}
|
|
@@ -2187,18 +2932,27 @@ function checkSource(filePath, source, phi = pTrue, opts = {}) {
|
|
|
2187
2932
|
}
|
|
2188
2933
|
function checkSourceInScope(filePath, source, phi, opts) {
|
|
2189
2934
|
let callOpts = opts;
|
|
2935
|
+
const loadReadFailures = [];
|
|
2936
|
+
const loadFailureKeys = /* @__PURE__ */ new Set();
|
|
2937
|
+
let speculativeLoad;
|
|
2190
2938
|
if (opts.loadModule && opts.fromFile) {
|
|
2191
2939
|
const loadCache = /* @__PURE__ */ new Map();
|
|
2192
2940
|
const raw = opts.loadModule;
|
|
2193
|
-
const
|
|
2941
|
+
const cachedLoad = (spec, from) => {
|
|
2942
|
+
const k = `${from}\0${spec}`;
|
|
2943
|
+
if (!loadCache.has(k)) loadCache.set(k, raw(spec, from));
|
|
2944
|
+
return loadCache.get(k);
|
|
2945
|
+
};
|
|
2194
2946
|
callOpts = {
|
|
2195
2947
|
...opts,
|
|
2196
|
-
loadModule: (
|
|
2197
|
-
const
|
|
2198
|
-
if (
|
|
2199
|
-
|
|
2200
|
-
|
|
2948
|
+
loadModule: guardLoadModule(cachedLoad, (failure) => {
|
|
2949
|
+
const key = `${failure.path}\0${failure.code}`;
|
|
2950
|
+
if (loadFailureKeys.has(key)) return;
|
|
2951
|
+
loadFailureKeys.add(key);
|
|
2952
|
+
loadReadFailures.push(failure);
|
|
2953
|
+
})
|
|
2201
2954
|
};
|
|
2955
|
+
speculativeLoad = cachedLoad;
|
|
2202
2956
|
}
|
|
2203
2957
|
setRefineDiagCollector(null);
|
|
2204
2958
|
setInterfaceDiagCollector(null);
|
|
@@ -2208,11 +2962,20 @@ function checkSourceInScope(filePath, source, phi, opts) {
|
|
|
2208
2962
|
const useMemo = phi.op === "true";
|
|
2209
2963
|
let memoKey;
|
|
2210
2964
|
const stable = stableAnalyzeKeySource(source);
|
|
2211
|
-
const depsFp =
|
|
2212
|
-
|
|
2965
|
+
const depsFp = callOpts.loadModule && callOpts.fromFile ? loadModuleDepsFingerprint(stable, callOpts.loadModule, callOpts.fromFile, {
|
|
2966
|
+
...speculativeLoad ? { ambientLoadModule: speculativeLoad } : {},
|
|
2967
|
+
ambientAllowed: (sidecarPath) => sidecarAutoBindAllowed(sidecarPath, opts.autoBind, opts.projectDir)
|
|
2968
|
+
}) : { fp: "-", paths: [], contents: [], truncated: false, readError: false };
|
|
2969
|
+
const sidecarFp = callOpts.loadModule && callOpts.fromFile ? sidecarClosureFingerprint(callOpts.fromFile, {
|
|
2970
|
+
...callOpts,
|
|
2971
|
+
...speculativeLoad ? { loadModule: speculativeLoad } : {}
|
|
2972
|
+
}) : void 0;
|
|
2213
2973
|
const allowMemo = useMemo && !depsFp.truncated && // 读错误轮键不可信(#135):错误轮报告(nudo:interface-load)与真 miss 轮
|
|
2214
2974
|
// (干净)若共享 miss 键会互为跨次陈旧命中——不读也不写 memo
|
|
2215
|
-
!depsFp.readError &&
|
|
2975
|
+
!depsFp.readError && // Bug 499:本闸把读失败降级成 miss(指纹面看不到 readError)——但
|
|
2976
|
+
// 「不可读」轮与「真不存在」轮的报告不同(前者多一条读失败诊断),
|
|
2977
|
+
// 共用同一键会互为陈旧命中 ⇒ 读失败轮一律不碰 memo。
|
|
2978
|
+
loadReadFailures.length === 0 && !(sidecarFp?.startsWith("trunc:") ?? false);
|
|
2216
2979
|
if (allowMemo) {
|
|
2217
2980
|
memoKey = checkMemoKey(filePath, stable, opts, depsFp, sidecarFp);
|
|
2218
2981
|
const hit = checkMemoGet(memoKey);
|
|
@@ -2225,7 +2988,10 @@ function checkSourceInScope(filePath, source, phi, opts) {
|
|
|
2225
2988
|
const issues = [];
|
|
2226
2989
|
const signatures = [];
|
|
2227
2990
|
const file = parseSource(source);
|
|
2228
|
-
const names =
|
|
2991
|
+
const names = [
|
|
2992
|
+
...listTopFunctions(source, file),
|
|
2993
|
+
...listClassEntryNames(source, file)
|
|
2994
|
+
];
|
|
2229
2995
|
const memoPaths = sidecarFp !== void 0 && callOpts.fromFile ? [.../* @__PURE__ */ new Set([...depsFp.paths, normPath(sidecarPathOf(callOpts.fromFile))])] : depsFp.paths;
|
|
2230
2996
|
const truncated = /* @__PURE__ */ new Map();
|
|
2231
2997
|
resetAbsCallBudget();
|
|
@@ -2239,7 +3005,7 @@ function checkSourceInScope(filePath, source, phi, opts) {
|
|
|
2239
3005
|
makeSameFileFaceResolver(callOpts, filePath, source, file)
|
|
2240
3006
|
);
|
|
2241
3007
|
try {
|
|
2242
|
-
attachCrossModuleContractFaces(callOpts, filePath);
|
|
3008
|
+
const crossModuleIssues = attachCrossModuleContractFaces(callOpts, filePath);
|
|
2243
3009
|
const report = checkSourceInner(
|
|
2244
3010
|
filePath,
|
|
2245
3011
|
source,
|
|
@@ -2258,8 +3024,10 @@ function checkSourceInScope(filePath, source, phi, opts) {
|
|
|
2258
3024
|
...takeRefineDiagsSince(refineSince),
|
|
2259
3025
|
...takeInterfaceDiagsSince(ifaceSince)
|
|
2260
3026
|
]);
|
|
2261
|
-
|
|
2262
|
-
|
|
3027
|
+
const loadIssues = loadReadFailureIssues(loadReadFailures);
|
|
3028
|
+
const allIssues = [...diagIssues, ...loadIssues, ...crossModuleIssues];
|
|
3029
|
+
if (allIssues.length > 0) {
|
|
3030
|
+
report.issues.push(...allIssues);
|
|
2263
3031
|
const errors = report.issues.filter((i) => i.severity === "error").length;
|
|
2264
3032
|
const warnings = report.issues.filter((i) => i.severity === "warning").length;
|
|
2265
3033
|
const infos = report.issues.filter((i) => i.severity === "info").length;
|
|
@@ -2271,7 +3039,9 @@ function checkSourceInScope(filePath, source, phi, opts) {
|
|
|
2271
3039
|
functions: report.summary.functions
|
|
2272
3040
|
};
|
|
2273
3041
|
}
|
|
2274
|
-
if (memoKey
|
|
3042
|
+
if (memoKey && loadReadFailures.length === 0) {
|
|
3043
|
+
checkMemoSet(memoKey, report, memoPaths);
|
|
3044
|
+
}
|
|
2275
3045
|
return cloneCheckReport(report);
|
|
2276
3046
|
} finally {
|
|
2277
3047
|
setContractFaceResolver(prevFaceResolver);
|
|
@@ -2280,6 +3050,14 @@ function checkSourceInScope(filePath, source, phi, opts) {
|
|
|
2280
3050
|
setAbsTruncationCollector(prevTrunc);
|
|
2281
3051
|
}
|
|
2282
3052
|
}
|
|
3053
|
+
function loadReadFailureIssues(failures) {
|
|
3054
|
+
return failures.map((f) => ({
|
|
3055
|
+
severity: "error",
|
|
3056
|
+
code: DIAG.moduleUnreadable,
|
|
3057
|
+
message: `cannot read module '${f.path}' (${f.code}): ${f.message}`,
|
|
3058
|
+
suggestion: "fix the file permissions (or the module path) \u2014 the dependency is present but unreadable, so its types are unknown"
|
|
3059
|
+
}));
|
|
3060
|
+
}
|
|
2283
3061
|
function makeSameFileFaceResolver(opts, filePath, source, file) {
|
|
2284
3062
|
const loadModule = opts.loadModule;
|
|
2285
3063
|
const fromFile = opts.fromFile ?? filePath;
|
|
@@ -2307,24 +3085,48 @@ function makeSameFileFaceResolver(opts, filePath, source, file) {
|
|
|
2307
3085
|
return faces;
|
|
2308
3086
|
};
|
|
2309
3087
|
}
|
|
3088
|
+
function unmatchedContractParams(eff, formalDisplay, formals) {
|
|
3089
|
+
const contractNames = contractParamNameSet(formals);
|
|
3090
|
+
const display = new Set(formalDisplay);
|
|
3091
|
+
const conflictNames = new Set(eff.conflict?.params ?? []);
|
|
3092
|
+
return eff.params.map((p) => p.param).filter(
|
|
3093
|
+
(p) => p && !display.has(p) && !contractNames.has(p) && !conflictNames.has(p)
|
|
3094
|
+
);
|
|
3095
|
+
}
|
|
3096
|
+
function paramMismatchIssue(name, unknownParams, formalDisplay, formals) {
|
|
3097
|
+
const contractNames = contractParamNameSet(formals);
|
|
3098
|
+
const surface = [...contractNames].filter((n) => !n.startsWith("_p") && n !== "_").join(", ");
|
|
3099
|
+
return {
|
|
3100
|
+
severity: "error",
|
|
3101
|
+
code: DIAG.interfaceParamMismatch,
|
|
3102
|
+
message: `${name}: contract parameter name(s) not in the formal parameter list (${unknownParams.join(", ")})`,
|
|
3103
|
+
actual: unknownParams.join(", "),
|
|
3104
|
+
expected: surface || formalDisplay.join(", ") || "(no params)",
|
|
3105
|
+
suggestion: `rename the @nudo:contract / sidecar binding parameter to one of: ${surface || formalDisplay.join(", ") || "(function has no params)"}`,
|
|
3106
|
+
fn: name
|
|
3107
|
+
};
|
|
3108
|
+
}
|
|
2310
3109
|
function attachCrossModuleContractFaces(opts, filePath) {
|
|
2311
3110
|
const loadModule = opts.loadModule;
|
|
2312
3111
|
const fromFile = opts.fromFile ?? filePath;
|
|
2313
|
-
if (!loadModule) return;
|
|
3112
|
+
if (!loadModule) return [];
|
|
2314
3113
|
const tables = [];
|
|
2315
3114
|
for (const t of [opts.modules, opts.inject?.modules]) {
|
|
2316
3115
|
if (t && typeof t === "object" && !tables.includes(t)) tables.push(t);
|
|
2317
3116
|
}
|
|
2318
|
-
if (tables.length === 0) return;
|
|
3117
|
+
if (tables.length === 0) return [];
|
|
2319
3118
|
const autoBind = opts.autoBind;
|
|
2320
3119
|
const projectDir = opts.projectDir;
|
|
3120
|
+
const mismatchIssues = [];
|
|
2321
3121
|
const facesCache = /* @__PURE__ */ new Map();
|
|
2322
3122
|
const facesFor = (depSource, depPath, name, paramNames) => {
|
|
2323
3123
|
const key = `${depPath}\0${name}`;
|
|
2324
|
-
|
|
3124
|
+
const hit = facesCache.get(key);
|
|
3125
|
+
if (hit) return hit;
|
|
2325
3126
|
let faces;
|
|
3127
|
+
let eff;
|
|
2326
3128
|
try {
|
|
2327
|
-
|
|
3129
|
+
eff = effectiveInterface(depSource, name, {
|
|
2328
3130
|
loadModule,
|
|
2329
3131
|
fromFile: depPath,
|
|
2330
3132
|
...autoBind !== void 0 ? { autoBind } : {},
|
|
@@ -2333,22 +3135,36 @@ function attachCrossModuleContractFaces(opts, filePath) {
|
|
|
2333
3135
|
if (eff) faces = contractFacesFromEffectiveInterface(eff, paramNames);
|
|
2334
3136
|
} catch {
|
|
2335
3137
|
faces = void 0;
|
|
3138
|
+
eff = void 0;
|
|
2336
3139
|
}
|
|
2337
|
-
|
|
2338
|
-
|
|
3140
|
+
const out = { faces, eff };
|
|
3141
|
+
facesCache.set(key, out);
|
|
3142
|
+
return out;
|
|
2339
3143
|
};
|
|
2340
3144
|
const attachFnAbs = (v2, depSource, depPath, name) => {
|
|
2341
3145
|
if (!v2 || typeof v2 !== "object" || !("shape" in v2)) return;
|
|
2342
3146
|
const shape2 = v2.shape;
|
|
2343
3147
|
if (shape2.k !== "fn") return;
|
|
2344
|
-
let paramNames;
|
|
3148
|
+
let paramNames = [];
|
|
3149
|
+
let formals = [];
|
|
2345
3150
|
try {
|
|
2346
|
-
|
|
3151
|
+
const meta = extractFn(depSource, name);
|
|
3152
|
+
paramNames = meta?.params ?? [];
|
|
3153
|
+
formals = meta?.formals ?? [];
|
|
2347
3154
|
} catch {
|
|
2348
3155
|
paramNames = [];
|
|
3156
|
+
formals = [];
|
|
2349
3157
|
}
|
|
2350
|
-
const faces = facesFor(depSource, depPath, name, paramNames);
|
|
3158
|
+
const { faces, eff } = facesFor(depSource, depPath, name, paramNames);
|
|
2351
3159
|
if (faces) attachContractFaces(v2, faces);
|
|
3160
|
+
if (eff?.source === "handwritten") {
|
|
3161
|
+
const unknownParams = unmatchedContractParams(eff, paramNames, formals);
|
|
3162
|
+
if (unknownParams.length > 0) {
|
|
3163
|
+
mismatchIssues.push(
|
|
3164
|
+
paramMismatchIssue(name, unknownParams, paramNames, formals)
|
|
3165
|
+
);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
2352
3168
|
};
|
|
2353
3169
|
for (const table of tables) {
|
|
2354
3170
|
for (const [spec, mod2] of Object.entries(table)) {
|
|
@@ -2370,6 +3186,7 @@ function attachCrossModuleContractFaces(opts, filePath) {
|
|
|
2370
3186
|
if (def !== void 0) attachFnAbs(def, depSource, depPath, "default");
|
|
2371
3187
|
}
|
|
2372
3188
|
}
|
|
3189
|
+
return mismatchIssues;
|
|
2373
3190
|
}
|
|
2374
3191
|
function sidecarDiagIssues(diags) {
|
|
2375
3192
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2379,7 +3196,14 @@ function sidecarDiagIssues(diags) {
|
|
|
2379
3196
|
if (seen.has(k)) continue;
|
|
2380
3197
|
seen.add(k);
|
|
2381
3198
|
const severity = d.code === DIAG.contractSyntax || d.code === DIAG.directiveSyntax ? "warning" : "error";
|
|
2382
|
-
out.push({
|
|
3199
|
+
out.push({
|
|
3200
|
+
severity,
|
|
3201
|
+
code: d.code,
|
|
3202
|
+
message: d.message,
|
|
3203
|
+
// Bug 374:位置透传(nudo:contract-syntax 不再落在文件第 1 行第 1 列)
|
|
3204
|
+
...d.line !== void 0 ? { line: d.line } : {},
|
|
3205
|
+
...d.column !== void 0 ? { column: d.column } : {}
|
|
3206
|
+
});
|
|
2383
3207
|
}
|
|
2384
3208
|
return out;
|
|
2385
3209
|
}
|
|
@@ -2397,6 +3221,7 @@ function assumePremisesPhiOf(name, source, file, issues) {
|
|
|
2397
3221
|
const preds = [];
|
|
2398
3222
|
const display = [];
|
|
2399
3223
|
for (const seg of segs) {
|
|
3224
|
+
const pos = seg.line !== void 0 ? { line: seg.line, ...seg.column !== void 0 ? { column: seg.column } : {} } : {};
|
|
2400
3225
|
if (seg.kind === "bad") {
|
|
2401
3226
|
issues.push({
|
|
2402
3227
|
severity: "error",
|
|
@@ -2405,7 +3230,8 @@ function assumePremisesPhiOf(name, source, file, issues) {
|
|
|
2405
3230
|
actual: seg.raw,
|
|
2406
3231
|
expected: "<param> <= <param>",
|
|
2407
3232
|
suggestion: "cross-variable lemmas: @nudo:assume d <= max \u2014 constant bounds: sidecar number().le(10)",
|
|
2408
|
-
fn: name
|
|
3233
|
+
fn: name,
|
|
3234
|
+
...pos
|
|
2409
3235
|
});
|
|
2410
3236
|
continue;
|
|
2411
3237
|
}
|
|
@@ -2418,7 +3244,8 @@ function assumePremisesPhiOf(name, source, file, issues) {
|
|
|
2418
3244
|
actual: bad.join(", "),
|
|
2419
3245
|
expected: surface || "(no params/locals)",
|
|
2420
3246
|
suggestion: `reference one of: ${surface || "(no params or top-level locals)"}`,
|
|
2421
|
-
fn: name
|
|
3247
|
+
fn: name,
|
|
3248
|
+
...pos
|
|
2422
3249
|
});
|
|
2423
3250
|
continue;
|
|
2424
3251
|
}
|
|
@@ -2426,7 +3253,7 @@ function assumePremisesPhiOf(name, source, file, issues) {
|
|
|
2426
3253
|
display.push(`${seg.lhs} <= ${seg.rhs}`);
|
|
2427
3254
|
}
|
|
2428
3255
|
if (preds.length === 0) return void 0;
|
|
2429
|
-
return { phi: and(...preds), display };
|
|
3256
|
+
return { phi: and(...preds), display, premises: preds };
|
|
2430
3257
|
}
|
|
2431
3258
|
function invariantSpecsOf(name, source, file, issues) {
|
|
2432
3259
|
const segs = extractInvariantSegs(source, name);
|
|
@@ -2441,6 +3268,7 @@ function invariantSpecsOf(name, source, file, issues) {
|
|
|
2441
3268
|
const specs = [];
|
|
2442
3269
|
const display = [];
|
|
2443
3270
|
for (const seg of segs) {
|
|
3271
|
+
const pos = seg.line !== void 0 ? { line: seg.line, ...seg.column !== void 0 ? { column: seg.column } : {} } : {};
|
|
2444
3272
|
if (seg.kind === "bad") {
|
|
2445
3273
|
issues.push({
|
|
2446
3274
|
severity: "warning",
|
|
@@ -2449,7 +3277,8 @@ function invariantSpecsOf(name, source, file, issues) {
|
|
|
2449
3277
|
actual: seg.raw,
|
|
2450
3278
|
expected: "<name><path> <rel> <number>",
|
|
2451
3279
|
suggestion: "loop-carried bounds: @nudo:invariant dp[*][*] >= 0 \u2014 scalar: @nudo:invariant last >= 1",
|
|
2452
|
-
fn: name
|
|
3280
|
+
fn: name,
|
|
3281
|
+
...pos
|
|
2453
3282
|
});
|
|
2454
3283
|
continue;
|
|
2455
3284
|
}
|
|
@@ -2461,7 +3290,8 @@ function invariantSpecsOf(name, source, file, issues) {
|
|
|
2461
3290
|
actual: seg.name,
|
|
2462
3291
|
expected: surface || "(no params/locals)",
|
|
2463
3292
|
suggestion: `reference one of: ${surface || "(no params or top-level locals)"}`,
|
|
2464
|
-
fn: name
|
|
3293
|
+
fn: name,
|
|
3294
|
+
...pos
|
|
2465
3295
|
});
|
|
2466
3296
|
continue;
|
|
2467
3297
|
}
|
|
@@ -2471,6 +3301,36 @@ function invariantSpecsOf(name, source, file, issues) {
|
|
|
2471
3301
|
if (specs.length === 0) return void 0;
|
|
2472
3302
|
return { specs, display };
|
|
2473
3303
|
}
|
|
3304
|
+
function dedupeAssignRecords(records) {
|
|
3305
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3306
|
+
const out = [];
|
|
3307
|
+
for (const r of records) {
|
|
3308
|
+
const key = JSON.stringify([
|
|
3309
|
+
r.name,
|
|
3310
|
+
r.line ?? "",
|
|
3311
|
+
r.column ?? "",
|
|
3312
|
+
r.conditional ? "1" : "0",
|
|
3313
|
+
assignValueKey(r.prev),
|
|
3314
|
+
assignValueKey(r.next)
|
|
3315
|
+
]);
|
|
3316
|
+
if (seen.has(key)) continue;
|
|
3317
|
+
seen.add(key);
|
|
3318
|
+
out.push(r);
|
|
3319
|
+
}
|
|
3320
|
+
return out;
|
|
3321
|
+
}
|
|
3322
|
+
function assignValueKey(v2) {
|
|
3323
|
+
if (v2 === void 0) return "-";
|
|
3324
|
+
if (v2 === null) return "null";
|
|
3325
|
+
if (typeof v2 !== "object") return `host:${typeof v2}:${String(v2)}`;
|
|
3326
|
+
const o = v2;
|
|
3327
|
+
if (!("shape" in o) || !("conf" in o)) return `host:${typeof v2}`;
|
|
3328
|
+
try {
|
|
3329
|
+
return formatAbs(v2);
|
|
3330
|
+
} catch {
|
|
3331
|
+
return `abs:${o.shape?.k ?? "?"}:${o.conf ?? "?"}`;
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
2474
3334
|
var OPAQUE_EVIDENCE_HINT = "add @nudo:case or a call site";
|
|
2475
3335
|
var OPAQUE_ENV_HINT = "declare the runtime env to type native modules: '/// @nudo:env node' (or web/es) \u2014 see guides/env-harvest.md";
|
|
2476
3336
|
function isBuiltinModuleSpec(spec) {
|
|
@@ -2505,9 +3365,12 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2505
3365
|
eiCache.set(key, eff);
|
|
2506
3366
|
return eff;
|
|
2507
3367
|
};
|
|
3368
|
+
const records = [];
|
|
3369
|
+
const prevAssign = setEvalAssignCollector((r) => records.push(r));
|
|
2508
3370
|
for (const name of names) {
|
|
2509
3371
|
const assumePhi = assumePremisesPhiOf(name, source, file, issues);
|
|
2510
3372
|
const inv = invariantSpecsOf(name, source, file, issues);
|
|
3373
|
+
resetAssumeConsumed();
|
|
2511
3374
|
const runSeeded = (body) => {
|
|
2512
3375
|
if (assumePhi === void 0 && inv === void 0) return body();
|
|
2513
3376
|
return withAssumePremises(assumePhi?.phi ?? pTrue, () => withInvariantSpecs(inv?.specs ?? [], body));
|
|
@@ -2525,6 +3388,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2525
3388
|
contractParam.set(p.param, constraintToEntryAbs(p.constraint, p.param));
|
|
2526
3389
|
}
|
|
2527
3390
|
}
|
|
3391
|
+
const skipFmt = formatSigCached(retAbs, name);
|
|
2528
3392
|
signatures.push({
|
|
2529
3393
|
name,
|
|
2530
3394
|
params,
|
|
@@ -2533,8 +3397,8 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2533
3397
|
return a ? formatShape(a) : "any";
|
|
2534
3398
|
}),
|
|
2535
3399
|
abs: retAbs,
|
|
2536
|
-
display:
|
|
2537
|
-
detail:
|
|
3400
|
+
display: skipFmt.display,
|
|
3401
|
+
detail: skipFmt.detail,
|
|
2538
3402
|
conf: retAbs.conf,
|
|
2539
3403
|
...isEntryFn ? { entry: true } : {}
|
|
2540
3404
|
});
|
|
@@ -2544,12 +3408,29 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2544
3408
|
fromFile: refineFrom
|
|
2545
3409
|
});
|
|
2546
3410
|
const display = named?.name ?? formatConstraint(eff.returns.constraint);
|
|
2547
|
-
|
|
3411
|
+
const contractSource = named ? "directive" : "sidecar";
|
|
3412
|
+
returnContracts.set(name, {
|
|
3413
|
+
display,
|
|
3414
|
+
constraint: eff.returns.constraint,
|
|
3415
|
+
source: contractSource
|
|
3416
|
+
});
|
|
2548
3417
|
const retIssues = checkReturnConstraint(
|
|
2549
3418
|
name,
|
|
2550
3419
|
display,
|
|
2551
3420
|
eff.returns.constraint,
|
|
2552
|
-
declared
|
|
3421
|
+
declared,
|
|
3422
|
+
{
|
|
3423
|
+
// #369 / #372:issue 构造处带函数声明位置
|
|
3424
|
+
loc: findFnLoc(file, name),
|
|
3425
|
+
source: contractSource,
|
|
3426
|
+
// Bug 1094:返回点的路径前提 Φ(用户引理前提)——skip 分支同样
|
|
3427
|
+
// 透传(函数级公理在 skip 面也成立;证明通道按「同项 ∧ 蕴含」
|
|
3428
|
+
// 口径消费 ⇒ 不会凭空制造确定违约)。assumeDisplay 一并带上 ⇒
|
|
3429
|
+
// Φ 被消费时归因(nudo:assume-relied)仍然可见(不得静默消费)。
|
|
3430
|
+
phi: assumePhi?.phi,
|
|
3431
|
+
phiPremises: assumePhi?.premises,
|
|
3432
|
+
assumeDisplay: assumePhi?.display
|
|
3433
|
+
}
|
|
2553
3434
|
);
|
|
2554
3435
|
if (retIssues.some((i) => i.severity === "error")) returnViolated.add(name);
|
|
2555
3436
|
issues.push(...retIssues);
|
|
@@ -2587,7 +3468,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2587
3468
|
};
|
|
2588
3469
|
const effects = collectEntryMayThrows(source, name, synthetic, file, phi, opts);
|
|
2589
3470
|
const throwsDisplayFallback = formatThrowsAbs(mayThrowEffectsToAbs(effects));
|
|
2590
|
-
const declared =
|
|
3471
|
+
const declared = declaredThrowsForEntry(source, name) ?? effectiveInterfaceCached(name)?.throws?.kinds;
|
|
2591
3472
|
const remaining = filterGateThrows(effects, declared, ignoreThrows);
|
|
2592
3473
|
const gateDisplay = formatThrowsAbs(mayThrowEffectsToAbs(remaining));
|
|
2593
3474
|
if (gateDisplay && remaining.length > 0) {
|
|
@@ -2632,7 +3513,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2632
3513
|
if (isEntry && entryThrowsMode !== "off") {
|
|
2633
3514
|
const effects = runSeeded(() => collectEntryMayThrows(source, name, g, file, phi, opts));
|
|
2634
3515
|
throwsDisplay = formatThrowsAbs(mayThrowEffectsToAbs(effects));
|
|
2635
|
-
const declared =
|
|
3516
|
+
const declared = declaredThrowsForEntry(source, name) ?? effectiveInterfaceCached(name)?.throws?.kinds;
|
|
2636
3517
|
const remaining = filterGateThrows(effects, declared, ignoreThrows);
|
|
2637
3518
|
const gateDisplay = formatThrowsAbs(mayThrowEffectsToAbs(remaining));
|
|
2638
3519
|
if (gateDisplay && remaining.length > 0) {
|
|
@@ -2658,7 +3539,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2658
3539
|
const paramTypes = g.typeParams.map((t) => {
|
|
2659
3540
|
if (!t.value) return "any";
|
|
2660
3541
|
if (t.value.shape.k === "unknown") return "unknown";
|
|
2661
|
-
return
|
|
3542
|
+
return formatEntryParamShape(t.value);
|
|
2662
3543
|
});
|
|
2663
3544
|
signatures.push({
|
|
2664
3545
|
name,
|
|
@@ -2678,7 +3559,12 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2678
3559
|
// 的排除一致。
|
|
2679
3560
|
(t) => t.value && t.value.shape.k === "unknown" && t.value.term?.op !== "lit"
|
|
2680
3561
|
);
|
|
2681
|
-
const budgetExplained = truncated.has(name) ||
|
|
3562
|
+
const budgetExplained = truncated.has(name) || // Bug 389:`conf=opaque` 通常意味着「路径没覆盖」⇒ 已解释;但**因 env
|
|
3563
|
+
// 门禁而 opaque**(源码声明了 `@nudo:env` 而那个 env 没解析出全局 ⇒
|
|
3564
|
+
// 函数体 fail-closed)不是「已解释」——那正是「推断没成功」,唯一能提示
|
|
3565
|
+
// 它的 `nudo:unknown-inference` 不能被吞(否则用户把 env 名写错的那一刻,
|
|
3566
|
+
// 唯一的失败信号恰好消失)。
|
|
3567
|
+
g.symbolic?.conf === "opaque" && !g.envGated || g.symbolic?.shape?.k === "sum" && g.symbolic.shape.members.every((m) => m.shape.k === "unknown" || m.shape.k === "any");
|
|
2682
3568
|
const symbolicTrueUnknown = g.symbolic?.shape?.k === "unknown" && g.symbolic.term?.op !== "lit";
|
|
2683
3569
|
if ((symbolicTrueUnknown || unknownParamIdx >= 0) && !budgetExplained) {
|
|
2684
3570
|
const where = symbolicTrueUnknown ? `${name} => unknown` : `${name} param ${g.params[unknownParamIdx] ?? `#${unknownParamIdx}`}: unknown`;
|
|
@@ -2688,32 +3574,32 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2688
3574
|
message: `${name}: signature has true unknown (inference failed)`,
|
|
2689
3575
|
actual: where,
|
|
2690
3576
|
expected: "computable Abs (any = unconstrained, unknown = engine debt)",
|
|
2691
|
-
|
|
3577
|
+
// Bug 1062(族 46):本诊断此前把「`@nudo:replace` 的目标文本没命中」
|
|
3578
|
+
// 说成「推断精度不够」⇒ 归因错误 ⇒ 用户只看到 unknown-inference。
|
|
3579
|
+
// service 侧已加「点名目标文本」的显式诊断(`nudo:directive-syntax`),
|
|
3580
|
+
// 这里补一句归因提示,把「指令没生效」这条可能性点出来。
|
|
3581
|
+
suggestion: "add @nudo:case / env mock / refine, or confirm the body is algebraically evaluable; if you used @nudo:replace, check the target text matches the source",
|
|
2692
3582
|
fn: name
|
|
2693
3583
|
});
|
|
2694
3584
|
}
|
|
2695
3585
|
if (hasRefineDirective || (exportedNames?.has(name) ?? false)) {
|
|
2696
3586
|
const eff = effectiveInterfaceCached(name);
|
|
2697
3587
|
if (eff?.source === "handwritten") {
|
|
2698
|
-
const
|
|
2699
|
-
const formalDisplay = new Set(g.params);
|
|
2700
|
-
const conflictNames = new Set(eff.conflict?.params ?? []);
|
|
2701
|
-
const unknownParams = eff.params.map((p) => p.param).filter(
|
|
2702
|
-
(p) => p && !formalDisplay.has(p) && !contractNames.has(p) && !conflictNames.has(p)
|
|
2703
|
-
);
|
|
3588
|
+
const unknownParams = unmatchedContractParams(eff, g.params, g.formals ?? []);
|
|
2704
3589
|
if (unknownParams.length > 0) {
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
code: DIAG.interfaceParamMismatch,
|
|
2709
|
-
message: `${name}: contract parameter name(s) not in the formal parameter list (${unknownParams.join(", ")})`,
|
|
2710
|
-
actual: unknownParams.join(", "),
|
|
2711
|
-
expected: surface || g.params.join(", ") || "(no params)",
|
|
2712
|
-
suggestion: `rename the @nudo:contract / sidecar binding parameter to one of: ${surface || g.params.join(", ") || "(function has no params)"}`,
|
|
2713
|
-
fn: name
|
|
2714
|
-
});
|
|
3590
|
+
issues.push(
|
|
3591
|
+
paramMismatchIssue(name, unknownParams, g.params, g.formals ?? [])
|
|
3592
|
+
);
|
|
2715
3593
|
}
|
|
2716
3594
|
}
|
|
3595
|
+
const contractLocs = contractSegmentLocs(source, name);
|
|
3596
|
+
const locOf = (param) => {
|
|
3597
|
+
const hit = contractLocs.find((c) => {
|
|
3598
|
+
const head = c.segment.trim().split(/\s+/)[0];
|
|
3599
|
+
return head === param;
|
|
3600
|
+
});
|
|
3601
|
+
return hit?.loc ? { line: hit.loc.line, ...hit.loc.column !== void 0 ? { column: hit.loc.column } : {} } : {};
|
|
3602
|
+
};
|
|
2717
3603
|
if (eff?.conflict) {
|
|
2718
3604
|
if (eff.conflict.params.length > 0) {
|
|
2719
3605
|
issues.push({
|
|
@@ -2721,7 +3607,8 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2721
3607
|
code: DIAG.interfaceConflict,
|
|
2722
3608
|
message: `${name}: handwritten contract conjunction unsatisfiable (${eff.conflict.params.join(", ")})`,
|
|
2723
3609
|
suggestion: "check whether the source @nudo:contract and the same-name sidecar binding have contradictory constant bounds",
|
|
2724
|
-
fn: name
|
|
3610
|
+
fn: name,
|
|
3611
|
+
...locOf(eff.conflict.params[0])
|
|
2725
3612
|
});
|
|
2726
3613
|
}
|
|
2727
3614
|
if (eff.conflict.returns) {
|
|
@@ -2730,24 +3617,70 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2730
3617
|
code: DIAG.interfaceConflict,
|
|
2731
3618
|
message: `${name}: handwritten contract conjunction unsatisfiable on the return slot`,
|
|
2732
3619
|
suggestion: "check whether the source @nudo:contract return and the sidecar fn() return constraint are contradictory (the return slot is not enforced when they are)",
|
|
2733
|
-
fn: name
|
|
3620
|
+
fn: name,
|
|
3621
|
+
...locOf("return")
|
|
2734
3622
|
});
|
|
2735
3623
|
}
|
|
2736
3624
|
}
|
|
3625
|
+
const undecidable = eff?.undecidable;
|
|
3626
|
+
if (undecidable !== void 0 && undecidable.params.length > 0) {
|
|
3627
|
+
issues.push({
|
|
3628
|
+
severity: "warning",
|
|
3629
|
+
code: DIAG.interfaceConflict,
|
|
3630
|
+
message: `${name}: contract conjunction not computable (${undecidable.params.join(", ")}) \u2014 only one side is enforced`,
|
|
3631
|
+
suggestion: "structural contracts (shape / array / union / fn) cannot be combined in this slot; the other side's obligations are NOT enforced \u2014 merge them by hand",
|
|
3632
|
+
fn: name,
|
|
3633
|
+
...locOf(undecidable.params[0])
|
|
3634
|
+
});
|
|
3635
|
+
}
|
|
3636
|
+
if (undecidable?.returns) {
|
|
3637
|
+
issues.push({
|
|
3638
|
+
severity: "warning",
|
|
3639
|
+
code: DIAG.interfaceConflict,
|
|
3640
|
+
message: `${name}: contract conjunction not computable on the return slot \u2014 only one side is enforced`,
|
|
3641
|
+
suggestion: "structural return constraints cannot be combined in this slot; the other side's obligations are NOT enforced \u2014 merge them by hand",
|
|
3642
|
+
fn: name,
|
|
3643
|
+
...locOf("return")
|
|
3644
|
+
});
|
|
3645
|
+
}
|
|
2737
3646
|
if (eff && eff.source === "handwritten" && eff.returns && !eff.conflict?.returns) {
|
|
2738
3647
|
const named = extractRefineReturnFromSource(source, name, {
|
|
2739
3648
|
loadModule: refineLoad,
|
|
2740
3649
|
fromFile: refineFrom
|
|
2741
3650
|
});
|
|
2742
3651
|
const display = named?.name ?? formatConstraint(eff.returns.constraint);
|
|
2743
|
-
|
|
3652
|
+
const contractSource = named ? "directive" : "sidecar";
|
|
3653
|
+
returnContracts.set(name, {
|
|
3654
|
+
display,
|
|
3655
|
+
constraint: eff.returns.constraint,
|
|
3656
|
+
source: contractSource
|
|
3657
|
+
});
|
|
3658
|
+
const consumedAssume = assumePhi ? new Set(
|
|
3659
|
+
assumePhi.display.filter(
|
|
3660
|
+
(_, i) => assumeConsumed().includes(assumePhi.premises[i])
|
|
3661
|
+
)
|
|
3662
|
+
) : void 0;
|
|
2744
3663
|
const retIssues = checkReturnConstraint(
|
|
2745
3664
|
name,
|
|
2746
3665
|
display,
|
|
2747
3666
|
eff.returns.constraint,
|
|
2748
3667
|
g.symbolic,
|
|
2749
|
-
|
|
2750
|
-
|
|
3668
|
+
{
|
|
3669
|
+
// #369 / #372:issue 构造处带函数声明位置
|
|
3670
|
+
loc: findFnLoc(file, name),
|
|
3671
|
+
source: contractSource,
|
|
3672
|
+
assumeDisplay: assumePhi?.display,
|
|
3673
|
+
invariantDisplay: inv?.display,
|
|
3674
|
+
consumedAssume,
|
|
3675
|
+
// Bug 1094:返回点的**路径前提 Φ** = 用户引理前提
|
|
3676
|
+
//(`@nudo:assume` 的前提 Φ)——证明通道的 Φ 侧。
|
|
3677
|
+
// 项身份:前提项用**真实形参名**(`termVar(name)`),泛化后的返回
|
|
3678
|
+
// 项在形参**有契约**时同样锚在真实名上(`constraintToEntryAbs`)
|
|
3679
|
+
// ⇒ 同项判定能对上;无契约时返回项锚在 α 名(A1…)⇒ 对不上 ⇒
|
|
3680
|
+
// Φ 侧不生效(保守少证,不假红)。
|
|
3681
|
+
phi: assumePhi?.phi,
|
|
3682
|
+
phiPremises: assumePhi?.premises
|
|
3683
|
+
}
|
|
2751
3684
|
);
|
|
2752
3685
|
if (retIssues.some((i) => i.severity === "error")) returnViolated.add(name);
|
|
2753
3686
|
issues.push(...retIssues);
|
|
@@ -2760,7 +3693,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2760
3693
|
});
|
|
2761
3694
|
}
|
|
2762
3695
|
}
|
|
2763
|
-
if (g.symbolic.conf === "opaque" && !truncated.has(name)) {
|
|
3696
|
+
if (g.symbolic.conf === "opaque" && !truncated.has(name) && !g.envGated) {
|
|
2764
3697
|
issues.push({
|
|
2765
3698
|
severity: "info",
|
|
2766
3699
|
code: DIAG.opaqueResult,
|
|
@@ -2839,16 +3772,19 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2839
3772
|
fn: label
|
|
2840
3773
|
});
|
|
2841
3774
|
}
|
|
2842
|
-
const records = [];
|
|
2843
3775
|
const varAbs = /* @__PURE__ */ new Map();
|
|
2844
3776
|
const callRecords = [];
|
|
2845
3777
|
const bCalls = [];
|
|
2846
|
-
const prevAssign = setEvalAssignCollector((r) => records.push(r));
|
|
2847
3778
|
const prevCall = setEvalCallCollector((r) => bCalls.push(r));
|
|
2848
3779
|
let evalBindings;
|
|
2849
3780
|
const evalAnalyze = evalAnalyzeOpts(opts);
|
|
3781
|
+
let moduleThrow;
|
|
2850
3782
|
try {
|
|
2851
|
-
const
|
|
3783
|
+
const evalAttempt = tryRunTranspiledDetailed(source, evalAnalyze);
|
|
3784
|
+
const evalRun = evalAttempt.run;
|
|
3785
|
+
if (evalRun === void 0 && evalAttempt.fallback?.reason === "module-throw") {
|
|
3786
|
+
moduleThrow = definiteModuleThrowOf(file);
|
|
3787
|
+
}
|
|
2852
3788
|
evalBindings = evalRun ? bindingsOf(evalRun) : void 0;
|
|
2853
3789
|
if (evalBindings) {
|
|
2854
3790
|
for (const [k, v2] of evalBindings) {
|
|
@@ -2871,6 +3807,22 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2871
3807
|
setEvalCallCollector(prevCall);
|
|
2872
3808
|
}
|
|
2873
3809
|
void evalBindings;
|
|
3810
|
+
if (moduleThrow !== void 0 && entryThrowsMode !== "off") {
|
|
3811
|
+
const kind = moduleThrow.kind;
|
|
3812
|
+
if (kind === void 0 || !isThrowsIgnored(kind, ignoreThrows)) {
|
|
3813
|
+
const thrown = kind !== void 0 ? ` ${kind}` : "";
|
|
3814
|
+
issues.push({
|
|
3815
|
+
severity: entryThrowsMode === "warning" ? "warning" : "error",
|
|
3816
|
+
code: DIAG.moduleThrow,
|
|
3817
|
+
message: `module initializer always throws${thrown} \u2014 the module cannot be imported, so every export is unavailable (module not analyzed)`,
|
|
3818
|
+
actual: `throw at line ${moduleThrow.line}`,
|
|
3819
|
+
expected: "module initializer that can complete, or a conditional throw",
|
|
3820
|
+
suggestion: kind !== void 0 ? `guard the throw if the module should be importable; if throwing at load is intended, ignore it with --ignore-throws ${kind} (or --entry-throws off)` : "guard the throw if the module should be importable; --entry-throws off disables this gate",
|
|
3821
|
+
line: moduleThrow.line,
|
|
3822
|
+
column: moduleThrow.column
|
|
3823
|
+
});
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
2874
3826
|
const callIssues = canSkipLiteralCallScan(source, file) ? [] : scanLiteralCalls(source, names, phi, {
|
|
2875
3827
|
loadModule: opts.loadModule,
|
|
2876
3828
|
// 与 generalize / 返回后置同一 fromFile 口径:ambient 侧车按真实
|
|
@@ -2896,15 +3848,22 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2896
3848
|
rec.fnName,
|
|
2897
3849
|
rc.display,
|
|
2898
3850
|
rc.constraint,
|
|
2899
|
-
rec.result
|
|
3851
|
+
rec.result,
|
|
3852
|
+
{
|
|
3853
|
+
// #369 / #372:issue 构造处带函数声明位置(被违约函数的声明处)
|
|
3854
|
+
loc: findFnLoc(file, rec.fnName),
|
|
3855
|
+
// #378:约束来源随 returnContracts 走(源码指令 vs 侧车 fn() 返回槽)
|
|
3856
|
+
source: rc.source
|
|
3857
|
+
}
|
|
2900
3858
|
);
|
|
2901
3859
|
if (retIssues.some((i) => i.severity === "error")) returnViolated.add(rec.fnName);
|
|
2902
3860
|
issues.push(...retIssues);
|
|
2903
3861
|
}
|
|
2904
3862
|
}
|
|
2905
3863
|
if (driftCandidates.length > 0) {
|
|
3864
|
+
const driftRecords = opts.from && opts.from.length > 0 ? [...callRecords, ...opts.from.map((r) => ({ fnName: r.fnName, args: r.args, result: r.result }))] : callRecords;
|
|
2906
3865
|
issues.push(
|
|
2907
|
-
...interfaceDriftIssues(driftCandidates,
|
|
3866
|
+
...interfaceDriftIssues(driftCandidates, driftRecords, /* @__PURE__ */ (() => {
|
|
2908
3867
|
let driftRun;
|
|
2909
3868
|
let driftInit = false;
|
|
2910
3869
|
return (fnName, args) => {
|
|
@@ -2934,7 +3893,16 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2934
3893
|
...opts.inject ? { inject: opts.inject } : {}
|
|
2935
3894
|
})
|
|
2936
3895
|
);
|
|
2937
|
-
|
|
3896
|
+
try {
|
|
3897
|
+
issues.push(...structuralAssignIssues(dedupeAssignRecords(records)));
|
|
3898
|
+
} catch (err) {
|
|
3899
|
+
issues.push({
|
|
3900
|
+
severity: "warning",
|
|
3901
|
+
code: DIAG.internal,
|
|
3902
|
+
message: `assign channel check failed: ${err.message}`,
|
|
3903
|
+
suggestion: "this is an internal error; please report it together with the checked source file"
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
2938
3906
|
const errors = issues.filter((i) => i.severity === "error").length;
|
|
2939
3907
|
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
2940
3908
|
const infos = issues.filter((i) => i.severity === "info").length;
|
|
@@ -2948,60 +3916,6 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2948
3916
|
budget: { ...budget }
|
|
2949
3917
|
};
|
|
2950
3918
|
}
|
|
2951
|
-
function checkReturnConstraint(fnName, cName, constraint, ret, assumeDisplay, invariantDisplay) {
|
|
2952
|
-
const proof = assertImplies(ret, constraint);
|
|
2953
|
-
if (proof.status === "proved") {
|
|
2954
|
-
const relied = [];
|
|
2955
|
-
if (assumeDisplay && assumeDisplay.length > 0) {
|
|
2956
|
-
relied.push({
|
|
2957
|
-
severity: "info",
|
|
2958
|
-
code: DIAG.assumeRelied,
|
|
2959
|
-
message: `${fnName}: return contract proved via @nudo:assume ${assumeDisplay.join("; ")}`,
|
|
2960
|
-
actual: `proof relies on user lemma${assumeDisplay.length > 1 ? "s" : ""}`,
|
|
2961
|
-
expected: "engine verifies the obligation, not the lemma itself",
|
|
2962
|
-
suggestion: "@nudo:assume is a user-supplied axiom \u2014 falsify it with concrete @nudo:case runs (nudo test)",
|
|
2963
|
-
fn: fnName
|
|
2964
|
-
});
|
|
2965
|
-
}
|
|
2966
|
-
if (invariantDisplay && invariantDisplay.length > 0) {
|
|
2967
|
-
relied.push({
|
|
2968
|
-
severity: "info",
|
|
2969
|
-
code: DIAG.invariantRelied,
|
|
2970
|
-
message: `${fnName}: return contract proved via @nudo:invariant ${invariantDisplay.join("; ")}`,
|
|
2971
|
-
actual: `proof relies on user invariant${invariantDisplay.length > 1 ? "s" : ""}`,
|
|
2972
|
-
expected: "engine verifies the obligation, not the loop invariant itself",
|
|
2973
|
-
suggestion: "@nudo:invariant is a user-supplied axiom \u2014 falsify it with concrete @nudo:case runs (nudo test)",
|
|
2974
|
-
fn: fnName
|
|
2975
|
-
});
|
|
2976
|
-
}
|
|
2977
|
-
return relied;
|
|
2978
|
-
}
|
|
2979
|
-
const push = (severity, actual, expected, suggestion) => [
|
|
2980
|
-
{
|
|
2981
|
-
severity,
|
|
2982
|
-
code: severity === "error" ? DIAG.constraintViolated : DIAG.unprovenReturn,
|
|
2983
|
-
message: `${fnName}: return value \u22AD @nudo:contract return ${cName}`,
|
|
2984
|
-
actual,
|
|
2985
|
-
expected,
|
|
2986
|
-
suggestion,
|
|
2987
|
-
fn: fnName
|
|
2988
|
-
}
|
|
2989
|
-
];
|
|
2990
|
-
if (proof.status === "disproved") {
|
|
2991
|
-
return push(
|
|
2992
|
-
"error",
|
|
2993
|
-
formatAbs(ret),
|
|
2994
|
-
proof.reason,
|
|
2995
|
-
proof.reason.includes("nullish") ? `use nullable(${formatConstraint(constraint)}) or union(..., lit(null)) to allow nullish returns` : `return a value satisfying ${formatConstraint(constraint)}`
|
|
2996
|
-
);
|
|
2997
|
-
}
|
|
2998
|
-
return push(
|
|
2999
|
-
"warning",
|
|
3000
|
-
formatAbs(ret),
|
|
3001
|
-
formatConstraint(constraint),
|
|
3002
|
-
`${proof.reason}; add a precondition, narrow the return expression, or relax the return contract`
|
|
3003
|
-
);
|
|
3004
|
-
}
|
|
3005
3919
|
export {
|
|
3006
3920
|
$add,
|
|
3007
3921
|
$arguments,
|