@nudojs/core 0.1.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 +27 -0
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/index.d.ts +1633 -11
- package/dist/index.js +9965 -107
- package/package.json +28 -2
- package/src/__tests__/fn-sig-impl.test.ts +57 -0
- package/src/__tests__/ops-refined.test.ts +196 -0
- package/src/__tests__/ops.test.ts +8 -33
- package/src/__tests__/type-value-refined.test.ts +210 -0
- 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 +52 -1
- package/src/mock-helpers.ts +166 -0
- package/src/ops.ts +106 -62
- package/src/refinements/range.ts +86 -0
- package/src/refinements/template-predicates.ts +11 -0
- package/src/refinements/template.ts +176 -0
- package/src/strip-types.ts +236 -0
- package/src/type-value.ts +193 -12
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
T,
|
|
4
|
+
type LiteralValue,
|
|
5
|
+
type TypeValue,
|
|
4
6
|
typeValueEquals,
|
|
5
7
|
simplifyUnion,
|
|
6
8
|
widenLiteral,
|
|
9
|
+
collapseLiteralUnion,
|
|
7
10
|
isSubtypeOf,
|
|
8
11
|
typeValueToString,
|
|
9
12
|
narrowType,
|
|
10
13
|
subtractType,
|
|
11
14
|
getPrimitiveTypeOf,
|
|
12
15
|
} from "../type-value.ts";
|
|
16
|
+
import { createTemplate } from "../refinements/template.ts";
|
|
13
17
|
|
|
14
18
|
describe("T factory", () => {
|
|
15
19
|
it("creates literal type values", () => {
|
|
@@ -118,6 +122,46 @@ describe("widenLiteral", () => {
|
|
|
118
122
|
it("returns non-literal unchanged", () => {
|
|
119
123
|
expect(widenLiteral(T.number)).toBe(T.number);
|
|
120
124
|
});
|
|
125
|
+
|
|
126
|
+
it("collapses 20 number literals beyond maxLiterals to number", () => {
|
|
127
|
+
const members = Array.from({ length: 20 }, (_, i) => T.literal((i + 1) ** 2));
|
|
128
|
+
const u = { kind: "union", members } as const;
|
|
129
|
+
const result = collapseLiteralUnion(u, 4);
|
|
130
|
+
expect(result).toEqual(T.number);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("collapses 5 string literals to string", () => {
|
|
134
|
+
const u = {
|
|
135
|
+
kind: "union",
|
|
136
|
+
members: ["a", "b", "c", "d", "e"].map((s) => T.literal(s)),
|
|
137
|
+
} as const;
|
|
138
|
+
const result = collapseLiteralUnion(u, 4);
|
|
139
|
+
expect(result).toEqual(T.string);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns union unchanged when member count equals maxLiterals", () => {
|
|
143
|
+
const u: TypeValue = {
|
|
144
|
+
kind: "union",
|
|
145
|
+
members: [T.literal(1), T.literal(2), T.literal(3), T.literal(4)],
|
|
146
|
+
};
|
|
147
|
+
expect(collapseLiteralUnion(u, 4)).toBe(u);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("mixed union: number literals absorbed by T.number, string literal kept (行为已修复:吸收律)", () => {
|
|
151
|
+
const u: TypeValue = {
|
|
152
|
+
kind: "union",
|
|
153
|
+
members: [T.literal(1), T.number, T.literal("x"), T.literal(2), T.literal(3)],
|
|
154
|
+
};
|
|
155
|
+
// 原断言期望原样返回 u;现 collapseLiteralUnion 复用 simplifyUnion 的吸收律,
|
|
156
|
+
// 数字字面量 1/2/3 被共存的 T.number 吸收,仅剩 ["x", number](数量 ≤ 阈值不再坍缩)
|
|
157
|
+
expect(collapseLiteralUnion(u, 4)).toEqual(T.union(T.number, T.literal("x")));
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("returns non-union unchanged (single literal / T.number)", () => {
|
|
161
|
+
const lit = T.literal(1);
|
|
162
|
+
expect(collapseLiteralUnion(lit, 0)).toBe(lit);
|
|
163
|
+
expect(collapseLiteralUnion(T.number, 0)).toBe(T.number);
|
|
164
|
+
});
|
|
121
165
|
});
|
|
122
166
|
|
|
123
167
|
describe("isSubtypeOf", () => {
|
|
@@ -211,3 +255,65 @@ describe("getPrimitiveTypeOf", () => {
|
|
|
211
255
|
expect(getPrimitiveTypeOf(T.array(T.number))).toBe("object");
|
|
212
256
|
});
|
|
213
257
|
});
|
|
258
|
+
|
|
259
|
+
describe("union absorption law (3 | number → number)", () => {
|
|
260
|
+
it("absorbs number literal into number", () => {
|
|
261
|
+
expect(T.union(T.literal(3), T.number)).toBe(T.number);
|
|
262
|
+
expect(T.union(T.number, T.literal(3))).toBe(T.number);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("absorbs string literal into string", () => {
|
|
266
|
+
expect(T.union(T.literal("a"), T.string)).toBe(T.string);
|
|
267
|
+
expect(T.union(T.string, T.literal("a"))).toBe(T.string);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("absorbs boolean literal into boolean", () => {
|
|
271
|
+
expect(T.union(T.literal(true), T.boolean)).toBe(T.boolean);
|
|
272
|
+
expect(T.union(T.boolean, T.literal(false))).toBe(T.boolean);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("absorbs bigint literal into bigint", () => {
|
|
276
|
+
// LiteralValue 类型暂不含 bigint,运行时以 cast 构造
|
|
277
|
+
expect(T.union(T.literal(10n as unknown as LiteralValue), T.bigint)).toBe(T.bigint);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("absorbs template literal into string", () => {
|
|
281
|
+
const tmpl = createTemplate([T.literal("x"), T.string]);
|
|
282
|
+
expect(T.union(tmpl, T.string)).toBe(T.string);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("absorbs only matching base across mixed members (order-stable)", () => {
|
|
286
|
+
// 1 被 number 吸收,"a" 无 string 在场而保留
|
|
287
|
+
const u = T.union(T.literal(1), T.number, T.literal("a"));
|
|
288
|
+
expect(u.kind).toBe("union");
|
|
289
|
+
if (u.kind === "union") {
|
|
290
|
+
expect(u.members).toHaveLength(2);
|
|
291
|
+
expect(typeValueEquals(u.members[0], T.number)).toBe(true);
|
|
292
|
+
expect(typeValueEquals(u.members[1], T.literal("a"))).toBe(true);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("keeps different-base literal union (1 | \"a\")", () => {
|
|
297
|
+
const u = T.union(T.literal(1), T.literal("a"));
|
|
298
|
+
expect(u.kind).toBe("union");
|
|
299
|
+
if (u.kind === "union") {
|
|
300
|
+
expect(u.members).toHaveLength(2);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("keeps pure literal unions (\"a\" | \"b\", 2 | -9)", () => {
|
|
305
|
+
expect(typeValueToString(T.union(T.literal("a"), T.literal("b")))).toBe('"a" | "b"');
|
|
306
|
+
expect(typeValueToString(T.union(T.literal(2), T.literal(-9)))).toBe("2 | -9");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("keeps undefined/null literal unions untouched", () => {
|
|
310
|
+
expect(typeValueToString(T.union(T.undefined, T.string))).toBe("undefined | string");
|
|
311
|
+
expect(typeValueToString(T.union(T.null, T.literal(1)))).toBe("null | 1");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("keeps pure primitive unions untouched (number | string | boolean)", () => {
|
|
315
|
+
expect(typeValueToString(T.union(T.number, T.string, T.boolean))).toBe(
|
|
316
|
+
"number | string | boolean",
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { analyzeFn, evalSource } from "../ast-eval.ts";
|
|
3
|
+
import { abs, num, numLit, unknown } from "../abs.ts";
|
|
4
|
+
import { v, lit } from "../term.ts";
|
|
5
|
+
import { gt, pTrue } from "../pred.ts";
|
|
6
|
+
import { typeValueToString } from "../../type-value.ts";
|
|
7
|
+
import { absToTypeValue } from "../bridge.ts";
|
|
8
|
+
import { formatAbs } from "../format.ts";
|
|
9
|
+
|
|
10
|
+
function show(a: ReturnType<typeof analyzeFn>): string {
|
|
11
|
+
return typeValueToString(absToTypeValue(a));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("abs-native ast-eval", () => {
|
|
15
|
+
it("literal arith", () => {
|
|
16
|
+
const r = analyzeFn(`function f(){ return 1+2*3; }`, "f", []);
|
|
17
|
+
expect(show(r)).toBe("7");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("param + 1 with constraint via phi in source", () => {
|
|
21
|
+
const src = `
|
|
22
|
+
function f(x) {
|
|
23
|
+
if (x > 0) return x + 1;
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
const r = analyzeFn(src, "f", [abs(num().shape, v("x"), gt(v("x"), lit(0)), "path")]);
|
|
28
|
+
// x>0 时 x+1 应带约束
|
|
29
|
+
expect(r.shape.k === "prim" || r.shape.k === "sum").toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("logical && short-circuit", () => {
|
|
33
|
+
const r = analyzeFn(`function f(){ return false && 1; }`, "f", []);
|
|
34
|
+
expect(show(r)).toBe("false");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("typeof", () => {
|
|
38
|
+
const r = analyzeFn(`function f(){ return typeof 1; }`, "f", []);
|
|
39
|
+
expect(show(r)).toBe('"number"');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("template literal", () => {
|
|
43
|
+
const r = analyzeFn(`function f(x){ return \`n=\${x}\`; }`, "f", [numLit(3)]);
|
|
44
|
+
expect(show(r)).toBe('"n=3"');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("first-class arrow", () => {
|
|
48
|
+
const src = `
|
|
49
|
+
function f() {
|
|
50
|
+
const add = (a, b) => a + b;
|
|
51
|
+
return add(2, 3);
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
const r = analyzeFn(src, "f", []);
|
|
55
|
+
expect(show(r)).toBe("5");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("div and mod", () => {
|
|
59
|
+
expect(show(analyzeFn(`function f(){ return 10/2; }`, "f", []))).toBe("5");
|
|
60
|
+
expect(show(analyzeFn(`function f(){ return 10%3; }`, "f", []))).toBe("1");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("nullish === decided", () => {
|
|
64
|
+
const r = analyzeFn(`function f(x){ return x === null; }`, "f", [numLit(1)]);
|
|
65
|
+
expect(show(r)).toBe("false");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("for loop runs with update", () => {
|
|
69
|
+
const src = `
|
|
70
|
+
function f() {
|
|
71
|
+
let s = 0;
|
|
72
|
+
for (let i = 0; i < 3; i++) s = s + i;
|
|
73
|
+
return s;
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
const r = analyzeFn(src, "f", []);
|
|
77
|
+
expect(r.shape.k === "prim" || r.shape.k === "unknown").toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("for-of over tuple", () => {
|
|
81
|
+
const src = `
|
|
82
|
+
function f(xs) {
|
|
83
|
+
let s = 0;
|
|
84
|
+
for (const x of xs) s = s + x;
|
|
85
|
+
return s;
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
const tup = abs({ k: "tuple", elements: [numLit(1), numLit(2)] }, undefined, undefined, "exact");
|
|
89
|
+
const r = analyzeFn(src, "f", [tup]);
|
|
90
|
+
expect(r.shape.k === "prim" || r.shape.k === "unknown").toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("while with break", () => {
|
|
94
|
+
const src = `
|
|
95
|
+
function f() {
|
|
96
|
+
let i = 0;
|
|
97
|
+
while (true) {
|
|
98
|
+
i = i + 1;
|
|
99
|
+
if (i > 2) break;
|
|
100
|
+
}
|
|
101
|
+
return i;
|
|
102
|
+
}
|
|
103
|
+
`;
|
|
104
|
+
const r = analyzeFn(src, "f", []);
|
|
105
|
+
expect(r.shape.k === "prim" || r.shape.k === "unknown").toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("try/catch binds thrown value", () => {
|
|
109
|
+
const src = `
|
|
110
|
+
function f() {
|
|
111
|
+
try {
|
|
112
|
+
throw 1;
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return e;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
`;
|
|
118
|
+
const r = analyzeFn(src, "f", []);
|
|
119
|
+
expect(show(r)).toBe("1");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("string methods fold", () => {
|
|
123
|
+
expect(show(analyzeFn(`function f(){ return "abc".startsWith("a"); }`, "f", []))).toBe("true");
|
|
124
|
+
expect(show(analyzeFn(`function f(){ return "abc".toUpperCase(); }`, "f", []))).toBe('"ABC"');
|
|
125
|
+
expect(show(analyzeFn(`function f(){ return "hello".length; }`, "f", []))).toBe("5");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("array join returns string", () => {
|
|
129
|
+
const r = analyzeFn(`function f(xs){ return xs.join(","); }`, "f", [
|
|
130
|
+
abs({ k: "arr", element: abs({ k: "prim", type: "string" }, undefined, undefined, "exact") }, undefined, undefined, "exact"),
|
|
131
|
+
]);
|
|
132
|
+
expect(r.shape).toEqual({ k: "prim", type: "string" });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("Math and global builtins on Abs", () => {
|
|
136
|
+
expect(show(analyzeFn(`function f(){ return Math.floor(3.7); }`, "f", []))).toBe("3");
|
|
137
|
+
expect(show(analyzeFn(`function f(){ return Math.max(1, 5); }`, "f", []))).toBe("5");
|
|
138
|
+
expect(show(analyzeFn(`function f(){ return Array.isArray([]); }`, "f", []))).toBe("true");
|
|
139
|
+
expect(show(analyzeFn(`function f(){ return parseInt("42"); }`, "f", []))).toBe("42");
|
|
140
|
+
expect(show(analyzeFn(`function f(){ return Number.isInteger(3); }`, "f", []))).toBe("true");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("Object.keys of object shape", () => {
|
|
144
|
+
const src = `function f(){ return Object.keys({ a: 1, b: "x" }); }`;
|
|
145
|
+
const r = analyzeFn(src, "f", []);
|
|
146
|
+
expect(r.shape.k === "tuple" || r.shape.k === "arr").toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("Promise.resolve and then", () => {
|
|
150
|
+
const src = `
|
|
151
|
+
function f() {
|
|
152
|
+
return Promise.resolve(1).then((x) => x + 1);
|
|
153
|
+
}
|
|
154
|
+
`;
|
|
155
|
+
const r = analyzeFn(src, "f", []);
|
|
156
|
+
expect(r.shape.k).toBe("eff");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("Date.now and RegExp.test", () => {
|
|
160
|
+
const n = analyzeFn(`function f(){ return Date.now(); }`, "f", []);
|
|
161
|
+
expect(n.shape.k === "prim" && (n.shape as { type: string }).type === "number").toBe(true);
|
|
162
|
+
const t = analyzeFn(`function f(){ return /a/.test("a"); }`, "f", []);
|
|
163
|
+
// 字面正则:.test 可能走 member 路径;至少 boolean 或 unknown
|
|
164
|
+
expect(t.shape.k === "prim" || t.shape.k === "unknown").toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("evalProgramAbs executes top-level", async () => {
|
|
168
|
+
const { evalProgramAbs } = await import("../ast-eval.ts");
|
|
169
|
+
const { env, last } = evalProgramAbs(`
|
|
170
|
+
const x = 1 + 2;
|
|
171
|
+
function inc(n) { return n + 1; }
|
|
172
|
+
`);
|
|
173
|
+
expect(env.vars.get("x")).toBeDefined();
|
|
174
|
+
expect(env.fns.has("inc")).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
add,
|
|
4
|
+
sub,
|
|
5
|
+
mul,
|
|
6
|
+
div,
|
|
7
|
+
mod,
|
|
8
|
+
cmp,
|
|
9
|
+
numLit,
|
|
10
|
+
numVar,
|
|
11
|
+
strLit,
|
|
12
|
+
gtNum,
|
|
13
|
+
geNum,
|
|
14
|
+
ltNum,
|
|
15
|
+
lit,
|
|
16
|
+
v,
|
|
17
|
+
termToString,
|
|
18
|
+
predToString,
|
|
19
|
+
litValue,
|
|
20
|
+
implies,
|
|
21
|
+
simplifyTerm,
|
|
22
|
+
app,
|
|
23
|
+
} from "../index.ts";
|
|
24
|
+
|
|
25
|
+
describe("term simplify", () => {
|
|
26
|
+
it("folds 1+3", () => {
|
|
27
|
+
expect(simplifyTerm(app("+", [lit(1), lit(3)]))).toEqual(lit(4));
|
|
28
|
+
});
|
|
29
|
+
it("x+0 = x", () => {
|
|
30
|
+
expect(simplifyTerm(app("+", [v("x"), lit(0)]))).toEqual(v("x"));
|
|
31
|
+
});
|
|
32
|
+
it("0+x = x", () => {
|
|
33
|
+
expect(simplifyTerm(app("+", [lit(0), v("x")]))).toEqual(v("x"));
|
|
34
|
+
});
|
|
35
|
+
it("x*0 = 0", () => {
|
|
36
|
+
expect(simplifyTerm(app("*", [v("x"), lit(0)]))).toEqual(lit(0));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("add monotonicity", () => {
|
|
41
|
+
it("unconstrained any + 1: number|string 并集,term kept(real JS)", () => {
|
|
42
|
+
// 无契约时 x 是 any;score('x') 合法,+ 可能是拼接也可能是加法
|
|
43
|
+
const anyA = {
|
|
44
|
+
shape: { k: "any" as const },
|
|
45
|
+
term: v("A1"),
|
|
46
|
+
conf: "path" as const,
|
|
47
|
+
};
|
|
48
|
+
const r = add(anyA, numLit(1));
|
|
49
|
+
expect(r.shape.k).toBe("sum");
|
|
50
|
+
if (r.shape.k === "sum") {
|
|
51
|
+
const kinds = r.shape.members.map((m) =>
|
|
52
|
+
m.shape.k === "prim" ? (m.shape as { type: string }).type : m.shape.k,
|
|
53
|
+
);
|
|
54
|
+
expect(kinds.sort()).toEqual(["number", "string"]);
|
|
55
|
+
}
|
|
56
|
+
expect(termToString(r.term!)).toBe("(A1 + 1)");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("unconstrained any - * / % → number (JS ToNumber)", () => {
|
|
60
|
+
const anyA = {
|
|
61
|
+
shape: { k: "any" as const },
|
|
62
|
+
term: v("A1"),
|
|
63
|
+
conf: "path" as const,
|
|
64
|
+
};
|
|
65
|
+
const one = numLit(1);
|
|
66
|
+
for (const [name, r] of [
|
|
67
|
+
["sub", sub(anyA, one)],
|
|
68
|
+
["mul", mul(anyA, one)],
|
|
69
|
+
["div", div(anyA, one)],
|
|
70
|
+
["mod", mod(anyA, one)],
|
|
71
|
+
] as const) {
|
|
72
|
+
expect(r.shape, name).toEqual({ k: "prim", type: "number" });
|
|
73
|
+
expect(r.term, name).toBeDefined();
|
|
74
|
+
}
|
|
75
|
+
expect(termToString(sub(anyA, one).term!)).toBe("(A1 - 1)");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("x>0 + 1 ⇒ term>1", () => {
|
|
79
|
+
const phi = gtNum(v("x"), 0);
|
|
80
|
+
const r = add(numVar("x", gtNum(v("x"), 0)), numLit(1), phi);
|
|
81
|
+
expect(termToString(r.term!)).toBe("(x + 1)");
|
|
82
|
+
expect(r.pred!.op).toBe("gt");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("x>2 + y>3 ⇒ (x+y)>5", () => {
|
|
86
|
+
const phi = { op: "and" as const, args: [gtNum(v("x"), 2), gtNum(v("y"), 3)] };
|
|
87
|
+
const r = add(
|
|
88
|
+
numVar("x", gtNum(v("x"), 2)),
|
|
89
|
+
numVar("y", gtNum(v("y"), 3)),
|
|
90
|
+
phi,
|
|
91
|
+
);
|
|
92
|
+
expect(r.pred!.op).toBe("gt");
|
|
93
|
+
if (r.pred!.op === "gt") {
|
|
94
|
+
expect(r.pred!.b).toEqual(lit(5));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("x≥1 + 0 literal keeps ≥ via simplify to x", () => {
|
|
99
|
+
const phi = geNum(v("x"), 1);
|
|
100
|
+
const r = add(numVar("x", geNum(v("x"), 1)), numLit(0), phi);
|
|
101
|
+
// x+0 simplifies to x
|
|
102
|
+
expect(termToString(r.term!)).toBe("x");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("literal 2+3 = 5 exact", () => {
|
|
106
|
+
const r = add(numLit(2), numLit(3));
|
|
107
|
+
expect(litValue(r)).toBe(5);
|
|
108
|
+
expect(r.conf).toBe("exact");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("sub / mul", () => {
|
|
113
|
+
it("5-2 = 3", () => {
|
|
114
|
+
expect(litValue(sub(numLit(5), numLit(2)))).toBe(3);
|
|
115
|
+
});
|
|
116
|
+
it("x>0 - 1 ⇒ (x-1)>-1? x>0 ⇒ x-1 > -1", () => {
|
|
117
|
+
const phi = gtNum(v("x"), 0);
|
|
118
|
+
const r = sub(numVar("x", gtNum(v("x"), 0)), numLit(1), phi);
|
|
119
|
+
expect(r.term).toBeDefined();
|
|
120
|
+
expect(r.pred!.op).toBe("gt");
|
|
121
|
+
if (r.pred!.op === "gt") {
|
|
122
|
+
expect(r.pred!.b).toEqual(lit(-1));
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
it("3*4 = 12", () => {
|
|
126
|
+
expect(litValue(mul(numLit(3), numLit(4)))).toBe(12);
|
|
127
|
+
});
|
|
128
|
+
it("x>0 * 2 ⇒ (x*2)>0", () => {
|
|
129
|
+
const phi = gtNum(v("x"), 0);
|
|
130
|
+
const r = mul(numVar("x", gtNum(v("x"), 0)), numLit(2), phi);
|
|
131
|
+
expect(r.pred!.op).toBe("gt");
|
|
132
|
+
if (r.pred!.op === "gt") {
|
|
133
|
+
expect(r.pred!.b).toEqual(lit(0));
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("implies", () => {
|
|
139
|
+
it("x>0 ⊢ x>0", () => {
|
|
140
|
+
expect(implies(gtNum(v("x"), 0), gtNum(v("x"), 0))).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
it("x>0 ⊢ x≥0", () => {
|
|
143
|
+
expect(implies(gtNum(v("x"), 0), geNum(v("x"), 0))).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
it("x>0 ⊬ x>5", () => {
|
|
146
|
+
expect(implies(gtNum(v("x"), 0), gtNum(v("x"), 5))).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
it("x>5 ⊢ x>0", () => {
|
|
149
|
+
expect(implies(gtNum(v("x"), 5), gtNum(v("x"), 0))).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
it("x>0 ⊢ x+1>1", () => {
|
|
152
|
+
const phi = gtNum(v("x"), 0);
|
|
153
|
+
expect(implies(phi, gtNum(app("+", [v("x"), lit(1)]), 1))).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
it("x>0 ⊬ x+1>2", () => {
|
|
156
|
+
const phi = gtNum(v("x"), 0);
|
|
157
|
+
expect(implies(phi, gtNum(app("+", [v("x"), lit(1)]), 2))).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("cmp under phi", () => {
|
|
162
|
+
it("x>0, cmp(x, 0, >) → true", () => {
|
|
163
|
+
const phi = gtNum(v("x"), 0);
|
|
164
|
+
const r = cmp("gt", numVar("x"), numLit(0), phi);
|
|
165
|
+
expect(litValue(r)).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
it("1<3 → true", () => {
|
|
168
|
+
expect(litValue(cmp("lt", numLit(1), numLit(3)))).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -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
|
+
});
|