@nudojs/cli 0.2.1 → 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 (58) hide show
  1. package/package.json +12 -7
  2. package/src/abs-route.ts +273 -0
  3. package/src/builtins/builtin-prototype.ts +279 -0
  4. package/src/builtins/builtin-static.ts +260 -0
  5. package/src/eval-binary.ts +113 -0
  6. package/src/evaluator-api.ts +3 -0
  7. package/src/evaluator.ts +174 -633
  8. package/src/index.ts +373 -270
  9. package/src/narrowing.ts +7 -0
  10. package/src/phi-from-test.ts +42 -0
  11. package/src/run-test.ts +92 -0
  12. package/src/term-registry.ts +96 -0
  13. package/CHANGELOG.md +0 -41
  14. package/dist/chunk-3HCPXPEW.js +0 -5286
  15. package/dist/evaluator-api.d.ts +0 -151
  16. package/dist/evaluator-api.js +0 -214
  17. package/dist/index.d.ts +0 -2
  18. package/dist/index.js +0 -4035
  19. package/examples/real-world.js +0 -66
  20. package/examples/validate-real-world.ts +0 -160
  21. package/src/__tests__/builtin-advanced.test.ts +0 -292
  22. package/src/__tests__/builtin-api.test.ts +0 -308
  23. package/src/__tests__/builtin-functions.test.ts +0 -313
  24. package/src/__tests__/builtin-map.test.ts +0 -129
  25. package/src/__tests__/builtin-promise.test.ts +0 -150
  26. package/src/__tests__/builtin-regexp.test.ts +0 -133
  27. package/src/__tests__/builtin-set.test.ts +0 -163
  28. package/src/__tests__/builtin-url.test.ts +0 -256
  29. package/src/__tests__/builtin-web.test.ts +0 -340
  30. package/src/__tests__/combination-scenarios.test.ts +0 -384
  31. package/src/__tests__/edge-cases.test.ts +0 -749
  32. package/src/__tests__/evaluator-loops.test.ts +0 -283
  33. package/src/__tests__/evaluator-nullish-coalescing.test.ts +0 -86
  34. package/src/__tests__/evaluator-optional-chaining.test.ts +0 -37
  35. package/src/__tests__/evaluator-phase2.test.ts +0 -564
  36. package/src/__tests__/evaluator-phase3.test.ts +0 -864
  37. package/src/__tests__/evaluator-refined.test.ts +0 -121
  38. package/src/__tests__/evaluator-string-methods.test.ts +0 -151
  39. package/src/__tests__/evaluator.test.ts +0 -943
  40. package/src/__tests__/mock-arrow-function.test.ts +0 -169
  41. package/src/__tests__/mock-comparison.test.ts +0 -221
  42. package/src/__tests__/mock-comprehensive.test.ts +0 -250
  43. package/src/__tests__/mock-final-integration.test.ts +0 -263
  44. package/src/__tests__/mock-helpers.test.ts +0 -174
  45. package/src/__tests__/mock-realworld.test.ts +0 -191
  46. package/src/__tests__/mock-rest-debug.test.ts +0 -110
  47. package/src/__tests__/mock-rest-edge.test.ts +0 -176
  48. package/src/__tests__/mock-sinon-runtime.test.ts +0 -168
  49. package/src/__tests__/narrowing-discriminated.test.ts +0 -35
  50. package/src/__tests__/narrowing-in-operator.test.ts +0 -56
  51. package/src/__tests__/narrowing-switch.test.ts +0 -56
  52. package/src/__tests__/narrowing-truthiness.test.ts +0 -52
  53. package/src/__tests__/narrowing.test.ts +0 -82
  54. package/src/__tests__/nudo-mock-syntax.test.ts +0 -245
  55. package/src/__tests__/realworld-patterns.test.ts +0 -430
  56. package/src/__tests__/syntax-sugar.test.ts +0 -345
  57. package/src/builtins/index.ts +0 -61
  58. package/tsconfig.json +0 -9
