@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.
Files changed (117) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +26 -0
  4. package/dist/index.d.ts +1633 -11
  5. package/dist/index.js +9965 -107
  6. package/package.json +28 -2
  7. package/src/__tests__/fn-sig-impl.test.ts +57 -0
  8. package/src/__tests__/ops-refined.test.ts +196 -0
  9. package/src/__tests__/ops.test.ts +8 -33
  10. package/src/__tests__/type-value-refined.test.ts +210 -0
  11. package/src/__tests__/type-value.test.ts +106 -0
  12. package/src/algebra/__tests__/abs-native.test.ts +176 -0
  13. package/src/algebra/__tests__/arithmetic.test.ts +170 -0
  14. package/src/algebra/__tests__/ast-eval.test.ts +154 -0
  15. package/src/algebra/__tests__/bridge.test.ts +160 -0
  16. package/src/algebra/__tests__/call-budget.test.ts +117 -0
  17. package/src/algebra/__tests__/check-case-consistency.test.ts +69 -0
  18. package/src/algebra/__tests__/check-gold.test.ts +290 -0
  19. package/src/algebra/__tests__/check-json.test.ts +57 -0
  20. package/src/algebra/__tests__/check-mini-repo.test.ts +49 -0
  21. package/src/algebra/__tests__/check-real-commander.test.ts +43 -0
  22. package/src/algebra/__tests__/check-real-packages.test.ts +165 -0
  23. package/src/algebra/__tests__/check-recall-gold.test.ts +1024 -0
  24. package/src/algebra/__tests__/check-require-dep-memo.test.ts +45 -0
  25. package/src/algebra/__tests__/check-return-contract.test.ts +90 -0
  26. package/src/algebra/__tests__/check-shape-gold.test.ts +303 -0
  27. package/src/algebra/__tests__/check-source-memo.test.ts +204 -0
  28. package/src/algebra/__tests__/check.test.ts +69 -0
  29. package/src/algebra/__tests__/denote.test.ts +77 -0
  30. package/src/algebra/__tests__/diagnostics.test.ts +64 -0
  31. package/src/algebra/__tests__/differential.test.ts +124 -0
  32. package/src/algebra/__tests__/div-mod.test.ts +38 -0
  33. package/src/algebra/__tests__/example0.test.ts +103 -0
  34. package/src/algebra/__tests__/exec-analyze-throws.test.ts +60 -0
  35. package/src/algebra/__tests__/exec-async.test.ts +46 -0
  36. package/src/algebra/__tests__/exec-bpath.test.ts +193 -0
  37. package/src/algebra/__tests__/exec-class.test.ts +56 -0
  38. package/src/algebra/__tests__/exec-e2e.test.ts +97 -0
  39. package/src/algebra/__tests__/exec-generator.test.ts +53 -0
  40. package/src/algebra/__tests__/exec-inherit-default.test.ts +111 -0
  41. package/src/algebra/__tests__/exec-optional-nested.test.ts +87 -0
  42. package/src/algebra/__tests__/exec-rest.test.ts +55 -0
  43. package/src/algebra/__tests__/exec-spread-forof.test.ts +85 -0
  44. package/src/algebra/__tests__/exec-static-computed.test.ts +61 -0
  45. package/src/algebra/__tests__/exec-switch.test.ts +74 -0
  46. package/src/algebra/__tests__/exec-trycatch.test.ts +71 -0
  47. package/src/algebra/__tests__/fn-fp-memo.test.ts +166 -0
  48. package/src/algebra/__tests__/fn-fp-multidecl.test.ts +49 -0
  49. package/src/algebra/__tests__/format.test.ts +34 -0
  50. package/src/algebra/__tests__/generalize-memo.test.ts +285 -0
  51. package/src/algebra/__tests__/generalize.test.ts +113 -0
  52. package/src/algebra/__tests__/hash-source.test.ts +19 -0
  53. package/src/algebra/__tests__/hof.test.ts +80 -0
  54. package/src/algebra/__tests__/inlay.test.ts +30 -0
  55. package/src/algebra/__tests__/language.test.ts +77 -0
  56. package/src/algebra/__tests__/leq.test.ts +109 -0
  57. package/src/algebra/__tests__/load-deps-fp-memo.test.ts +132 -0
  58. package/src/algebra/__tests__/methods.test.ts +37 -0
  59. package/src/algebra/__tests__/node-types.test.ts +26 -0
  60. package/src/algebra/__tests__/nudo-constraints.ts +46 -0
  61. package/src/algebra/__tests__/objects.test.ts +117 -0
  62. package/src/algebra/__tests__/parse-source-cache.test.ts +42 -0
  63. package/src/algebra/__tests__/refine-import.test.ts +109 -0
  64. package/src/algebra/__tests__/stable-source-key-block.test.ts +23 -0
  65. package/src/algebra/__tests__/stable-source-key.test.ts +31 -0
  66. package/src/algebra/__tests__/surface.test.ts +41 -0
  67. package/src/algebra/__tests__/this-inherit.test.ts +130 -0
  68. package/src/algebra/abs-fn.ts +49 -0
  69. package/src/algebra/abs-modules.ts +139 -0
  70. package/src/algebra/abs.ts +206 -0
  71. package/src/algebra/arithmetic.ts +691 -0
  72. package/src/algebra/ast-eval.ts +1763 -0
  73. package/src/algebra/bridge.ts +350 -0
  74. package/src/algebra/builtins.ts +373 -0
  75. package/src/algebra/check-report.ts +150 -0
  76. package/src/algebra/check.ts +1867 -0
  77. package/src/algebra/constraint.ts +304 -0
  78. package/src/algebra/denote.ts +166 -0
  79. package/src/algebra/diagnostics.ts +210 -0
  80. package/src/algebra/exec/call.ts +29 -0
  81. package/src/algebra/exec/calls.ts +96 -0
  82. package/src/algebra/exec/class-registry.ts +29 -0
  83. package/src/algebra/exec/class.ts +329 -0
  84. package/src/algebra/exec/index.ts +6 -0
  85. package/src/algebra/exec/member-diag.ts +141 -0
  86. package/src/algebra/exec/run.ts +341 -0
  87. package/src/algebra/exec/runtime.ts +583 -0
  88. package/src/algebra/exec/transpile.ts +993 -0
  89. package/src/algebra/fn-fp.ts +294 -0
  90. package/src/algebra/format.ts +91 -0
  91. package/src/algebra/generalize.ts +776 -0
  92. package/src/algebra/hash-source.ts +27 -0
  93. package/src/algebra/index.ts +34 -0
  94. package/src/algebra/inlay.ts +198 -0
  95. package/src/algebra/language.ts +289 -0
  96. package/src/algebra/leak.ts +68 -0
  97. package/src/algebra/leq.ts +301 -0
  98. package/src/algebra/load-deps-fp.ts +97 -0
  99. package/src/algebra/methods.ts +128 -0
  100. package/src/algebra/objects.ts +251 -0
  101. package/src/algebra/parse-source.ts +64 -0
  102. package/src/algebra/phi.ts +42 -0
  103. package/src/algebra/pred.ts +395 -0
  104. package/src/algebra/refine.ts +227 -0
  105. package/src/algebra/stable-source-key.ts +81 -0
  106. package/src/algebra/surface.ts +206 -0
  107. package/src/algebra/template.ts +143 -0
  108. package/src/algebra/term.ts +119 -0
  109. package/src/environment.ts +5 -0
  110. package/src/index.ts +52 -1
  111. package/src/mock-helpers.ts +166 -0
  112. package/src/ops.ts +106 -62
  113. package/src/refinements/range.ts +86 -0
  114. package/src/refinements/template-predicates.ts +11 -0
  115. package/src/refinements/template.ts +176 -0
  116. package/src/strip-types.ts +236 -0
  117. 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
+ });