@nudojs/core 1.15.2 → 1.17.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-NXZSRLCT.js → chunk-4MRLOJMI.js} +4141 -1119
- package/dist/{chunk-WKIRPUHZ.js → chunk-ARIQO5XX.js} +33102 -23074
- package/dist/exec.d.ts +666 -129
- package/dist/exec.js +16 -16
- package/dist/index.d.ts +428 -96
- package/dist/index.js +1244 -517
- package/dist/internal.d.ts +82 -43
- package/dist/internal.js +144 -141
- package/dist/{projection-budget-07VnMktV.d.ts → projection-budget-BkFV9pcs.d.ts} +178 -42
- package/dist/{run-DfloWX6i.d.ts → run-tV2J7PNI.d.ts} +114 -41
- package/package.json +1 -1
- /package/dist/{chunk-US532PZA.js → chunk-II73GZOY.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import "./chunk-
|
|
1
|
+
import "./chunk-II73GZOY.js";
|
|
2
2
|
import {
|
|
3
|
+
DependencyMemo,
|
|
3
4
|
INTERFACE_SOURCE_ABBR,
|
|
4
5
|
NudoSidecarError,
|
|
6
|
+
absWithStableTermIds,
|
|
5
7
|
actionsForIssue,
|
|
6
8
|
buildArgsFromAssume,
|
|
7
9
|
canSkipLiteralCallScan,
|
|
10
|
+
checkArg,
|
|
11
|
+
checkCall,
|
|
8
12
|
collectAbsFreeVars,
|
|
13
|
+
contractSegmentLocs,
|
|
14
|
+
ctorParamCountOf,
|
|
15
|
+
deferredExecutionSpans,
|
|
9
16
|
effectiveInterface,
|
|
10
17
|
evictGeneralizeMemoForPaths,
|
|
11
18
|
execNudoModule,
|
|
@@ -27,28 +34,36 @@ import {
|
|
|
27
34
|
fnTopLevelLocalNames,
|
|
28
35
|
formatCheckReport,
|
|
29
36
|
formatConstraint,
|
|
37
|
+
formatDiagnostics,
|
|
30
38
|
formatEffectiveInterfaceDisplay,
|
|
31
39
|
formatGithubAnnotations,
|
|
32
40
|
formatGitlabCodeQuality,
|
|
33
41
|
formatInterfaceTierLine,
|
|
34
|
-
generalizeAll,
|
|
35
42
|
generalizeFromAst,
|
|
36
43
|
generatedExportNames,
|
|
37
44
|
getGeneralizeMemoSize,
|
|
45
|
+
guardLoadModule,
|
|
38
46
|
hashSource,
|
|
47
|
+
instanceForMethodBridge,
|
|
39
48
|
interfaceDiagCount,
|
|
40
49
|
interfaceSourceOf,
|
|
41
50
|
interfaceTierOf,
|
|
42
51
|
isNodeModulesPath,
|
|
52
|
+
listClassEntryNames,
|
|
43
53
|
listFnDirectiveScopes,
|
|
44
54
|
listFunctionNames,
|
|
45
55
|
listTopFunctions,
|
|
46
56
|
loadModuleDepsFingerprint,
|
|
57
|
+
loadModuleId,
|
|
47
58
|
localNamedExports,
|
|
59
|
+
moduleMapId,
|
|
60
|
+
nodeSpan,
|
|
48
61
|
normPath,
|
|
49
62
|
parseEnvPayload,
|
|
50
63
|
parseMockModulePayload,
|
|
51
64
|
parseNudoImportPayload,
|
|
65
|
+
projectAbsPath,
|
|
66
|
+
reachableTopBindings,
|
|
52
67
|
refineDiagCount,
|
|
53
68
|
refineToIndexedFull,
|
|
54
69
|
resetGeneralizeMemo,
|
|
@@ -56,6 +71,7 @@ import {
|
|
|
56
71
|
resetNudoModuleExecCache,
|
|
57
72
|
resetSidecarLoadFailureCache,
|
|
58
73
|
resolveDepPath,
|
|
74
|
+
resolveExportSlotCall,
|
|
59
75
|
scanBudgetDecl,
|
|
60
76
|
scanCaseArgSpans,
|
|
61
77
|
scanCaseTags,
|
|
@@ -66,6 +82,7 @@ import {
|
|
|
66
82
|
serializeCheckJsonMulti,
|
|
67
83
|
setInterfaceDiagCollector,
|
|
68
84
|
setRefineDiagCollector,
|
|
85
|
+
sidecarAutoBindAllowed,
|
|
69
86
|
sidecarClosureFingerprint,
|
|
70
87
|
sidecarPathOf,
|
|
71
88
|
stableAnalyzeKeySource,
|
|
@@ -74,7 +91,7 @@ import {
|
|
|
74
91
|
takeInterfaceDiagsSince,
|
|
75
92
|
takeRefineDiags,
|
|
76
93
|
takeRefineDiagsSince
|
|
77
|
-
} from "./chunk-
|
|
94
|
+
} from "./chunk-4MRLOJMI.js";
|
|
78
95
|
import {
|
|
79
96
|
$add,
|
|
80
97
|
$arguments,
|
|
@@ -84,7 +101,6 @@ import {
|
|
|
84
101
|
$arrWithHoles,
|
|
85
102
|
$assignRecord,
|
|
86
103
|
$async,
|
|
87
|
-
$asyncReturn,
|
|
88
104
|
$await,
|
|
89
105
|
$bitand,
|
|
90
106
|
$bitnot,
|
|
@@ -108,7 +124,6 @@ import {
|
|
|
108
124
|
$fnVal,
|
|
109
125
|
$for,
|
|
110
126
|
$forInKeys,
|
|
111
|
-
$forIter,
|
|
112
127
|
$forOf,
|
|
113
128
|
$fork,
|
|
114
129
|
$ge,
|
|
@@ -163,8 +178,6 @@ import {
|
|
|
163
178
|
$sub,
|
|
164
179
|
$super,
|
|
165
180
|
$switch,
|
|
166
|
-
$thisGet,
|
|
167
|
-
$thisSet,
|
|
168
181
|
$throw,
|
|
169
182
|
$toNumber,
|
|
170
183
|
$toNumeric,
|
|
@@ -183,7 +196,6 @@ import {
|
|
|
183
196
|
$updateAdd,
|
|
184
197
|
$updateSub,
|
|
185
198
|
$ushr,
|
|
186
|
-
$while,
|
|
187
199
|
$whileSeq,
|
|
188
200
|
$yield,
|
|
189
201
|
CONSTRAINT_BUILDERS,
|
|
@@ -193,6 +205,7 @@ import {
|
|
|
193
205
|
DIAG,
|
|
194
206
|
FORK_TRUNCATION_LABEL,
|
|
195
207
|
HOST_EFFECT_LABEL_PREFIX,
|
|
208
|
+
LAZY_TEMPLATE_DEPTH,
|
|
196
209
|
LOOP_TRUNCATION_LABEL,
|
|
197
210
|
MATH_FOLD_ERROR_LABEL,
|
|
198
211
|
MAX_EVAL_CALL_DEPTH,
|
|
@@ -213,8 +226,8 @@ import {
|
|
|
213
226
|
absOnly,
|
|
214
227
|
absShapeKey,
|
|
215
228
|
absToString,
|
|
229
|
+
accessorReadDomain,
|
|
216
230
|
add,
|
|
217
|
-
alphaOf,
|
|
218
231
|
and,
|
|
219
232
|
andC,
|
|
220
233
|
any,
|
|
@@ -227,11 +240,11 @@ import {
|
|
|
227
240
|
asAbs,
|
|
228
241
|
asAbsVal,
|
|
229
242
|
assignSourceSlots,
|
|
243
|
+
assumeConsumed,
|
|
230
244
|
assumeFinite,
|
|
231
245
|
attachContractFaces,
|
|
232
246
|
attachFnImpl,
|
|
233
247
|
beginCollectionFork,
|
|
234
|
-
betaOf,
|
|
235
248
|
bigintLit,
|
|
236
249
|
bindImports,
|
|
237
250
|
bindingsOf,
|
|
@@ -262,7 +275,6 @@ import {
|
|
|
262
275
|
constraintToEntryAbs,
|
|
263
276
|
contractFacesFromEffectiveInterface,
|
|
264
277
|
contractParamNameSet,
|
|
265
|
-
createHofCollectCtx,
|
|
266
278
|
ctorArgDefinitelyInvalid,
|
|
267
279
|
ctorNameOfRecv,
|
|
268
280
|
currentExecPhi,
|
|
@@ -345,6 +357,7 @@ import {
|
|
|
345
357
|
isDefinitelyTrue,
|
|
346
358
|
isErrorCtorName,
|
|
347
359
|
isExactLit,
|
|
360
|
+
isHiddenInternalSlot,
|
|
348
361
|
isIntFlag,
|
|
349
362
|
isMapAbs,
|
|
350
363
|
isNudoBreak,
|
|
@@ -362,6 +375,7 @@ import {
|
|
|
362
375
|
isSetAbs,
|
|
363
376
|
isStrPrim,
|
|
364
377
|
isSymbolAbs,
|
|
378
|
+
isThrowsIgnored,
|
|
365
379
|
joinAbs,
|
|
366
380
|
joinFunctions,
|
|
367
381
|
joinObjects,
|
|
@@ -415,6 +429,7 @@ import {
|
|
|
415
429
|
never,
|
|
416
430
|
not,
|
|
417
431
|
notAbs,
|
|
432
|
+
noteAssumeConsumed,
|
|
418
433
|
noteCollectionWrite,
|
|
419
434
|
noteEvalCallRecord,
|
|
420
435
|
noteEvalFallback,
|
|
@@ -424,6 +439,7 @@ import {
|
|
|
424
439
|
numLit,
|
|
425
440
|
numVar,
|
|
426
441
|
number,
|
|
442
|
+
numericBoundsImply,
|
|
427
443
|
obj,
|
|
428
444
|
objOf,
|
|
429
445
|
objectProtoBrand,
|
|
@@ -443,7 +459,6 @@ import {
|
|
|
443
459
|
predVars,
|
|
444
460
|
primToTypeof,
|
|
445
461
|
projectFlatMapResult,
|
|
446
|
-
promoteParamShape,
|
|
447
462
|
propertyKeyOf,
|
|
448
463
|
protoBrandAbs,
|
|
449
464
|
protoOfRecv,
|
|
@@ -460,6 +475,7 @@ import {
|
|
|
460
475
|
relationFn,
|
|
461
476
|
requiredFnArity,
|
|
462
477
|
resetAbsCallBudget,
|
|
478
|
+
resetAssumeConsumed,
|
|
463
479
|
resetEvalCallBudget,
|
|
464
480
|
resetParseSourceCache,
|
|
465
481
|
runTranspiled,
|
|
@@ -521,11 +537,8 @@ import {
|
|
|
521
537
|
transpileSource,
|
|
522
538
|
trueConstraint,
|
|
523
539
|
tryMakeRegexAbs,
|
|
524
|
-
tryPromoteDirectCall,
|
|
525
|
-
tryPromoteForOfIteratee,
|
|
526
|
-
tryPromoteHofCallback,
|
|
527
|
-
tryPromoteReceiverAsArr,
|
|
528
540
|
tryRunTranspiled,
|
|
541
|
+
tryRunTranspiledDetailed,
|
|
529
542
|
typeofAbs,
|
|
530
543
|
undefAbs,
|
|
531
544
|
union,
|
|
@@ -536,7 +549,7 @@ import {
|
|
|
536
549
|
withExecPhi,
|
|
537
550
|
withInvariantSpecs,
|
|
538
551
|
withVar
|
|
539
|
-
} from "./chunk-
|
|
552
|
+
} from "./chunk-ARIQO5XX.js";
|
|
540
553
|
|
|
541
554
|
// src/environment.ts
|
|
542
555
|
function createEnvironment(parent, bindings = /* @__PURE__ */ new Map()) {
|
|
@@ -625,150 +638,6 @@ function mock() {
|
|
|
625
638
|
return { kind: "mock-helper" };
|
|
626
639
|
}
|
|
627
640
|
|
|
628
|
-
// src/algebra/phi.ts
|
|
629
|
-
var phiStack = [pTrue];
|
|
630
|
-
function currentPhi() {
|
|
631
|
-
return phiStack[phiStack.length - 1] ?? pTrue;
|
|
632
|
-
}
|
|
633
|
-
function pushPhi(p) {
|
|
634
|
-
phiStack.push(p);
|
|
635
|
-
}
|
|
636
|
-
function popPhi() {
|
|
637
|
-
if (phiStack.length > 1) phiStack.pop();
|
|
638
|
-
}
|
|
639
|
-
function resetPhi() {
|
|
640
|
-
phiStack = [pTrue];
|
|
641
|
-
}
|
|
642
|
-
function withPhiConstraint(extra, body) {
|
|
643
|
-
pushPhi(extra);
|
|
644
|
-
try {
|
|
645
|
-
body();
|
|
646
|
-
} finally {
|
|
647
|
-
popPhi();
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
function describePhi() {
|
|
651
|
-
const p = currentPhi();
|
|
652
|
-
return p.op === "true" ? "\u22A4" : "(phi)";
|
|
653
|
-
}
|
|
654
|
-
|
|
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
641
|
// src/algebra/check.ts
|
|
773
642
|
import { isBuiltin } from "module";
|
|
774
643
|
|
|
@@ -794,12 +663,43 @@ function isValueSetUnknown(ret) {
|
|
|
794
663
|
if (ret.shape.k === "any") return true;
|
|
795
664
|
return false;
|
|
796
665
|
}
|
|
797
|
-
function isBoundedEvidenceMissing(ret, constraint) {
|
|
666
|
+
function isBoundedEvidenceMissing(ret, constraint, phi) {
|
|
798
667
|
if (constraint.preds.length === 0 && !constraint.members) return false;
|
|
799
668
|
if (ret.term?.op === "lit") return false;
|
|
800
669
|
if (ret.pred && ret.pred.op !== "true") return false;
|
|
670
|
+
if (phiSuppliesConstraint(phi, constraint, ret)) return false;
|
|
801
671
|
return true;
|
|
802
672
|
}
|
|
673
|
+
function phiSuppliesConstraint(phi, constraint, ret) {
|
|
674
|
+
if (!phi || phi.op === "true") return false;
|
|
675
|
+
const item = retAnchorTerm(ret);
|
|
676
|
+
if (!hasSameItemBoundEvidence(phi, item)) return false;
|
|
677
|
+
return implies(phi, instantiateOnTerm(constraint, item));
|
|
678
|
+
}
|
|
679
|
+
function hasSameItemBoundEvidence(p, item) {
|
|
680
|
+
if (!p) return false;
|
|
681
|
+
switch (p.op) {
|
|
682
|
+
case "gt":
|
|
683
|
+
case "ge":
|
|
684
|
+
case "lt":
|
|
685
|
+
case "le":
|
|
686
|
+
return sameItemTerm(item, p.a);
|
|
687
|
+
case "and":
|
|
688
|
+
case "or":
|
|
689
|
+
return p.args.some((a) => hasSameItemBoundEvidence(a, item));
|
|
690
|
+
case "not":
|
|
691
|
+
return hasSameItemBoundEvidence(p.arg, item);
|
|
692
|
+
default:
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function sameItemTerm(item, left) {
|
|
697
|
+
if (termEquals(item, left)) return true;
|
|
698
|
+
if (left.op === "app" && left.fn === "length" && left.args.length === 1) {
|
|
699
|
+
return termEquals(item, left.args[0]);
|
|
700
|
+
}
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
803
703
|
function flattenSumArms(ret) {
|
|
804
704
|
if (ret.shape.k !== "sum") return [ret];
|
|
805
705
|
const out = [];
|
|
@@ -956,7 +856,11 @@ function assertImpliesSingle(arm, constraint, opts, phi) {
|
|
|
956
856
|
}
|
|
957
857
|
return r.status === "unprovable" ? unprovable(`array element: ${r.reason}`) : proved();
|
|
958
858
|
}
|
|
959
|
-
if (constraint.fn)
|
|
859
|
+
if (constraint.fn) {
|
|
860
|
+
return unprovable(
|
|
861
|
+
"fn-shaped return contract cannot be enforced (declare the returned value as the fn's returns constraint)"
|
|
862
|
+
);
|
|
863
|
+
}
|
|
960
864
|
return assertScalar(arm, constraint, phi);
|
|
961
865
|
}
|
|
962
866
|
function isAnyConstraint(c) {
|
|
@@ -981,7 +885,7 @@ function assertShapeFields(ret, constraint, opts) {
|
|
|
981
885
|
}
|
|
982
886
|
continue;
|
|
983
887
|
}
|
|
984
|
-
const r = assertImplies(slot.value, field.constraint, opts);
|
|
888
|
+
const r = assertImplies(accessorReadDomain(ret, key) ?? slot.value, field.constraint, opts);
|
|
985
889
|
if (r.status === "disproved") {
|
|
986
890
|
return disproved(`field ${key}: ${r.reason}`);
|
|
987
891
|
}
|
|
@@ -1022,10 +926,14 @@ function assertScalar(ret, constraint, phi) {
|
|
|
1022
926
|
}
|
|
1023
927
|
if (constraintPred.op !== "true") {
|
|
1024
928
|
if (implies(retPred, constraintPred)) return proved();
|
|
1025
|
-
if (ret.pred && ret.pred.op !== "true" &&
|
|
929
|
+
if (phi.op !== "true" && ret.pred && ret.pred.op !== "true" && implies(ret.pred, constraintPred)) {
|
|
1026
930
|
return proved();
|
|
1027
931
|
}
|
|
1028
|
-
if (
|
|
932
|
+
if (ret.pred && ret.pred.op !== "true" && numericBoundsImply(ret.pred, constraintPred)) {
|
|
933
|
+
return proved();
|
|
934
|
+
}
|
|
935
|
+
if (phiSuppliesConstraint(phi, constraint, ret)) return proved();
|
|
936
|
+
if (isBoundedEvidenceMissing(ret, constraint, phi)) {
|
|
1029
937
|
return unprovable(
|
|
1030
938
|
`cannot prove return satisfies ${formatConstraint(constraint)} (no bound evidence on the return value)`
|
|
1031
939
|
);
|
|
@@ -1044,7 +952,7 @@ function assertScalar(ret, constraint, phi) {
|
|
|
1044
952
|
}
|
|
1045
953
|
}
|
|
1046
954
|
if (constraintPred.op !== "true" || constraint.preds.length > 0) {
|
|
1047
|
-
if (isBoundedEvidenceMissing(ret, constraint)) {
|
|
955
|
+
if (isBoundedEvidenceMissing(ret, constraint, phi)) {
|
|
1048
956
|
return unprovable(
|
|
1049
957
|
`cannot prove return satisfies ${formatConstraint(constraint)} (no bound evidence on the return value)`
|
|
1050
958
|
);
|
|
@@ -1055,36 +963,6 @@ function assertScalar(ret, constraint, phi) {
|
|
|
1055
963
|
}
|
|
1056
964
|
return proved();
|
|
1057
965
|
}
|
|
1058
|
-
function numericBoundsImplyLocal(src, tgt) {
|
|
1059
|
-
const one = (p) => p.op === "gt" || p.op === "ge" || p.op === "lt" || p.op === "le";
|
|
1060
|
-
if (!one(src) || !one(tgt)) return false;
|
|
1061
|
-
if (src.op !== tgt.op) {
|
|
1062
|
-
if (src.op === "gt" && tgt.op === "ge") {
|
|
1063
|
-
return sameTermSide(src, tgt) && litGE(src.b, tgt.b);
|
|
1064
|
-
}
|
|
1065
|
-
if (src.op === "lt" && tgt.op === "le") {
|
|
1066
|
-
return sameTermSide(src, tgt) && litLE(src.b, tgt.b);
|
|
1067
|
-
}
|
|
1068
|
-
return false;
|
|
1069
|
-
}
|
|
1070
|
-
if (!sameTermSide(src, tgt)) return false;
|
|
1071
|
-
const sb = src.b.op === "lit" ? src.b.value : void 0;
|
|
1072
|
-
const tb = tgt.b.op === "lit" ? tgt.b.value : void 0;
|
|
1073
|
-
if (typeof sb !== "number" || typeof tb !== "number") return false;
|
|
1074
|
-
if (src.op === "gt" || src.op === "ge") return sb >= tb;
|
|
1075
|
-
return sb <= tb;
|
|
1076
|
-
}
|
|
1077
|
-
function sameTermSide(a, b) {
|
|
1078
|
-
return a.a.op === b.a.op && termEquals(a.a, b.a);
|
|
1079
|
-
}
|
|
1080
|
-
function litGE(a, b) {
|
|
1081
|
-
if (a.op !== "lit" || b.op !== "lit") return false;
|
|
1082
|
-
return typeof a.value === "number" && typeof b.value === "number" && a.value >= b.value;
|
|
1083
|
-
}
|
|
1084
|
-
function litLE(a, b) {
|
|
1085
|
-
if (a.op !== "lit" || b.op !== "lit") return false;
|
|
1086
|
-
return typeof a.value === "number" && typeof b.value === "number" && a.value <= b.value;
|
|
1087
|
-
}
|
|
1088
966
|
function primOfAbs(a) {
|
|
1089
967
|
if (a.shape.k === "prim") return a.shape.type;
|
|
1090
968
|
const lvR = litValue(a);
|
|
@@ -1127,6 +1005,104 @@ function isLengthSelfTerm(t) {
|
|
|
1127
1005
|
return t.op === "app" && t.fn === "length" && t.args.length === 1 && t.args[0].op === "var" && t.args[0].id === SELF;
|
|
1128
1006
|
}
|
|
1129
1007
|
|
|
1008
|
+
// src/algebra/check-return.ts
|
|
1009
|
+
function checkReturnConstraint(fnName, cName, constraint, ret, audit = {}) {
|
|
1010
|
+
const {
|
|
1011
|
+
loc,
|
|
1012
|
+
source = "directive",
|
|
1013
|
+
assumeDisplay,
|
|
1014
|
+
invariantDisplay,
|
|
1015
|
+
consumedAssume,
|
|
1016
|
+
phi,
|
|
1017
|
+
phiPremises
|
|
1018
|
+
} = audit;
|
|
1019
|
+
const proof = assertImplies(ret, constraint, phi ? { phi } : void 0);
|
|
1020
|
+
const phiUsedIdx = /* @__PURE__ */ new Set();
|
|
1021
|
+
if (phi && proof.status === "proved" && phiPremises && phiPremises.length > 0) {
|
|
1022
|
+
const withoutPhi = assertImplies(ret, constraint);
|
|
1023
|
+
if (withoutPhi.status !== "proved") {
|
|
1024
|
+
const solo = phiPremises.filter(
|
|
1025
|
+
(p) => phiSuppliesConstraint(p, constraint, ret)
|
|
1026
|
+
);
|
|
1027
|
+
const used = solo.length > 0 ? solo : [...phiPremises];
|
|
1028
|
+
for (const p of used) noteAssumeConsumed(p);
|
|
1029
|
+
phiPremises.forEach((p, i) => {
|
|
1030
|
+
if (used.includes(p)) phiUsedIdx.add(i);
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const where = () => {
|
|
1035
|
+
if (loc?.line === void 0) return {};
|
|
1036
|
+
return loc.column === void 0 ? { line: loc.line } : { line: loc.line, column: loc.column };
|
|
1037
|
+
};
|
|
1038
|
+
const origin = source === "sidecar" ? `sidecar fn() return slot (${cName})` : `@nudo:contract return ${cName}`;
|
|
1039
|
+
if (proof.status === "proved") {
|
|
1040
|
+
const ledgerUsedIdx = /* @__PURE__ */ new Set();
|
|
1041
|
+
if (assumeDisplay && phiPremises) {
|
|
1042
|
+
const consumed = assumeConsumed();
|
|
1043
|
+
for (let i = 0; i < assumeDisplay.length; i++) {
|
|
1044
|
+
const premise = phiPremises[i];
|
|
1045
|
+
if (premise !== void 0 && consumed.includes(premise)) ledgerUsedIdx.add(i);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
const relied = [];
|
|
1049
|
+
const usedAssume = (assumeDisplay ?? []).filter(
|
|
1050
|
+
(d, i) => consumedAssume?.has(d) === true || ledgerUsedIdx.has(i) || phiUsedIdx.has(i)
|
|
1051
|
+
);
|
|
1052
|
+
if (usedAssume.length > 0) {
|
|
1053
|
+
relied.push({
|
|
1054
|
+
severity: "info",
|
|
1055
|
+
code: DIAG.assumeRelied,
|
|
1056
|
+
message: `${fnName}: return contract proved via @nudo:assume ${usedAssume.join("; ")}`,
|
|
1057
|
+
actual: `proof relies on user lemma${usedAssume.length > 1 ? "s" : ""}`,
|
|
1058
|
+
expected: "engine verifies the obligation, not the lemma itself",
|
|
1059
|
+
suggestion: "@nudo:assume is a user-supplied axiom \u2014 falsify it with concrete @nudo:case runs (nudo test)",
|
|
1060
|
+
fn: fnName,
|
|
1061
|
+
...where()
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
if (invariantDisplay && invariantDisplay.length > 0) {
|
|
1065
|
+
relied.push({
|
|
1066
|
+
severity: "info",
|
|
1067
|
+
code: DIAG.invariantRelied,
|
|
1068
|
+
message: `${fnName}: return contract proved via @nudo:invariant ${invariantDisplay.join("; ")}`,
|
|
1069
|
+
actual: `proof relies on user invariant${invariantDisplay.length > 1 ? "s" : ""}`,
|
|
1070
|
+
expected: "engine verifies the obligation, not the loop invariant itself",
|
|
1071
|
+
suggestion: "@nudo:invariant is a user-supplied axiom \u2014 falsify it with concrete @nudo:case runs (nudo test)",
|
|
1072
|
+
fn: fnName,
|
|
1073
|
+
...where()
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
return relied;
|
|
1077
|
+
}
|
|
1078
|
+
const push = (severity, actual, expected, suggestion) => [
|
|
1079
|
+
{
|
|
1080
|
+
severity,
|
|
1081
|
+
code: severity === "error" ? DIAG.constraintViolated : DIAG.unprovenReturn,
|
|
1082
|
+
message: `${fnName}: return value \u22AD ${origin}`,
|
|
1083
|
+
actual,
|
|
1084
|
+
expected,
|
|
1085
|
+
suggestion,
|
|
1086
|
+
fn: fnName,
|
|
1087
|
+
...where()
|
|
1088
|
+
}
|
|
1089
|
+
];
|
|
1090
|
+
if (proof.status === "disproved") {
|
|
1091
|
+
return push(
|
|
1092
|
+
"error",
|
|
1093
|
+
formatAbs(ret),
|
|
1094
|
+
proof.reason,
|
|
1095
|
+
proof.reason.includes("nullish") ? `use nullable(${formatConstraint(constraint)}) or union(..., lit(null)) to allow nullish returns` : `return a value satisfying ${formatConstraint(constraint)}`
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
return push(
|
|
1099
|
+
"warning",
|
|
1100
|
+
formatAbs(ret),
|
|
1101
|
+
formatConstraint(constraint),
|
|
1102
|
+
`${proof.reason}; add a precondition, narrow the return expression, or relax the return contract`
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1130
1106
|
// src/algebra/projection.ts
|
|
1131
1107
|
var PROJECTABLE_CONF = /* @__PURE__ */ new Set(["exact", "path"]);
|
|
1132
1108
|
var ARR_FALLBACK_CONF = /* @__PURE__ */ new Set([
|
|
@@ -1162,7 +1138,7 @@ function absToConstraintInner(a, budget) {
|
|
|
1162
1138
|
case "any":
|
|
1163
1139
|
return a.pred?.op === "or" ? projectOrLiterals(a) : void 0;
|
|
1164
1140
|
case "obj":
|
|
1165
|
-
return projectObj(
|
|
1141
|
+
return projectObj(a, s, budget);
|
|
1166
1142
|
case "arr":
|
|
1167
1143
|
return projectArr(s.element, budget);
|
|
1168
1144
|
case "sum":
|
|
@@ -1347,11 +1323,12 @@ function projectOrLiterals(a) {
|
|
|
1347
1323
|
}
|
|
1348
1324
|
return lits.length > 0 ? union(...lits) : void 0;
|
|
1349
1325
|
}
|
|
1350
|
-
function projectObj(
|
|
1351
|
-
if (index || open) return void 0;
|
|
1352
|
-
const fields =
|
|
1353
|
-
for (const [k, slot] of Object.entries(slots)) {
|
|
1354
|
-
|
|
1326
|
+
function projectObj(a, s, budget) {
|
|
1327
|
+
if (s.index || s.open) return void 0;
|
|
1328
|
+
const fields = /* @__PURE__ */ Object.create(null);
|
|
1329
|
+
for (const [k, slot] of Object.entries(s.slots)) {
|
|
1330
|
+
if (isHiddenInternalSlot(k, slot)) continue;
|
|
1331
|
+
const fc = absToConstraint(accessorReadDomain(a, k) ?? slot.value, budget);
|
|
1355
1332
|
if (!fc) return void 0;
|
|
1356
1333
|
fields[k] = { constraint: fc, ...slot.optional ? { optional: true } : {} };
|
|
1357
1334
|
}
|
|
@@ -1590,11 +1567,21 @@ function isDefaultExportName(source, fnName) {
|
|
|
1590
1567
|
);
|
|
1591
1568
|
return re.test(source);
|
|
1592
1569
|
}
|
|
1570
|
+
function formatEntryParamShape(t) {
|
|
1571
|
+
if (t.shape.k === "unknown") {
|
|
1572
|
+
const term = t.term;
|
|
1573
|
+
if (term?.op === "lit" && (term.value === null || term.value === void 0)) {
|
|
1574
|
+
return formatShape(t);
|
|
1575
|
+
}
|
|
1576
|
+
return "unknown";
|
|
1577
|
+
}
|
|
1578
|
+
return formatShape(t, { showLits: true });
|
|
1579
|
+
}
|
|
1593
1580
|
function formatEntrySigLine(name, g, throws) {
|
|
1594
1581
|
const names = g.formals && g.formals.length > 0 ? formalParamSignatureNames(g.formals) : g.params;
|
|
1595
1582
|
const ps = names.map((p, i) => {
|
|
1596
1583
|
const t = g.typeParams[i]?.value;
|
|
1597
|
-
const shown = !t ? "any" :
|
|
1584
|
+
const shown = !t ? "any" : formatEntryParamShape(t);
|
|
1598
1585
|
return `${p}: ${shown}`;
|
|
1599
1586
|
}).join(", ");
|
|
1600
1587
|
return `${name}(${ps}) => ${formatShape(g.symbolic)} throws ${throws}`;
|
|
@@ -1633,70 +1620,38 @@ function findFnLoc(file, name) {
|
|
|
1633
1620
|
}
|
|
1634
1621
|
return {};
|
|
1635
1622
|
}
|
|
1623
|
+
function formatSigDetail(absVal, name) {
|
|
1624
|
+
return formatAbsMultiline(absWithStableTermIds(absVal), name);
|
|
1625
|
+
}
|
|
1636
1626
|
function formatSigCached(absVal, name) {
|
|
1637
|
-
|
|
1627
|
+
let byName = sigFormatCache.get(absVal);
|
|
1628
|
+
if (!byName) {
|
|
1629
|
+
byName = /* @__PURE__ */ new Map();
|
|
1630
|
+
sigFormatCache.set(absVal, byName);
|
|
1631
|
+
}
|
|
1632
|
+
const hit = byName.get(name);
|
|
1638
1633
|
if (hit) return hit;
|
|
1639
1634
|
const out = {
|
|
1640
|
-
|
|
1641
|
-
|
|
1635
|
+
// **人类面口径**(`formatShape`):与 `formatCheckReport` 的签名行**逐字
|
|
1636
|
+
// 同源**(Bug 413:机器面此前用 `formatAbs` ⇒ 把 term / pred 段渲进
|
|
1637
|
+
// 对外载荷,其中混着引擎内部计数器 id `len#N` ⇒ 载荷随参数列表位置漂移)。
|
|
1638
|
+
display: formatShape(absVal),
|
|
1639
|
+
detail: formatSigDetail(absVal, name)
|
|
1642
1640
|
};
|
|
1643
|
-
|
|
1641
|
+
byName.set(name, out);
|
|
1644
1642
|
return out;
|
|
1645
1643
|
}
|
|
1646
1644
|
|
|
1647
1645
|
// src/algebra/check-memo.ts
|
|
1648
|
-
var checkReportMemo = /* @__PURE__ */ new Map();
|
|
1649
|
-
var checkKeyDeps = /* @__PURE__ */ new Map();
|
|
1650
|
-
var checkDepIndex = /* @__PURE__ */ new Map();
|
|
1651
|
-
var loadModuleIds = /* @__PURE__ */ new WeakMap();
|
|
1652
|
-
var nextLoadModuleId = 1;
|
|
1653
1646
|
var MAX_CHECK_MEMO = 256;
|
|
1647
|
+
var checkReportMemo = new DependencyMemo(MAX_CHECK_MEMO);
|
|
1654
1648
|
function resetCheckSourceMemo() {
|
|
1655
|
-
checkReportMemo.
|
|
1656
|
-
checkKeyDeps.clear();
|
|
1657
|
-
checkDepIndex.clear();
|
|
1649
|
+
checkReportMemo.reset();
|
|
1658
1650
|
resetHashSourceCache();
|
|
1659
1651
|
resetSidecarLoadFailureCache();
|
|
1660
1652
|
}
|
|
1661
|
-
function loadModuleId(fn2) {
|
|
1662
|
-
if (!fn2) return 0;
|
|
1663
|
-
let id = loadModuleIds.get(fn2);
|
|
1664
|
-
if (id === void 0) {
|
|
1665
|
-
id = nextLoadModuleId++;
|
|
1666
|
-
loadModuleIds.set(fn2, id);
|
|
1667
|
-
}
|
|
1668
|
-
return id;
|
|
1669
|
-
}
|
|
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
|
-
function unindexCheckKey(key) {
|
|
1677
|
-
const deps = checkKeyDeps.get(key);
|
|
1678
|
-
if (!deps) return;
|
|
1679
|
-
for (const p of deps) {
|
|
1680
|
-
const set = checkDepIndex.get(p);
|
|
1681
|
-
if (!set) continue;
|
|
1682
|
-
set.delete(key);
|
|
1683
|
-
if (set.size === 0) checkDepIndex.delete(p);
|
|
1684
|
-
}
|
|
1685
|
-
checkKeyDeps.delete(key);
|
|
1686
|
-
}
|
|
1687
1653
|
function evictCheckSourceMemoForPaths(paths) {
|
|
1688
|
-
|
|
1689
|
-
for (const raw of paths) {
|
|
1690
|
-
const p = stablePathKey(raw);
|
|
1691
|
-
const keys = checkDepIndex.get(p);
|
|
1692
|
-
if (!keys) continue;
|
|
1693
|
-
for (const key of [...keys]) {
|
|
1694
|
-
checkReportMemo.delete(key);
|
|
1695
|
-
unindexCheckKey(key);
|
|
1696
|
-
n++;
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
return n;
|
|
1654
|
+
return checkReportMemo.evictForPaths(paths);
|
|
1700
1655
|
}
|
|
1701
1656
|
function cloneCheckReport(r) {
|
|
1702
1657
|
return {
|
|
@@ -1712,17 +1667,6 @@ function cloneCheckReport(r) {
|
|
|
1712
1667
|
...r.budget ? { budget: { ...r.budget } } : {}
|
|
1713
1668
|
};
|
|
1714
1669
|
}
|
|
1715
|
-
var moduleMapIds = /* @__PURE__ */ new WeakMap();
|
|
1716
|
-
var moduleMapIdSeq = 0;
|
|
1717
|
-
function moduleMapId(m) {
|
|
1718
|
-
if (!m) return "-";
|
|
1719
|
-
let id = moduleMapIds.get(m);
|
|
1720
|
-
if (id === void 0) {
|
|
1721
|
-
id = ++moduleMapIdSeq;
|
|
1722
|
-
moduleMapIds.set(m, id);
|
|
1723
|
-
}
|
|
1724
|
-
return `m${id}`;
|
|
1725
|
-
}
|
|
1726
1670
|
function checkMemoKey(filePath, source, identityOpts, deps, sidecarFp) {
|
|
1727
1671
|
return [
|
|
1728
1672
|
hashSource(source),
|
|
@@ -1741,41 +1685,29 @@ function checkMemoKey(filePath, source, identityOpts, deps, sidecarFp) {
|
|
|
1741
1685
|
runTranspiledOptionsMemoKey(identityOpts.inject),
|
|
1742
1686
|
// skips 必须进键:同 source 不同 skip 表(host 解析差异 / 测试注入)
|
|
1743
1687
|
// 不得回放另一档报告。
|
|
1744
|
-
skipsKey(identityOpts.skips)
|
|
1688
|
+
skipsKey(identityOpts.skips),
|
|
1689
|
+
// Bug 366:`from`(--from 的跨文件调用点证据)必须进键——同 source 不同
|
|
1690
|
+
// 证据不得互为陈旧命中:无证据那档会把有证据的 drift 吞掉,有证据那档
|
|
1691
|
+
// 会把 drift 回放进无证据的检查(假阳)。
|
|
1692
|
+
fromKey(identityOpts.from)
|
|
1745
1693
|
].join("|");
|
|
1746
1694
|
}
|
|
1747
1695
|
function skipsKey(skips) {
|
|
1748
1696
|
if (!skips || skips.size === 0) return "-";
|
|
1749
1697
|
return [...skips.entries()].map(([name, abs2]) => `${name}:${abs2 ? formatAbs(abs2) : "-"}`).sort().join(",");
|
|
1750
1698
|
}
|
|
1699
|
+
function fromKey(from) {
|
|
1700
|
+
if (!from || from.length === 0) return "-";
|
|
1701
|
+
return from.map(
|
|
1702
|
+
(r) => `${r.fnName}:${r.args.map((a) => formatAbs(a)).join(",")}:${formatAbs(r.result)}`
|
|
1703
|
+
).sort().join(";");
|
|
1704
|
+
}
|
|
1751
1705
|
function checkMemoGet(key) {
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
checkReportMemo.delete(key);
|
|
1755
|
-
checkReportMemo.set(key, v2);
|
|
1756
|
-
return v2;
|
|
1706
|
+
const hit = checkReportMemo.lookup(key);
|
|
1707
|
+
return hit.hit ? hit.value : null;
|
|
1757
1708
|
}
|
|
1758
1709
|
function checkMemoSet(key, value, depPaths) {
|
|
1759
|
-
|
|
1760
|
-
const oldest = checkReportMemo.keys().next().value;
|
|
1761
|
-
if (oldest !== void 0) {
|
|
1762
|
-
checkReportMemo.delete(oldest);
|
|
1763
|
-
unindexCheckKey(oldest);
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
checkReportMemo.set(key, value);
|
|
1767
|
-
if (depPaths.length > 0) {
|
|
1768
|
-
const normDeps = [...new Set(depPaths.map(stablePathKey))];
|
|
1769
|
-
checkKeyDeps.set(key, normDeps);
|
|
1770
|
-
for (const p of normDeps) {
|
|
1771
|
-
let set = checkDepIndex.get(p);
|
|
1772
|
-
if (!set) {
|
|
1773
|
-
set = /* @__PURE__ */ new Set();
|
|
1774
|
-
checkDepIndex.set(p, set);
|
|
1775
|
-
}
|
|
1776
|
-
set.add(key);
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1710
|
+
checkReportMemo.set(key, value, depPaths);
|
|
1779
1711
|
}
|
|
1780
1712
|
|
|
1781
1713
|
// src/algebra/check-may-throw.ts
|
|
@@ -1785,8 +1717,17 @@ function collectEntryMayThrows(source, fnName, g, file, phi, opts = {}) {
|
|
|
1785
1717
|
return runWithMayThrowSession(() => {
|
|
1786
1718
|
setMayThrowCollector((e) => effects.push(e));
|
|
1787
1719
|
try {
|
|
1788
|
-
const
|
|
1789
|
-
if (
|
|
1720
|
+
const outcome = evalThrowsOutcome(source, fnName, entryArgs, opts, phi, file);
|
|
1721
|
+
if (outcome.kind === "unanalyzable") {
|
|
1722
|
+
if (!unsupportedFallsInside(file, fnName, outcome.reason, outcome.loc)) return effects;
|
|
1723
|
+
effects.push({
|
|
1724
|
+
kind: "Error",
|
|
1725
|
+
cause: `entry not analyzed (${outcome.reason}) \u21D2 may-throw undeterminable`
|
|
1726
|
+
});
|
|
1727
|
+
return effects;
|
|
1728
|
+
}
|
|
1729
|
+
if (outcome.kind !== "analyzed") return effects;
|
|
1730
|
+
const full = outcome.result;
|
|
1790
1731
|
if (full.throws && full.throws.shape.k !== "never") {
|
|
1791
1732
|
for (const tName of throwAbsToKinds(full.throws)) {
|
|
1792
1733
|
if (!effects.some((e) => e.kind === tName && e.cause.startsWith("throw"))) {
|
|
@@ -1828,7 +1769,13 @@ function invokeAsThrows(fn2, phi = pTrue) {
|
|
|
1828
1769
|
throw e;
|
|
1829
1770
|
}
|
|
1830
1771
|
}
|
|
1831
|
-
function
|
|
1772
|
+
function declaredThrowsForEntry(source, fnName) {
|
|
1773
|
+
const own = extractDeclaredThrows(source, fnName);
|
|
1774
|
+
if (own !== void 0) return own;
|
|
1775
|
+
if (fnName.includes(".")) return void 0;
|
|
1776
|
+
return extractDeclaredThrows(source, `${fnName}.constructor`);
|
|
1777
|
+
}
|
|
1778
|
+
function evalThrowsOutcome(source, fnName, args, opts = {}, phi = pTrue, file) {
|
|
1832
1779
|
const runKey = `${source}|${runTranspiledOptionsMemoKey(opts.inject)}|${runTranspiledOptionsMemoKey(opts.modules ? { modules: opts.modules } : void 0)}`;
|
|
1833
1780
|
if (evalRunMemo.size >= MAX_CHECK_MEMO) {
|
|
1834
1781
|
const oldest = evalRunMemo.keys().next().value;
|
|
@@ -1836,9 +1783,16 @@ function evalThrowsOf(source, fnName, args, opts = {}, phi = pTrue) {
|
|
|
1836
1783
|
}
|
|
1837
1784
|
let exports = evalRunMemo.get(runKey);
|
|
1838
1785
|
if (exports === void 0) {
|
|
1839
|
-
const
|
|
1840
|
-
if (run === void 0)
|
|
1841
|
-
|
|
1786
|
+
const attempt = tryRunTranspiledDetailed(source, evalAnalyzeOpts(opts));
|
|
1787
|
+
if (attempt.run === void 0) {
|
|
1788
|
+
const reason = attempt.fallback?.reason ?? "transpile failed";
|
|
1789
|
+
if (reason.startsWith("unsupported:")) {
|
|
1790
|
+
const loc = attempt.fallback?.loc;
|
|
1791
|
+
return loc ? { kind: "unanalyzable", reason, loc } : { kind: "unanalyzable", reason };
|
|
1792
|
+
}
|
|
1793
|
+
return { kind: "absent" };
|
|
1794
|
+
}
|
|
1795
|
+
exports = attempt.run;
|
|
1842
1796
|
evalRunMemo.set(runKey, exports);
|
|
1843
1797
|
}
|
|
1844
1798
|
const isAbsVal2 = (v2) => !!v2 && typeof v2 === "object" && "shape" in v2 && "conf" in v2;
|
|
@@ -1850,29 +1804,317 @@ function evalThrowsOf(source, fnName, args, opts = {}, phi = pTrue) {
|
|
|
1850
1804
|
}
|
|
1851
1805
|
};
|
|
1852
1806
|
try {
|
|
1853
|
-
if (
|
|
1807
|
+
if (!fnName.includes(".")) {
|
|
1808
|
+
const clsAbs = classAbsOf(exports, file, fnName);
|
|
1809
|
+
if (isAbsVal2(clsAbs) && isClassValueAbs(clsAbs)) {
|
|
1810
|
+
return {
|
|
1811
|
+
kind: "analyzed",
|
|
1812
|
+
result: classEntryThrows(clsAbs, file, fnName, phi)
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (Object.hasOwn(exports, fnName)) {
|
|
1817
|
+
const r = call(fnName, args);
|
|
1818
|
+
return r ? { kind: "analyzed", result: r } : { kind: "absent" };
|
|
1819
|
+
}
|
|
1854
1820
|
if (fnName.includes(".")) {
|
|
1855
1821
|
const [clsName, methodName] = fnName.split(".", 2);
|
|
1856
|
-
const clsAbs = exports
|
|
1822
|
+
const clsAbs = classAbsOf(exports, file, clsName ?? "");
|
|
1857
1823
|
if (isAbsVal2(clsAbs)) {
|
|
1858
|
-
|
|
1824
|
+
if (classMethodIsStatic(file, clsName ?? "", methodName ?? "") === false) {
|
|
1825
|
+
return {
|
|
1826
|
+
kind: "analyzed",
|
|
1827
|
+
result: invokeAsThrows(() => {
|
|
1828
|
+
const inst = instanceForMethodBridge(
|
|
1829
|
+
clsAbs,
|
|
1830
|
+
clsName ?? "",
|
|
1831
|
+
ctorAnyArgs(file, clsName ?? ""),
|
|
1832
|
+
runWithMayThrowSession
|
|
1833
|
+
);
|
|
1834
|
+
return $invoke(inst, methodName ?? "", args);
|
|
1835
|
+
}, phi)
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
return {
|
|
1839
|
+
kind: "analyzed",
|
|
1840
|
+
result: invokeAsThrows(() => $staticInvoke(clsAbs, methodName ?? "", args), phi)
|
|
1841
|
+
};
|
|
1859
1842
|
}
|
|
1860
|
-
return
|
|
1843
|
+
return { kind: "absent" };
|
|
1861
1844
|
}
|
|
1862
1845
|
if ("default" in exports) {
|
|
1863
1846
|
const d = exports["default"];
|
|
1864
|
-
if (typeof d === "function")
|
|
1847
|
+
if (typeof d === "function") {
|
|
1848
|
+
const r = call("default", args);
|
|
1849
|
+
return r ? { kind: "analyzed", result: r } : { kind: "absent" };
|
|
1850
|
+
}
|
|
1865
1851
|
if (isAbsVal2(d)) {
|
|
1866
|
-
if (d.shape.k === "fn")
|
|
1852
|
+
if (d.shape.k === "fn") {
|
|
1853
|
+
const r = call("default", args);
|
|
1854
|
+
return r ? { kind: "analyzed", result: r } : { kind: "absent" };
|
|
1855
|
+
}
|
|
1867
1856
|
if (d.shape.k === "obj") {
|
|
1868
|
-
return
|
|
1857
|
+
return {
|
|
1858
|
+
kind: "analyzed",
|
|
1859
|
+
result: invokeAsThrows(() => $invoke(d, fnName, args), phi)
|
|
1860
|
+
};
|
|
1869
1861
|
}
|
|
1870
1862
|
}
|
|
1871
1863
|
}
|
|
1872
|
-
return
|
|
1864
|
+
return { kind: "absent" };
|
|
1873
1865
|
} catch {
|
|
1866
|
+
return { kind: "absent" };
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
function classAbsOf(exports, file, clsName) {
|
|
1870
|
+
const slot = file ? resolveExportSlotCall(file, clsName, exports) : void 0;
|
|
1871
|
+
if (slot && slot.kind !== "cjs-member") return exports[slot.key];
|
|
1872
|
+
return exports[clsName];
|
|
1873
|
+
}
|
|
1874
|
+
function isClassValueAbs(v2) {
|
|
1875
|
+
return v2.shape.k === "brand" && v2.shape.ctor === true;
|
|
1876
|
+
}
|
|
1877
|
+
function classEntryThrows(clsAbs, file, clsName, phi) {
|
|
1878
|
+
return invokeAsThrows(
|
|
1879
|
+
() => runWithMayThrowSession(() => $new(clsAbs, ctorAnyArgs(file, clsName))),
|
|
1880
|
+
phi
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
function classMethodIsStatic(file, clsName, methodName) {
|
|
1884
|
+
if (!file) return void 0;
|
|
1885
|
+
for (const stmt of file.program.body) {
|
|
1886
|
+
const s = stmt;
|
|
1887
|
+
let decl = stmt;
|
|
1888
|
+
if (s.type === "ExportNamedDeclaration" || s.type === "ExportDefaultDeclaration") {
|
|
1889
|
+
decl = s.declaration;
|
|
1890
|
+
}
|
|
1891
|
+
const c = decl;
|
|
1892
|
+
if (!c || c.type !== "ClassDeclaration") continue;
|
|
1893
|
+
if (c.id?.name !== clsName) continue;
|
|
1894
|
+
for (const m of c.body?.body ?? []) {
|
|
1895
|
+
const mem = m;
|
|
1896
|
+
if (mem.kind === "constructor") continue;
|
|
1897
|
+
if (classMemberName(mem) !== methodName) continue;
|
|
1898
|
+
return mem.static === true;
|
|
1899
|
+
}
|
|
1900
|
+
return void 0;
|
|
1901
|
+
}
|
|
1902
|
+
return void 0;
|
|
1903
|
+
}
|
|
1904
|
+
function ctorAnyArgs(file, clsName) {
|
|
1905
|
+
const n = file ? ctorParamCountOf(file, clsName) : 0;
|
|
1906
|
+
return Array.from({ length: n }, () => abs({ k: "any" }, void 0, pTrue, "path"));
|
|
1907
|
+
}
|
|
1908
|
+
function classMemberName(m) {
|
|
1909
|
+
const key = m.key;
|
|
1910
|
+
if (!key) return void 0;
|
|
1911
|
+
return key.name ?? (typeof key.value === "string" ? key.value : void 0);
|
|
1912
|
+
}
|
|
1913
|
+
function isExportsTarget(node) {
|
|
1914
|
+
const n = node;
|
|
1915
|
+
if (!n) return false;
|
|
1916
|
+
if (n.type === "Identifier") return n.name === "exports";
|
|
1917
|
+
if (n.type !== "MemberExpression") return false;
|
|
1918
|
+
const obj2 = n.object;
|
|
1919
|
+
const prop = n.property;
|
|
1920
|
+
return obj2?.type === "Identifier" && obj2.name === "module" && prop?.name === "exports";
|
|
1921
|
+
}
|
|
1922
|
+
function cjsExportName(left) {
|
|
1923
|
+
const l = left;
|
|
1924
|
+
if (!l || l.type !== "MemberExpression" || l.computed === true) return void 0;
|
|
1925
|
+
if (!isExportsTarget(l.object)) return void 0;
|
|
1926
|
+
const prop = l.property;
|
|
1927
|
+
return prop?.name;
|
|
1928
|
+
}
|
|
1929
|
+
function keyNameOf(node) {
|
|
1930
|
+
const n = node;
|
|
1931
|
+
if (!n) return void 0;
|
|
1932
|
+
if (n.type === "Identifier" && typeof n.name === "string") return n.name;
|
|
1933
|
+
if (n.type === "StringLiteral" && typeof n.value === "string") return n.value;
|
|
1934
|
+
return void 0;
|
|
1935
|
+
}
|
|
1936
|
+
function entryValueNode(node) {
|
|
1937
|
+
const n = node;
|
|
1938
|
+
if (!n) return void 0;
|
|
1939
|
+
return n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression" || n.type === "ClassDeclaration" || n.type === "ClassExpression" ? node : void 0;
|
|
1940
|
+
}
|
|
1941
|
+
function classMethodEntryNodes(cls, methodName) {
|
|
1942
|
+
const members = cls.body?.body ?? [];
|
|
1943
|
+
let method;
|
|
1944
|
+
for (const m of members) {
|
|
1945
|
+
if (m.kind === "constructor") continue;
|
|
1946
|
+
if (classMemberName(m) !== methodName) continue;
|
|
1947
|
+
method = m;
|
|
1948
|
+
break;
|
|
1949
|
+
}
|
|
1950
|
+
if (!method) return void 0;
|
|
1951
|
+
const isStatic = method.static === true;
|
|
1952
|
+
const nodes = [method];
|
|
1953
|
+
for (const m of members) {
|
|
1954
|
+
if (m === method) continue;
|
|
1955
|
+
if (m.kind === "constructor") {
|
|
1956
|
+
if (!isStatic) nodes.push(m);
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
if (m.type === "StaticBlock") {
|
|
1960
|
+
if (isStatic) nodes.push(m);
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
if (m.type === "ClassProperty" || m.type === "ClassPrivateProperty") {
|
|
1964
|
+
if (m.static === true !== isStatic) continue;
|
|
1965
|
+
nodes.push(m.value ?? m);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
return nodes;
|
|
1969
|
+
}
|
|
1970
|
+
function cjsEntryNodes(file, expression, fnName, depth) {
|
|
1971
|
+
const e = expression;
|
|
1972
|
+
if (!e || e.type !== "AssignmentExpression" || e.operator !== "=") return void 0;
|
|
1973
|
+
const right = e.right;
|
|
1974
|
+
if (!right) return void 0;
|
|
1975
|
+
const fromValue = (node) => {
|
|
1976
|
+
const value = entryValueNode(node);
|
|
1977
|
+
if (value) return [value];
|
|
1978
|
+
const ident = node;
|
|
1979
|
+
if (ident?.type === "Identifier" && ident.name) {
|
|
1980
|
+
return entryNodesOf(file, ident.name, depth + 1);
|
|
1981
|
+
}
|
|
1982
|
+
return void 0;
|
|
1983
|
+
};
|
|
1984
|
+
if (cjsExportName(e.left) === fnName) return fromValue(right);
|
|
1985
|
+
if (!isExportsTarget(e.left)) return void 0;
|
|
1986
|
+
if (right.type === "ObjectExpression") {
|
|
1987
|
+
for (const p of right.properties ?? []) {
|
|
1988
|
+
const prop = p;
|
|
1989
|
+
if (keyNameOf(prop.key) !== fnName) continue;
|
|
1990
|
+
if (prop.type === "ObjectMethod") return [prop];
|
|
1991
|
+
return fromValue(prop.value);
|
|
1992
|
+
}
|
|
1874
1993
|
return void 0;
|
|
1875
1994
|
}
|
|
1995
|
+
if (right.type === "Identifier" && right.name === fnName) {
|
|
1996
|
+
return entryNodesOf(file, fnName, depth + 1);
|
|
1997
|
+
}
|
|
1998
|
+
return void 0;
|
|
1999
|
+
}
|
|
2000
|
+
function entryNodesOf(file, fnName, depth = 0) {
|
|
2001
|
+
if (!file || depth > 3) return void 0;
|
|
2002
|
+
for (const stmt of file.program.body) {
|
|
2003
|
+
const s = stmt;
|
|
2004
|
+
if (s.type === "ExpressionStatement") {
|
|
2005
|
+
const hit = cjsEntryNodes(file, s.expression, fnName, depth);
|
|
2006
|
+
if (hit) return hit;
|
|
2007
|
+
continue;
|
|
2008
|
+
}
|
|
2009
|
+
let decl = stmt;
|
|
2010
|
+
if (s.type === "ExportNamedDeclaration" || s.type === "ExportDefaultDeclaration") {
|
|
2011
|
+
decl = s.declaration;
|
|
2012
|
+
}
|
|
2013
|
+
const d = decl;
|
|
2014
|
+
if (!d || typeof d !== "object") continue;
|
|
2015
|
+
if (fnName.includes(".")) {
|
|
2016
|
+
const [clsName, methodName] = fnName.split(".", 2);
|
|
2017
|
+
if (d.type !== "ClassDeclaration" || d.id?.name !== clsName) continue;
|
|
2018
|
+
return classMethodEntryNodes(d, methodName ?? "");
|
|
2019
|
+
}
|
|
2020
|
+
if (d.type === "FunctionDeclaration" && d.id?.name === fnName) return [d];
|
|
2021
|
+
if (fnName === "default" && s.type === "ExportDefaultDeclaration") {
|
|
2022
|
+
const t = d.type;
|
|
2023
|
+
if (t === "FunctionDeclaration" || t === "ArrowFunctionExpression" || t === "FunctionExpression" || t === "ClassDeclaration") {
|
|
2024
|
+
return [d];
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
if (d.type === "ClassDeclaration" && d.id?.name === fnName) return [d];
|
|
2028
|
+
if (d.type === "VariableDeclaration") {
|
|
2029
|
+
for (const dec of d.declarations ?? []) {
|
|
2030
|
+
const id = dec.id;
|
|
2031
|
+
if (id?.name === fnName) return [dec];
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
return void 0;
|
|
2036
|
+
}
|
|
2037
|
+
function locInSpan(loc, span) {
|
|
2038
|
+
if (!loc || !span) return false;
|
|
2039
|
+
if (loc.line < span.startLine || loc.line > span.endLine) return false;
|
|
2040
|
+
if (loc.line === span.startLine && loc.column < span.startColumn) return false;
|
|
2041
|
+
if (loc.line === span.endLine && loc.column > span.endColumn) return false;
|
|
2042
|
+
return true;
|
|
2043
|
+
}
|
|
2044
|
+
function isModuleInitLoc(file, loc) {
|
|
2045
|
+
if (!loc) return false;
|
|
2046
|
+
if (!locInSpan(loc, nodeSpan(file.program))) return false;
|
|
2047
|
+
for (const span of deferredExecutionSpans(file)) {
|
|
2048
|
+
if (locInSpan(loc, span)) return false;
|
|
2049
|
+
}
|
|
2050
|
+
return true;
|
|
2051
|
+
}
|
|
2052
|
+
function definiteModuleThrowOf(file) {
|
|
2053
|
+
return file ? scanDefiniteThrowSeq(file.program.body) : void 0;
|
|
2054
|
+
}
|
|
2055
|
+
function scanDefiniteThrowSeq(stmts) {
|
|
2056
|
+
for (const stmt of stmts) {
|
|
2057
|
+
const s = stmt;
|
|
2058
|
+
switch (s.type) {
|
|
2059
|
+
case "ThrowStatement": {
|
|
2060
|
+
const at = s.loc?.start;
|
|
2061
|
+
if (at?.line === void 0) return void 0;
|
|
2062
|
+
const kind = thrownKindOf(s.argument);
|
|
2063
|
+
return {
|
|
2064
|
+
line: at.line,
|
|
2065
|
+
column: at.column ?? 0,
|
|
2066
|
+
...kind !== void 0 ? { kind } : {}
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
// 裸块 = 无条件的语句序列 ⇒ 继续下钻
|
|
2070
|
+
case "BlockStatement": {
|
|
2071
|
+
const hit = scanDefiniteThrowSeq(s.body ?? []);
|
|
2072
|
+
if (hit) return hit;
|
|
2073
|
+
continue;
|
|
2074
|
+
}
|
|
2075
|
+
case "ReturnStatement":
|
|
2076
|
+
return void 0;
|
|
2077
|
+
case "IfStatement":
|
|
2078
|
+
case "SwitchStatement":
|
|
2079
|
+
case "TryStatement":
|
|
2080
|
+
case "ForStatement":
|
|
2081
|
+
case "ForInStatement":
|
|
2082
|
+
case "ForOfStatement":
|
|
2083
|
+
case "WhileStatement":
|
|
2084
|
+
case "DoWhileStatement":
|
|
2085
|
+
case "LabeledStatement":
|
|
2086
|
+
case "WithStatement":
|
|
2087
|
+
return void 0;
|
|
2088
|
+
default:
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return void 0;
|
|
2093
|
+
}
|
|
2094
|
+
function thrownKindOf(arg) {
|
|
2095
|
+
const a = arg;
|
|
2096
|
+
const callee = a?.callee;
|
|
2097
|
+
if (callee?.type !== "Identifier" || typeof callee.name !== "string") return void 0;
|
|
2098
|
+
if (a?.type === "NewExpression") return callee.name;
|
|
2099
|
+
return a?.type === "CallExpression" && callee.name.endsWith("Error") ? callee.name : void 0;
|
|
2100
|
+
}
|
|
2101
|
+
var TS_ONLY_STATEMENT_REASON_PREFIX = "unsupported:statement:TS";
|
|
2102
|
+
function unsupportedFallsInside(file, fnName, reason, loc) {
|
|
2103
|
+
if (!file || !loc) return false;
|
|
2104
|
+
if (!reason.startsWith(TS_ONLY_STATEMENT_REASON_PREFIX) && isModuleInitLoc(file, loc)) {
|
|
2105
|
+
return true;
|
|
2106
|
+
}
|
|
2107
|
+
const entryNodes = entryNodesOf(file, fnName);
|
|
2108
|
+
if (!entryNodes || entryNodes.length === 0) return false;
|
|
2109
|
+
for (const node of entryNodes) {
|
|
2110
|
+
if (locInSpan(loc, nodeSpan(node))) return true;
|
|
2111
|
+
}
|
|
2112
|
+
for (const node of entryNodes) {
|
|
2113
|
+
for (const reach of reachableTopBindings(file, node, { includeClasses: true })) {
|
|
2114
|
+
if (locInSpan(loc, nodeSpan(reach.node))) return true;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
return false;
|
|
1876
2118
|
}
|
|
1877
2119
|
|
|
1878
2120
|
// src/algebra/check-assign.ts
|
|
@@ -1884,6 +2126,8 @@ function widenForAssign(a) {
|
|
|
1884
2126
|
const el = els.reduce((x, y) => joinAbs(x, y));
|
|
1885
2127
|
return abs({ k: "arr", element: el }, void 0, void 0, a.conf);
|
|
1886
2128
|
}
|
|
2129
|
+
const prim = samePrimOfSum(s.members);
|
|
2130
|
+
if (prim) return abs({ k: "prim", type: prim }, void 0, void 0, a.conf);
|
|
1887
2131
|
return a;
|
|
1888
2132
|
}
|
|
1889
2133
|
if (s.k === "tuple") {
|
|
@@ -1935,14 +2179,40 @@ function widenForAssign(a) {
|
|
|
1935
2179
|
}
|
|
1936
2180
|
return a;
|
|
1937
2181
|
}
|
|
2182
|
+
function samePrimOfSum(members) {
|
|
2183
|
+
let out;
|
|
2184
|
+
for (const m of members) {
|
|
2185
|
+
const t = primNameOf(m);
|
|
2186
|
+
if (!t) return void 0;
|
|
2187
|
+
if (out === void 0) out = t;
|
|
2188
|
+
else if (out !== t) return void 0;
|
|
2189
|
+
}
|
|
2190
|
+
return out;
|
|
2191
|
+
}
|
|
2192
|
+
function primNameOf(a) {
|
|
2193
|
+
if (a.shape.k === "prim") return a.shape.type;
|
|
2194
|
+
const lv = a.term?.op === "lit" ? a.term.value : void 0;
|
|
2195
|
+
if (typeof lv === "string") return "string";
|
|
2196
|
+
if (typeof lv === "number") return "number";
|
|
2197
|
+
if (typeof lv === "boolean") return "boolean";
|
|
2198
|
+
if (typeof lv === "bigint") return "bigint";
|
|
2199
|
+
return void 0;
|
|
2200
|
+
}
|
|
2201
|
+
function isAbsLike(v2) {
|
|
2202
|
+
return !!v2 && typeof v2 === "object" && "shape" in v2 && "conf" in v2;
|
|
2203
|
+
}
|
|
2204
|
+
function shapeOnly(a) {
|
|
2205
|
+
return abs(a.shape, void 0, void 0, a.conf);
|
|
2206
|
+
}
|
|
1938
2207
|
function structuralAssignIssues(records) {
|
|
1939
2208
|
const out = [];
|
|
1940
2209
|
for (const r of records) {
|
|
1941
2210
|
if (!r.prev) continue;
|
|
2211
|
+
if (!isAbsLike(r.prev) || !isAbsLike(r.next)) continue;
|
|
1942
2212
|
if (r.conditional) continue;
|
|
1943
2213
|
if (r.next.shape.k === "unknown" && !r.next.term) continue;
|
|
1944
2214
|
if (r.prev.shape.k === "unknown" && !r.prev.term) continue;
|
|
1945
|
-
const leq = leqAbs(widenForAssign(r.next), widenForAssign(r.prev));
|
|
2215
|
+
const leq = leqAbs(widenForAssign(r.next), shapeOnly(widenForAssign(r.prev)));
|
|
1946
2216
|
if (!leq.ok) {
|
|
1947
2217
|
out.push({
|
|
1948
2218
|
severity: "error",
|
|
@@ -1961,18 +2231,339 @@ function structuralAssignIssues(records) {
|
|
|
1961
2231
|
}
|
|
1962
2232
|
|
|
1963
2233
|
// src/algebra/check-case-scan.ts
|
|
1964
|
-
function
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2234
|
+
function parseNumericLiteral(text) {
|
|
2235
|
+
const s = text.trim();
|
|
2236
|
+
if (s === "") return void 0;
|
|
2237
|
+
let sign = 1;
|
|
2238
|
+
let body = s;
|
|
2239
|
+
if (body.startsWith("-")) {
|
|
2240
|
+
sign = -1;
|
|
2241
|
+
body = body.slice(1).trimStart();
|
|
2242
|
+
} else if (body.startsWith("+")) {
|
|
2243
|
+
body = body.slice(1).trimStart();
|
|
2244
|
+
}
|
|
2245
|
+
if (body === "") return void 0;
|
|
2246
|
+
if (body.endsWith("n")) {
|
|
2247
|
+
const d = body.slice(0, -1);
|
|
2248
|
+
if (d.includes("_")) return void 0;
|
|
2249
|
+
if (/^0[xX][0-9a-fA-F]+$/.test(d)) return BigInt(d) * BigInt(sign);
|
|
2250
|
+
if (/^0[bB][01]+$/.test(d)) return BigInt(d) * BigInt(sign);
|
|
2251
|
+
if (/^0[oO][0-7]+$/.test(d)) return BigInt(d) * BigInt(sign);
|
|
2252
|
+
if (/^[0-9]+$/.test(d)) return BigInt(d) * BigInt(sign);
|
|
2253
|
+
return void 0;
|
|
2254
|
+
}
|
|
2255
|
+
if (body.includes("__")) return void 0;
|
|
2256
|
+
const clean = body.replace(/_/g, "");
|
|
2257
|
+
if (clean === "") return void 0;
|
|
2258
|
+
if (/^0[xX][0-9a-fA-F]+$/.test(clean)) return sign * Number(BigInt(clean));
|
|
2259
|
+
if (/^0[bB][01]+$/.test(clean)) return sign * Number(BigInt(clean));
|
|
2260
|
+
if (/^0[oO][0-7]+$/.test(clean)) return sign * Number(BigInt(clean));
|
|
2261
|
+
if (clean === "Infinity") return sign * Infinity;
|
|
2262
|
+
if (clean === "NaN") return NaN;
|
|
2263
|
+
if (/^0[0-7]+$/.test(clean) && !body.includes("_")) return sign * parseInt(clean.slice(1), 8);
|
|
2264
|
+
if (/^(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/.test(clean)) return sign * Number(clean);
|
|
2265
|
+
return void 0;
|
|
2266
|
+
}
|
|
2267
|
+
function parseStringLiteralValue(text) {
|
|
2268
|
+
const t = text.trim();
|
|
2269
|
+
if (t.length < 2) return void 0;
|
|
2270
|
+
const q = t[0];
|
|
2271
|
+
if (q !== '"' && q !== "'") return void 0;
|
|
2272
|
+
if (t[t.length - 1] !== q) return void 0;
|
|
2273
|
+
const inner = t.slice(1, -1);
|
|
2274
|
+
for (let i = 0; i < inner.length; i++) {
|
|
2275
|
+
const ch = inner[i];
|
|
2276
|
+
if (ch === "\\") {
|
|
2277
|
+
i++;
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
if (ch === q) return void 0;
|
|
2281
|
+
}
|
|
2282
|
+
let out = "";
|
|
2283
|
+
for (let i = 0; i < inner.length; i++) {
|
|
2284
|
+
const ch = inner[i];
|
|
2285
|
+
if (ch !== "\\") {
|
|
2286
|
+
out += ch;
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
const n = inner[i + 1];
|
|
2290
|
+
if (n === void 0) return void 0;
|
|
2291
|
+
i++;
|
|
2292
|
+
switch (n) {
|
|
2293
|
+
case "n":
|
|
2294
|
+
out += "\n";
|
|
2295
|
+
break;
|
|
2296
|
+
case "t":
|
|
2297
|
+
out += " ";
|
|
2298
|
+
break;
|
|
2299
|
+
case "r":
|
|
2300
|
+
out += "\r";
|
|
2301
|
+
break;
|
|
2302
|
+
case "b":
|
|
2303
|
+
out += "\b";
|
|
2304
|
+
break;
|
|
2305
|
+
case "f":
|
|
2306
|
+
out += "\f";
|
|
2307
|
+
break;
|
|
2308
|
+
case "v":
|
|
2309
|
+
out += "\v";
|
|
2310
|
+
break;
|
|
2311
|
+
case "0":
|
|
2312
|
+
out += "\0";
|
|
2313
|
+
break;
|
|
2314
|
+
case "x": {
|
|
2315
|
+
const hex = inner.slice(i + 1, i + 3);
|
|
2316
|
+
if (!/^[0-9a-fA-F]{2}$/.test(hex)) return void 0;
|
|
2317
|
+
out += String.fromCharCode(parseInt(hex, 16));
|
|
2318
|
+
i += 2;
|
|
2319
|
+
break;
|
|
2320
|
+
}
|
|
2321
|
+
case "u": {
|
|
2322
|
+
if (inner[i + 1] === "{") {
|
|
2323
|
+
const end = inner.indexOf("}", i + 1);
|
|
2324
|
+
if (end < 0) return void 0;
|
|
2325
|
+
const hex = inner.slice(i + 2, end);
|
|
2326
|
+
if (!/^[0-9a-fA-F]{1,6}$/.test(hex)) return void 0;
|
|
2327
|
+
out += String.fromCodePoint(parseInt(hex, 16));
|
|
2328
|
+
i = end;
|
|
2329
|
+
} else {
|
|
2330
|
+
const hex = inner.slice(i + 1, i + 5);
|
|
2331
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return void 0;
|
|
2332
|
+
out += String.fromCharCode(parseInt(hex, 16));
|
|
2333
|
+
i += 4;
|
|
2334
|
+
}
|
|
2335
|
+
break;
|
|
2336
|
+
}
|
|
2337
|
+
case "\n":
|
|
2338
|
+
break;
|
|
2339
|
+
// 行连接
|
|
2340
|
+
case "\r":
|
|
2341
|
+
if (inner[i + 1] === "\n") i++;
|
|
2342
|
+
break;
|
|
2343
|
+
default:
|
|
2344
|
+
out += n;
|
|
2345
|
+
break;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
return out;
|
|
2349
|
+
}
|
|
2350
|
+
function parseScalarArgAbs(s) {
|
|
2351
|
+
const t = s.trim();
|
|
2352
|
+
if (t === "") return void 0;
|
|
2353
|
+
if (t === "true") return { shape: { k: "prim", type: "boolean" }, term: { op: "lit", value: true }, conf: "exact" };
|
|
2354
|
+
if (t === "false") return { shape: { k: "prim", type: "boolean" }, term: { op: "lit", value: false }, conf: "exact" };
|
|
2355
|
+
if (t === "null") return { shape: { k: "unknown" }, term: { op: "lit", value: null }, conf: "exact" };
|
|
2356
|
+
if (t === "undefined") return { shape: { k: "unknown" }, term: { op: "lit", value: void 0 }, conf: "exact" };
|
|
2357
|
+
const num2 = parseNumericLiteral(t);
|
|
2358
|
+
if (num2 !== void 0) {
|
|
2359
|
+
return typeof num2 === "bigint" ? bigintLit(num2) : numLit(num2);
|
|
2360
|
+
}
|
|
2361
|
+
const str2 = parseStringLiteralValue(t);
|
|
2362
|
+
if (str2 !== void 0) {
|
|
2363
|
+
return {
|
|
2364
|
+
shape: { k: "prim", type: "string" },
|
|
2365
|
+
term: { op: "lit", value: str2 },
|
|
2366
|
+
conf: "exact"
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
return void 0;
|
|
2370
|
+
}
|
|
2371
|
+
function splitCaseArgsText(text) {
|
|
2372
|
+
const result = [];
|
|
2373
|
+
let current = "";
|
|
2374
|
+
let depth = 0;
|
|
2375
|
+
let inString = null;
|
|
2376
|
+
for (let i = 0; i < text.length; i++) {
|
|
2377
|
+
const ch = text[i];
|
|
2378
|
+
if (inString) {
|
|
2379
|
+
current += ch;
|
|
2380
|
+
if (ch === inString) inString = null;
|
|
2381
|
+
continue;
|
|
2382
|
+
}
|
|
2383
|
+
if (ch === '"' || ch === "'") {
|
|
2384
|
+
inString = ch;
|
|
2385
|
+
current += ch;
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2388
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
2389
|
+
if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
2390
|
+
if (ch === "," && depth === 0) {
|
|
2391
|
+
result.push(current.trim());
|
|
2392
|
+
current = "";
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
current += ch;
|
|
2396
|
+
}
|
|
2397
|
+
if (current.trim()) result.push(current.trim());
|
|
2398
|
+
return result;
|
|
2399
|
+
}
|
|
2400
|
+
function stripCaseContinuation(text) {
|
|
2401
|
+
return text.split("\n").map((line) => line.replace(/^\s*\*\s?/, "")).join("\n");
|
|
2402
|
+
}
|
|
2403
|
+
function findTopLevelColon(text) {
|
|
2404
|
+
let found = -1;
|
|
2405
|
+
let depth = 0;
|
|
2406
|
+
let inString = null;
|
|
2407
|
+
for (let i = 0; i < text.length && found < 0; i++) {
|
|
2408
|
+
const ch = text[i];
|
|
2409
|
+
if (inString) {
|
|
2410
|
+
if (ch === inString) inString = null;
|
|
2411
|
+
continue;
|
|
2412
|
+
}
|
|
2413
|
+
if (ch === '"' || ch === "'") {
|
|
2414
|
+
inString = ch;
|
|
2415
|
+
continue;
|
|
2416
|
+
}
|
|
2417
|
+
if (ch === "(" || ch === "[" || ch === "{") {
|
|
2418
|
+
depth++;
|
|
2419
|
+
continue;
|
|
1970
2420
|
}
|
|
1971
|
-
if (
|
|
1972
|
-
|
|
2421
|
+
if (ch === ")" || ch === "]" || ch === "}") {
|
|
2422
|
+
depth--;
|
|
2423
|
+
continue;
|
|
2424
|
+
}
|
|
2425
|
+
if (ch === ":" && depth === 0) found = i;
|
|
2426
|
+
}
|
|
2427
|
+
return found;
|
|
2428
|
+
}
|
|
2429
|
+
function parseStructureArgAbs(text) {
|
|
2430
|
+
const t = text.trim();
|
|
2431
|
+
if (t.startsWith("{") && t.endsWith("}")) {
|
|
2432
|
+
const body = t.slice(1, -1).trim();
|
|
2433
|
+
const slots = {};
|
|
2434
|
+
if (body !== "") {
|
|
2435
|
+
for (const entry of splitCaseArgsText(body)) {
|
|
2436
|
+
if (entry === "") continue;
|
|
2437
|
+
const colon = findTopLevelColon(entry);
|
|
2438
|
+
if (colon < 0) return void 0;
|
|
2439
|
+
const keyText = entry.slice(0, colon).trim();
|
|
2440
|
+
if (keyText === "" || keyText.includes("[")) return void 0;
|
|
2441
|
+
const key = keyText.replace(/^["']|["']$/g, "");
|
|
2442
|
+
if (key === "") return void 0;
|
|
2443
|
+
const value = parseCaseArgAbs(entry.slice(colon + 1));
|
|
2444
|
+
if (!value) return void 0;
|
|
2445
|
+
slots[key] = { value };
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
return abs({ k: "obj", slots }, void 0, void 0, "exact");
|
|
2449
|
+
}
|
|
2450
|
+
if (t.startsWith("[") && t.endsWith("]")) {
|
|
2451
|
+
const body = t.slice(1, -1).trim();
|
|
2452
|
+
const elements = [];
|
|
2453
|
+
if (body !== "") {
|
|
2454
|
+
for (const el of splitCaseArgsText(body)) {
|
|
2455
|
+
if (el === "") continue;
|
|
2456
|
+
const value = parseCaseArgAbs(el);
|
|
2457
|
+
if (!value) return void 0;
|
|
2458
|
+
elements.push(value);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
return abs({ k: "tuple", elements }, void 0, void 0, "exact");
|
|
2462
|
+
}
|
|
2463
|
+
return void 0;
|
|
2464
|
+
}
|
|
2465
|
+
function parseCaseArgAbs(text) {
|
|
2466
|
+
return parseScalarArgAbs(text) ?? parseStructureArgAbs(text);
|
|
2467
|
+
}
|
|
2468
|
+
function isConstraintBuilderArg(text) {
|
|
2469
|
+
return CONSTRAINT_EXPR_RE.test(text.trim());
|
|
2470
|
+
}
|
|
2471
|
+
function derefDeepConstraint(c) {
|
|
2472
|
+
let cur = c;
|
|
2473
|
+
for (let i = 0; i < LAZY_TEMPLATE_DEPTH && cur.lazy; i++) {
|
|
2474
|
+
cur = derefConstraint(cur);
|
|
1973
2475
|
}
|
|
1974
2476
|
return cur;
|
|
1975
2477
|
}
|
|
2478
|
+
function witnessNoInfo(w) {
|
|
2479
|
+
const k = w.shape.k;
|
|
2480
|
+
return k === "never" || (k === "unknown" || k === "any") && !w.term;
|
|
2481
|
+
}
|
|
2482
|
+
function witnessIsStructure(w) {
|
|
2483
|
+
const k = w.shape.k;
|
|
2484
|
+
return k === "obj" || k === "arr" || k === "tuple";
|
|
2485
|
+
}
|
|
2486
|
+
function witnessPrimType(w) {
|
|
2487
|
+
const shape2 = w.shape.k === "brand" ? w.shape.shape.shape : w.shape;
|
|
2488
|
+
if (shape2.k === "prim") return shape2.type;
|
|
2489
|
+
return void 0;
|
|
2490
|
+
}
|
|
2491
|
+
function constraintIsAnyLike(c) {
|
|
2492
|
+
return c.prim === void 0 && c.preds.length === 0 && (c.members?.length ?? 0) === 0 && !c.fields && !c.element && !c.fn;
|
|
2493
|
+
}
|
|
2494
|
+
function witnessViolations(c, w, path, depth) {
|
|
2495
|
+
if (depth > 6) return [];
|
|
2496
|
+
const cc = derefDeepConstraint(c);
|
|
2497
|
+
if (cc.members && cc.members.length > 0) {
|
|
2498
|
+
let first = [];
|
|
2499
|
+
for (const m of cc.members) {
|
|
2500
|
+
const vs = witnessViolations(m, w, path, depth + 1);
|
|
2501
|
+
if (vs.length === 0) return [];
|
|
2502
|
+
if (first.length === 0) first = vs;
|
|
2503
|
+
}
|
|
2504
|
+
return first;
|
|
2505
|
+
}
|
|
2506
|
+
if (cc.fields) {
|
|
2507
|
+
const shape2 = w.shape.k === "brand" ? w.shape.shape.shape : w.shape;
|
|
2508
|
+
if (shape2.k !== "obj") {
|
|
2509
|
+
if (witnessNoInfo(w)) return [];
|
|
2510
|
+
return [{ actual: w, expected: formatConstraint(cc), path, missing: false }];
|
|
2511
|
+
}
|
|
2512
|
+
const out = [];
|
|
2513
|
+
for (const [key, field] of Object.entries(cc.fields)) {
|
|
2514
|
+
const slot = getSlot(shape2.slots, key);
|
|
2515
|
+
const fieldPath = path === "" ? key : `${path}.${key}`;
|
|
2516
|
+
if (!slot) {
|
|
2517
|
+
if (!field.optional && !field.constraint.isOptional) {
|
|
2518
|
+
out.push({
|
|
2519
|
+
actual: w,
|
|
2520
|
+
expected: `missing field ${fieldPath}`,
|
|
2521
|
+
path: fieldPath,
|
|
2522
|
+
missing: true
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
continue;
|
|
2526
|
+
}
|
|
2527
|
+
out.push(...witnessViolations(field.constraint, slot.value, fieldPath, depth + 1));
|
|
2528
|
+
}
|
|
2529
|
+
return out;
|
|
2530
|
+
}
|
|
2531
|
+
if (cc.element) {
|
|
2532
|
+
const shape2 = w.shape.k === "brand" ? w.shape.shape.shape : w.shape;
|
|
2533
|
+
const elements = shape2.k === "tuple" ? shape2.elements : shape2.k === "arr" ? [shape2.element] : void 0;
|
|
2534
|
+
if (!elements) {
|
|
2535
|
+
if (witnessNoInfo(w)) return [];
|
|
2536
|
+
return [{ actual: w, expected: formatConstraint(cc), path, missing: false }];
|
|
2537
|
+
}
|
|
2538
|
+
const out = [];
|
|
2539
|
+
elements.forEach((el, i) => {
|
|
2540
|
+
out.push(...witnessViolations(cc.element, el, `${path}[${i}]`, depth + 1));
|
|
2541
|
+
});
|
|
2542
|
+
return out;
|
|
2543
|
+
}
|
|
2544
|
+
if (cc.fn) return [];
|
|
2545
|
+
if (witnessIsStructure(w)) {
|
|
2546
|
+
if (constraintIsAnyLike(cc)) return [];
|
|
2547
|
+
return [{ actual: w, expected: formatConstraint(cc), path, missing: false }];
|
|
2548
|
+
}
|
|
2549
|
+
const argLit = w.term?.op === "lit" ? w.term.value : void 0;
|
|
2550
|
+
if (w.term?.op !== "lit") return [];
|
|
2551
|
+
if (typeof argLit !== "number" && typeof argLit !== "string" && typeof argLit !== "boolean" && argLit !== null && argLit !== void 0) {
|
|
2552
|
+
return [];
|
|
2553
|
+
}
|
|
2554
|
+
if (cc.prim) {
|
|
2555
|
+
const prim = witnessPrimType(w);
|
|
2556
|
+
if (prim !== void 0 && prim !== cc.prim) {
|
|
2557
|
+
return [
|
|
2558
|
+
{ actual: w, expected: `typeof ${path} = "${cc.prim}"`, path, missing: false }
|
|
2559
|
+
];
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
if (!literalMeetsConstraint(argLit, cc)) {
|
|
2563
|
+
return [{ actual: w, expected: formatConstraint(cc), path, missing: false }];
|
|
2564
|
+
}
|
|
2565
|
+
return [];
|
|
2566
|
+
}
|
|
1976
2567
|
function scanCaseInconsistency(source, knownFns, opts) {
|
|
1977
2568
|
const out = [];
|
|
1978
2569
|
const directiveSrc = stripStringsKeepComments(source);
|
|
@@ -1980,57 +2571,6 @@ function scanCaseInconsistency(source, knownFns, opts) {
|
|
|
1980
2571
|
const hasContractOrigin = directiveSrc.includes("@nudo:contract") || opts.sidecarPresent === true;
|
|
1981
2572
|
if (!hasContractOrigin) return out;
|
|
1982
2573
|
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
2574
|
const checkCaseAgainstReqs = (fnName, caseName, args, line) => {
|
|
2035
2575
|
const g = generalizeFromAst(fnName, source, {
|
|
2036
2576
|
...file ? { file } : {},
|
|
@@ -2072,20 +2612,45 @@ function scanCaseInconsistency(source, knownFns, opts) {
|
|
|
2072
2612
|
]);
|
|
2073
2613
|
}
|
|
2074
2614
|
if (reqs.length === 0) return;
|
|
2075
|
-
const absArgs = args.map((a) =>
|
|
2615
|
+
const absArgs = args.map((a) => parseCaseArgAbs(a));
|
|
2076
2616
|
for (const req of reqs) {
|
|
2077
2617
|
const idx = req[0];
|
|
2078
2618
|
const entry = req[1];
|
|
2079
2619
|
const fieldPath = req[2];
|
|
2080
|
-
|
|
2620
|
+
const paramName = entry.param || paramNames[idx] || `arg${idx}`;
|
|
2621
|
+
if (idx >= args.length) {
|
|
2622
|
+
out.push({
|
|
2623
|
+
severity: "warning",
|
|
2624
|
+
code: DIAG.directiveSyntax,
|
|
2625
|
+
message: `${fnName} case "${caseName}": no witness argument for contract slot ${paramName} (${args.length} argument(s) given)`,
|
|
2626
|
+
actual: `${args.length} argument(s)`,
|
|
2627
|
+
expected: `${idx + 1} argument(s)`,
|
|
2628
|
+
suggestion: `add a witness argument for ${paramName}, or drop the contract on ${paramName}`,
|
|
2629
|
+
fn: fnName,
|
|
2630
|
+
line
|
|
2631
|
+
});
|
|
2632
|
+
continue;
|
|
2633
|
+
}
|
|
2634
|
+
if (absArgs[idx] === void 0) {
|
|
2635
|
+
if (isConstraintBuilderArg(args[idx])) continue;
|
|
2636
|
+
out.push({
|
|
2637
|
+
severity: "warning",
|
|
2638
|
+
code: DIAG.directiveSyntax,
|
|
2639
|
+
message: `${fnName} case "${caseName}": witness argument ${idx + 1} could not be parsed \u2014 contract slot ${paramName} was not checked`,
|
|
2640
|
+
actual: args[idx],
|
|
2641
|
+
expected: "a literal witness (number / string / boolean / null / undefined / object literal / array literal)",
|
|
2642
|
+
suggestion: `use a literal witness for ${paramName}, or relax the refine on ${paramName}`,
|
|
2643
|
+
fn: fnName,
|
|
2644
|
+
line
|
|
2645
|
+
});
|
|
2646
|
+
continue;
|
|
2647
|
+
}
|
|
2081
2648
|
let arg = absArgs[idx];
|
|
2082
|
-
if (!arg) continue;
|
|
2083
2649
|
if (fieldPath) {
|
|
2084
|
-
const projected =
|
|
2650
|
+
const projected = projectAbsPath(arg, fieldPath);
|
|
2085
2651
|
if (!projected) {
|
|
2086
2652
|
const k = arg.shape.k;
|
|
2087
2653
|
if (k !== "unknown" && k !== "any") {
|
|
2088
|
-
const paramName = entry.param || paramNames[idx] || `arg${idx}`;
|
|
2089
2654
|
const field = fieldPath.join(".");
|
|
2090
2655
|
out.push({
|
|
2091
2656
|
severity: "error",
|
|
@@ -2102,6 +2667,22 @@ function scanCaseInconsistency(source, knownFns, opts) {
|
|
|
2102
2667
|
}
|
|
2103
2668
|
arg = projected;
|
|
2104
2669
|
}
|
|
2670
|
+
const cc = derefDeepConstraint(entry.constraint);
|
|
2671
|
+
if (cc.fields || witnessIsStructure(arg)) {
|
|
2672
|
+
for (const v2 of witnessViolations(cc, arg, "", 0)) {
|
|
2673
|
+
out.push({
|
|
2674
|
+
severity: "error",
|
|
2675
|
+
code: DIAG.caseInconsistency,
|
|
2676
|
+
message: `${fnName} case "${caseName}": witness \u22AD contract`,
|
|
2677
|
+
actual: formatAbs(v2.actual),
|
|
2678
|
+
expected: v2.expected,
|
|
2679
|
+
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}`,
|
|
2680
|
+
fn: fnName,
|
|
2681
|
+
line
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
continue;
|
|
2685
|
+
}
|
|
2105
2686
|
const argLit = arg.term?.op === "lit" ? arg.term.value : void 0;
|
|
2106
2687
|
const argIsLit = arg.term?.op === "lit";
|
|
2107
2688
|
if (!argIsLit || typeof argLit !== "number" && typeof argLit !== "string" && typeof argLit !== "boolean" && argLit !== null && argLit !== void 0) {
|
|
@@ -2110,7 +2691,6 @@ function scanCaseInconsistency(source, knownFns, opts) {
|
|
|
2110
2691
|
const hasEqOr = (entry.constraint.members?.length ?? 0) > 0 || entry.constraint.preds.some((p) => p.op === "eq");
|
|
2111
2692
|
if (hasEqOr) {
|
|
2112
2693
|
if (!literalMeetsConstraint(argLit, entry.constraint)) {
|
|
2113
|
-
const paramName = entry.param || paramNames[idx] || `arg${idx}`;
|
|
2114
2694
|
out.push({
|
|
2115
2695
|
severity: "error",
|
|
2116
2696
|
code: DIAG.caseInconsistency,
|
|
@@ -2126,7 +2706,6 @@ function scanCaseInconsistency(source, knownFns, opts) {
|
|
|
2126
2706
|
}
|
|
2127
2707
|
if (typeof argLit !== "number") continue;
|
|
2128
2708
|
if (isIntFlag(entry.constraint) && !Number.isInteger(argLit)) {
|
|
2129
|
-
const paramName = entry.param || paramNames[idx] || `arg${idx}`;
|
|
2130
2709
|
out.push({
|
|
2131
2710
|
severity: "error",
|
|
2132
2711
|
code: DIAG.caseInconsistency,
|
|
@@ -2148,7 +2727,6 @@ function scanCaseInconsistency(source, knownFns, opts) {
|
|
|
2148
2727
|
if (p.op === "lt") ok = argLit < n;
|
|
2149
2728
|
if (p.op === "le") ok = argLit <= n;
|
|
2150
2729
|
if (!ok) {
|
|
2151
|
-
const paramName = entry.param || paramNames[idx] || `arg${idx}`;
|
|
2152
2730
|
out.push({
|
|
2153
2731
|
severity: "error",
|
|
2154
2732
|
code: DIAG.caseInconsistency,
|
|
@@ -2170,8 +2748,7 @@ function scanCaseInconsistency(source, knownFns, opts) {
|
|
|
2170
2748
|
const text = scope.commentTexts[ci];
|
|
2171
2749
|
const startLine = scope.commentStartLines[ci] ?? 0;
|
|
2172
2750
|
for (const tag of scanCaseTags(text)) {
|
|
2173
|
-
const caseArgs =
|
|
2174
|
-
if (!caseArgs) continue;
|
|
2751
|
+
const caseArgs = splitCaseArgsText(stripCaseContinuation(tag.argsText));
|
|
2175
2752
|
checkCaseAgainstReqs(scope.name, tag.name, caseArgs, startLine);
|
|
2176
2753
|
}
|
|
2177
2754
|
}
|
|
@@ -2187,18 +2764,27 @@ function checkSource(filePath, source, phi = pTrue, opts = {}) {
|
|
|
2187
2764
|
}
|
|
2188
2765
|
function checkSourceInScope(filePath, source, phi, opts) {
|
|
2189
2766
|
let callOpts = opts;
|
|
2767
|
+
const loadReadFailures = [];
|
|
2768
|
+
const loadFailureKeys = /* @__PURE__ */ new Set();
|
|
2769
|
+
let speculativeLoad;
|
|
2190
2770
|
if (opts.loadModule && opts.fromFile) {
|
|
2191
2771
|
const loadCache = /* @__PURE__ */ new Map();
|
|
2192
2772
|
const raw = opts.loadModule;
|
|
2193
|
-
const
|
|
2773
|
+
const cachedLoad = (spec, from) => {
|
|
2774
|
+
const k = `${from}\0${spec}`;
|
|
2775
|
+
if (!loadCache.has(k)) loadCache.set(k, raw(spec, from));
|
|
2776
|
+
return loadCache.get(k);
|
|
2777
|
+
};
|
|
2194
2778
|
callOpts = {
|
|
2195
2779
|
...opts,
|
|
2196
|
-
loadModule: (
|
|
2197
|
-
const
|
|
2198
|
-
if (
|
|
2199
|
-
|
|
2200
|
-
|
|
2780
|
+
loadModule: guardLoadModule(cachedLoad, (failure) => {
|
|
2781
|
+
const key = `${failure.path}\0${failure.code}`;
|
|
2782
|
+
if (loadFailureKeys.has(key)) return;
|
|
2783
|
+
loadFailureKeys.add(key);
|
|
2784
|
+
loadReadFailures.push(failure);
|
|
2785
|
+
})
|
|
2201
2786
|
};
|
|
2787
|
+
speculativeLoad = cachedLoad;
|
|
2202
2788
|
}
|
|
2203
2789
|
setRefineDiagCollector(null);
|
|
2204
2790
|
setInterfaceDiagCollector(null);
|
|
@@ -2208,11 +2794,20 @@ function checkSourceInScope(filePath, source, phi, opts) {
|
|
|
2208
2794
|
const useMemo = phi.op === "true";
|
|
2209
2795
|
let memoKey;
|
|
2210
2796
|
const stable = stableAnalyzeKeySource(source);
|
|
2211
|
-
const depsFp =
|
|
2212
|
-
|
|
2797
|
+
const depsFp = callOpts.loadModule && callOpts.fromFile ? loadModuleDepsFingerprint(stable, callOpts.loadModule, callOpts.fromFile, {
|
|
2798
|
+
...speculativeLoad ? { ambientLoadModule: speculativeLoad } : {},
|
|
2799
|
+
ambientAllowed: (sidecarPath) => sidecarAutoBindAllowed(sidecarPath, opts.autoBind, opts.projectDir)
|
|
2800
|
+
}) : { fp: "-", paths: [], contents: [], truncated: false, readError: false };
|
|
2801
|
+
const sidecarFp = callOpts.loadModule && callOpts.fromFile ? sidecarClosureFingerprint(callOpts.fromFile, {
|
|
2802
|
+
...callOpts,
|
|
2803
|
+
...speculativeLoad ? { loadModule: speculativeLoad } : {}
|
|
2804
|
+
}) : void 0;
|
|
2213
2805
|
const allowMemo = useMemo && !depsFp.truncated && // 读错误轮键不可信(#135):错误轮报告(nudo:interface-load)与真 miss 轮
|
|
2214
2806
|
// (干净)若共享 miss 键会互为跨次陈旧命中——不读也不写 memo
|
|
2215
|
-
!depsFp.readError &&
|
|
2807
|
+
!depsFp.readError && // Bug 499:本闸把读失败降级成 miss(指纹面看不到 readError)——但
|
|
2808
|
+
// 「不可读」轮与「真不存在」轮的报告不同(前者多一条读失败诊断),
|
|
2809
|
+
// 共用同一键会互为陈旧命中 ⇒ 读失败轮一律不碰 memo。
|
|
2810
|
+
loadReadFailures.length === 0 && !(sidecarFp?.startsWith("trunc:") ?? false);
|
|
2216
2811
|
if (allowMemo) {
|
|
2217
2812
|
memoKey = checkMemoKey(filePath, stable, opts, depsFp, sidecarFp);
|
|
2218
2813
|
const hit = checkMemoGet(memoKey);
|
|
@@ -2225,7 +2820,10 @@ function checkSourceInScope(filePath, source, phi, opts) {
|
|
|
2225
2820
|
const issues = [];
|
|
2226
2821
|
const signatures = [];
|
|
2227
2822
|
const file = parseSource(source);
|
|
2228
|
-
const names =
|
|
2823
|
+
const names = [
|
|
2824
|
+
...listTopFunctions(source, file),
|
|
2825
|
+
...listClassEntryNames(source, file)
|
|
2826
|
+
];
|
|
2229
2827
|
const memoPaths = sidecarFp !== void 0 && callOpts.fromFile ? [.../* @__PURE__ */ new Set([...depsFp.paths, normPath(sidecarPathOf(callOpts.fromFile))])] : depsFp.paths;
|
|
2230
2828
|
const truncated = /* @__PURE__ */ new Map();
|
|
2231
2829
|
resetAbsCallBudget();
|
|
@@ -2239,7 +2837,7 @@ function checkSourceInScope(filePath, source, phi, opts) {
|
|
|
2239
2837
|
makeSameFileFaceResolver(callOpts, filePath, source, file)
|
|
2240
2838
|
);
|
|
2241
2839
|
try {
|
|
2242
|
-
attachCrossModuleContractFaces(callOpts, filePath);
|
|
2840
|
+
const crossModuleIssues = attachCrossModuleContractFaces(callOpts, filePath);
|
|
2243
2841
|
const report = checkSourceInner(
|
|
2244
2842
|
filePath,
|
|
2245
2843
|
source,
|
|
@@ -2258,8 +2856,10 @@ function checkSourceInScope(filePath, source, phi, opts) {
|
|
|
2258
2856
|
...takeRefineDiagsSince(refineSince),
|
|
2259
2857
|
...takeInterfaceDiagsSince(ifaceSince)
|
|
2260
2858
|
]);
|
|
2261
|
-
|
|
2262
|
-
|
|
2859
|
+
const loadIssues = loadReadFailureIssues(loadReadFailures);
|
|
2860
|
+
const allIssues = [...diagIssues, ...loadIssues, ...crossModuleIssues];
|
|
2861
|
+
if (allIssues.length > 0) {
|
|
2862
|
+
report.issues.push(...allIssues);
|
|
2263
2863
|
const errors = report.issues.filter((i) => i.severity === "error").length;
|
|
2264
2864
|
const warnings = report.issues.filter((i) => i.severity === "warning").length;
|
|
2265
2865
|
const infos = report.issues.filter((i) => i.severity === "info").length;
|
|
@@ -2271,7 +2871,9 @@ function checkSourceInScope(filePath, source, phi, opts) {
|
|
|
2271
2871
|
functions: report.summary.functions
|
|
2272
2872
|
};
|
|
2273
2873
|
}
|
|
2274
|
-
if (memoKey
|
|
2874
|
+
if (memoKey && loadReadFailures.length === 0) {
|
|
2875
|
+
checkMemoSet(memoKey, report, memoPaths);
|
|
2876
|
+
}
|
|
2275
2877
|
return cloneCheckReport(report);
|
|
2276
2878
|
} finally {
|
|
2277
2879
|
setContractFaceResolver(prevFaceResolver);
|
|
@@ -2280,6 +2882,14 @@ function checkSourceInScope(filePath, source, phi, opts) {
|
|
|
2280
2882
|
setAbsTruncationCollector(prevTrunc);
|
|
2281
2883
|
}
|
|
2282
2884
|
}
|
|
2885
|
+
function loadReadFailureIssues(failures) {
|
|
2886
|
+
return failures.map((f) => ({
|
|
2887
|
+
severity: "error",
|
|
2888
|
+
code: DIAG.moduleUnreadable,
|
|
2889
|
+
message: `cannot read module '${f.path}' (${f.code}): ${f.message}`,
|
|
2890
|
+
suggestion: "fix the file permissions (or the module path) \u2014 the dependency is present but unreadable, so its types are unknown"
|
|
2891
|
+
}));
|
|
2892
|
+
}
|
|
2283
2893
|
function makeSameFileFaceResolver(opts, filePath, source, file) {
|
|
2284
2894
|
const loadModule = opts.loadModule;
|
|
2285
2895
|
const fromFile = opts.fromFile ?? filePath;
|
|
@@ -2307,24 +2917,48 @@ function makeSameFileFaceResolver(opts, filePath, source, file) {
|
|
|
2307
2917
|
return faces;
|
|
2308
2918
|
};
|
|
2309
2919
|
}
|
|
2920
|
+
function unmatchedContractParams(eff, formalDisplay, formals) {
|
|
2921
|
+
const contractNames = contractParamNameSet(formals);
|
|
2922
|
+
const display = new Set(formalDisplay);
|
|
2923
|
+
const conflictNames = new Set(eff.conflict?.params ?? []);
|
|
2924
|
+
return eff.params.map((p) => p.param).filter(
|
|
2925
|
+
(p) => p && !display.has(p) && !contractNames.has(p) && !conflictNames.has(p)
|
|
2926
|
+
);
|
|
2927
|
+
}
|
|
2928
|
+
function paramMismatchIssue(name, unknownParams, formalDisplay, formals) {
|
|
2929
|
+
const contractNames = contractParamNameSet(formals);
|
|
2930
|
+
const surface = [...contractNames].filter((n) => !n.startsWith("_p") && n !== "_").join(", ");
|
|
2931
|
+
return {
|
|
2932
|
+
severity: "error",
|
|
2933
|
+
code: DIAG.interfaceParamMismatch,
|
|
2934
|
+
message: `${name}: contract parameter name(s) not in the formal parameter list (${unknownParams.join(", ")})`,
|
|
2935
|
+
actual: unknownParams.join(", "),
|
|
2936
|
+
expected: surface || formalDisplay.join(", ") || "(no params)",
|
|
2937
|
+
suggestion: `rename the @nudo:contract / sidecar binding parameter to one of: ${surface || formalDisplay.join(", ") || "(function has no params)"}`,
|
|
2938
|
+
fn: name
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2310
2941
|
function attachCrossModuleContractFaces(opts, filePath) {
|
|
2311
2942
|
const loadModule = opts.loadModule;
|
|
2312
2943
|
const fromFile = opts.fromFile ?? filePath;
|
|
2313
|
-
if (!loadModule) return;
|
|
2944
|
+
if (!loadModule) return [];
|
|
2314
2945
|
const tables = [];
|
|
2315
2946
|
for (const t of [opts.modules, opts.inject?.modules]) {
|
|
2316
2947
|
if (t && typeof t === "object" && !tables.includes(t)) tables.push(t);
|
|
2317
2948
|
}
|
|
2318
|
-
if (tables.length === 0) return;
|
|
2949
|
+
if (tables.length === 0) return [];
|
|
2319
2950
|
const autoBind = opts.autoBind;
|
|
2320
2951
|
const projectDir = opts.projectDir;
|
|
2952
|
+
const mismatchIssues = [];
|
|
2321
2953
|
const facesCache = /* @__PURE__ */ new Map();
|
|
2322
2954
|
const facesFor = (depSource, depPath, name, paramNames) => {
|
|
2323
2955
|
const key = `${depPath}\0${name}`;
|
|
2324
|
-
|
|
2956
|
+
const hit = facesCache.get(key);
|
|
2957
|
+
if (hit) return hit;
|
|
2325
2958
|
let faces;
|
|
2959
|
+
let eff;
|
|
2326
2960
|
try {
|
|
2327
|
-
|
|
2961
|
+
eff = effectiveInterface(depSource, name, {
|
|
2328
2962
|
loadModule,
|
|
2329
2963
|
fromFile: depPath,
|
|
2330
2964
|
...autoBind !== void 0 ? { autoBind } : {},
|
|
@@ -2333,22 +2967,36 @@ function attachCrossModuleContractFaces(opts, filePath) {
|
|
|
2333
2967
|
if (eff) faces = contractFacesFromEffectiveInterface(eff, paramNames);
|
|
2334
2968
|
} catch {
|
|
2335
2969
|
faces = void 0;
|
|
2970
|
+
eff = void 0;
|
|
2336
2971
|
}
|
|
2337
|
-
|
|
2338
|
-
|
|
2972
|
+
const out = { faces, eff };
|
|
2973
|
+
facesCache.set(key, out);
|
|
2974
|
+
return out;
|
|
2339
2975
|
};
|
|
2340
2976
|
const attachFnAbs = (v2, depSource, depPath, name) => {
|
|
2341
2977
|
if (!v2 || typeof v2 !== "object" || !("shape" in v2)) return;
|
|
2342
2978
|
const shape2 = v2.shape;
|
|
2343
2979
|
if (shape2.k !== "fn") return;
|
|
2344
|
-
let paramNames;
|
|
2980
|
+
let paramNames = [];
|
|
2981
|
+
let formals = [];
|
|
2345
2982
|
try {
|
|
2346
|
-
|
|
2983
|
+
const meta = extractFn(depSource, name);
|
|
2984
|
+
paramNames = meta?.params ?? [];
|
|
2985
|
+
formals = meta?.formals ?? [];
|
|
2347
2986
|
} catch {
|
|
2348
2987
|
paramNames = [];
|
|
2988
|
+
formals = [];
|
|
2349
2989
|
}
|
|
2350
|
-
const faces = facesFor(depSource, depPath, name, paramNames);
|
|
2990
|
+
const { faces, eff } = facesFor(depSource, depPath, name, paramNames);
|
|
2351
2991
|
if (faces) attachContractFaces(v2, faces);
|
|
2992
|
+
if (eff?.source === "handwritten") {
|
|
2993
|
+
const unknownParams = unmatchedContractParams(eff, paramNames, formals);
|
|
2994
|
+
if (unknownParams.length > 0) {
|
|
2995
|
+
mismatchIssues.push(
|
|
2996
|
+
paramMismatchIssue(name, unknownParams, paramNames, formals)
|
|
2997
|
+
);
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
2352
3000
|
};
|
|
2353
3001
|
for (const table of tables) {
|
|
2354
3002
|
for (const [spec, mod2] of Object.entries(table)) {
|
|
@@ -2370,6 +3018,7 @@ function attachCrossModuleContractFaces(opts, filePath) {
|
|
|
2370
3018
|
if (def !== void 0) attachFnAbs(def, depSource, depPath, "default");
|
|
2371
3019
|
}
|
|
2372
3020
|
}
|
|
3021
|
+
return mismatchIssues;
|
|
2373
3022
|
}
|
|
2374
3023
|
function sidecarDiagIssues(diags) {
|
|
2375
3024
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2379,7 +3028,14 @@ function sidecarDiagIssues(diags) {
|
|
|
2379
3028
|
if (seen.has(k)) continue;
|
|
2380
3029
|
seen.add(k);
|
|
2381
3030
|
const severity = d.code === DIAG.contractSyntax || d.code === DIAG.directiveSyntax ? "warning" : "error";
|
|
2382
|
-
out.push({
|
|
3031
|
+
out.push({
|
|
3032
|
+
severity,
|
|
3033
|
+
code: d.code,
|
|
3034
|
+
message: d.message,
|
|
3035
|
+
// Bug 374:位置透传(nudo:contract-syntax 不再落在文件第 1 行第 1 列)
|
|
3036
|
+
...d.line !== void 0 ? { line: d.line } : {},
|
|
3037
|
+
...d.column !== void 0 ? { column: d.column } : {}
|
|
3038
|
+
});
|
|
2383
3039
|
}
|
|
2384
3040
|
return out;
|
|
2385
3041
|
}
|
|
@@ -2397,6 +3053,7 @@ function assumePremisesPhiOf(name, source, file, issues) {
|
|
|
2397
3053
|
const preds = [];
|
|
2398
3054
|
const display = [];
|
|
2399
3055
|
for (const seg of segs) {
|
|
3056
|
+
const pos = seg.line !== void 0 ? { line: seg.line, ...seg.column !== void 0 ? { column: seg.column } : {} } : {};
|
|
2400
3057
|
if (seg.kind === "bad") {
|
|
2401
3058
|
issues.push({
|
|
2402
3059
|
severity: "error",
|
|
@@ -2405,7 +3062,8 @@ function assumePremisesPhiOf(name, source, file, issues) {
|
|
|
2405
3062
|
actual: seg.raw,
|
|
2406
3063
|
expected: "<param> <= <param>",
|
|
2407
3064
|
suggestion: "cross-variable lemmas: @nudo:assume d <= max \u2014 constant bounds: sidecar number().le(10)",
|
|
2408
|
-
fn: name
|
|
3065
|
+
fn: name,
|
|
3066
|
+
...pos
|
|
2409
3067
|
});
|
|
2410
3068
|
continue;
|
|
2411
3069
|
}
|
|
@@ -2418,7 +3076,8 @@ function assumePremisesPhiOf(name, source, file, issues) {
|
|
|
2418
3076
|
actual: bad.join(", "),
|
|
2419
3077
|
expected: surface || "(no params/locals)",
|
|
2420
3078
|
suggestion: `reference one of: ${surface || "(no params or top-level locals)"}`,
|
|
2421
|
-
fn: name
|
|
3079
|
+
fn: name,
|
|
3080
|
+
...pos
|
|
2422
3081
|
});
|
|
2423
3082
|
continue;
|
|
2424
3083
|
}
|
|
@@ -2426,7 +3085,7 @@ function assumePremisesPhiOf(name, source, file, issues) {
|
|
|
2426
3085
|
display.push(`${seg.lhs} <= ${seg.rhs}`);
|
|
2427
3086
|
}
|
|
2428
3087
|
if (preds.length === 0) return void 0;
|
|
2429
|
-
return { phi: and(...preds), display };
|
|
3088
|
+
return { phi: and(...preds), display, premises: preds };
|
|
2430
3089
|
}
|
|
2431
3090
|
function invariantSpecsOf(name, source, file, issues) {
|
|
2432
3091
|
const segs = extractInvariantSegs(source, name);
|
|
@@ -2441,6 +3100,7 @@ function invariantSpecsOf(name, source, file, issues) {
|
|
|
2441
3100
|
const specs = [];
|
|
2442
3101
|
const display = [];
|
|
2443
3102
|
for (const seg of segs) {
|
|
3103
|
+
const pos = seg.line !== void 0 ? { line: seg.line, ...seg.column !== void 0 ? { column: seg.column } : {} } : {};
|
|
2444
3104
|
if (seg.kind === "bad") {
|
|
2445
3105
|
issues.push({
|
|
2446
3106
|
severity: "warning",
|
|
@@ -2449,7 +3109,8 @@ function invariantSpecsOf(name, source, file, issues) {
|
|
|
2449
3109
|
actual: seg.raw,
|
|
2450
3110
|
expected: "<name><path> <rel> <number>",
|
|
2451
3111
|
suggestion: "loop-carried bounds: @nudo:invariant dp[*][*] >= 0 \u2014 scalar: @nudo:invariant last >= 1",
|
|
2452
|
-
fn: name
|
|
3112
|
+
fn: name,
|
|
3113
|
+
...pos
|
|
2453
3114
|
});
|
|
2454
3115
|
continue;
|
|
2455
3116
|
}
|
|
@@ -2461,7 +3122,8 @@ function invariantSpecsOf(name, source, file, issues) {
|
|
|
2461
3122
|
actual: seg.name,
|
|
2462
3123
|
expected: surface || "(no params/locals)",
|
|
2463
3124
|
suggestion: `reference one of: ${surface || "(no params or top-level locals)"}`,
|
|
2464
|
-
fn: name
|
|
3125
|
+
fn: name,
|
|
3126
|
+
...pos
|
|
2465
3127
|
});
|
|
2466
3128
|
continue;
|
|
2467
3129
|
}
|
|
@@ -2471,6 +3133,36 @@ function invariantSpecsOf(name, source, file, issues) {
|
|
|
2471
3133
|
if (specs.length === 0) return void 0;
|
|
2472
3134
|
return { specs, display };
|
|
2473
3135
|
}
|
|
3136
|
+
function dedupeAssignRecords(records) {
|
|
3137
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3138
|
+
const out = [];
|
|
3139
|
+
for (const r of records) {
|
|
3140
|
+
const key = JSON.stringify([
|
|
3141
|
+
r.name,
|
|
3142
|
+
r.line ?? "",
|
|
3143
|
+
r.column ?? "",
|
|
3144
|
+
r.conditional ? "1" : "0",
|
|
3145
|
+
assignValueKey(r.prev),
|
|
3146
|
+
assignValueKey(r.next)
|
|
3147
|
+
]);
|
|
3148
|
+
if (seen.has(key)) continue;
|
|
3149
|
+
seen.add(key);
|
|
3150
|
+
out.push(r);
|
|
3151
|
+
}
|
|
3152
|
+
return out;
|
|
3153
|
+
}
|
|
3154
|
+
function assignValueKey(v2) {
|
|
3155
|
+
if (v2 === void 0) return "-";
|
|
3156
|
+
if (v2 === null) return "null";
|
|
3157
|
+
if (typeof v2 !== "object") return `host:${typeof v2}:${String(v2)}`;
|
|
3158
|
+
const o = v2;
|
|
3159
|
+
if (!("shape" in o) || !("conf" in o)) return `host:${typeof v2}`;
|
|
3160
|
+
try {
|
|
3161
|
+
return formatAbs(v2);
|
|
3162
|
+
} catch {
|
|
3163
|
+
return `abs:${o.shape?.k ?? "?"}:${o.conf ?? "?"}`;
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
2474
3166
|
var OPAQUE_EVIDENCE_HINT = "add @nudo:case or a call site";
|
|
2475
3167
|
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
3168
|
function isBuiltinModuleSpec(spec) {
|
|
@@ -2505,9 +3197,12 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2505
3197
|
eiCache.set(key, eff);
|
|
2506
3198
|
return eff;
|
|
2507
3199
|
};
|
|
3200
|
+
const records = [];
|
|
3201
|
+
const prevAssign = setEvalAssignCollector((r) => records.push(r));
|
|
2508
3202
|
for (const name of names) {
|
|
2509
3203
|
const assumePhi = assumePremisesPhiOf(name, source, file, issues);
|
|
2510
3204
|
const inv = invariantSpecsOf(name, source, file, issues);
|
|
3205
|
+
resetAssumeConsumed();
|
|
2511
3206
|
const runSeeded = (body) => {
|
|
2512
3207
|
if (assumePhi === void 0 && inv === void 0) return body();
|
|
2513
3208
|
return withAssumePremises(assumePhi?.phi ?? pTrue, () => withInvariantSpecs(inv?.specs ?? [], body));
|
|
@@ -2525,6 +3220,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2525
3220
|
contractParam.set(p.param, constraintToEntryAbs(p.constraint, p.param));
|
|
2526
3221
|
}
|
|
2527
3222
|
}
|
|
3223
|
+
const skipFmt = formatSigCached(retAbs, name);
|
|
2528
3224
|
signatures.push({
|
|
2529
3225
|
name,
|
|
2530
3226
|
params,
|
|
@@ -2533,8 +3229,8 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2533
3229
|
return a ? formatShape(a) : "any";
|
|
2534
3230
|
}),
|
|
2535
3231
|
abs: retAbs,
|
|
2536
|
-
display:
|
|
2537
|
-
detail:
|
|
3232
|
+
display: skipFmt.display,
|
|
3233
|
+
detail: skipFmt.detail,
|
|
2538
3234
|
conf: retAbs.conf,
|
|
2539
3235
|
...isEntryFn ? { entry: true } : {}
|
|
2540
3236
|
});
|
|
@@ -2544,12 +3240,29 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2544
3240
|
fromFile: refineFrom
|
|
2545
3241
|
});
|
|
2546
3242
|
const display = named?.name ?? formatConstraint(eff.returns.constraint);
|
|
2547
|
-
|
|
3243
|
+
const contractSource = named ? "directive" : "sidecar";
|
|
3244
|
+
returnContracts.set(name, {
|
|
3245
|
+
display,
|
|
3246
|
+
constraint: eff.returns.constraint,
|
|
3247
|
+
source: contractSource
|
|
3248
|
+
});
|
|
2548
3249
|
const retIssues = checkReturnConstraint(
|
|
2549
3250
|
name,
|
|
2550
3251
|
display,
|
|
2551
3252
|
eff.returns.constraint,
|
|
2552
|
-
declared
|
|
3253
|
+
declared,
|
|
3254
|
+
{
|
|
3255
|
+
// #369 / #372:issue 构造处带函数声明位置
|
|
3256
|
+
loc: findFnLoc(file, name),
|
|
3257
|
+
source: contractSource,
|
|
3258
|
+
// Bug 1094:返回点的路径前提 Φ(用户引理前提)——skip 分支同样
|
|
3259
|
+
// 透传(函数级公理在 skip 面也成立;证明通道按「同项 ∧ 蕴含」
|
|
3260
|
+
// 口径消费 ⇒ 不会凭空制造确定违约)。assumeDisplay 一并带上 ⇒
|
|
3261
|
+
// Φ 被消费时归因(nudo:assume-relied)仍然可见(不得静默消费)。
|
|
3262
|
+
phi: assumePhi?.phi,
|
|
3263
|
+
phiPremises: assumePhi?.premises,
|
|
3264
|
+
assumeDisplay: assumePhi?.display
|
|
3265
|
+
}
|
|
2553
3266
|
);
|
|
2554
3267
|
if (retIssues.some((i) => i.severity === "error")) returnViolated.add(name);
|
|
2555
3268
|
issues.push(...retIssues);
|
|
@@ -2587,7 +3300,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2587
3300
|
};
|
|
2588
3301
|
const effects = collectEntryMayThrows(source, name, synthetic, file, phi, opts);
|
|
2589
3302
|
const throwsDisplayFallback = formatThrowsAbs(mayThrowEffectsToAbs(effects));
|
|
2590
|
-
const declared =
|
|
3303
|
+
const declared = declaredThrowsForEntry(source, name) ?? effectiveInterfaceCached(name)?.throws?.kinds;
|
|
2591
3304
|
const remaining = filterGateThrows(effects, declared, ignoreThrows);
|
|
2592
3305
|
const gateDisplay = formatThrowsAbs(mayThrowEffectsToAbs(remaining));
|
|
2593
3306
|
if (gateDisplay && remaining.length > 0) {
|
|
@@ -2632,7 +3345,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2632
3345
|
if (isEntry && entryThrowsMode !== "off") {
|
|
2633
3346
|
const effects = runSeeded(() => collectEntryMayThrows(source, name, g, file, phi, opts));
|
|
2634
3347
|
throwsDisplay = formatThrowsAbs(mayThrowEffectsToAbs(effects));
|
|
2635
|
-
const declared =
|
|
3348
|
+
const declared = declaredThrowsForEntry(source, name) ?? effectiveInterfaceCached(name)?.throws?.kinds;
|
|
2636
3349
|
const remaining = filterGateThrows(effects, declared, ignoreThrows);
|
|
2637
3350
|
const gateDisplay = formatThrowsAbs(mayThrowEffectsToAbs(remaining));
|
|
2638
3351
|
if (gateDisplay && remaining.length > 0) {
|
|
@@ -2658,7 +3371,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2658
3371
|
const paramTypes = g.typeParams.map((t) => {
|
|
2659
3372
|
if (!t.value) return "any";
|
|
2660
3373
|
if (t.value.shape.k === "unknown") return "unknown";
|
|
2661
|
-
return
|
|
3374
|
+
return formatEntryParamShape(t.value);
|
|
2662
3375
|
});
|
|
2663
3376
|
signatures.push({
|
|
2664
3377
|
name,
|
|
@@ -2678,7 +3391,12 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2678
3391
|
// 的排除一致。
|
|
2679
3392
|
(t) => t.value && t.value.shape.k === "unknown" && t.value.term?.op !== "lit"
|
|
2680
3393
|
);
|
|
2681
|
-
const budgetExplained = truncated.has(name) ||
|
|
3394
|
+
const budgetExplained = truncated.has(name) || // Bug 389:`conf=opaque` 通常意味着「路径没覆盖」⇒ 已解释;但**因 env
|
|
3395
|
+
// 门禁而 opaque**(源码声明了 `@nudo:env` 而那个 env 没解析出全局 ⇒
|
|
3396
|
+
// 函数体 fail-closed)不是「已解释」——那正是「推断没成功」,唯一能提示
|
|
3397
|
+
// 它的 `nudo:unknown-inference` 不能被吞(否则用户把 env 名写错的那一刻,
|
|
3398
|
+
// 唯一的失败信号恰好消失)。
|
|
3399
|
+
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
3400
|
const symbolicTrueUnknown = g.symbolic?.shape?.k === "unknown" && g.symbolic.term?.op !== "lit";
|
|
2683
3401
|
if ((symbolicTrueUnknown || unknownParamIdx >= 0) && !budgetExplained) {
|
|
2684
3402
|
const where = symbolicTrueUnknown ? `${name} => unknown` : `${name} param ${g.params[unknownParamIdx] ?? `#${unknownParamIdx}`}: unknown`;
|
|
@@ -2688,32 +3406,32 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2688
3406
|
message: `${name}: signature has true unknown (inference failed)`,
|
|
2689
3407
|
actual: where,
|
|
2690
3408
|
expected: "computable Abs (any = unconstrained, unknown = engine debt)",
|
|
2691
|
-
|
|
3409
|
+
// Bug 1062(族 46):本诊断此前把「`@nudo:replace` 的目标文本没命中」
|
|
3410
|
+
// 说成「推断精度不够」⇒ 归因错误 ⇒ 用户只看到 unknown-inference。
|
|
3411
|
+
// service 侧已加「点名目标文本」的显式诊断(`nudo:directive-syntax`),
|
|
3412
|
+
// 这里补一句归因提示,把「指令没生效」这条可能性点出来。
|
|
3413
|
+
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
3414
|
fn: name
|
|
2693
3415
|
});
|
|
2694
3416
|
}
|
|
2695
3417
|
if (hasRefineDirective || (exportedNames?.has(name) ?? false)) {
|
|
2696
3418
|
const eff = effectiveInterfaceCached(name);
|
|
2697
3419
|
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
|
-
);
|
|
3420
|
+
const unknownParams = unmatchedContractParams(eff, g.params, g.formals ?? []);
|
|
2704
3421
|
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
|
-
});
|
|
3422
|
+
issues.push(
|
|
3423
|
+
paramMismatchIssue(name, unknownParams, g.params, g.formals ?? [])
|
|
3424
|
+
);
|
|
2715
3425
|
}
|
|
2716
3426
|
}
|
|
3427
|
+
const contractLocs = contractSegmentLocs(source, name);
|
|
3428
|
+
const locOf = (param) => {
|
|
3429
|
+
const hit = contractLocs.find((c) => {
|
|
3430
|
+
const head = c.segment.trim().split(/\s+/)[0];
|
|
3431
|
+
return head === param;
|
|
3432
|
+
});
|
|
3433
|
+
return hit?.loc ? { line: hit.loc.line, ...hit.loc.column !== void 0 ? { column: hit.loc.column } : {} } : {};
|
|
3434
|
+
};
|
|
2717
3435
|
if (eff?.conflict) {
|
|
2718
3436
|
if (eff.conflict.params.length > 0) {
|
|
2719
3437
|
issues.push({
|
|
@@ -2721,7 +3439,8 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2721
3439
|
code: DIAG.interfaceConflict,
|
|
2722
3440
|
message: `${name}: handwritten contract conjunction unsatisfiable (${eff.conflict.params.join(", ")})`,
|
|
2723
3441
|
suggestion: "check whether the source @nudo:contract and the same-name sidecar binding have contradictory constant bounds",
|
|
2724
|
-
fn: name
|
|
3442
|
+
fn: name,
|
|
3443
|
+
...locOf(eff.conflict.params[0])
|
|
2725
3444
|
});
|
|
2726
3445
|
}
|
|
2727
3446
|
if (eff.conflict.returns) {
|
|
@@ -2730,24 +3449,70 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2730
3449
|
code: DIAG.interfaceConflict,
|
|
2731
3450
|
message: `${name}: handwritten contract conjunction unsatisfiable on the return slot`,
|
|
2732
3451
|
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
|
|
3452
|
+
fn: name,
|
|
3453
|
+
...locOf("return")
|
|
2734
3454
|
});
|
|
2735
3455
|
}
|
|
2736
3456
|
}
|
|
3457
|
+
const undecidable = eff?.undecidable;
|
|
3458
|
+
if (undecidable !== void 0 && undecidable.params.length > 0) {
|
|
3459
|
+
issues.push({
|
|
3460
|
+
severity: "warning",
|
|
3461
|
+
code: DIAG.interfaceConflict,
|
|
3462
|
+
message: `${name}: contract conjunction not computable (${undecidable.params.join(", ")}) \u2014 only one side is enforced`,
|
|
3463
|
+
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",
|
|
3464
|
+
fn: name,
|
|
3465
|
+
...locOf(undecidable.params[0])
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
if (undecidable?.returns) {
|
|
3469
|
+
issues.push({
|
|
3470
|
+
severity: "warning",
|
|
3471
|
+
code: DIAG.interfaceConflict,
|
|
3472
|
+
message: `${name}: contract conjunction not computable on the return slot \u2014 only one side is enforced`,
|
|
3473
|
+
suggestion: "structural return constraints cannot be combined in this slot; the other side's obligations are NOT enforced \u2014 merge them by hand",
|
|
3474
|
+
fn: name,
|
|
3475
|
+
...locOf("return")
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
2737
3478
|
if (eff && eff.source === "handwritten" && eff.returns && !eff.conflict?.returns) {
|
|
2738
3479
|
const named = extractRefineReturnFromSource(source, name, {
|
|
2739
3480
|
loadModule: refineLoad,
|
|
2740
3481
|
fromFile: refineFrom
|
|
2741
3482
|
});
|
|
2742
3483
|
const display = named?.name ?? formatConstraint(eff.returns.constraint);
|
|
2743
|
-
|
|
3484
|
+
const contractSource = named ? "directive" : "sidecar";
|
|
3485
|
+
returnContracts.set(name, {
|
|
3486
|
+
display,
|
|
3487
|
+
constraint: eff.returns.constraint,
|
|
3488
|
+
source: contractSource
|
|
3489
|
+
});
|
|
3490
|
+
const consumedAssume = assumePhi ? new Set(
|
|
3491
|
+
assumePhi.display.filter(
|
|
3492
|
+
(_, i) => assumeConsumed().includes(assumePhi.premises[i])
|
|
3493
|
+
)
|
|
3494
|
+
) : void 0;
|
|
2744
3495
|
const retIssues = checkReturnConstraint(
|
|
2745
3496
|
name,
|
|
2746
3497
|
display,
|
|
2747
3498
|
eff.returns.constraint,
|
|
2748
3499
|
g.symbolic,
|
|
2749
|
-
|
|
2750
|
-
|
|
3500
|
+
{
|
|
3501
|
+
// #369 / #372:issue 构造处带函数声明位置
|
|
3502
|
+
loc: findFnLoc(file, name),
|
|
3503
|
+
source: contractSource,
|
|
3504
|
+
assumeDisplay: assumePhi?.display,
|
|
3505
|
+
invariantDisplay: inv?.display,
|
|
3506
|
+
consumedAssume,
|
|
3507
|
+
// Bug 1094:返回点的**路径前提 Φ** = 用户引理前提
|
|
3508
|
+
//(`@nudo:assume` 的前提 Φ)——证明通道的 Φ 侧。
|
|
3509
|
+
// 项身份:前提项用**真实形参名**(`termVar(name)`),泛化后的返回
|
|
3510
|
+
// 项在形参**有契约**时同样锚在真实名上(`constraintToEntryAbs`)
|
|
3511
|
+
// ⇒ 同项判定能对上;无契约时返回项锚在 α 名(A1…)⇒ 对不上 ⇒
|
|
3512
|
+
// Φ 侧不生效(保守少证,不假红)。
|
|
3513
|
+
phi: assumePhi?.phi,
|
|
3514
|
+
phiPremises: assumePhi?.premises
|
|
3515
|
+
}
|
|
2751
3516
|
);
|
|
2752
3517
|
if (retIssues.some((i) => i.severity === "error")) returnViolated.add(name);
|
|
2753
3518
|
issues.push(...retIssues);
|
|
@@ -2760,7 +3525,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2760
3525
|
});
|
|
2761
3526
|
}
|
|
2762
3527
|
}
|
|
2763
|
-
if (g.symbolic.conf === "opaque" && !truncated.has(name)) {
|
|
3528
|
+
if (g.symbolic.conf === "opaque" && !truncated.has(name) && !g.envGated) {
|
|
2764
3529
|
issues.push({
|
|
2765
3530
|
severity: "info",
|
|
2766
3531
|
code: DIAG.opaqueResult,
|
|
@@ -2839,16 +3604,19 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2839
3604
|
fn: label
|
|
2840
3605
|
});
|
|
2841
3606
|
}
|
|
2842
|
-
const records = [];
|
|
2843
3607
|
const varAbs = /* @__PURE__ */ new Map();
|
|
2844
3608
|
const callRecords = [];
|
|
2845
3609
|
const bCalls = [];
|
|
2846
|
-
const prevAssign = setEvalAssignCollector((r) => records.push(r));
|
|
2847
3610
|
const prevCall = setEvalCallCollector((r) => bCalls.push(r));
|
|
2848
3611
|
let evalBindings;
|
|
2849
3612
|
const evalAnalyze = evalAnalyzeOpts(opts);
|
|
3613
|
+
let moduleThrow;
|
|
2850
3614
|
try {
|
|
2851
|
-
const
|
|
3615
|
+
const evalAttempt = tryRunTranspiledDetailed(source, evalAnalyze);
|
|
3616
|
+
const evalRun = evalAttempt.run;
|
|
3617
|
+
if (evalRun === void 0 && evalAttempt.fallback?.reason === "module-throw") {
|
|
3618
|
+
moduleThrow = definiteModuleThrowOf(file);
|
|
3619
|
+
}
|
|
2852
3620
|
evalBindings = evalRun ? bindingsOf(evalRun) : void 0;
|
|
2853
3621
|
if (evalBindings) {
|
|
2854
3622
|
for (const [k, v2] of evalBindings) {
|
|
@@ -2871,6 +3639,22 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2871
3639
|
setEvalCallCollector(prevCall);
|
|
2872
3640
|
}
|
|
2873
3641
|
void evalBindings;
|
|
3642
|
+
if (moduleThrow !== void 0 && entryThrowsMode !== "off") {
|
|
3643
|
+
const kind = moduleThrow.kind;
|
|
3644
|
+
if (kind === void 0 || !isThrowsIgnored(kind, ignoreThrows)) {
|
|
3645
|
+
const thrown = kind !== void 0 ? ` ${kind}` : "";
|
|
3646
|
+
issues.push({
|
|
3647
|
+
severity: entryThrowsMode === "warning" ? "warning" : "error",
|
|
3648
|
+
code: DIAG.moduleThrow,
|
|
3649
|
+
message: `module initializer always throws${thrown} \u2014 the module cannot be imported, so every export is unavailable (module not analyzed)`,
|
|
3650
|
+
actual: `throw at line ${moduleThrow.line}`,
|
|
3651
|
+
expected: "module initializer that can complete, or a conditional throw",
|
|
3652
|
+
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",
|
|
3653
|
+
line: moduleThrow.line,
|
|
3654
|
+
column: moduleThrow.column
|
|
3655
|
+
});
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
2874
3658
|
const callIssues = canSkipLiteralCallScan(source, file) ? [] : scanLiteralCalls(source, names, phi, {
|
|
2875
3659
|
loadModule: opts.loadModule,
|
|
2876
3660
|
// 与 generalize / 返回后置同一 fromFile 口径:ambient 侧车按真实
|
|
@@ -2896,15 +3680,22 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2896
3680
|
rec.fnName,
|
|
2897
3681
|
rc.display,
|
|
2898
3682
|
rc.constraint,
|
|
2899
|
-
rec.result
|
|
3683
|
+
rec.result,
|
|
3684
|
+
{
|
|
3685
|
+
// #369 / #372:issue 构造处带函数声明位置(被违约函数的声明处)
|
|
3686
|
+
loc: findFnLoc(file, rec.fnName),
|
|
3687
|
+
// #378:约束来源随 returnContracts 走(源码指令 vs 侧车 fn() 返回槽)
|
|
3688
|
+
source: rc.source
|
|
3689
|
+
}
|
|
2900
3690
|
);
|
|
2901
3691
|
if (retIssues.some((i) => i.severity === "error")) returnViolated.add(rec.fnName);
|
|
2902
3692
|
issues.push(...retIssues);
|
|
2903
3693
|
}
|
|
2904
3694
|
}
|
|
2905
3695
|
if (driftCandidates.length > 0) {
|
|
3696
|
+
const driftRecords = opts.from && opts.from.length > 0 ? [...callRecords, ...opts.from.map((r) => ({ fnName: r.fnName, args: r.args, result: r.result }))] : callRecords;
|
|
2906
3697
|
issues.push(
|
|
2907
|
-
...interfaceDriftIssues(driftCandidates,
|
|
3698
|
+
...interfaceDriftIssues(driftCandidates, driftRecords, /* @__PURE__ */ (() => {
|
|
2908
3699
|
let driftRun;
|
|
2909
3700
|
let driftInit = false;
|
|
2910
3701
|
return (fnName, args) => {
|
|
@@ -2914,7 +3705,9 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2914
3705
|
driftInit = true;
|
|
2915
3706
|
}
|
|
2916
3707
|
if (!driftRun || !(fnName in driftRun)) return void 0;
|
|
2917
|
-
return callTranspiledExportFull(driftRun, fnName, args
|
|
3708
|
+
return callTranspiledExportFull(driftRun, fnName, args, {
|
|
3709
|
+
preferLocal: true
|
|
3710
|
+
}).result;
|
|
2918
3711
|
} catch {
|
|
2919
3712
|
return void 0;
|
|
2920
3713
|
}
|
|
@@ -2934,7 +3727,16 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2934
3727
|
...opts.inject ? { inject: opts.inject } : {}
|
|
2935
3728
|
})
|
|
2936
3729
|
);
|
|
2937
|
-
|
|
3730
|
+
try {
|
|
3731
|
+
issues.push(...structuralAssignIssues(dedupeAssignRecords(records)));
|
|
3732
|
+
} catch (err) {
|
|
3733
|
+
issues.push({
|
|
3734
|
+
severity: "warning",
|
|
3735
|
+
code: DIAG.internal,
|
|
3736
|
+
message: `assign channel check failed: ${err.message}`,
|
|
3737
|
+
suggestion: "this is an internal error; please report it together with the checked source file"
|
|
3738
|
+
});
|
|
3739
|
+
}
|
|
2938
3740
|
const errors = issues.filter((i) => i.severity === "error").length;
|
|
2939
3741
|
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
2940
3742
|
const infos = issues.filter((i) => i.severity === "info").length;
|
|
@@ -2948,60 +3750,6 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
|
|
|
2948
3750
|
budget: { ...budget }
|
|
2949
3751
|
};
|
|
2950
3752
|
}
|
|
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
3753
|
export {
|
|
3006
3754
|
$add,
|
|
3007
3755
|
$arguments,
|
|
@@ -3011,7 +3759,6 @@ export {
|
|
|
3011
3759
|
$arrWithHoles,
|
|
3012
3760
|
$assignRecord,
|
|
3013
3761
|
$async,
|
|
3014
|
-
$asyncReturn,
|
|
3015
3762
|
$await,
|
|
3016
3763
|
$bitand,
|
|
3017
3764
|
$bitnot,
|
|
@@ -3035,7 +3782,6 @@ export {
|
|
|
3035
3782
|
$fnVal,
|
|
3036
3783
|
$for,
|
|
3037
3784
|
$forInKeys,
|
|
3038
|
-
$forIter,
|
|
3039
3785
|
$forOf,
|
|
3040
3786
|
$fork,
|
|
3041
3787
|
$ge,
|
|
@@ -3086,12 +3832,9 @@ export {
|
|
|
3086
3832
|
$shl,
|
|
3087
3833
|
$shr,
|
|
3088
3834
|
$spread,
|
|
3089
|
-
$staticInvoke,
|
|
3090
3835
|
$sub,
|
|
3091
3836
|
$super,
|
|
3092
3837
|
$switch,
|
|
3093
|
-
$thisGet,
|
|
3094
|
-
$thisSet,
|
|
3095
3838
|
$throw,
|
|
3096
3839
|
$toNumber,
|
|
3097
3840
|
$toNumeric,
|
|
@@ -3110,7 +3853,6 @@ export {
|
|
|
3110
3853
|
$updateAdd,
|
|
3111
3854
|
$updateSub,
|
|
3112
3855
|
$ushr,
|
|
3113
|
-
$while,
|
|
3114
3856
|
$whileSeq,
|
|
3115
3857
|
$yield,
|
|
3116
3858
|
CONSTRAINT_BUILDERS,
|
|
@@ -3137,7 +3879,6 @@ export {
|
|
|
3137
3879
|
absToString,
|
|
3138
3880
|
actionsForIssue,
|
|
3139
3881
|
add,
|
|
3140
|
-
alphaOf,
|
|
3141
3882
|
and,
|
|
3142
3883
|
andC,
|
|
3143
3884
|
any,
|
|
@@ -3153,7 +3894,6 @@ export {
|
|
|
3153
3894
|
assumeFinite,
|
|
3154
3895
|
attachFnImpl,
|
|
3155
3896
|
beginCollectionFork,
|
|
3156
|
-
betaOf,
|
|
3157
3897
|
bigintLit,
|
|
3158
3898
|
bindImports,
|
|
3159
3899
|
bindingsOf,
|
|
@@ -3189,14 +3929,11 @@ export {
|
|
|
3189
3929
|
constraintToEntryAbs,
|
|
3190
3930
|
contractParamNameSet,
|
|
3191
3931
|
createEnvironment,
|
|
3192
|
-
createHofCollectCtx,
|
|
3193
3932
|
ctorArgDefinitelyInvalid,
|
|
3194
3933
|
ctorNameOfRecv,
|
|
3195
3934
|
currentExecPhi,
|
|
3196
|
-
currentPhi,
|
|
3197
3935
|
definitelyNotNullishShape,
|
|
3198
3936
|
derefConstraint,
|
|
3199
|
-
describePhi,
|
|
3200
3937
|
div,
|
|
3201
3938
|
drainPromiseMicros,
|
|
3202
3939
|
effectiveInterface,
|
|
@@ -3266,7 +4003,6 @@ export {
|
|
|
3266
4003
|
formatShapeSlot,
|
|
3267
4004
|
ge,
|
|
3268
4005
|
geNum,
|
|
3269
|
-
generalizeAll,
|
|
3270
4006
|
generalizeFromAst,
|
|
3271
4007
|
generatedExportNames,
|
|
3272
4008
|
getAbsProperty,
|
|
@@ -3397,14 +4133,12 @@ export {
|
|
|
3397
4133
|
phiAnd,
|
|
3398
4134
|
pick,
|
|
3399
4135
|
popCollectionArm,
|
|
3400
|
-
popPhi,
|
|
3401
4136
|
powAbs,
|
|
3402
4137
|
predEquals,
|
|
3403
4138
|
predToString,
|
|
3404
4139
|
predVars,
|
|
3405
4140
|
primToTypeof,
|
|
3406
4141
|
projectFlatMapResult,
|
|
3407
|
-
promoteParamShape,
|
|
3408
4142
|
propertyKeyOf,
|
|
3409
4143
|
protoBrandAbs,
|
|
3410
4144
|
protoOfRecv,
|
|
@@ -3412,7 +4146,6 @@ export {
|
|
|
3412
4146
|
pureFnNameOf,
|
|
3413
4147
|
pushCollectionArm,
|
|
3414
4148
|
pushLoopExit,
|
|
3415
|
-
pushPhi,
|
|
3416
4149
|
pushThrowExit,
|
|
3417
4150
|
queuePromiseMicro,
|
|
3418
4151
|
refineAbsForRelTrue,
|
|
@@ -3428,7 +4161,6 @@ export {
|
|
|
3428
4161
|
resetGeneralizeMemo,
|
|
3429
4162
|
resetNudoModuleExecCache,
|
|
3430
4163
|
resetParseSourceCache,
|
|
3431
|
-
resetPhi,
|
|
3432
4164
|
resetSidecarLoadFailureCache,
|
|
3433
4165
|
runTranspiled,
|
|
3434
4166
|
runTranspiledOptionsMemoKey,
|
|
@@ -3499,10 +4231,6 @@ export {
|
|
|
3499
4231
|
transpileSource,
|
|
3500
4232
|
trueConstraint,
|
|
3501
4233
|
tryMakeRegexAbs,
|
|
3502
|
-
tryPromoteDirectCall,
|
|
3503
|
-
tryPromoteForOfIteratee,
|
|
3504
|
-
tryPromoteHofCallback,
|
|
3505
|
-
tryPromoteReceiverAsArr,
|
|
3506
4234
|
tryRunTranspiled,
|
|
3507
4235
|
typeofAbs,
|
|
3508
4236
|
undefAbs,
|
|
@@ -3511,6 +4239,5 @@ export {
|
|
|
3511
4239
|
ushrAbs,
|
|
3512
4240
|
v,
|
|
3513
4241
|
withExecPhi,
|
|
3514
|
-
withPhiConstraint,
|
|
3515
4242
|
withVar
|
|
3516
4243
|
};
|