package/src/evaluator.ts CHANGED
@@ -3,7 +3,6 @@ import {
3
3
  type TypeValue,
4
4
  T,
5
5
  simplifyUnion,
6
- applyBinaryOp,
7
6
  dispatchBinaryOp,
8
7
  dispatchMethod,
9
8
  dispatchProperty,
@@ -23,7 +22,22 @@ import {
23
22
  } from "@nudojs/core";
24
23
  import { extractInlineDirectives, type InlineDirective } from "@nudojs/parser";
25
24
  import { narrow } from "./narrowing.ts";
26
- import { PROMISE_STATIC_METHODS, evaluatePromiseStaticMethod, evaluatePromiseInstanceMethod } from "./builtins/builtin-promise.ts";
25
+ import {
26
+ tryAbsObjectSpread,
27
+ tryAbsJoinObjects,
28
+ tryAbsUnary,
29
+ pushPhi,
30
+ popPhi,
31
+ currentPhi,
32
+ resetPhi,
33
+ } from "./abs-route.ts";
34
+ import {
35
+ evalBinaryValue,
36
+ distributeBinaryOverUnion,
37
+ } from "./eval-binary.ts";
38
+ import { phiFromTest, combinePhi } from "./phi-from-test.ts";
39
+ import { tagParamArg } from "./term-registry.ts";
40
+ import { evaluatePromiseStaticMethod, evaluatePromiseInstanceMethod } from "./builtins/builtin-promise.ts";
27
41
  import { MAP_INSTANCE_METHODS, createMapType, mapEntriesIterable, exactMapEntries } from "./builtins/builtin-map.ts";
28
42
  import { SET_INSTANCE_METHODS, createSetType, setValuesIterable, exactSetValues } from "./builtins/builtin-set.ts";
29
43
  import { REGEXP_INSTANCE_METHODS, createRegExpType } from "./builtins/builtin-regexp.ts";
@@ -39,427 +53,26 @@ import {
39
53
  createAbortControllerType,
40
54
  } from "./builtins/builtin-web.ts";
41
55
  import { WEAKMAP_INSTANCE_METHODS, WEAKSET_INSTANCE_METHODS, createWeakMapType, createWeakSetType } from "./builtins/builtin-weak.ts";
42
- import { SYMBOL_STATIC_METHODS, SYMBOL_STATIC_PROPS } from "./builtins/builtin-symbol.ts";
56
+ import { SYMBOL_STATIC_PROPS } from "./builtins/builtin-symbol.ts";
43
57
  import { REFLECT_METHODS } from "./builtins/builtin-reflect.ts";
44
58
  import { INTL_DATETIMEFORMAT_METHODS, INTL_NUMBERFORMAT_METHODS, createDateTimeFormatType, createNumberFormatType } from "./builtins/builtin-intl.ts";
59
+ import {
60
+ BUILTIN_ERROR_CLASSES,
61
+ BUILTIN_PROTOTYPE_CLASSES,
62
+ OBJECT_PROTOTYPE_METHODS,
63
+ BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS,
64
+ builtinClassValue,
65
+ builtinPrototype,
66
+ } from "./builtins/builtin-prototype.ts";
67
+ import { BUILTIN_STATIC_METHODS, builtinInstanceTest, jsonToTypeValue } from "./builtins/builtin-static.ts";
68
+
45
69
 
46
- // Built-in JavaScript API type mappings
47
- // Namespace objects (e.g. Math.floor) and direct global values (e.g. parseInt)
48
- const BUILTIN_STATIC_METHODS: Record<string, Record<string, TypeValue> | TypeValue> = {
49
- // Node CJS 库普遍依赖的 process 全局——缺它时 process.nextTick(cb) 走
50
- // 未知全局 → undefined → no-method 假错(hoek wait 的 usage-site 执行)
51
- process: {
52
- nextTick: T.fn(["callback", "...args"], { type: "BlockStatement", body: [], directives: [] } as any, undefined as any),
53
- env: T.object({}),
54
- platform: T.string,
55
- versions: T.object({}),
56
- argv: T.array(T.string),
57
- cwd: T.fn([], { type: "BlockStatement", body: [], directives: [] } as any, undefined as any),
58
- exit: T.fn(["code"], { type: "BlockStatement", body: [], directives: [] } as any, undefined as any),
59
- hrtime: T.fn(["time"], { type: "BlockStatement", body: [], directives: [] } as any, undefined as any),
60
- stdout: T.instanceOf("Socket"),
61
- stderr: T.instanceOf("Socket"),
62
- },
63
- Date: {
64
- now: T.number,
65
- parse: T.number,
66
- UTC: T.number,
67
- },
68
- Math: {
69
- random: T.number,
70
- floor: T.fn(["x"], { type: "BlockStatement", body: [] } as any, undefined as any),
71
- ceil: T.fn(["x"], { type: "BlockStatement", body: [] } as any, undefined as any),
72
- round: T.fn(["x"], { type: "BlockStatement", body: [] } as any, undefined as any),
73
- abs: T.fn(["x"], { type: "BlockStatement", body: [] } as any, undefined as any),
74
- max: T.fn(["...args"], { type: "BlockStatement", body: [] } as any, undefined as any),
75
- min: T.fn(["...args"], { type: "BlockStatement", body: [] } as any, undefined as any),
76
- sqrt: T.fn(["x"], { type: "BlockStatement", body: [] } as any, undefined as any),
77
- pow: T.fn(["base", "exp"], { type: "BlockStatement", body: [] } as any, undefined as any),
78
- // Math 常量与 @nudo:env es 的 Math 定义对齐(env 绑定优先);无 env
79
- // 指令时也在此解析,避免 Math.PI 走 unknown-property。
80
- PI: T.number,
81
- E: T.number,
82
- LN2: T.number,
83
- LN10: T.number,
84
- LOG2E: T.number,
85
- LOG10E: T.number,
86
- SQRT2: T.number,
87
- SQRT1_2: T.number,
88
- },
89
- JSON: {
90
- // JSON.parse(literal-string): evaluate with the real JSON.parse and
91
- // re-encode through jsonToTypeValue, so fixture-style calls
92
- // (`JSON.parse('{"a":1}')`) keep literal-level precision instead of
93
- // poisoning downstream clone/deepEqual args with unknown. Degrades to
94
- // the unknown fallback when the argument is not a literal string or
95
- // the text is not valid JSON. A reviver argument also degrades: it can
96
- // replace every value (and the root) with an arbitrary one, so the
97
- // parse structure is not a sound answer (hoek/json-ext never pass one).
98
- parse: T.fnSig([T.string], T.unknown, T.never, (args) => {
99
- const text = args[0];
100
- if (text?.kind !== "literal" || typeof text.value !== "string") return undefined;
101
- const reviver = args[1];
102
- if (reviver && !(reviver.kind === "literal" && reviver.value === undefined)) return undefined;
103
- try {
104
- return jsonToTypeValue(JSON.parse(text.value));
105
- } catch {
106
- return undefined;
107
- }
108
- }),
109
- stringify: T.string,
110
- },
111
- Object: {
112
- keys: T.array(T.string),
113
- values: T.array(T.unknown),
114
- entries: T.array(T.tuple([T.string, T.unknown])),
115
- assign: T.fnSig([T.unknown], T.unknown, T.never, (args) => {
116
- // Object.assign(target, ...sources): later sources overwrite; a
117
- // symbolic source keeps the symbolic unknown fallback.
118
- const target = args[0];
119
- if (target?.kind !== "object") return undefined;
120
- const props: Record<string, TypeValue> = { ...target.properties };
121
- for (const src of args.slice(1)) {
122
- if (src?.kind !== "object") return undefined;
123
- Object.assign(props, src.properties);
124
- }
125
- return T.object(props);
126
- }),
127
- // Prototype-reflection statics. getPrototypeOf maps receivers onto the
128
- // cached prototype singletons so `getPrototypeOf(x) !== getPrototypeOf(y)`
129
- // resolves literally; create/setPrototypeOf cover the clone-style
130
- // prototype dance; getOwnPropertyDescriptor feeds descriptor.get/set
131
- // branches with the property's value type.
132
- getPrototypeOf: T.fnSig([T.unknown], T.unknown, T.never, (args) => protoOfValue(args[0])),
133
- create: T.fnSig([T.unknown], T.object({}), T.never, () => T.object({})),
134
- setPrototypeOf: T.fnSig([T.unknown, T.unknown], T.unknown, T.never, (args) => args[0]),
135
- defineProperty: T.fnSig([T.unknown, T.unknown, T.unknown], T.unknown, T.never, (args) => args[0]),
136
- getOwnPropertyDescriptor: T.fnSig([T.unknown, T.unknown], T.union(T.object({}), T.undefined), T.never, (args) => {
137
- const obj = args[0];
138
- const key = args[1];
139
- if (obj?.kind !== "object" && obj?.kind !== "instance") return T.undefined;
140
- if (!key) return T.undefined;
141
- if (key.kind === "literal" && typeof key.value === "string") {
142
- const prop = hasOwnProp(obj.properties, key.value) ? obj.properties[key.value] : undefined;
143
- if (!prop) return T.undefined;
144
- return descriptorOf(prop);
145
- }
146
- if (key.kind === "primitive" && key.type === "string") {
147
- const props = Object.keys(obj.properties);
148
- if (props.length === 0) return T.undefined;
149
- return descriptorOf(simplifyUnion(props.map((k) => obj.properties[k])));
150
- }
151
- return T.undefined;
152
- }),
153
- },
154
- Buffer: {
155
- from: T.fnSig([T.unknown], T.instanceOf("Buffer")),
156
- alloc: T.fnSig([T.number], T.instanceOf("Buffer")),
157
- concat: T.fnSig([T.unknown], T.instanceOf("Buffer")),
158
- byteLength: T.fnSig([T.unknown], T.number),
159
- isEncoding: T.fnSig([T.unknown], T.boolean),
160
- isBuffer: T.fnSig([T.unknown], T.boolean, T.never, (args) => {
161
- const lit = builtinInstanceTest(args[0], "Buffer");
162
- return lit === undefined ? undefined : T.literal(lit);
163
- }),
164
- },
165
- Array: {
166
- isArray: T.boolean,
167
- from: T.array(T.unknown),
168
- },
169
- Number: {
170
- isNaN: T.boolean,
171
- isFinite: T.boolean,
172
- parseInt: T.number,
173
- parseFloat: T.number,
174
- // Number 常量与 @nudo:env es 的 Number 定义对齐(env 绑定优先);
175
- // 均为 number(case 序列化器对非有限数返回 null,不产出字面量值)。
176
- MAX_SAFE_INTEGER: T.number,
177
- MIN_SAFE_INTEGER: T.number,
178
- MAX_VALUE: T.number,
179
- MIN_VALUE: T.number,
180
- POSITIVE_INFINITY: T.number,
181
- NEGATIVE_INFINITY: T.number,
182
- NaN: T.number,
183
- EPSILON: T.number,
184
- },
185
- String: {
186
- // fromCharCode/fromCodePoint over all-number-literal args evaluate
187
- // with the real String static (hoek's escaper builds its 0x2028
188
- // separator literal this way before handing it to escapeJson); any
189
- // non-literal arg keeps only the string return type (the result is
190
- // always a string, just not a knowable one). fromCodePoint also
191
- // falls back on out-of-range literals (real RangeError → string).
192
- fromCharCode: T.fnSig([T.number], T.string, T.never, (args) => {
193
- const codes = literalCodeUnits(args);
194
- return codes === null ? T.string : T.literal(String.fromCharCode(...codes));
195
- }),
196
- fromCodePoint: T.fnSig([T.number], T.string, T.never, (args) => {
197
- const codes = literalCodeUnits(args);
198
- if (codes === null) return T.string;
199
- try {
200
- return T.literal(String.fromCodePoint(...codes));
201
- } catch {
202
- return undefined;
203
- }
204
- }),
205
- },
206
- Promise: PROMISE_STATIC_METHODS,
207
- Symbol: { ...SYMBOL_STATIC_METHODS, ...SYMBOL_STATIC_PROPS },
208
- Reflect: REFLECT_METHODS as unknown as Record<string, TypeValue>,
209
- Intl: {
210
- DateTimeFormat: T.fn(["...args"], { type: "BlockStatement", body: [] } as any, undefined as any),
211
- NumberFormat: T.fn(["...args"], { type: "BlockStatement", body: [] } as any, undefined as any),
212
- },
213
- parseInt: T.number,
214
- parseFloat: T.number,
215
- isNaN: T.boolean,
216
- isFinite: T.boolean,
217
- // 全局数值常量:与 @nudo:env es 的 globals 对齐(env 绑定优先)。无 env
218
- // 指令时在此解析为 number——否则 Infinity/NaN 命中大写开头的未知全局
219
- // 检测,触发 nudo:unknown-global 与 nudo:builtin-unknown 诊断。
220
- Infinity: T.number,
221
- NaN: T.number,
222
- };
223
70
 
224
- /** Harvest String.fromCharCode/fromCodePoint arguments: a list of number
225
- * literals ready for real evaluation, or null when any argument is
226
- * non-literal (symbolic → the caller keeps its plain string fallback).
227
- * An empty argument list yields [] (both statics return ""). */
228
- function literalCodeUnits(args: TypeValue[]): number[] | null {
229
- const codes: number[] = [];
230
- for (const a of args) {
231
- if (a?.kind !== "literal" || typeof a.value !== "number") return null;
232
- codes.push(a.value);
233
- }
234
- return codes;
235
- }
236
71
 
237
- /** `==`/`!=` 静态求值可用的字面量值:number/string/boolean/null/undefined
238
- * 这五种原始值走 JS 宽松相等的 ToNumber 强转路径,可直接用宿主 `==`
239
- * 求值(与规范一致);bigint/symbol 无该路径,hit=false 交给调用方退化
240
- * 为 T.boolean。undefined 不能作哨兵值(它本身就是合法字面量值)。 */
241
- function looseLiteralValue(
242
- tv: TypeValue,
243
- ): { hit: boolean; v: number | string | boolean | null | undefined } {
244
- if (tv.kind === "literal") {
245
- const v = tv.value;
246
- if (
247
- typeof v === "number" ||
248
- typeof v === "string" ||
249
- typeof v === "boolean" ||
250
- v === null ||
251
- v === undefined
252
- ) {
253
- return { hit: true, v };
254
- }
255
- }
256
- return { hit: false, v: undefined };
257
- }
258
72
 
259
- const BUILTIN_INSTANCE_METHODS: Record<string, Record<string, (...args: TypeValue[]) => TypeValue>> = {
260
- Date: {
261
- getTime: () => T.number,
262
- getFullYear: () => T.number,
263
- getMonth: () => T.number,
264
- getDate: () => T.number,
265
- getHours: () => T.number,
266
- getMinutes: () => T.number,
267
- getSeconds: () => T.number,
268
- getMilliseconds: () => T.number,
269
- toISOString: () => T.string,
270
- toString: () => T.string,
271
- valueOf: () => T.number,
272
- },
273
- WeakMap: WEAKMAP_INSTANCE_METHODS,
274
- WeakSet: WEAKSET_INSTANCE_METHODS,
275
- DateTimeFormat: INTL_DATETIMEFORMAT_METHODS,
276
- NumberFormat: INTL_NUMBERFORMAT_METHODS,
277
- };
278
73
 
279
- const BUILTIN_ERROR_CLASSES = new Set([
280
- "Error", "TypeError", "SyntaxError", "RangeError", "ReferenceError", "URIError", "EvalError",
281
- ]);
282
-
283
- // Constructible built-in classes. A bare reference to one of these names
284
- // resolves to a namespace object (like the BUILTIN_STATIC_METHODS entries),
285
- // and `X.prototype` evaluates to an instance of X instead of degrading to
286
- // undefined/unknown.
287
- const BUILTIN_PROTOTYPE_CLASSES = new Set([
288
- ...BUILTIN_ERROR_CLASSES,
289
- "Date", "Object", "Map", "Set", "Promise", "RegExp", "Array", "Function",
290
- "String", "Number", "Boolean", "Symbol", "WeakMap", "WeakSet", "Buffer",
291
- ]);
292
-
293
- // Object.prototype members. Real property access reads through the
294
- // prototype chain, so every object-typed receiver materializes these —
295
- // destructuring `const { hasOwnProperty } = obj` yields a function (not
296
- // undefined) and `Object.prototype.hasOwnProperty` types as boolean.
297
- // Lookup must be own-property guarded: a plain `{}` record would otherwise
298
- // leak native JS functions (e.g. for "constructor") into the type system.
299
- //
300
- // toString/valueOf carry impls so the receiver (thisVal) shapes the result:
301
- // `Object.prototype.toString.call(x)` yields the brand literal
302
- // ('[object Map]', '[object Null]', ...) that Map-based type dispatch
303
- // (hoek internals.typeMap) keys on; valueOf returns its receiver.
304
- const OBJECT_PROTOTYPE_METHODS: Record<string, TypeValue> = {
305
- hasOwnProperty: T.fnSig([T.unknown], T.boolean),
306
- isPrototypeOf: T.fnSig([T.unknown], T.boolean),
307
- propertyIsEnumerable: T.fnSig([T.unknown], T.boolean),
308
- toString: T.fnSig([], T.string, T.never, (_args, thisVal) => objectToStringBrand(thisVal)),
309
- toLocaleString: T.fnSig([], T.string),
310
- valueOf: T.fnSig([], T.unknown, T.never, (_args, thisVal) => thisVal),
311
- };
312
74
 
313
- // Common prototype members approximated as unknown-result signatures
314
- // (mirrors the knownInstanceMethods fallback pattern). `X.prototype.m`
315
- // stays a callable function value instead of degrading to undefined.
316
- const BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS: Record<string, Record<string, TypeValue>> = {
317
- Object: { ...OBJECT_PROTOTYPE_METHODS },
318
- Array: {
319
- push: T.fnSig([T.unknown], T.number),
320
- pop: T.fnSig([], T.unknown),
321
- shift: T.fnSig([], T.unknown),
322
- unshift: T.fnSig([T.unknown], T.number),
323
- slice: T.fnSig([T.number, T.number], T.array(T.unknown)),
324
- splice: T.fnSig([T.number, T.number], T.array(T.unknown)),
325
- concat: T.fnSig([T.unknown], T.array(T.unknown)),
326
- join: T.fnSig([T.string], T.string),
327
- indexOf: T.fnSig([T.unknown], T.number),
328
- lastIndexOf: T.fnSig([T.unknown], T.number),
329
- includes: T.fnSig([T.unknown], T.boolean),
330
- map: T.fnSig([T.unknown], T.array(T.unknown)),
331
- filter: T.fnSig([T.unknown], T.array(T.unknown)),
332
- forEach: T.fnSig([T.unknown], T.undefined),
333
- find: T.fnSig([T.unknown], T.unknown),
334
- findIndex: T.fnSig([T.unknown], T.number),
335
- some: T.fnSig([T.unknown], T.boolean),
336
- every: T.fnSig([T.unknown], T.boolean),
337
- reduce: T.fnSig([T.unknown, T.unknown], T.unknown),
338
- sort: T.fnSig([T.unknown], T.array(T.unknown)),
339
- reverse: T.fnSig([], T.array(T.unknown)),
340
- toString: T.fnSig([], T.string),
341
- },
342
- Function: {
343
- call: T.fnSig([T.unknown], T.unknown),
344
- apply: T.fnSig([T.unknown, T.unknown], T.unknown),
345
- bind: T.fnSig([T.unknown], T.unknown),
346
- toString: T.fnSig([], T.string),
347
- },
348
- Map: {
349
- // Impl-routed: `Map.prototype.m.call(instance)` (and direct prototype
350
- // calls) consult the receiver's exact entry side table, making the
351
- // hoek deepEqual reflection idioms decide literally.
352
- get: T.fnSig([T.unknown], T.unknown, T.never, (args, thisVal) => MAP_INSTANCE_METHODS.get(args[0] ?? T.unknown, thisVal ?? T.unknown)),
353
- set: T.fnSig([T.unknown, T.unknown], T.unknown, T.never, (args, thisVal) => MAP_INSTANCE_METHODS.set(args[0] ?? T.unknown, args[1] ?? T.unknown, thisVal ?? T.unknown)),
354
- has: T.fnSig([T.unknown], T.boolean, T.never, (args, thisVal) => MAP_INSTANCE_METHODS.has(args[0] ?? T.unknown, thisVal ?? T.unknown)),
355
- delete: T.fnSig([T.unknown], T.boolean, T.never, (args, thisVal) => MAP_INSTANCE_METHODS.delete(args[0] ?? T.unknown, thisVal ?? T.unknown)),
356
- clear: T.fnSig([], T.undefined, T.never, (args, thisVal) => MAP_INSTANCE_METHODS.clear(args[0] ?? T.unknown, thisVal ?? T.unknown)),
357
- forEach: T.fnSig([T.unknown], T.undefined),
358
- keys: T.fnSig([], T.array(T.unknown), T.never, (_args, thisVal) => MAP_INSTANCE_METHODS.keys(thisVal ?? T.unknown)),
359
- values: T.fnSig([], T.array(T.unknown), T.never, (_args, thisVal) => MAP_INSTANCE_METHODS.values(thisVal ?? T.unknown)),
360
- entries: T.fnSig([], T.array(T.tuple([T.unknown, T.unknown])), T.never, (_args, thisVal) => mapEntriesIterable(thisVal ?? T.unknown)),
361
- toString: T.fnSig([], T.string),
362
- },
363
- Set: {
364
- // Impl-routed like Map above (Set.prototype.values.call(s) ≡ s.values()).
365
- add: T.fnSig([T.unknown], T.unknown, T.never, (args, thisVal) => SET_INSTANCE_METHODS.add(args[0] ?? T.unknown, thisVal ?? T.unknown)),
366
- has: T.fnSig([T.unknown], T.boolean, T.never, (args, thisVal) => SET_INSTANCE_METHODS.has(args[0] ?? T.unknown, thisVal ?? T.unknown)),
367
- delete: T.fnSig([T.unknown], T.boolean, T.never, (args, thisVal) => SET_INSTANCE_METHODS.delete(args[0] ?? T.unknown, thisVal ?? T.unknown)),
368
- clear: T.fnSig([], T.undefined, T.never, (args, thisVal) => SET_INSTANCE_METHODS.clear(args[0] ?? T.unknown, thisVal ?? T.unknown)),
369
- forEach: T.fnSig([T.unknown], T.undefined),
370
- keys: T.fnSig([], T.array(T.unknown), T.never, (_args, thisVal) => SET_INSTANCE_METHODS.keys(thisVal ?? T.unknown)),
371
- values: T.fnSig([], T.array(T.unknown), T.never, (_args, thisVal) => SET_INSTANCE_METHODS.values(thisVal ?? T.unknown)),
372
- entries: T.fnSig([], T.array(T.tuple([T.unknown, T.unknown])), T.never, (_args, thisVal) => SET_INSTANCE_METHODS.entries(thisVal ?? T.unknown)),
373
- toString: T.fnSig([], T.string),
374
- },
375
- WeakMap: {
376
- get: T.fnSig([T.unknown], T.unknown),
377
- set: T.fnSig([T.unknown, T.unknown], T.unknown),
378
- has: T.fnSig([T.unknown], T.boolean),
379
- delete: T.fnSig([T.unknown], T.boolean),
380
- toString: T.fnSig([], T.string),
381
- },
382
- WeakSet: {
383
- add: T.fnSig([T.unknown], T.unknown),
384
- has: T.fnSig([T.unknown], T.boolean),
385
- delete: T.fnSig([T.unknown], T.boolean),
386
- toString: T.fnSig([], T.string),
387
- },
388
- Promise: {
389
- then: T.fnSig([T.unknown], T.promise(T.unknown)),
390
- catch: T.fnSig([T.unknown], T.promise(T.unknown)),
391
- finally: T.fnSig([T.unknown], T.promise(T.unknown)),
392
- toString: T.fnSig([], T.string),
393
- },
394
- Date: {
395
- getTime: T.fnSig([], T.number),
396
- valueOf: T.fnSig([], T.number),
397
- toISOString: T.fnSig([], T.string),
398
- toJSON: T.fnSig([], T.string),
399
- toLocaleString: T.fnSig([], T.string),
400
- toString: T.fnSig([], T.string),
401
- },
402
- RegExp: {
403
- test: T.fnSig([T.string], T.boolean),
404
- exec: T.fnSig([T.string], T.union(T.object({}), T.null)),
405
- toString: T.fnSig([], T.string),
406
- },
407
- String: {
408
- charAt: T.fnSig([T.number], T.string),
409
- charCodeAt: T.fnSig([T.number], T.number),
410
- indexOf: T.fnSig([T.string], T.number),
411
- lastIndexOf: T.fnSig([T.string], T.number),
412
- includes: T.fnSig([T.string], T.boolean),
413
- startsWith: T.fnSig([T.string], T.boolean),
414
- endsWith: T.fnSig([T.string], T.boolean),
415
- slice: T.fnSig([T.number, T.number], T.string),
416
- substring: T.fnSig([T.number, T.number], T.string),
417
- toUpperCase: T.fnSig([], T.string),
418
- toLowerCase: T.fnSig([], T.string),
419
- trim: T.fnSig([], T.string),
420
- replace: T.fnSig([T.unknown, T.string], T.string),
421
- split: T.fnSig([T.string], T.array(T.string)),
422
- toString: T.fnSig([], T.string),
423
- valueOf: T.fnSig([], T.string),
424
- },
425
- Number: {
426
- toFixed: T.fnSig([T.number], T.string),
427
- toPrecision: T.fnSig([T.number], T.string),
428
- valueOf: T.fnSig([], T.number),
429
- toString: T.fnSig([T.number], T.string),
430
- },
431
- Boolean: {
432
- valueOf: T.fnSig([], T.boolean),
433
- toString: T.fnSig([], T.string),
434
- },
435
- Symbol: {
436
- toString: T.fnSig([], T.string),
437
- valueOf: T.fnSig([], T.symbol),
438
- },
439
- Buffer: {
440
- equals: T.fnSig([T.unknown], T.boolean),
441
- compare: T.fnSig([T.unknown], T.number),
442
- toString: T.fnSig([T.unknown], T.string),
443
- toJSON: T.fnSig([], T.unknown),
444
- },
445
- Error: {
446
- toString: T.fnSig([], T.string),
447
- },
448
- };
449
75
 
450
- // Memoized namespace values for built-in classes. Reference-stable so
451
- // `'x'.constructor === String` compares identical objects (typeValueEquals
452
- // falls back to reference equality for object kinds).
453
- const _builtinClassValues = new Map<string, TypeValue>();
454
- function builtinClassValue(name: string): TypeValue {
455
- let v = _builtinClassValues.get(name);
456
- if (v === undefined) {
457
- v = T.object({});
458
- (v as any)._builtinName = name;
459
- _builtinClassValues.set(name, v);
460
- }
461
- return v;
462
- }
463
76
 
464
77
  // Wrapper-prototype fallback for primitive receivers: real JS auto-boxes
465
78
  // member access on primitives ('x'.constructor === String), so a named
@@ -503,109 +116,10 @@ function wrapperPrototypeMember(obj: TypeValue, propName: string): TypeValue | u
503
116
  return undefined;
504
117
  }
