@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
package/package.json
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nudojs/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Nudo type system: type-as-computation (Abs) with TypeValue evaluation IR",
|
|
4
5
|
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"nudo",
|
|
9
|
+
"type-inference",
|
|
10
|
+
"javascript",
|
|
11
|
+
"abstract-interpretation",
|
|
12
|
+
"type-system"
|
|
13
|
+
],
|
|
5
14
|
"main": "src/index.ts",
|
|
6
15
|
"exports": {
|
|
7
|
-
".": "./src/index.ts"
|
|
16
|
+
".": "./src/index.ts",
|
|
17
|
+
"./exec": "./src/algebra/exec/index.ts"
|
|
8
18
|
},
|
|
9
19
|
"repository": {
|
|
10
20
|
"type": "git",
|
|
11
21
|
"url": "git+https://github.com/nudojs/nudo.git",
|
|
12
22
|
"directory": "packages/core"
|
|
13
23
|
},
|
|
24
|
+
"homepage": "https://github.com/nudojs/nudo/tree/main/packages/core",
|
|
25
|
+
"bugs": "https://github.com/nudojs/nudo/issues",
|
|
14
26
|
"publishConfig": {
|
|
15
27
|
"access": "public",
|
|
16
28
|
"provenance": true,
|
|
17
29
|
"registry": "https://registry.npmjs.org"
|
|
18
30
|
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@babel/parser": "^7.29.0",
|
|
33
|
+
"@babel/types": "^7.29.0"
|
|
34
|
+
},
|
|
19
35
|
"scripts": {
|
|
20
36
|
"build": "tsup src/index.ts --format esm --dts"
|
|
21
37
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { T, getFnSig, isFnSig, typeValueEquals, typeValueToString } from "../index.ts";
|
|
3
|
+
|
|
4
|
+
describe("T.fnSig with impl", () => {
|
|
5
|
+
it("creates fnSig without impl (backward compatible)", () => {
|
|
6
|
+
const fn = T.fnSig([T.number], T.string);
|
|
7
|
+
expect(isFnSig(fn)).toBe(true);
|
|
8
|
+
const sig = getFnSig(fn)!;
|
|
9
|
+
expect(sig.impl).toBeUndefined();
|
|
10
|
+
expect(typeValueEquals(sig.returnType, T.string)).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("creates fnSig with impl", () => {
|
|
14
|
+
const fn = T.fnSig([T.number], T.number, T.never, (args) => {
|
|
15
|
+
if (args[0].kind === "literal" && typeof args[0].value === "number") {
|
|
16
|
+
return T.literal(args[0].value * 2);
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
});
|
|
20
|
+
expect(isFnSig(fn)).toBe(true);
|
|
21
|
+
const sig = getFnSig(fn)!;
|
|
22
|
+
expect(sig.impl).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("impl returns concrete value for literal args", () => {
|
|
26
|
+
const fn = T.fnSig([T.number], T.number, T.never, (args) => {
|
|
27
|
+
if (args[0].kind === "literal" && typeof args[0].value === "number") {
|
|
28
|
+
return T.literal(args[0].value * 2);
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
});
|
|
32
|
+
const sig = getFnSig(fn)!;
|
|
33
|
+
const result = sig.impl!([T.literal(5)]);
|
|
34
|
+
expect(result).toBeDefined();
|
|
35
|
+
expect(typeValueEquals(result!, T.literal(10))).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("impl returns undefined for symbolic args (fallback to returnType)", () => {
|
|
39
|
+
const fn = T.fnSig([T.number], T.number, T.never, (args) => {
|
|
40
|
+
if (args[0].kind === "literal" && typeof args[0].value === "number") {
|
|
41
|
+
return T.literal(args[0].value * 2);
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
});
|
|
45
|
+
const sig = getFnSig(fn)!;
|
|
46
|
+
const result = sig.impl!([T.number]);
|
|
47
|
+
expect(result).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("typeValueToString still works for fnSig with impl", () => {
|
|
51
|
+
const fn = T.fnSig([T.number, T.string], T.boolean, T.never, () => T.literal(true));
|
|
52
|
+
const str = typeValueToString(fn);
|
|
53
|
+
expect(str).toContain("number");
|
|
54
|
+
expect(str).toContain("string");
|
|
55
|
+
expect(str).toContain("boolean");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -38,8 +38,10 @@ describe("dispatchBinaryOp", () => {
|
|
|
38
38
|
},
|
|
39
39
|
},
|
|
40
40
|
});
|
|
41
|
+
// Ops.mod 已删:dispatchBinaryOp 的 % 最终落到 unknown;
|
|
42
|
+
// evaluator 侧由 evalBinaryValue 的 numberish 兜底补 number。
|
|
41
43
|
const result = dispatchBinaryOp("%", odd, T.literal(3));
|
|
42
|
-
expect(result.kind).toBe("
|
|
44
|
+
expect(result.kind).toBe("unknown");
|
|
43
45
|
});
|
|
44
46
|
});
|
|
45
47
|
|
|
@@ -21,19 +21,7 @@ describe("Ops.add", () => {
|
|
|
21
21
|
});
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
it("computes literal arithmetic", () => {
|
|
26
|
-
expect(typeValueEquals(Ops.sub(T.literal(5), T.literal(3)), T.literal(2))).toBe(true);
|
|
27
|
-
expect(typeValueEquals(Ops.mul(T.literal(3), T.literal(4)), T.literal(12))).toBe(true);
|
|
28
|
-
expect(typeValueEquals(Ops.div(T.literal(10), T.literal(2)), T.literal(5))).toBe(true);
|
|
29
|
-
expect(typeValueEquals(Ops.mod(T.literal(7), T.literal(3)), T.literal(1))).toBe(true);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("returns number for abstract operands", () => {
|
|
33
|
-
expect(typeValueEquals(Ops.sub(T.number, T.number), T.number)).toBe(true);
|
|
34
|
-
expect(typeValueEquals(Ops.mul(T.number, T.literal(2)), T.number)).toBe(true);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
24
|
+
// sub/mul/div/mod 已删除:数值路径唯一真理在 algebra;非数应得 unknown。
|
|
37
25
|
|
|
38
26
|
describe("Ops.strictEq / strictNeq", () => {
|
|
39
27
|
it("compares literals", () => {
|
|
@@ -60,16 +48,6 @@ describe("Ops.gt / lt / gte / lte", () => {
|
|
|
60
48
|
});
|
|
61
49
|
});
|
|
62
50
|
|
|
63
|
-
describe("Ops.typeof_", () => {
|
|
64
|
-
it("returns literal typeof for known types", () => {
|
|
65
|
-
expect(typeValueEquals(Ops.typeof_(T.literal(1)), T.literal("number"))).toBe(true);
|
|
66
|
-
expect(typeValueEquals(Ops.typeof_(T.literal("hi")), T.literal("string"))).toBe(true);
|
|
67
|
-
expect(typeValueEquals(Ops.typeof_(T.literal(null)), T.literal("object"))).toBe(true);
|
|
68
|
-
expect(typeValueEquals(Ops.typeof_(T.number), T.literal("number"))).toBe(true);
|
|
69
|
-
expect(typeValueEquals(Ops.typeof_(T.object({})), T.literal("object"))).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
51
|
describe("Ops.not", () => {
|
|
74
52
|
it("negates literal booleans", () => {
|
|
75
53
|
expect(typeValueEquals(Ops.not(T.literal(true)), T.literal(false))).toBe(true);
|
|
@@ -87,15 +65,7 @@ describe("Ops.not", () => {
|
|
|
87
65
|
});
|
|
88
66
|
});
|
|
89
67
|
|
|
90
|
-
|
|
91
|
-
it("negates literal number", () => {
|
|
92
|
-
expect(typeValueEquals(Ops.neg(T.literal(5)), T.literal(-5))).toBe(true);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("returns number for abstract", () => {
|
|
96
|
-
expect(typeValueEquals(Ops.neg(T.number), T.number)).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
68
|
+
// typeof_ / neg 已迁入 algebra surface(tryAbsUnary)
|
|
99
69
|
|
|
100
70
|
describe("applyBinaryOp", () => {
|
|
101
71
|
it("dispatches to correct op", () => {
|
|
@@ -103,7 +73,12 @@ describe("applyBinaryOp", () => {
|
|
|
103
73
|
expect(typeValueEquals(applyBinaryOp("===", T.literal(1), T.literal(1)), T.literal(true))).toBe(true);
|
|
104
74
|
});
|
|
105
75
|
|
|
106
|
-
it("returns unknown for unsupported
|
|
76
|
+
it("returns unknown for deleted / unsupported ops", () => {
|
|
107
77
|
expect(applyBinaryOp("**", T.number, T.number)).toEqual(T.unknown);
|
|
78
|
+
// sub/mul/div/mod 已从 Ops 删除,代数是唯一路径
|
|
79
|
+
expect(applyBinaryOp("-", T.literal(5), T.literal(3))).toEqual(T.unknown);
|
|
80
|
+
expect(applyBinaryOp("*", T.number, T.number)).toEqual(T.unknown);
|
|
81
|
+
expect(applyBinaryOp("/", T.literal(10), T.literal(2))).toEqual(T.unknown);
|
|
82
|
+
expect(applyBinaryOp("%", T.literal(7), T.literal(3))).toEqual(T.unknown);
|
|
108
83
|
});
|
|
109
84
|
});
|
|
@@ -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
|
+
});
|