@nudojs/core 0.2.0 → 0.3.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/CHANGELOG.md +19 -0
- package/README.md +26 -0
- package/dist/index.d.ts +1601 -12
- package/dist/index.js +9657 -110
- package/package.json +18 -2
- package/src/__tests__/fn-sig-impl.test.ts +57 -0
- package/src/__tests__/ops-refined.test.ts +3 -1
- package/src/__tests__/ops.test.ts +8 -33
- package/src/__tests__/type-value.test.ts +106 -0
- package/src/algebra/__tests__/abs-native.test.ts +176 -0
- package/src/algebra/__tests__/arithmetic.test.ts +170 -0
- package/src/algebra/__tests__/ast-eval.test.ts +154 -0
- package/src/algebra/__tests__/bridge.test.ts +160 -0
- package/src/algebra/__tests__/call-budget.test.ts +117 -0
- package/src/algebra/__tests__/check-case-consistency.test.ts +69 -0
- package/src/algebra/__tests__/check-gold.test.ts +290 -0
- package/src/algebra/__tests__/check-json.test.ts +57 -0
- package/src/algebra/__tests__/check-mini-repo.test.ts +49 -0
- package/src/algebra/__tests__/check-real-commander.test.ts +43 -0
- package/src/algebra/__tests__/check-real-packages.test.ts +165 -0
- package/src/algebra/__tests__/check-recall-gold.test.ts +1024 -0
- package/src/algebra/__tests__/check-require-dep-memo.test.ts +45 -0
- package/src/algebra/__tests__/check-return-contract.test.ts +90 -0
- package/src/algebra/__tests__/check-shape-gold.test.ts +303 -0
- package/src/algebra/__tests__/check-source-memo.test.ts +204 -0
- package/src/algebra/__tests__/check.test.ts +69 -0
- package/src/algebra/__tests__/denote.test.ts +77 -0
- package/src/algebra/__tests__/diagnostics.test.ts +64 -0
- package/src/algebra/__tests__/differential.test.ts +124 -0
- package/src/algebra/__tests__/div-mod.test.ts +38 -0
- package/src/algebra/__tests__/example0.test.ts +103 -0
- package/src/algebra/__tests__/exec-analyze-throws.test.ts +60 -0
- package/src/algebra/__tests__/exec-async.test.ts +46 -0
- package/src/algebra/__tests__/exec-bpath.test.ts +193 -0
- package/src/algebra/__tests__/exec-class.test.ts +56 -0
- package/src/algebra/__tests__/exec-e2e.test.ts +97 -0
- package/src/algebra/__tests__/exec-generator.test.ts +53 -0
- package/src/algebra/__tests__/exec-inherit-default.test.ts +111 -0
- package/src/algebra/__tests__/exec-optional-nested.test.ts +87 -0
- package/src/algebra/__tests__/exec-rest.test.ts +55 -0
- package/src/algebra/__tests__/exec-spread-forof.test.ts +85 -0
- package/src/algebra/__tests__/exec-static-computed.test.ts +61 -0
- package/src/algebra/__tests__/exec-switch.test.ts +74 -0
- package/src/algebra/__tests__/exec-trycatch.test.ts +71 -0
- package/src/algebra/__tests__/fn-fp-memo.test.ts +166 -0
- package/src/algebra/__tests__/fn-fp-multidecl.test.ts +49 -0
- package/src/algebra/__tests__/format.test.ts +34 -0
- package/src/algebra/__tests__/generalize-memo.test.ts +285 -0
- package/src/algebra/__tests__/generalize.test.ts +113 -0
- package/src/algebra/__tests__/hash-source.test.ts +19 -0
- package/src/algebra/__tests__/hof.test.ts +80 -0
- package/src/algebra/__tests__/inlay.test.ts +30 -0
- package/src/algebra/__tests__/language.test.ts +77 -0
- package/src/algebra/__tests__/leq.test.ts +109 -0
- package/src/algebra/__tests__/load-deps-fp-memo.test.ts +132 -0
- package/src/algebra/__tests__/methods.test.ts +37 -0
- package/src/algebra/__tests__/node-types.test.ts +26 -0
- package/src/algebra/__tests__/nudo-constraints.ts +46 -0
- package/src/algebra/__tests__/objects.test.ts +117 -0
- package/src/algebra/__tests__/parse-source-cache.test.ts +42 -0
- package/src/algebra/__tests__/refine-import.test.ts +109 -0
- package/src/algebra/__tests__/stable-source-key-block.test.ts +23 -0
- package/src/algebra/__tests__/stable-source-key.test.ts +31 -0
- package/src/algebra/__tests__/surface.test.ts +41 -0
- package/src/algebra/__tests__/this-inherit.test.ts +130 -0
- package/src/algebra/abs-fn.ts +49 -0
- package/src/algebra/abs-modules.ts +139 -0
- package/src/algebra/abs.ts +206 -0
- package/src/algebra/arithmetic.ts +691 -0
- package/src/algebra/ast-eval.ts +1763 -0
- package/src/algebra/bridge.ts +350 -0
- package/src/algebra/builtins.ts +373 -0
- package/src/algebra/check-report.ts +150 -0
- package/src/algebra/check.ts +1867 -0
- package/src/algebra/constraint.ts +304 -0
- package/src/algebra/denote.ts +166 -0
- package/src/algebra/diagnostics.ts +210 -0
- package/src/algebra/exec/call.ts +29 -0
- package/src/algebra/exec/calls.ts +96 -0
- package/src/algebra/exec/class-registry.ts +29 -0
- package/src/algebra/exec/class.ts +329 -0
- package/src/algebra/exec/index.ts +6 -0
- package/src/algebra/exec/member-diag.ts +141 -0
- package/src/algebra/exec/run.ts +341 -0
- package/src/algebra/exec/runtime.ts +583 -0
- package/src/algebra/exec/transpile.ts +993 -0
- package/src/algebra/fn-fp.ts +294 -0
- package/src/algebra/format.ts +91 -0
- package/src/algebra/generalize.ts +776 -0
- package/src/algebra/hash-source.ts +27 -0
- package/src/algebra/index.ts +34 -0
- package/src/algebra/inlay.ts +198 -0
- package/src/algebra/language.ts +289 -0
- package/src/algebra/leak.ts +68 -0
- package/src/algebra/leq.ts +301 -0
- package/src/algebra/load-deps-fp.ts +97 -0
- package/src/algebra/methods.ts +128 -0
- package/src/algebra/objects.ts +251 -0
- package/src/algebra/parse-source.ts +64 -0
- package/src/algebra/phi.ts +42 -0
- package/src/algebra/pred.ts +395 -0
- package/src/algebra/refine.ts +227 -0
- package/src/algebra/stable-source-key.ts +81 -0
- package/src/algebra/surface.ts +206 -0
- package/src/algebra/template.ts +143 -0
- package/src/algebra/term.ts +119 -0
- package/src/environment.ts +5 -0
- package/src/index.ts +33 -0
- package/src/mock-helpers.ts +166 -0
- package/src/ops.ts +44 -63
- package/src/refinements/template-predicates.ts +11 -0
- package/src/refinements/template.ts +1 -3
- package/src/strip-types.ts +236 -0
- package/src/type-value.ts +153 -12
- package/src/refinements/index.ts +0 -2
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
analyzeFn,
|
|
4
|
+
numLit,
|
|
5
|
+
numVar,
|
|
6
|
+
gtNum,
|
|
7
|
+
geNum,
|
|
8
|
+
v,
|
|
9
|
+
lit,
|
|
10
|
+
termToString,
|
|
11
|
+
predToString,
|
|
12
|
+
litValue,
|
|
13
|
+
leakIfNeeded,
|
|
14
|
+
maybeLeak,
|
|
15
|
+
exceedsBudget,
|
|
16
|
+
termDepth,
|
|
17
|
+
termNodes,
|
|
18
|
+
app,
|
|
19
|
+
resetLeakCounter,
|
|
20
|
+
mul,
|
|
21
|
+
add,
|
|
22
|
+
} from "../index.ts";
|
|
23
|
+
|
|
24
|
+
describe("AST eval: source → Abs", () => {
|
|
25
|
+
it("add(1,3) from real source → 4", () => {
|
|
26
|
+
const src = `
|
|
27
|
+
const add = (a, b) => a + b;
|
|
28
|
+
function main() { return add(1, 3); }
|
|
29
|
+
`;
|
|
30
|
+
const r = analyzeFn(src, "main", []);
|
|
31
|
+
expect(litValue(r)).toBe(4);
|
|
32
|
+
expect(r.conf).toBe("exact");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("scale(x) = add(x,1) under x>0 ⇒ (x+1)>1", () => {
|
|
36
|
+
const src = `
|
|
37
|
+
const add = (a, b) => a + b;
|
|
38
|
+
function scale(x) { return add(x, 1); }
|
|
39
|
+
`;
|
|
40
|
+
const phi = gtNum(v("x"), 0);
|
|
41
|
+
const r = analyzeFn(src, "scale", [numVar("x", gtNum(v("x"), 0))], phi);
|
|
42
|
+
expect(r.term).toBeDefined();
|
|
43
|
+
expect(termToString(r.term!)).toBe("(x + 1)");
|
|
44
|
+
expect(r.pred!.op).toBe("gt");
|
|
45
|
+
if (r.pred!.op === "gt") {
|
|
46
|
+
expect(r.pred!.b).toEqual(lit(1));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("twice(x) nested add under x>0 ⇒ >2", () => {
|
|
51
|
+
const src = `
|
|
52
|
+
const add = (a, b) => a + b;
|
|
53
|
+
function twice(x) {
|
|
54
|
+
const c = add(x, 1);
|
|
55
|
+
return add(c, 1);
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
const phi = gtNum(v("x"), 0);
|
|
59
|
+
const r = analyzeFn(src, "twice", [numVar("x", gtNum(v("x"), 0))], phi);
|
|
60
|
+
expect(r.term).toBeDefined();
|
|
61
|
+
expect(r.pred).toBeDefined();
|
|
62
|
+
// ((x+1)+1) > 2 或 (x+2) > 2
|
|
63
|
+
const ps = predToString(r.pred!);
|
|
64
|
+
expect(ps).toContain("2");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("if (x > 5) return x; return 0 with x>10 ⇒ x", () => {
|
|
68
|
+
const src = `
|
|
69
|
+
function pick(x) {
|
|
70
|
+
if (x > 5) return x;
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
const phi = gtNum(v("x"), 10);
|
|
75
|
+
const r = analyzeFn(src, "pick", [numVar("x", gtNum(v("x"), 10))], phi);
|
|
76
|
+
// true 分支返回 x(term=x),false 分支 0;join 后丢 term
|
|
77
|
+
// 但 true 分支在 Φ 下 x>5 成立,我们至少得到 number
|
|
78
|
+
expect(r.shape).toEqual({ k: "prim", type: "number" });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("if (x > 0) return x + 1; return 0 with Φ x>0 ⇒ true branch", () => {
|
|
82
|
+
const src = `
|
|
83
|
+
function addOneIfPositive(x) {
|
|
84
|
+
if (x > 0) return x + 1;
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
const phi = gtNum(v("x"), 0);
|
|
89
|
+
const r = analyzeFn(src, "addOneIfPositive", [numVar("x", gtNum(v("x"), 0))], phi);
|
|
90
|
+
expect(termToString(r.term!)).toBe("(x + 1)");
|
|
91
|
+
expect(r.pred!.op).toBe("gt");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("literal only function", () => {
|
|
95
|
+
const src = `
|
|
96
|
+
function f() { return 1 + 2 * 3; }
|
|
97
|
+
`;
|
|
98
|
+
// 注意:JS 里 1+2*3 = 7,我们的 BinaryExpression 是左结合遍历
|
|
99
|
+
// Babel AST: (1 + (2 * 3)) —— 乘法优先,AST 已带优先级
|
|
100
|
+
const r = analyzeFn(src, "f", []);
|
|
101
|
+
expect(litValue(r)).toBe(7);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("string concat from source", () => {
|
|
105
|
+
const src = `
|
|
106
|
+
function greet(name) { return "hi, " + name; }
|
|
107
|
+
`;
|
|
108
|
+
const r = analyzeFn(src, "greet", [numLit(1)]);
|
|
109
|
+
// number + string 在 JS 里是字符串,我们 isStrPrim 一侧即可
|
|
110
|
+
// 实参是 number,左侧是 string → 走 string 拼接
|
|
111
|
+
expect(r.shape).toEqual({ k: "prim", type: "string" });
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("leak", () => {
|
|
116
|
+
it("small term does not leak", () => {
|
|
117
|
+
resetLeakCounter();
|
|
118
|
+
const t = app("+", [v("x"), lit(1)]);
|
|
119
|
+
expect(exceedsBudget(t)).toBe(false);
|
|
120
|
+
const a = add(numVar("x"), numLit(1));
|
|
121
|
+
const leaked = leakIfNeeded(a);
|
|
122
|
+
expect(leaked.term).toEqual(a.term);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("deep term leaks to fresh var + eq", () => {
|
|
126
|
+
resetLeakCounter();
|
|
127
|
+
let t = v("x");
|
|
128
|
+
for (let i = 0; i < 10; i++) {
|
|
129
|
+
t = app("+", [t, lit(1)]);
|
|
130
|
+
}
|
|
131
|
+
expect(termDepth(t)).toBeGreaterThan(6);
|
|
132
|
+
expect(exceedsBudget(t)).toBe(true);
|
|
133
|
+
|
|
134
|
+
const a = { shape: { k: "prim" as const, type: "number" as const }, term: t, conf: "path" as const };
|
|
135
|
+
const leaked = maybeLeak(a, { maxDepth: 4, maxNodes: 20 }, "acc");
|
|
136
|
+
expect(leaked.term?.op).toBe("var");
|
|
137
|
+
if (leaked.term?.op === "var") {
|
|
138
|
+
expect(leaked.term.id.startsWith("acc$")).toBe(true);
|
|
139
|
+
}
|
|
140
|
+
expect(leaked.pred?.op).toBe("eq");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("mul negative flips bounds", () => {
|
|
145
|
+
it("x>0 * -2 ⇒ (x*-2) < 0", () => {
|
|
146
|
+
const phi = gtNum(v("x"), 0);
|
|
147
|
+
const r = mul(numVar("x", gtNum(v("x"), 0)), numLit(-2), phi);
|
|
148
|
+
expect(r.pred).toBeDefined();
|
|
149
|
+
expect(r.pred!.op).toBe("lt");
|
|
150
|
+
if (r.pred!.op === "lt") {
|
|
151
|
+
expect(r.pred!.b).toEqual(lit(0));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
absToTypeValue,
|
|
4
|
+
typeValueToAbs,
|
|
5
|
+
bridgeIsLossy,
|
|
6
|
+
getTvConfidence,
|
|
7
|
+
numLit,
|
|
8
|
+
numVar,
|
|
9
|
+
gtNum,
|
|
10
|
+
v,
|
|
11
|
+
litValue,
|
|
12
|
+
formatShape,
|
|
13
|
+
analyzeFn,
|
|
14
|
+
objOf,
|
|
15
|
+
strLit,
|
|
16
|
+
boolLit,
|
|
17
|
+
} from "../index.ts";
|
|
18
|
+
import { typeValueToString, T } from "../../type-value.ts";
|
|
19
|
+
|
|
20
|
+
describe("bridge Abs → TypeValue", () => {
|
|
21
|
+
it("literal 4 → literal 4 #exact", () => {
|
|
22
|
+
const tv = absToTypeValue(numLit(4));
|
|
23
|
+
expect(tv.kind).toBe("literal");
|
|
24
|
+
if (tv.kind === "literal") expect(tv.value).toBe(4);
|
|
25
|
+
expect(getTvConfidence(tv)).toBe("exact");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("plain number prim → primitive number", () => {
|
|
29
|
+
const tv = absToTypeValue({
|
|
30
|
+
shape: { k: "prim", type: "number" },
|
|
31
|
+
conf: "exact",
|
|
32
|
+
});
|
|
33
|
+
expect(typeValueToString(tv)).toBe("number");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("x>0 with pred → refined number", () => {
|
|
37
|
+
const a = numVar("x", gtNum(v("x"), 0));
|
|
38
|
+
const tv = absToTypeValue(a);
|
|
39
|
+
// term 是 var,会丢 term;pred 可 encode 为 refined
|
|
40
|
+
expect(tv.kind === "refined" || tv.kind === "primitive").toBe(true);
|
|
41
|
+
const lossy = bridgeIsLossy(a);
|
|
42
|
+
// 丢 term 是预期的
|
|
43
|
+
expect(lossy.reasons.some((r) => r.includes("term"))).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("object slots map to object TypeValue", () => {
|
|
47
|
+
const o = objOf({
|
|
48
|
+
host: { value: strLit("localhost") },
|
|
49
|
+
port: { value: numLit(8080) },
|
|
50
|
+
debug: { value: boolLit(false) },
|
|
51
|
+
});
|
|
52
|
+
const tv = absToTypeValue(o);
|
|
53
|
+
expect(tv.kind).toBe("object");
|
|
54
|
+
expect(typeValueToString(tv)).toContain("host");
|
|
55
|
+
expect(typeValueToString(tv)).toContain("8080");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("bridge TypeValue → Abs", () => {
|
|
60
|
+
it("literal 6 → Abs exact 6", () => {
|
|
61
|
+
const abs = typeValueToAbs(T.literal(6));
|
|
62
|
+
expect(litValue(abs)).toBe(6);
|
|
63
|
+
expect(abs.conf).toBe("exact");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("primitive string → prim string", () => {
|
|
67
|
+
const abs = typeValueToAbs(T.string);
|
|
68
|
+
expect(formatShape(abs)).toBe("string");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("object → obj slots", () => {
|
|
72
|
+
const abs = typeValueToAbs(
|
|
73
|
+
T.object({ a: T.literal(1), b: T.string }),
|
|
74
|
+
);
|
|
75
|
+
expect(abs.shape.k).toBe("obj");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("round-trip literal", () => {
|
|
79
|
+
const tv = absToTypeValue(numLit(42));
|
|
80
|
+
const back = typeValueToAbs(tv);
|
|
81
|
+
expect(litValue(back)).toBe(42);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("round-trip primitive (lossless)", () => {
|
|
85
|
+
const abs0 = {
|
|
86
|
+
shape: { k: "prim" as const, type: "number" as const },
|
|
87
|
+
conf: "exact" as const,
|
|
88
|
+
};
|
|
89
|
+
const tv = absToTypeValue(abs0);
|
|
90
|
+
const back = typeValueToAbs(tv);
|
|
91
|
+
expect(formatShape(back)).toBe("number");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("bridgeIsLossy", () => {
|
|
96
|
+
it("literal is not lossy", () => {
|
|
97
|
+
expect(bridgeIsLossy(numLit(1)).lossy).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
it("symbolic term is lossy", () => {
|
|
100
|
+
const src = `
|
|
101
|
+
function scale(x) { return x + 1; }
|
|
102
|
+
`;
|
|
103
|
+
const r = analyzeFn(src, "scale", [numVar("x")]);
|
|
104
|
+
expect(bridgeIsLossy(r).lossy).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("bridge any / conf / refined reverse (rewrite closeout)", () => {
|
|
109
|
+
it("Abs any projects as unknown but keeps path conf (not partial)", () => {
|
|
110
|
+
const a: import("../abs.ts").Abs = { shape: { k: "any" }, conf: "path" };
|
|
111
|
+
const tv = absToTypeValue(a);
|
|
112
|
+
expect(tv.kind).toBe("unknown");
|
|
113
|
+
expect(getTvConfidence(tv)).toBe("path");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("Abs analysis-fail unknown keeps partial conf", () => {
|
|
117
|
+
const a: import("../abs.ts").Abs = { shape: { k: "unknown" }, conf: "partial" };
|
|
118
|
+
const tv = absToTypeValue(a);
|
|
119
|
+
expect(tv.kind).toBe("unknown");
|
|
120
|
+
expect(getTvConfidence(tv)).toBe("partial");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("widened TypeValue does not round-trip as exact", () => {
|
|
124
|
+
const src = `
|
|
125
|
+
function scale(x) { return x + 1; }
|
|
126
|
+
`;
|
|
127
|
+
const r = analyzeFn(src, "scale", [numVar("x")]);
|
|
128
|
+
const tv = absToTypeValue(r);
|
|
129
|
+
const back = typeValueToAbs(tv);
|
|
130
|
+
expect(back.conf === "exact").toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("refined numeric pred reverse-encodes into Abs pred", () => {
|
|
134
|
+
const a = numVar("x", gtNum(v("x"), 0));
|
|
135
|
+
const tv = absToTypeValue(a);
|
|
136
|
+
expect(tv.kind).toBe("refined");
|
|
137
|
+
const back = typeValueToAbs(tv);
|
|
138
|
+
expect(back.shape.k).toBe("prim");
|
|
139
|
+
expect(back.pred).toBeDefined();
|
|
140
|
+
expect(back.pred!.op).toBe("gt");
|
|
141
|
+
// pred 右端恢复为字面量 0
|
|
142
|
+
if (back.pred!.op === "gt") {
|
|
143
|
+
expect(back.pred!.b.op).toBe("lit");
|
|
144
|
+
if (back.pred!.b.op === "lit") expect(back.pred!.b.value).toBe(0);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("refined le reverse-encodes", () => {
|
|
149
|
+
const a: import("../abs.ts").Abs = {
|
|
150
|
+
shape: { k: "prim", type: "number" },
|
|
151
|
+
term: v("n"),
|
|
152
|
+
pred: { op: "le", a: v("n"), b: { op: "lit", value: 10 } },
|
|
153
|
+
conf: "path",
|
|
154
|
+
};
|
|
155
|
+
const tv = absToTypeValue(a);
|
|
156
|
+
expect(tv.kind).toBe("refined");
|
|
157
|
+
const back = typeValueToAbs(tv);
|
|
158
|
+
expect(back.pred?.op).toBe("le");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { checkSource } from "../check.ts";
|
|
3
|
+
import {
|
|
4
|
+
analyzeFn,
|
|
5
|
+
evalProgramAbs,
|
|
6
|
+
resetAbsCallBudget,
|
|
7
|
+
MAX_CALL_DEPTH,
|
|
8
|
+
} from "../ast-eval.ts";
|
|
9
|
+
import { pTrue } from "../pred.ts";
|
|
10
|
+
import { num, unknown } from "../abs.ts";
|
|
11
|
+
import type { Abs } from "../abs.ts";
|
|
12
|
+
|
|
13
|
+
const FAC = `
|
|
14
|
+
function fac(n) {
|
|
15
|
+
if (n <= 1) return 1;
|
|
16
|
+
return n * fac(n - 1);
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const FIB = `
|
|
21
|
+
function fib(n) {
|
|
22
|
+
if (n <= 1) return n;
|
|
23
|
+
return fib(n - 1) + fib(n - 2);
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
const MUTUAL = `
|
|
28
|
+
function isEven(n) {
|
|
29
|
+
if (n === 0) return true;
|
|
30
|
+
return isOdd(n - 1);
|
|
31
|
+
}
|
|
32
|
+
function isOdd(n) {
|
|
33
|
+
if (n === 0) return false;
|
|
34
|
+
return isEven(n - 1);
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
describe("Abs call budget (recursion guard)", () => {
|
|
39
|
+
it("analyzeFn on factorial does not stack-overflow", () => {
|
|
40
|
+
resetAbsCallBudget();
|
|
41
|
+
const r = analyzeFn(FAC, "fac", [num()], pTrue);
|
|
42
|
+
expect(r).toBeDefined();
|
|
43
|
+
expect(r.shape.k === "unknown" || r.shape.k === "prim" || r.shape.k === "never").toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("analyzeFn on fib does not stack-overflow", () => {
|
|
47
|
+
resetAbsCallBudget();
|
|
48
|
+
const r = analyzeFn(FIB, "fib", [num()], pTrue);
|
|
49
|
+
expect(r).toBeDefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("mutual recursion does not stack-overflow", () => {
|
|
53
|
+
resetAbsCallBudget();
|
|
54
|
+
const r = analyzeFn(MUTUAL, "isEven", [num()], pTrue);
|
|
55
|
+
expect(r).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("evalProgramAbs with top-level recursive call does not stack-overflow", () => {
|
|
59
|
+
resetAbsCallBudget();
|
|
60
|
+
const { last } = evalProgramAbs(`
|
|
61
|
+
function count(n) {
|
|
62
|
+
if (n <= 0) return 0;
|
|
63
|
+
return 1 + count(n - 1);
|
|
64
|
+
}
|
|
65
|
+
const x = count(5);
|
|
66
|
+
`);
|
|
67
|
+
expect(last).toBeDefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("concrete base-case recursion still evaluates", () => {
|
|
71
|
+
// 调用深度在 64 内:count(5) 应给出字面量结果而非 opaque
|
|
72
|
+
resetAbsCallBudget();
|
|
73
|
+
const lit5: Abs = {
|
|
74
|
+
shape: { k: "prim", type: "number" },
|
|
75
|
+
term: { op: "lit", value: 5 },
|
|
76
|
+
conf: "exact",
|
|
77
|
+
};
|
|
78
|
+
const r = analyzeFn(
|
|
79
|
+
`
|
|
80
|
+
function count(n) {
|
|
81
|
+
if (n <= 0) return 0;
|
|
82
|
+
return 1 + count(n - 1);
|
|
83
|
+
}
|
|
84
|
+
`,
|
|
85
|
+
"count",
|
|
86
|
+
[lit5],
|
|
87
|
+
pTrue,
|
|
88
|
+
);
|
|
89
|
+
// 5 层展开应收敛为字面量 5(或至少 number)
|
|
90
|
+
expect(r.shape.k === "prim" || r.shape.k === "never").toBe(true);
|
|
91
|
+
if (r.term && r.term.op === "lit") {
|
|
92
|
+
expect(r.term.value).toBe(5);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("checkSource completes on factorial with structured diagnostics", () => {
|
|
97
|
+
const report = checkSource("/tmp/fac.js", FAC, pTrue, {});
|
|
98
|
+
expect(report).toBeDefined();
|
|
99
|
+
// 不得崩溃;递归截断记 warning
|
|
100
|
+
const codes = report.issues.map((i) => i.code);
|
|
101
|
+
expect(codes).toContain("nudo:recursion-truncated");
|
|
102
|
+
expect(report.issues.every((i) => i.severity !== undefined)).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("depth budget constant is aligned with TypeValue evaluator", () => {
|
|
106
|
+
expect(MAX_CALL_DEPTH).toBe(64);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("truncated result uses opaque conf, not partial-any", () => {
|
|
110
|
+
resetAbsCallBudget();
|
|
111
|
+
const r = analyzeFn(FAC, "fac", [num()], pTrue);
|
|
112
|
+
// 截断后应为 unknown+opaque,或仍能算出的 prim;绝不能是 exact 的假结果
|
|
113
|
+
if (r.shape.k === "unknown") {
|
|
114
|
+
expect(r.conf === "opaque" || r.conf === "partial" || r.conf === "widened").toBe(true);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* case ⊄ refine → nudo:case-inconsistency
|
|
3
|
+
* case 是契约的见证,不是另一套前置来源。
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { checkSource, pTrue } from "../index.ts";
|
|
7
|
+
import { withStdImport, stdOpts } from "./nudo-constraints.ts";
|
|
8
|
+
|
|
9
|
+
function issuesOf(src: string) {
|
|
10
|
+
return checkSource("/t/case.js", withStdImport(src), pTrue, stdOpts);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("case vs refine", () => {
|
|
14
|
+
it("ok: case 实参满足契约", () => {
|
|
15
|
+
const r = issuesOf(`
|
|
16
|
+
/**
|
|
17
|
+
* @nudo:refine x positive
|
|
18
|
+
* @nudo:case "ok" (5)
|
|
19
|
+
*/
|
|
20
|
+
function needsPositive(x) {
|
|
21
|
+
return x;
|
|
22
|
+
}
|
|
23
|
+
`);
|
|
24
|
+
expect(r.issues.filter((i) => i.code === "nudo:case-inconsistency")).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("error: case 实参 ⊭ refine", () => {
|
|
28
|
+
const r = issuesOf(`
|
|
29
|
+
/**
|
|
30
|
+
* @nudo:refine x positive
|
|
31
|
+
* @nudo:case "neg" (-1)
|
|
32
|
+
*/
|
|
33
|
+
function needsPositive(x) {
|
|
34
|
+
return x;
|
|
35
|
+
}
|
|
36
|
+
`);
|
|
37
|
+
const err = r.issues.find((i) => i.code === "nudo:case-inconsistency");
|
|
38
|
+
expect(err).toBeDefined();
|
|
39
|
+
expect(err!.severity).toBe("error");
|
|
40
|
+
expect(err!.expected).toContain(">");
|
|
41
|
+
expect(err!.message).toContain("neg");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("error: percent 上界违例", () => {
|
|
45
|
+
const r = issuesOf(`
|
|
46
|
+
/**
|
|
47
|
+
* @nudo:refine n percent
|
|
48
|
+
* @nudo:case "big" (150)
|
|
49
|
+
*/
|
|
50
|
+
function pct(n) {
|
|
51
|
+
return n;
|
|
52
|
+
}
|
|
53
|
+
`);
|
|
54
|
+
expect(r.ok).toBe(false);
|
|
55
|
+
expect(r.issues.some((i) => i.code === "nudo:case-inconsistency")).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("ok: 无 refine 的 case 不报", () => {
|
|
59
|
+
const r = issuesOf(`
|
|
60
|
+
/**
|
|
61
|
+
* @nudo:case "any" (-1)
|
|
62
|
+
*/
|
|
63
|
+
function id(x) {
|
|
64
|
+
return x;
|
|
65
|
+
}
|
|
66
|
+
`);
|
|
67
|
+
expect(r.issues.filter((i) => i.code === "nudo:case-inconsistency")).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
});
|