505
118
 
506
- // Cached `X.prototype` singletons. hoek-style modules assign
507
- // `exports.array = Array.prototype` and later compare
508
- // `baseProto === Types.buffer`: strict-equality on instances only stays
509
- // precise when every evaluation of `X.prototype` yields the same TypeValue.
510
- const BUILTIN_PROTOTYPE_SINGLETONS = new Map<string, TypeValue>();
511
119
 
512
- function builtinPrototype(className: string): TypeValue {
513
- let proto = BUILTIN_PROTOTYPE_SINGLETONS.get(className);
514
- if (!proto) {
515
- const methods = hasOwnProp(BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS, className)
516
- ? BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS[className]
517
- : BUILTIN_ERROR_CLASSES.has(className)
518
- ? BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS.Error
519
- : {};
520
- proto = T.instanceOf(className, { ...methods });
521
- (proto as any)._builtinProto = className;
522
- BUILTIN_PROTOTYPE_SINGLETONS.set(className, proto);
523
- }
524
- return proto;
525
- }
526
120
 
527
- // `Object.prototype.toString` brand string ('[object Map]', '[object Null]',
528
- // ...). Returns undefined when the receiver has no representable brand, so
529
- // the fnSig falls back to its plain `string` return type.
530
- function objectToStringBrand(v: TypeValue | undefined): TypeValue | undefined {
531
- if (!v) return undefined;
532
- if (v.kind === "union") {
533
- const parts = v.members.map((m) => objectToStringBrand(m));
534
- if (parts.some((p) => p === undefined)) return undefined;
535
- return simplifyUnion(parts as TypeValue[]);
536
- }
537
- let base: TypeValue = v;
538
- while (base.kind === "refined") base = base.base;
539
- switch (base.kind) {
540
- case "object": return T.literal("[object Object]");
541
- case "array":
542
- case "tuple": return T.literal("[object Array]");
543
- case "function": return T.literal("[object Function]");
544
- case "promise": return T.literal("[object Promise]");
545
- case "instance": return T.literal(`[object ${base.className}]`);
546
- case "literal": {
547
- if (base.value === null) return T.literal("[object Null]");
548
- if (base.value === undefined) return T.literal("[object Undefined]");
549
- const t = typeof base.value;
550
- return T.literal(`[object ${t === "number" ? "Number" : t === "string" ? "String" : "Boolean"}]`);
551
- }
552
- case "primitive": {
553
- const brands: Record<string, string> = {
554
- number: "Number", string: "String", boolean: "Boolean", bigint: "BigInt", symbol: "Symbol",
555
- };
556
- return T.literal(`[object ${brands[base.type] ?? "Object"}]`);
557
- }
558
- default: return undefined;
559
- }
560
- }
561
-
562
- // Object.getPrototypeOf: map a receiver onto its class's cached prototype
563
- // singleton (plain objects → Object.prototype, arrays → Array.prototype,
564
- // instances → their class's prototype). Unrepresentable receivers degrade
565
- // to unknown.
566
- function protoOfValue(v: TypeValue | undefined): TypeValue {
567
- if (!v) return T.unknown;
568
- if (v.kind === "union") return simplifyUnion(v.members.map((m) => protoOfValue(m)));
569
- let base: TypeValue = v;
570
- while (base.kind === "refined") base = base.base;
571
- switch (base.kind) {
572
- case "object": return builtinPrototype("Object");
573
- case "array":
574
- case "tuple": return builtinPrototype("Array");
575
- case "function": return builtinPrototype("Function");
576
- case "promise": return builtinPrototype("Promise");
577
- case "instance": return builtinPrototype(base.className);
578
- default: return T.unknown;
579
- }
580
- }
581
121
 
