@nudojs/cli 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/README.md +34 -0
- package/package.json +24 -4
- package/src/abs-route.ts +273 -0
- package/src/builtins/builtin-intl.ts +17 -0
- package/src/builtins/builtin-map.ts +174 -0
- package/src/builtins/builtin-promise.ts +116 -0
- package/src/builtins/builtin-prototype.ts +279 -0
- package/src/builtins/builtin-reflect.ts +30 -0
- package/src/builtins/builtin-regexp.ts +34 -0
- package/src/builtins/builtin-set.ts +131 -0
- package/src/builtins/builtin-static.ts +260 -0
- package/src/builtins/builtin-symbol.ts +16 -0
- package/src/builtins/builtin-url.ts +58 -0
- package/src/builtins/builtin-weak.ts +22 -0
- package/src/builtins/builtin-web.ts +111 -0
- package/src/config.ts +32 -0
- package/src/env-loader.ts +174 -0
- package/src/eval-binary.ts +113 -0
- package/src/evaluator-api.ts +23 -0
- package/src/evaluator.ts +3502 -285
- package/src/index.ts +922 -180
- package/src/narrowing.ts +237 -15
- package/src/phi-from-test.ts +42 -0
- package/src/resolve-npm.ts +63 -0
- package/src/run-test.ts +92 -0
- package/src/term-registry.ts +96 -0
- package/CHANGELOG.md +0 -29
- package/dist/chunk-LGWVGEMB.js +0 -1817
- package/dist/evaluator-api.d.ts +0 -59
- package/dist/evaluator-api.js +0 -30
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -3570
- package/src/__tests__/evaluator-loops.test.ts +0 -163
- package/src/__tests__/evaluator-phase2.test.ts +0 -564
- package/src/__tests__/evaluator-phase3.test.ts +0 -513
- package/src/__tests__/evaluator-refined.test.ts +0 -121
- package/src/__tests__/evaluator-string-methods.test.ts +0 -151
- package/src/__tests__/evaluator.test.ts +0 -243
- package/src/__tests__/narrowing.test.ts +0 -82
- package/tsconfig.json +0 -9
package/src/narrowing.ts
CHANGED
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
typeValueEquals,
|
|
10
10
|
isSubtypeOf,
|
|
11
11
|
createRange,
|
|
12
|
+
v as termVar,
|
|
12
13
|
} from "@nudojs/core";
|
|
14
|
+
import { attachTerm } from "./term-registry.ts";
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Given a test expression and the current environment, produce two
|
|
@@ -19,6 +21,20 @@ export function narrow(
|
|
|
19
21
|
test: Node,
|
|
20
22
|
env: Environment,
|
|
21
23
|
): [trueEnv: Environment, falseEnv: Environment] {
|
|
24
|
+
// a || b:fallthrough(both-false)是链式交集 —— narrow(a) 的 false
|
|
25
|
+
// env 再 narrow(b) 的 false。这是 `if (typeof x !== 'object' || x === null)
|
|
26
|
+
// return x;` 守卫模式的关键:后续代码里 x 只剩 object 成员。true 侧
|
|
27
|
+
// 环境无法并集合并,保守退回未 narrow 的 env。
|
|
28
|
+
// a && b 对称:true 侧链式(both-true),false 侧保守。
|
|
29
|
+
if (test.type === "LogicalExpression" && (test.operator === "||" || test.operator === "&&")) {
|
|
30
|
+
const [trueOfLeft, falseOfLeft] = narrow(test.left, env);
|
|
31
|
+
const [, falseOfBoth] = narrow(test.right, falseOfLeft);
|
|
32
|
+
const [trueOfBoth] = narrow(test.right, trueOfLeft);
|
|
33
|
+
return test.operator === "||"
|
|
34
|
+
? [env, falseOfBoth]
|
|
35
|
+
: [trueOfBoth, env];
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
// typeof x === "string"
|
|
23
39
|
if (
|
|
24
40
|
test.type === "BinaryExpression" &&
|
|
@@ -26,9 +42,9 @@ export function narrow(
|
|
|
26
42
|
isTypeofExpr(test.left) &&
|
|
27
43
|
test.right.type === "StringLiteral"
|
|
28
44
|
) {
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
return narrowByTypeof(
|
|
45
|
+
const target = getTypeofTarget(test.left);
|
|
46
|
+
if (target) {
|
|
47
|
+
return target ? narrowByTypeof(target, test.right.value, env) : [env, env];
|
|
32
48
|
}
|
|
33
49
|
}
|
|
34
50
|
|
|
@@ -39,9 +55,9 @@ export function narrow(
|
|
|
39
55
|
test.left.type === "StringLiteral" &&
|
|
40
56
|
isTypeofExpr(test.right)
|
|
41
57
|
) {
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
return narrowByTypeof(
|
|
58
|
+
const target = getTypeofTarget(test.right);
|
|
59
|
+
if (target) {
|
|
60
|
+
return target ? narrowByTypeof(target, test.left.value, env) : [env, env];
|
|
45
61
|
}
|
|
46
62
|
}
|
|
47
63
|
|
|
@@ -52,9 +68,9 @@ export function narrow(
|
|
|
52
68
|
isTypeofExpr(test.left) &&
|
|
53
69
|
test.right.type === "StringLiteral"
|
|
54
70
|
) {
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
-
const [trueEnv, falseEnv] = narrowByTypeof(
|
|
71
|
+
const target = getTypeofTarget(test.left);
|
|
72
|
+
if (target) {
|
|
73
|
+
const [trueEnv, falseEnv] = target ? narrowByTypeof(target, test.right.value, env) : [env, env];
|
|
58
74
|
return [falseEnv, trueEnv];
|
|
59
75
|
}
|
|
60
76
|
}
|
|
@@ -66,13 +82,50 @@ export function narrow(
|
|
|
66
82
|
test.left.type === "StringLiteral" &&
|
|
67
83
|
isTypeofExpr(test.right)
|
|
68
84
|
) {
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
71
|
-
const [trueEnv, falseEnv] = narrowByTypeof(
|
|
85
|
+
const target = getTypeofTarget(test.right);
|
|
86
|
+
if (target) {
|
|
87
|
+
const [trueEnv, falseEnv] = narrowByTypeof(target, test.left.value, env);
|
|
72
88
|
return [falseEnv, trueEnv];
|
|
73
89
|
}
|
|
74
90
|
}
|
|
75
91
|
|
|
92
|
+
// obj.prop === literal (discriminated union)
|
|
93
|
+
if (
|
|
94
|
+
test.type === "BinaryExpression" &&
|
|
95
|
+
test.operator === "===" &&
|
|
96
|
+
test.left.type === "MemberExpression" &&
|
|
97
|
+
test.left.object.type === "Identifier" &&
|
|
98
|
+
test.left.property.type === "Identifier" &&
|
|
99
|
+
isLiteralNode(test.right)
|
|
100
|
+
) {
|
|
101
|
+
return narrowByDiscriminant(test.left.object.name, test.left.property.name, getLiteralValue(test.right), env);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// literal === obj.prop
|
|
105
|
+
if (
|
|
106
|
+
test.type === "BinaryExpression" &&
|
|
107
|
+
test.operator === "===" &&
|
|
108
|
+
isLiteralNode(test.left) &&
|
|
109
|
+
test.right.type === "MemberExpression" &&
|
|
110
|
+
test.right.object.type === "Identifier" &&
|
|
111
|
+
test.right.property.type === "Identifier"
|
|
112
|
+
) {
|
|
113
|
+
return narrowByDiscriminant(test.right.object.name, test.right.property.name, getLiteralValue(test.left), env);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// obj.prop !== literal
|
|
117
|
+
if (
|
|
118
|
+
test.type === "BinaryExpression" &&
|
|
119
|
+
test.operator === "!==" &&
|
|
120
|
+
test.left.type === "MemberExpression" &&
|
|
121
|
+
test.left.object.type === "Identifier" &&
|
|
122
|
+
test.left.property.type === "Identifier" &&
|
|
123
|
+
isLiteralNode(test.right)
|
|
124
|
+
) {
|
|
125
|
+
const [trueEnv, falseEnv] = narrowByDiscriminant(test.left.object.name, test.left.property.name, getLiteralValue(test.right), env);
|
|
126
|
+
return [falseEnv, trueEnv];
|
|
127
|
+
}
|
|
128
|
+
|
|
76
129
|
// x === literal
|
|
77
130
|
if (
|
|
78
131
|
test.type === "BinaryExpression" &&
|
|
@@ -152,12 +205,41 @@ export function narrow(
|
|
|
152
205
|
}
|
|
153
206
|
}
|
|
154
207
|
|
|
208
|
+
// "key" in obj
|
|
209
|
+
if (
|
|
210
|
+
test.type === "BinaryExpression" &&
|
|
211
|
+
test.operator === "in" &&
|
|
212
|
+
test.left.type === "StringLiteral" &&
|
|
213
|
+
test.right.type === "Identifier"
|
|
214
|
+
) {
|
|
215
|
+
return narrowByPropertyIn(test.left.value, test.right.name, env);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Array.isArray(x)
|
|
219
|
+
if (
|
|
220
|
+
test.type === "CallExpression" &&
|
|
221
|
+
test.callee.type === "MemberExpression" &&
|
|
222
|
+
test.callee.object.type === "Identifier" &&
|
|
223
|
+
test.callee.object.name === "Array" &&
|
|
224
|
+
test.callee.property.type === "Identifier" &&
|
|
225
|
+
test.callee.property.name === "isArray" &&
|
|
226
|
+
test.arguments.length === 1 &&
|
|
227
|
+
test.arguments[0].type === "Identifier"
|
|
228
|
+
) {
|
|
229
|
+
return narrowByIsArray(test.arguments[0].name, env);
|
|
230
|
+
}
|
|
231
|
+
|
|
155
232
|
// !expr (negate)
|
|
156
233
|
if (test.type === "UnaryExpression" && test.operator === "!") {
|
|
157
234
|
const [trueEnv, falseEnv] = narrow(test.argument, env);
|
|
158
235
|
return [falseEnv, trueEnv];
|
|
159
236
|
}
|
|
160
237
|
|
|
238
|
+
// Truthiness narrowing: if (x)
|
|
239
|
+
if (test.type === "Identifier") {
|
|
240
|
+
return narrowByTruthy(test.name, env);
|
|
241
|
+
}
|
|
242
|
+
|
|
161
243
|
return [env, env];
|
|
162
244
|
}
|
|
163
245
|
|
|
@@ -165,13 +247,24 @@ function isTypeofExpr(node: Node): boolean {
|
|
|
165
247
|
return node.type === "UnaryExpression" && node.operator === "typeof";
|
|
166
248
|
}
|
|
167
249
|
|
|
168
|
-
|
|
250
|
+
/** typeof 目标:标识符(x)或成员访问链(ref.has / ns.fn) */
|
|
251
|
+
function getTypeofTarget(node: Node): { kind: "ident"; name: string } | { kind: "member"; objName: string; propName: string } | null {
|
|
169
252
|
if (
|
|
170
253
|
node.type === "UnaryExpression" &&
|
|
171
254
|
node.operator === "typeof" &&
|
|
172
255
|
node.argument.type === "Identifier"
|
|
173
256
|
) {
|
|
174
|
-
return node.argument.name;
|
|
257
|
+
return { kind: "ident", name: node.argument.name };
|
|
258
|
+
}
|
|
259
|
+
if (
|
|
260
|
+
node.type === "UnaryExpression" &&
|
|
261
|
+
node.operator === "typeof" &&
|
|
262
|
+
node.argument.type === "MemberExpression" &&
|
|
263
|
+
node.argument.object.type === "Identifier" &&
|
|
264
|
+
!node.argument.computed &&
|
|
265
|
+
node.argument.property.type === "Identifier"
|
|
266
|
+
) {
|
|
267
|
+
return { kind: "member", objName: node.argument.object.name, propName: node.argument.property.name };
|
|
175
268
|
}
|
|
176
269
|
return null;
|
|
177
270
|
}
|
|
@@ -203,11 +296,56 @@ const typeofToPrimitive: Record<string, TypeValue> = {
|
|
|
203
296
|
symbol: T.symbol,
|
|
204
297
|
};
|
|
205
298
|
|
|
299
|
+
/** 内置类实例的原型方法归属(与 evaluator classMethods 表对齐的子集) */
|
|
300
|
+
const INSTANCE_METHOD_OWNERS: Record<string, Set<string>> = {
|
|
301
|
+
has: new Set(["Set", "Map", "WeakSet", "WeakMap"]),
|
|
302
|
+
get: new Set(["Map", "WeakMap"]),
|
|
303
|
+
set: new Set(["Map", "WeakMap", "Set"]),
|
|
304
|
+
add: new Set(["Set", "WeakSet"]),
|
|
305
|
+
delete: new Set(["Set", "Map", "WeakSet", "WeakMap"]),
|
|
306
|
+
clear: new Set(["Set", "Map"]),
|
|
307
|
+
forEach: new Set(["Set", "Map", "Array"]),
|
|
308
|
+
size: new Set(["Set", "Map"]),
|
|
309
|
+
keys: new Set(["Set", "Map"]),
|
|
310
|
+
values: new Set(["Set", "Map"]),
|
|
311
|
+
entries: new Set(["Set", "Map"]),
|
|
312
|
+
test: new Set(["RegExp"]),
|
|
313
|
+
exec: new Set(["RegExp"]),
|
|
314
|
+
};
|
|
315
|
+
|
|
206
316
|
function narrowByTypeof(
|
|
207
|
-
|
|
317
|
+
target: { kind: "ident"; name: string } | { kind: "member"; objName: string; propName: string },
|
|
208
318
|
typeStr: string,
|
|
209
319
|
env: Environment,
|
|
210
320
|
): [Environment, Environment] {
|
|
321
|
+
// 成员目标(typeof ref.has === 'function'):按对象成员中 prop 的类型
|
|
322
|
+
// 过滤 union——留下 has 为函数的成员(如 Set),剔除 tuple/{}
|
|
323
|
+
if (target.kind === "member") {
|
|
324
|
+
const current = env.lookup(target.objName);
|
|
325
|
+
if (!current || current.kind !== "union") return [env, env];
|
|
326
|
+
const propMatches = (m: TypeValue): boolean => {
|
|
327
|
+
let prop: TypeValue | undefined;
|
|
328
|
+
if (m.kind === "object") prop = m.properties[target.propName];
|
|
329
|
+
else if (m.kind === "instance") {
|
|
330
|
+
// instance 的 properties 不含原型方法:按内置类方法表判断
|
|
331
|
+
// (Set/Map 的 has 等——与 evaluator 的 classMethods 表保持一致)
|
|
332
|
+
if (INSTANCE_METHOD_OWNERS[target.propName]?.has(m.className)) return true;
|
|
333
|
+
prop = m.properties[target.propName];
|
|
334
|
+
}
|
|
335
|
+
if (typeStr === "function") return prop?.kind === "function" || prop?.kind === "unknown";
|
|
336
|
+
return true;
|
|
337
|
+
};
|
|
338
|
+
const narrowed = narrowType(current, propMatches);
|
|
339
|
+
const excluded = subtractType(current, propMatches);
|
|
340
|
+
if (narrowed.kind === "never") return [env, env];
|
|
341
|
+
const trueEnv = env.extend({});
|
|
342
|
+
trueEnv.bind(target.objName, narrowed);
|
|
343
|
+
const falseEnv = env.extend({});
|
|
344
|
+
falseEnv.bind(target.objName, excluded.kind === "never" ? current : excluded);
|
|
345
|
+
return [trueEnv, falseEnv];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const varName = target.name;
|
|
211
349
|
const current = env.lookup(varName);
|
|
212
350
|
const targetPrimitive = typeofToPrimitive[typeStr];
|
|
213
351
|
|
|
@@ -316,7 +454,91 @@ function narrowByComparison(
|
|
|
316
454
|
: op === "<=" ? createRange({ min: value + 1 })
|
|
317
455
|
: createRange({ min: value });
|
|
318
456
|
|
|
457
|
+
// 保留项身份:range 界经 toAbsWithTerms 编码为 Pred,代数继续传播
|
|
458
|
+
const term = termVar(varName);
|
|
459
|
+
attachTerm(trueRange, term);
|
|
460
|
+
attachTerm(falseRange, term);
|
|
461
|
+
|
|
319
462
|
trueEnv.bind(varName, trueRange);
|
|
320
463
|
falseEnv.bind(varName, falseRange);
|
|
321
464
|
return [trueEnv, falseEnv];
|
|
322
465
|
}
|
|
466
|
+
|
|
467
|
+
function narrowByPropertyIn(propName: string, varName: string, env: Environment): [Environment, Environment] {
|
|
468
|
+
const current = env.lookup(varName);
|
|
469
|
+
|
|
470
|
+
const narrowed = narrowType(current, (m) =>
|
|
471
|
+
m.kind === "object" && propName in m.properties
|
|
472
|
+
);
|
|
473
|
+
const excluded = subtractType(current, (m) =>
|
|
474
|
+
m.kind === "object" && propName in m.properties
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
const trueEnv = env.extend({});
|
|
478
|
+
trueEnv.bind(varName, narrowed.kind === "never" ? current : narrowed);
|
|
479
|
+
const falseEnv = env.extend({});
|
|
480
|
+
falseEnv.bind(varName, excluded.kind === "never" ? current : excluded);
|
|
481
|
+
return [trueEnv, falseEnv];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function narrowByIsArray(varName: string, env: Environment): [Environment, Environment] {
|
|
485
|
+
const current = env.lookup(varName);
|
|
486
|
+
|
|
487
|
+
const narrowed = narrowType(current, (m) => m.kind === "array" || m.kind === "tuple");
|
|
488
|
+
const excluded = subtractType(current, (m) => m.kind === "array" || m.kind === "tuple");
|
|
489
|
+
|
|
490
|
+
const trueEnv = env.extend({});
|
|
491
|
+
trueEnv.bind(varName, narrowed.kind === "never" ? T.array(T.unknown) : narrowed);
|
|
492
|
+
const falseEnv = env.extend({});
|
|
493
|
+
falseEnv.bind(varName, excluded.kind === "never" ? current : excluded);
|
|
494
|
+
return [trueEnv, falseEnv];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function narrowByDiscriminant(
|
|
498
|
+
objName: string,
|
|
499
|
+
propName: string,
|
|
500
|
+
literalValue: TypeValue,
|
|
501
|
+
env: Environment,
|
|
502
|
+
): [Environment, Environment] {
|
|
503
|
+
const current = env.lookup(objName);
|
|
504
|
+
|
|
505
|
+
const narrowed = narrowType(current, (m) => {
|
|
506
|
+
if (m.kind !== "object") return false;
|
|
507
|
+
const propType = m.properties[propName];
|
|
508
|
+
if (!propType) return false;
|
|
509
|
+
return typeValueEquals(propType, literalValue);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const excluded = subtractType(current, (m) => {
|
|
513
|
+
if (m.kind !== "object") return false;
|
|
514
|
+
const propType = m.properties[propName];
|
|
515
|
+
if (!propType) return false;
|
|
516
|
+
return typeValueEquals(propType, literalValue);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const trueEnv = env.extend({});
|
|
520
|
+
trueEnv.bind(objName, narrowed.kind === "never" ? current : narrowed);
|
|
521
|
+
const falseEnv = env.extend({});
|
|
522
|
+
falseEnv.bind(objName, excluded.kind === "never" ? current : excluded);
|
|
523
|
+
return [trueEnv, falseEnv];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function isFalsyType(tv: TypeValue): boolean {
|
|
527
|
+
if (tv.kind === "literal") {
|
|
528
|
+
return tv.value === null || tv.value === undefined || tv.value === false || tv.value === 0 || tv.value === "";
|
|
529
|
+
}
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function narrowByTruthy(varName: string, env: Environment): [Environment, Environment] {
|
|
534
|
+
const current = env.lookup(varName);
|
|
535
|
+
|
|
536
|
+
const narrowed = narrowType(current, (m) => !isFalsyType(m));
|
|
537
|
+
const excluded = subtractType(current, (m) => !isFalsyType(m));
|
|
538
|
+
|
|
539
|
+
const trueEnv = env.extend({});
|
|
540
|
+
trueEnv.bind(varName, narrowed.kind === "never" ? current : narrowed);
|
|
541
|
+
const falseEnv = env.extend({});
|
|
542
|
+
falseEnv.bind(varName, excluded.kind === "never" ? current : excluded);
|
|
543
|
+
return [trueEnv, falseEnv];
|
|
544
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从 Babel 测试表达式提取代数 Φ 约束。
|
|
3
|
+
* 支持 `x > n` / `x >= n` / `x < n` / `x <= n`(Identifier × NumericLiteral)。
|
|
4
|
+
* false 分支用对偶比较(`>` ↔ `≤`),使 else 路径也能携带约束。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Node } from "@babel/types";
|
|
8
|
+
import type { Phi } from "@nudojs/core";
|
|
9
|
+
import { pTrue, and as phiAnd, gt, ge, lt, le, v as termVar, lit } from "@nudojs/core";
|
|
10
|
+
|
|
11
|
+
export function phiFromTest(test: Node): { whenTrue: Phi; whenFalse: Phi } {
|
|
12
|
+
if (test.type !== "BinaryExpression") {
|
|
13
|
+
return { whenTrue: pTrue, whenFalse: pTrue };
|
|
14
|
+
}
|
|
15
|
+
const op = test.operator;
|
|
16
|
+
const left = test.left;
|
|
17
|
+
const right = test.right;
|
|
18
|
+
|
|
19
|
+
// x ≷ n
|
|
20
|
+
if (
|
|
21
|
+
left.type === "Identifier" &&
|
|
22
|
+
right.type === "NumericLiteral" &&
|
|
23
|
+
(op === ">" || op === ">=" || op === "<" || op === "<=")
|
|
24
|
+
) {
|
|
25
|
+
const t = termVar(left.name);
|
|
26
|
+
const n = lit(right.value);
|
|
27
|
+
const pred =
|
|
28
|
+
op === ">" ? gt(t, n) : op === ">=" ? ge(t, n) : op === "<" ? lt(t, n) : le(t, n);
|
|
29
|
+
// 对偶:¬(a≷b) = a≲b(数值全序)
|
|
30
|
+
const dual =
|
|
31
|
+
op === ">" ? le(t, n) : op === ">=" ? lt(t, n) : op === "<" ? ge(t, n) : gt(t, n);
|
|
32
|
+
return { whenTrue: pred, whenFalse: dual };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { whenTrue: pTrue, whenFalse: pTrue };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function combinePhi(base: Phi, extra: Phi): Phi {
|
|
39
|
+
if (extra.op === "true") return base;
|
|
40
|
+
if (base.op === "true") return extra;
|
|
41
|
+
return phiAnd(base, extra);
|
|
42
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { resolve, join, dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
function findNodeModules(startDir: string): string | null {
|
|
5
|
+
let dir = resolve(startDir);
|
|
6
|
+
const root = resolve("/");
|
|
7
|
+
|
|
8
|
+
while (dir !== root) {
|
|
9
|
+
const nmPath = join(dir, "node_modules");
|
|
10
|
+
if (existsSync(nmPath)) return nmPath;
|
|
11
|
+
const parent = dirname(dir);
|
|
12
|
+
if (parent === dir) break;
|
|
13
|
+
dir = parent;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveExportsNudo(exports: unknown, subpath: string): string | null {
|
|
19
|
+
if (!exports || typeof exports !== "object") return null;
|
|
20
|
+
|
|
21
|
+
const entry = (exports as Record<string, unknown>)[subpath];
|
|
22
|
+
if (!entry) return null;
|
|
23
|
+
|
|
24
|
+
if (typeof entry === "object" && entry !== null && "nudo" in entry) {
|
|
25
|
+
const nudoEntry = (entry as Record<string, unknown>)["nudo"];
|
|
26
|
+
if (typeof nudoEntry === "string") return nudoEntry;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveNpmNudo(
|
|
33
|
+
source: string,
|
|
34
|
+
fromDir: string,
|
|
35
|
+
): string | null {
|
|
36
|
+
const isRelative = source.startsWith(".") || source.startsWith("/");
|
|
37
|
+
if (isRelative) return null;
|
|
38
|
+
|
|
39
|
+
const parts = source.startsWith("@")
|
|
40
|
+
? source.split("/").slice(0, 2)
|
|
41
|
+
: source.split("/").slice(0, 1);
|
|
42
|
+
const pkgName = parts.join("/");
|
|
43
|
+
const subpath = source.slice(pkgName.length) || ".";
|
|
44
|
+
|
|
45
|
+
const nodeModules = findNodeModules(fromDir);
|
|
46
|
+
if (!nodeModules) return null;
|
|
47
|
+
|
|
48
|
+
const pkgJsonPath = join(nodeModules, pkgName, "package.json");
|
|
49
|
+
if (!existsSync(pkgJsonPath)) return null;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
53
|
+
const nudoEntry = resolveExportsNudo(pkg.exports, subpath);
|
|
54
|
+
if (nudoEntry) {
|
|
55
|
+
const resolved = resolve(dirname(pkgJsonPath), nudoEntry);
|
|
56
|
+
if (existsSync(resolved)) return resolved;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// ignore parse errors
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
package/src/run-test.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nudo test — @nudo:case 即测试。
|
|
3
|
+
*
|
|
4
|
+
* directive case 带 `=> expected` 时,用 isSubtypeOf 校验推断结果;
|
|
5
|
+
* 失败记 FAIL,进程退出码 1。无期望的 directive case 记 unchecked。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { isSubtypeOf, typeValueToString, type TypeValue } from "@nudojs/core";
|
|
9
|
+
import type { AnalysisResult } from "@nudojs/service";
|
|
10
|
+
|
|
11
|
+
export type CaseTestOutcome = {
|
|
12
|
+
fn: string;
|
|
13
|
+
caseName: string;
|
|
14
|
+
ok: boolean;
|
|
15
|
+
unchecked: boolean;
|
|
16
|
+
expected?: string;
|
|
17
|
+
actual: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type TestReport = {
|
|
21
|
+
file: string;
|
|
22
|
+
passed: number;
|
|
23
|
+
failed: number;
|
|
24
|
+
unchecked: number;
|
|
25
|
+
outcomes: CaseTestOutcome[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function buildTestReport(file: string, result: AnalysisResult): TestReport {
|
|
29
|
+
const outcomes: CaseTestOutcome[] = [];
|
|
30
|
+
for (const fn of result.functions) {
|
|
31
|
+
for (const c of fn.cases) {
|
|
32
|
+
if (c.source !== "directive") continue;
|
|
33
|
+
const actual = typeValueToString(c.result);
|
|
34
|
+
if (!c.expected) {
|
|
35
|
+
outcomes.push({
|
|
36
|
+
fn: fn.name,
|
|
37
|
+
caseName: c.name,
|
|
38
|
+
ok: true,
|
|
39
|
+
unchecked: true,
|
|
40
|
+
actual,
|
|
41
|
+
});
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const expected = typeValueToString(c.expected as TypeValue);
|
|
45
|
+
let ok = false;
|
|
46
|
+
try {
|
|
47
|
+
ok = isSubtypeOf(c.result, c.expected as TypeValue);
|
|
48
|
+
} catch {
|
|
49
|
+
ok = false;
|
|
50
|
+
}
|
|
51
|
+
outcomes.push({
|
|
52
|
+
fn: fn.name,
|
|
53
|
+
caseName: c.name,
|
|
54
|
+
ok,
|
|
55
|
+
unchecked: false,
|
|
56
|
+
expected,
|
|
57
|
+
actual,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
file,
|
|
63
|
+
passed: outcomes.filter((o) => o.ok && !o.unchecked).length,
|
|
64
|
+
failed: outcomes.filter((o) => !o.ok).length,
|
|
65
|
+
unchecked: outcomes.filter((o) => o.unchecked).length,
|
|
66
|
+
outcomes,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function formatTestReport(r: TestReport): string {
|
|
71
|
+
const lines: string[] = [];
|
|
72
|
+
lines.push(`nudo test ${r.file}`);
|
|
73
|
+
if (r.passed + r.failed + r.unchecked === 0) {
|
|
74
|
+
lines.push("No @nudo:case directives found.");
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
77
|
+
lines.push(r.failed === 0 ? "PASS" : "FAILED");
|
|
78
|
+
lines.push(` ${r.passed} passed · ${r.failed} failed · ${r.unchecked} unchecked`);
|
|
79
|
+
lines.push("");
|
|
80
|
+
for (const o of r.outcomes) {
|
|
81
|
+
if (o.unchecked) {
|
|
82
|
+
lines.push(` [unchecked] ${o.fn} case "${o.caseName}" → ${o.actual}`);
|
|
83
|
+
} else if (o.ok) {
|
|
84
|
+
lines.push(` [ok] ${o.fn} case "${o.caseName}" → ${o.actual}`);
|
|
85
|
+
} else {
|
|
86
|
+
lines.push(` [FAIL] ${o.fn} case "${o.caseName}"`);
|
|
87
|
+
lines.push(` expected: ${o.expected}`);
|
|
88
|
+
lines.push(` actual: ${o.actual}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeValue 项(term)身份旁路。
|
|
3
|
+
*
|
|
4
|
+
* core 的 TypeValue 无 term 字段;用 WeakMap 旁路挂载,零侵入。
|
|
5
|
+
* 纪律:
|
|
6
|
+
* - 禁止给共享单例(T.number 等)挂 term——必须先 clone 再 attach
|
|
7
|
+
* - typeValueToAbs 优先读旁路;无旁路时按 kind 推导
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { TypeValue } from "@nudojs/core";
|
|
11
|
+
import { T } from "@nudojs/core";
|
|
12
|
+
import type { Term } from "@nudojs/core";
|
|
13
|
+
import { v as termVar, lit } from "@nudojs/core";
|
|
14
|
+
import type { Pred } from "@nudojs/core";
|
|
15
|
+
|
|
16
|
+
const termByTv = new WeakMap<object, Term>();
|
|
17
|
+
const predByTv = new WeakMap<object, Pred>();
|
|
18
|
+
|
|
19
|
+
export function attachTerm(tv: TypeValue, term: Term): void {
|
|
20
|
+
if (tv && typeof tv === "object") termByTv.set(tv as object, term);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getTerm(tv: TypeValue): Term | undefined {
|
|
24
|
+
if (tv && typeof tv === "object") return termByTv.get(tv as object);
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function attachPred(tv: TypeValue, pred: Pred): void {
|
|
29
|
+
if (tv && typeof tv === "object") predByTv.set(tv as object, pred);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getPred(tv: TypeValue): Pred | undefined {
|
|
33
|
+
if (tv && typeof tv === "object") return predByTv.get(tv as object);
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 为参数绑定克隆 TypeValue 并挂 term。
|
|
39
|
+
* - primitive number → clone + var(name)
|
|
40
|
+
* - literal number → clone + lit(value)(与 kind 推导一致,但显式)
|
|
41
|
+
* - refined → clone + 尽量挂 base term + pred
|
|
42
|
+
*/
|
|
43
|
+
export function tagParamArg(arg: TypeValue, paramName: string): TypeValue {
|
|
44
|
+
if (!paramName || paramName.startsWith("...")) return arg;
|
|
45
|
+
// 函数/实例不 clone:浅拷贝会丢 _signature / 原型表(Promise executor 等)
|
|
46
|
+
if (arg.kind === "function" || arg.kind === "instance") return arg;
|
|
47
|
+
const cloned = cloneTypeValue(arg);
|
|
48
|
+
if (cloned.kind === "primitive" && cloned.type === "number") {
|
|
49
|
+
attachTerm(cloned, termVar(paramName));
|
|
50
|
+
} else if (cloned.kind === "literal" && typeof cloned.value === "number") {
|
|
51
|
+
attachTerm(cloned, lit(cloned.value));
|
|
52
|
+
} else if (cloned.kind === "refined") {
|
|
53
|
+
attachTerm(cloned, termVar(paramName));
|
|
54
|
+
}
|
|
55
|
+
return cloned;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cloneTypeValue(tv: TypeValue): TypeValue {
|
|
59
|
+
switch (tv.kind) {
|
|
60
|
+
case "literal":
|
|
61
|
+
return { kind: "literal", value: tv.value };
|
|
62
|
+
case "primitive":
|
|
63
|
+
return { kind: "primitive", type: tv.type };
|
|
64
|
+
case "refined":
|
|
65
|
+
return { kind: "refined", base: tv.base, refinement: tv.refinement };
|
|
66
|
+
case "object":
|
|
67
|
+
return { kind: "object", properties: tv.properties, id: tv.id };
|
|
68
|
+
case "array":
|
|
69
|
+
return { kind: "array", element: tv.element };
|
|
70
|
+
case "tuple":
|
|
71
|
+
return { kind: "tuple", elements: tv.elements };
|
|
72
|
+
case "function":
|
|
73
|
+
return {
|
|
74
|
+
kind: "function",
|
|
75
|
+
params: tv.params,
|
|
76
|
+
body: tv.body,
|
|
77
|
+
closure: tv.closure,
|
|
78
|
+
};
|
|
79
|
+
case "promise":
|
|
80
|
+
return { kind: "promise", value: tv.value };
|
|
81
|
+
case "instance":
|
|
82
|
+
return {
|
|
83
|
+
kind: "instance",
|
|
84
|
+
className: tv.className,
|
|
85
|
+
properties: tv.properties,
|
|
86
|
+
};
|
|
87
|
+
case "union":
|
|
88
|
+
return { kind: "union", members: tv.members };
|
|
89
|
+
case "never":
|
|
90
|
+
return { kind: "never" };
|
|
91
|
+
case "unknown":
|
|
92
|
+
return { kind: "unknown" };
|
|
93
|
+
default:
|
|
94
|
+
return tv;
|
|
95
|
+
}
|
|
96
|
+
}
|
package/CHANGELOG.md
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# nudo
|
|
2
|
-
|
|
3
|
-
## 0.2.0
|
|
4
|
-
|
|
5
|
-
### Minor Changes
|
|
6
|
-
|
|
7
|
-
- 6c38283: docs and ci
|
|
8
|
-
- 9f7f819: fix pkg info
|
|
9
|
-
- c175f71: version
|
|
10
|
-
|
|
11
|
-
### Patch Changes
|
|
12
|
-
|
|
13
|
-
- Updated dependencies [6c38283]
|
|
14
|
-
- Updated dependencies [9f7f819]
|
|
15
|
-
- Updated dependencies [c175f71]
|
|
16
|
-
- @nudojs/core@0.2.0
|
|
17
|
-
- @nudojs/parser@0.2.0
|
|
18
|
-
|
|
19
|
-
## 0.1.0
|
|
20
|
-
|
|
21
|
-
### Minor Changes
|
|
22
|
-
|
|
23
|
-
- Conceptual design and basic implementation.
|
|
24
|
-
|
|
25
|
-
### Patch Changes
|
|
26
|
-
|
|
27
|
-
- Updated dependencies
|
|
28
|
-
- @nudojs/core@0.1.0
|
|
29
|
-
- @nudojs/parser@0.1.0
|