582
- function descriptorOf(prop: TypeValue): TypeValue {
583
- return T.object({
584
- value: prop,
585
- writable: T.boolean,
586
- enumerable: T.boolean,
587
- configurable: T.boolean,
588
- });
589
- }
590
122
 
591
- // `x instanceof C` / `Buffer.isBuffer(x)` / `Array.isArray(x)` literal
592
- // answer for structurally-known receivers; undefined keeps the symbolic
593
- // fallback (boolean).
594
- function builtinInstanceTest(v: TypeValue | undefined, className: string): boolean | undefined {
595
- if (!v) return undefined;
596
- let base: TypeValue = v;
597
- while (base.kind === "refined") base = base.base;
598
- switch (base.kind) {
599
- case "array":
600
- case "tuple": return className === "Array" || className === "Object";
601
- case "object": return className === "Object";
602
- case "function": return className === "Function" || className === "Object";
603
- case "promise": return className === "Promise" || className === "Object";
604
- case "literal":
605
- case "primitive": return false;
606
- default: return undefined;
607
- }
608
- }
609
123
 
610
124
  function arrayIsArrayLiteral(v: TypeValue): boolean | undefined {
611
125
  let base: TypeValue = v;
@@ -653,6 +167,18 @@ function definiteBoolean(tv: TypeValue): boolean | null {
653
167
  }
654
168
  }
655
169
 
170
+ /**
171
+ * 分支合并:双对象先走 Abs join(sum-of-products / 槽位合并),
172
+ * 失败或非对象再 simplifyUnion。保证 join 代数接到 evaluator 主路径。
173
+ */
174
+ function joinOrUnion(a: TypeValue, b: TypeValue): TypeValue {
175
+ if (a.kind === "object" && b.kind === "object") {
176
+ const joined = tryAbsJoinObjects(a, b);
177
+ if (joined) return joined;
178
+ }
179
+ return simplifyUnion([a, b]);
180
+ }
181
+
656
182
  // Receiver-binding for thisVal-dependent Object.prototype methods
657
183
  // (toString/valueOf): `const v = obj.valueOf; v()` loses the member-access
658
184
  // receiver, so accesses off a concrete receiver return a copy whose impl
@@ -1094,6 +620,9 @@ export type CallRecord = {
1094
620
  argTypes: TypeValue[];
1095
621
  resultType: TypeValue;
1096
622
  throws: TypeValue;
623
+ /** Line-relative. Per-fn cache replay shifts this by lineDelta — if you
624
+ * add another position field (callee loc, arg loc), extend
625
+ * shiftCallRecordLines in analyzer.ts in the same change. */
1097
626
  callLoc?: { line: number; column: number };
1098
627
  targetModule?: string;
1099
628
  targetExport?: string;
@@ -1309,6 +838,13 @@ export function setProvenanceTracking(enabled: boolean): void {
1309
838
  _provenanceEnabled = enabled;
1310
839
  }
1311
840
 
841
+ /** 克隆 TypeValue 后拷贝 provenance(tagParamArg 会 clone,丢 origin 会导致 M5 回归) */
842
+ function copyOrigin(from: TypeValue, to: TypeValue): void {
843
+ if (!_provenanceEnabled || !from || !to) return;
844
+ const o = _originMap.get(from);
845
+ if (o) _originMap.set(to, o);
846
+ }
847
+
1312
848
  function extractOrigin(
1313
849
  tv: TypeValue | undefined,
1314
850
  ): { line: number; column: number } | undefined {
@@ -1566,31 +1102,6 @@ function evaluateComputedMemberAccess(objVal: TypeValue, propVal: TypeValue, nod
1566
1102
  return result;
1567
1103
  }
1568
1104
 
1569
- const MAX_UNION_PRODUCT = 50;
1570
-
1571
- function distributeBinaryOverUnion(
1572
- left: TypeValue,
1573
- right: TypeValue,
1574
- fn: (l: TypeValue, r: TypeValue) => TypeValue,
1575
- ): TypeValue {
1576
- if (left.kind === "union" && right.kind === "union") {
1577
- // Cap combinatorial blowup
1578
- if (left.members.length * right.members.length > MAX_UNION_PRODUCT) {
1579
- return T.unknown;
1580
- }
1581
- return simplifyUnion(
1582
- left.members.flatMap((l) => right.members.map((r) => fn(l, r))),
1583
- );
1584
- }
1585
- if (left.kind === "union") {
1586
- return simplifyUnion(left.members.map((l) => fn(l, right)));
1587
- }
1588
- if (right.kind === "union") {
1589
- return simplifyUnion(right.members.map((r) => fn(left, r)));
1590
- }
1591
- return fn(left, right);
1592
- }
1593
-
1594
1105
  let _unreachableRanges: SourceRange[] = [];
1595
1106
 
1596
1107
  function collectUnreachable(stmts: readonly Node[], fromIndex: number): void {
@@ -1815,66 +1326,39 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
1815
1326
  return evaluateInstanceof(leftVal, rightVal, node.right, env);
1816
1327
  }
1817
1328
  if (node.operator === "in") {
1818
- // Literal `key in obj` / `Symbol.iterator in x` decisions; unions
1819
- // on either side distribute (mixed literals collapse per
1820
- // simplifyUnion), unknown receivers stay unknown.
1821
1329
  return distributeBinaryOverUnion(leftVal, rightVal, (l, r) => evaluateInMember(l, r));
1822
1330
  }
1823
1331
  if (node.operator === "===" || node.operator === "!==") {
1824
- // Prototype-singleton identity (`baseProto === Types.buffer`): the
1825
- // generic Op only literal-compares, so same-class instance pairs
1826
- // would collapse to boolean. Decide it when both sides are instances
1827
- // and at least one is a cached builtin prototype.
1828
1332
  const identity = builtinProtoIdentityEq(leftVal, rightVal);
1829
1333
  if (identity !== null) {
1830
1334
  return T.literal(node.operator === "===" ? identity : !identity);
1831
1335
  }
1832
- // Class-namespace identity (`x.constructor === Array`): memoized
1833
- // builtinClassValue objects compare by class name.
1834
1336
  const classIdentity = builtinClassIdentityEq(leftVal, rightVal);
1835
1337
  if (classIdentity !== null) {
1836
1338
  return T.literal(node.operator === "===" ? classIdentity : !classIdentity);
1837
1339
  }
1838
1340
  }
1839
- // `==`/`!=`:core 的 binaryOpMap 未收录这两个运算符(applyBinaryOp
1840
- // 会回落 unknown),在同一分派点补上——两操作数均为原始字面量
1841
- // (number/string/boolean/null/undefined)时按 JS 宽松相等语义
1842
- // (ToNumber 强转,如 "5" == 5 → true、null == undefined → true、
1843
- // NaN == NaN → false)直接求值出 boolean 字面量;任一操作数非字面量
1844
- // 时退化为 T.boolean。
1845
- if (node.operator === "==" || node.operator === "!=") {
1846
- const eq = node.operator === "==";
1847
- return distributeBinaryOverUnion(leftVal, rightVal, (l, r) => {
1848
- const lv = looseLiteralValue(l);
1849
- const rv = looseLiteralValue(r);
1850
- if (lv.hit && rv.hit) return T.literal(eq ? lv.v == rv.v : lv.v != rv.v);
1851
- return T.boolean;
1852
- });
1853
- }
1854
- return distributeBinaryOverUnion(leftVal, rightVal, (l, r) =>
1855
- dispatchBinaryOp(node.operator, l, r),
1856
- );
1341
+
1342
+ // 统一二元:代数优先 → 宽松相等 → 外延 Ops
1343
+ const bin = evalBinaryValue(node.operator, leftVal, rightVal);
1344
+ if (bin !== undefined) return bin;
1345
+ return T.unknown;
1857
1346
  }
1858
1347
 
1859
1348
  case "UnaryExpression": {
1860
1349
  const argVal = evaluate(node.argument, env);
1861
1350
  if (isReturn(argVal) || isBranch(argVal) || isThrow(argVal)) return argVal;
1862
- if (node.operator === "typeof") {
1863
- return distributeOverUnion(argVal, (v) => Ops.typeof_(v));
1864
- }
1351
+ // 一元语言表面经代数(typeof / ! / -)
1352
+ const unary = tryAbsUnary(node.operator, argVal);
1353
+ if (unary !== undefined) return unary;
1865
1354
  if (node.operator === "!") {
1355
+ // 代数未接住:对象恒真等 host 知识
1866
1356
  return distributeOverUnion(argVal, (v) => {
1867
- // Object-ish receivers are statically truthy: `!ref` must resolve
1868
- // to literal false or enclosing `if (!ref || …)` guards explore
1869
- // both branches and the ref-reassigning branch poisons the loop.
1870
1357
  const decided = definiteBoolean(v);
1871
1358
  if (decided !== null) return T.literal(!decided);
1872
1359
  return Ops.not(v);
1873
1360
  });
1874
1361
  }
1875
- if (node.operator === "-") {
1876
- return distributeOverUnion(argVal, (v) => Ops.neg(v));
1877
- }
1878
1362
  return T.unknown;
1879
1363
  }
1880
1364
 
@@ -1964,7 +1448,7 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
1964
1448
  const aResult = evaluate(node.alternate, falseEnv);
1965
1449
  const cVal = isReturn(cResult) ? cResult.value : isBranch(cResult) ? cResult.returnedValue : isThrow(cResult) ? T.never : cResult;
1966
1450
  const aVal = isReturn(aResult) ? aResult.value : isBranch(aResult) ? aResult.returnedValue : isThrow(aResult) ? T.never : aResult;
1967
- return simplifyUnion([cVal, aVal]);
1451
+ return joinOrUnion(cVal, aVal);
1968
1452
  }
1969
1453
 
1970
1454
  case "IfStatement": {
@@ -1973,6 +1457,9 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
1973
1457
  const testVal = evaluate(test, env);
1974
1458
  if (isReturn(testVal) || isBranch(testVal) || isThrow(testVal)) return testVal;
1975
1459
 
1460
+ // Kernel Φ:从测试表达式提取约束,真/假分支分别合取
1461
+ const phis = phiFromTest(test);
1462
+
1976
1463
  if (testVal.kind === "literal") {
1977
1464
  if (testVal.value) {
1978
1465
  if (node.alternate?.loc) {
@@ -1981,7 +1468,12 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
1981
1468
  end: { line: node.alternate.loc.end.line, column: node.alternate.loc.end.column },
1982
1469
  });
1983
1470
  }
1984
- return evaluate(node.consequent, trueEnv);
1471
+ pushPhi(combinePhi(currentPhi(), phis.whenTrue));
1472
+ try {
1473
+ return evaluate(node.consequent, trueEnv);
1474
+ } finally {
1475
+ popPhi();
1476
+ }
1985
1477
  }
1986
1478
  if (node.consequent.loc) {
1987
1479
  _unreachableRanges.push({
@@ -1989,26 +1481,52 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
1989
1481
  end: { line: node.consequent.loc.end.line, column: node.consequent.loc.end.column },
1990
1482
  });
1991
1483
  }
1992
- return node.alternate
1993
- ? evaluate(node.alternate, falseEnv)
1994
- : T.undefined;
1484
+ if (!node.alternate) return T.undefined;
1485
+ pushPhi(combinePhi(currentPhi(), phis.whenFalse));
1486
+ try {
1487
+ return evaluate(node.alternate, falseEnv);
1488
+ } finally {
1489
+ popPhi();
1490
+ }
1995
1491
  }
1996
1492
  // Object-ish / otherwise-decidable tests pick their branch statically
1997
1493
  // (e.g. `if (proto && proto.isImmutable)` once && resolves).
1998
1494
  const decided = definiteBoolean(testVal);
1999
1495
  if (decided !== null) {
2000
1496
  if (decided) {
2001
- return evaluate(node.consequent, trueEnv);
1497
+ pushPhi(combinePhi(currentPhi(), phis.whenTrue));
1498
+ try {
1499
+ return evaluate(node.consequent, trueEnv);
1500
+ } finally {
1501
+ popPhi();
1502
+ }
1503
+ }
1504
+ if (!node.alternate) return T.undefined;
1505
+ pushPhi(combinePhi(currentPhi(), phis.whenFalse));
1506
+ try {
1507
+ return evaluate(node.alternate, falseEnv);
1508
+ } finally {
1509
+ popPhi();
2002
1510
  }
2003
- return node.alternate
2004
- ? evaluate(node.alternate, falseEnv)
2005
- : T.undefined;
2006
1511
  }
2007
1512
 
2008
- const consequentResult = evaluate(node.consequent, trueEnv);
2009
- const alternateResult = node.alternate
2010
- ? evaluate(node.alternate, falseEnv)
2011
- : null;
1513
+ // 两边都探索:分别用各自 Φ
1514
+ pushPhi(combinePhi(currentPhi(), phis.whenTrue));
1515
+ let consequentResult: EvalResult;
1516
+ try {
1517
+ consequentResult = evaluate(node.consequent, trueEnv);
1518
+ } finally {
1519
+ popPhi();
1520
+ }
1521
+ pushPhi(combinePhi(currentPhi(), phis.whenFalse));
1522
+ let alternateResult: EvalResult | null = null;
1523
+ try {
1524
+ alternateResult = node.alternate
1525
+ ? evaluate(node.alternate, falseEnv)
1526
+ : null;
1527
+ } finally {
1528
+ popPhi();
1529
+ }
2012
1530
 
2013
1531
  const cReturns = isReturn(consequentResult);
2014
1532
  const cBranches = isBranch(consequentResult);
@@ -2033,21 +1551,21 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
2033
1551
  }
2034
1552
 
2035
1553
  if (aThrows) {
2036
- const cVal = cReturns ? consequentResult.value
2037
- : cBranches ? consequentResult.returnedValue
1554
+ const cVal = cReturns ? (consequentResult as ReturnSignal).value
1555
+ : cBranches ? (consequentResult as BranchSignal).returnedValue
2038
1556
  : consequentResult as TypeValue;
2039
1557
  return makeBranch(cVal, trueEnv);
2040
1558
  }
2041
1559
 
2042
- const cVal = cReturns ? consequentResult.value
2043
- : cBranches ? consequentResult.returnedValue
1560
+ const cVal = cReturns ? (consequentResult as ReturnSignal).value
1561
+ : cBranches ? (consequentResult as BranchSignal).returnedValue
2044
1562
  : consequentResult as TypeValue;
2045
1563
  const aVal = aReturns ? (alternateResult as ReturnSignal).value
2046
1564
  : aBranches ? (alternateResult as BranchSignal).returnedValue
2047
1565
  : alternateResult as TypeValue | null;
2048
1566
 
2049
1567
  if (cReturns && aReturns) {
2050
- return makeReturn(simplifyUnion([cVal, aVal!]));
1568
+ return makeReturn(joinOrUnion(cVal, aVal!));
2051
1569
  }
2052
1570
 
2053
1571
  if (cReturns && !node.alternate) {
@@ -2056,7 +1574,7 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
2056
1574
 
2057
1575
  if (cReturns && node.alternate) {
2058
1576
  if (aReturns) {
2059
- return makeReturn(simplifyUnion([cVal, aVal!]));
1577
+ return makeReturn(joinOrUnion(cVal, aVal!));
2060
1578
  }
2061
1579
  return makeBranch(cVal, falseEnv);
2062
1580
  }
@@ -2115,7 +1633,7 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
2115
1633
  if (leftVal && leftVal.kind !== "unknown") {
2116
1634
  // Extract the binary operator (e.g., "+=" -> "+")
2117
1635
  const binaryOp = node.operator.slice(0, -1);
2118
- val = dispatchBinaryOp(binaryOp, leftVal, rightVal);
1636
+ val = evalBinaryValue(binaryOp, leftVal, rightVal) ?? T.unknown;
2119
1637
  }
2120
1638
  }
2121
1639
 
@@ -2761,6 +2279,15 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
2761
2279
  } else if (prop.type === "SpreadElement") {
2762
2280
  const spreadVal = evaluate(prop.argument, env);
2763
2281
  if (isReturn(spreadVal) || isBranch(spreadVal) || isThrow(spreadVal)) return spreadVal;
2282
+ // M4:代数 spread(保留字面量/term);失败回退 Object.assign
2283
+ const absSpread = tryAbsObjectSpread(T.object(props), spreadVal);
2284
+ if (absSpread !== undefined && absSpread.kind === "object") {
2285
+ // 合并到 props 继续处理后续属性
2286
+ for (const [k, v] of Object.entries(absSpread.properties)) {
2287
+ props[k] = v;
2288
+ }
2289
+ continue;
2290
+ }
2764
2291
  if (spreadVal.kind === "object") {
2765
2292
  Object.assign(props, spreadVal.properties);
2766
2293
  } else if (spreadVal.kind === "union") {
@@ -3996,23 +3523,36 @@ function evaluateArrayMethodValues(
3996
3523
 
3997
3524
  const fn = callbackFn as TypeValue & { kind: "function" };
3998
3525
 
3526
+ // HOF:给回调元素挂 term 身份,使 Φ 中的约束可传播到 map/reduce 体
3527
+ const tagEl = (el: TypeValue, name: string): TypeValue => {
3528
+ const tagged = tagParamArg(el, name);
3529
+ copyOrigin(el, tagged);
3530
+ return tagged;
3531
+ };
3532
+
3999
3533
  if (method === "map") {
4000
3534
  if (arr.kind === "tuple") {
4001
3535
  const mapped = arr.elements.map((el, i) =>
4002
- callFunction(fn, [el, T.literal(i), arr]),
3536
+ callFunction(fn, [tagEl(el, fn.params[0] ?? "item"), T.literal(i), arr]),
4003
3537
  );
4004
3538
  return T.tuple(mapped);
4005
3539
  }
4006
- return T.array(callFunction(fn, [arr.element, T.number, arr]));
3540
+ return T.array(
3541
+ callFunction(fn, [tagEl(arr.element, fn.params[0] ?? "item"), T.number, arr]),
3542
+ );
4007
3543
  }
4008
3544
 
4009
3545
  if (method === "filter") {
4010
3546
  if (arr.kind === "tuple") {
4011
3547
  const kept: TypeValue[] = [];
4012
3548
  for (let i = 0; i < arr.elements.length; i++) {
4013
- const result = callFunction(fn, [arr.elements[i], T.literal(i), arr]);
3549
+ const result = callFunction(fn, [
3550
+ tagEl(arr.elements[i]!, fn.params[0] ?? "item"),
3551
+ T.literal(i),
3552
+ arr,
3553
+ ]);
4014
3554
  if (result.kind === "literal" && !result.value) continue;
4015
- kept.push(arr.elements[i]);
3555
+ kept.push(arr.elements[i]!);
4016
3556
  }
4017
3557
  if (kept.length === 0) return T.tuple([]);
4018
3558
  return T.array(simplifyUnion(kept));
@@ -4026,17 +3566,30 @@ function evaluateArrayMethodValues(
4026
3566
  let acc = init ?? arr.elements[0] ?? T.unknown;
4027
3567
  const startIdx = init ? 0 : 1;
4028
3568
  for (let i = startIdx; i < arr.elements.length; i++) {
4029
- acc = callFunction(fn, [acc, arr.elements[i], T.literal(i), arr]);
3569
+ acc = callFunction(fn, [
3570
+ acc,
3571
+ tagEl(arr.elements[i]!, fn.params[1] ?? "item"),
3572
+ T.literal(i),
3573
+ arr,
3574
+ ]);
4030
3575
  }
4031
3576
  return acc;
4032
3577
  }
4033
- // For arrays, we can't iterate all elements, but we can call the function
4034
- // with the accumulator and element type to infer the result type
4035
- const acc = init ?? arr.element;
4036
- const result = callFunction(fn, [acc, arr.element, T.number, arr]);
4037
- // If the result is the same type as the accumulator, it's likely correct
4038
- // (e.g., number + number = number)
4039
- return result;
3578
+ // 抽象数组:不动点迭代(代数纪律)。unknown 不 join 进 acc。
3579
+ const acc0 = init ?? arr.element;
3580
+ let acc = acc0;
3581
+ for (let i = 0; i < 6; i++) {
3582
+ const next = callFunction(fn, [
3583
+ acc,
3584
+ tagEl(arr.element, fn.params[1] ?? "item"),
3585
+ T.number,
3586
+ arr,
3587
+ ]);
3588
+ if (typeValueEquals(acc, next)) return next;
3589
+ if (next.kind === "unknown") return acc;
3590
+ acc = simplifyUnion([acc, next]);
3591
+ }
3592
+ return acc;
4040
3593
  }
4041
3594
 
4042
3595
  if (method === "find") {
@@ -4543,31 +4096,6 @@ function commonJsExportsValue(moduleEnv: Environment): TypeValue {
4543
4096
  return T.unknown;
4544
4097
  }
4545
4098
 
4546
- /** JSON 值 → TypeValue(字面量级精确:package.json 版本号等成为 string 字面量) */
4547
- function jsonToTypeValue(v: unknown): TypeValue {
4548
- if (v === null) return T.literal(null);
4549
- if (typeof v === "string") return T.literal(v);
4550
- if (typeof v === "number") return T.literal(v);
4551
- if (typeof v === "boolean") return T.literal(v);
4552
- if (Array.isArray(v)) return T.tuple(v.map(jsonToTypeValue));
4553
- if (typeof v === "object") {
4554
- const props: Record<string, TypeValue> = {};
4555
- for (const [k, val] of Object.entries(v)) {
4556
- // Define-as-own: plain assignment would let a "__proto__" key trip
4557
- // the setter on the props record (dropping it and corrupting the
4558
- // record's prototype) — JSON.parse creates an own property, and
4559
- // hoek's prototype-poisoning fixtures exercise exactly that key.
4560
- Object.defineProperty(props, k, {
4561
- value: jsonToTypeValue(val),
4562
- enumerable: true,
4563
- writable: true,
4564
- configurable: true,
4565
- });
4566
- }
4567
- return T.object(props);
4568
- }
4569
- return T.unknown;
4570
- }
4571
4099
 
4572
4100
  function evaluateRequireCall(node: Node & { type: "CallExpression" }, _env: Environment): EvalResult { const arg = (node.arguments as Node[])[0];
4573
4101
  if (!arg || arg.type !== "StringLiteral") {
@@ -4980,7 +4508,12 @@ function bindFunctionParams(
4980
4508
  callEnv.bind(paramName.slice(3), restValue); // Remove "..." prefix
4981
4509
  }
4982
4510
  } else {
4983
- const argVal = args[i] ?? T.undefined;
4511
+ let argVal = args[i] ?? T.undefined;
4512
+ {
4513
+ const tagged = tagParamArg(argVal, paramName);
4514
+ copyOrigin(argVal, tagged);
4515
+ argVal = tagged;
4516
+ }
4984
4517
  if (paramPatterns && paramPatterns[i]) {
4985
4518
  bindPattern(paramPatterns[i], argVal, callEnv);
4986
4519
  } else {
@@ -5582,7 +5115,14 @@ export function evaluateFunctionFull(
5582
5115
  callEnv.bind(fnName, selfRef);
5583
5116
  }
5584
5117
  for (let i = 0; i < actualNode.params.length; i++) {
5585
- bindPattern(actualNode.params[i], args[i] ?? T.undefined, callEnv);
5118
+ const pname = describeParam(actualNode.params[i]!);
5119
+ let argVal = args[i] ?? T.undefined;
5120
+ {
5121
+ const tagged = tagParamArg(argVal, pname);
5122
+ copyOrigin(argVal, tagged);
5123
+ argVal = tagged;
5124
+ }
5125
+ bindPattern(actualNode.params[i]!, argVal, callEnv);
5586
5126
  }
5587
5127
  const result = evaluate(actualNode.body, callEnv);
5588
5128
  if (isThrow(result)) return { value: T.never, throws: result.thrown, throwLoc: result.loc };
@@ -5603,6 +5143,7 @@ export function evaluateFunctionFull(
5603
5143
 
5604
5144
  export function evaluateProgram(node: Node, env: Environment): TypeValue {
5605
5145
  bindCommonJsGlobals(env);
5146
+ resetPhi();
5606
5147
  const result = evaluate(node, env);
5607
5148
  if (isReturn(result)) return result.value;
5608
5149
  if (isBranch(result)) return result.returnedValue;