@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.
Files changed (40) hide show
  1. package/README.md +34 -0
  2. package/package.json +24 -4
  3. package/src/abs-route.ts +273 -0
  4. package/src/builtins/builtin-intl.ts +17 -0
  5. package/src/builtins/builtin-map.ts +174 -0
  6. package/src/builtins/builtin-promise.ts +116 -0
  7. package/src/builtins/builtin-prototype.ts +279 -0
  8. package/src/builtins/builtin-reflect.ts +30 -0
  9. package/src/builtins/builtin-regexp.ts +34 -0
  10. package/src/builtins/builtin-set.ts +131 -0
  11. package/src/builtins/builtin-static.ts +260 -0
  12. package/src/builtins/builtin-symbol.ts +16 -0
  13. package/src/builtins/builtin-url.ts +58 -0
  14. package/src/builtins/builtin-weak.ts +22 -0
  15. package/src/builtins/builtin-web.ts +111 -0
  16. package/src/config.ts +32 -0
  17. package/src/env-loader.ts +174 -0
  18. package/src/eval-binary.ts +113 -0
  19. package/src/evaluator-api.ts +23 -0
  20. package/src/evaluator.ts +3502 -285
  21. package/src/index.ts +922 -180
  22. package/src/narrowing.ts +237 -15
  23. package/src/phi-from-test.ts +42 -0
  24. package/src/resolve-npm.ts +63 -0
  25. package/src/run-test.ts +92 -0
  26. package/src/term-registry.ts +96 -0
  27. package/CHANGELOG.md +0 -29
  28. package/dist/chunk-LGWVGEMB.js +0 -1817
  29. package/dist/evaluator-api.d.ts +0 -59
  30. package/dist/evaluator-api.js +0 -30
  31. package/dist/index.d.ts +0 -2
  32. package/dist/index.js +0 -3570
  33. package/src/__tests__/evaluator-loops.test.ts +0 -163
  34. package/src/__tests__/evaluator-phase2.test.ts +0 -564
  35. package/src/__tests__/evaluator-phase3.test.ts +0 -513
  36. package/src/__tests__/evaluator-refined.test.ts +0 -121
  37. package/src/__tests__/evaluator-string-methods.test.ts +0 -151
  38. package/src/__tests__/evaluator.test.ts +0 -243
  39. package/src/__tests__/narrowing.test.ts +0 -82
  40. 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,
@@ -16,12 +15,442 @@ import {
16
15
  typeValueToString,
17
16
  isSubtypeOf,
18
17
  widenLiteral,
18
+ collapseLiteralUnion,
19
19
  createTemplate,
20
+ subtractType,
21
+ getFnSig,
20
22
  } from "@nudojs/core";
23
+ import { extractInlineDirectives, type InlineDirective } from "@nudojs/parser";
21
24
  import { narrow } from "./narrowing.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";
41
+ import { MAP_INSTANCE_METHODS, createMapType, mapEntriesIterable, exactMapEntries } from "./builtins/builtin-map.ts";
42
+ import { SET_INSTANCE_METHODS, createSetType, setValuesIterable, exactSetValues } from "./builtins/builtin-set.ts";
43
+ import { REGEXP_INSTANCE_METHODS, createRegExpType } from "./builtins/builtin-regexp.ts";
44
+ import { URL_INSTANCE_METHODS, URLSearchParams_INSTANCE_METHODS, createURLType, createURLSearchParamsType } from "./builtins/builtin-url.ts";
45
+ import {
46
+ RESPONSE_INSTANCE_METHODS,
47
+ HEADERS_INSTANCE_METHODS,
48
+ FORMDATA_INSTANCE_METHODS,
49
+ ABORTCONTROLLER_INSTANCE_METHODS,
50
+ createResponseType,
51
+ createHeadersType,
52
+ createFormDataType,
53
+ createAbortControllerType,
54
+ } from "./builtins/builtin-web.ts";
55
+ import { WEAKMAP_INSTANCE_METHODS, WEAKSET_INSTANCE_METHODS, createWeakMapType, createWeakSetType } from "./builtins/builtin-weak.ts";
56
+ import { SYMBOL_STATIC_PROPS } from "./builtins/builtin-symbol.ts";
57
+ import { REFLECT_METHODS } from "./builtins/builtin-reflect.ts";
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
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+ // Wrapper-prototype fallback for primitive receivers: real JS auto-boxes
78
+ // member access on primitives ('x'.constructor === String), so a named
79
+ // member hit on a string/number/boolean/symbol consults the wrapper's
80
+ // prototype table (the same approximations `X.prototype` uses) plus the
81
+ // wrapper's constructor before anything reports a missing property.
82
+ // hasOwnProp-guarded like every other table lookup here.
83
+ /** 值上命名成员是否可能存在(union 部分成员持有的 no-method 降 warning
84
+ * 用):primitive/literal 走 wrapper 原型近似表('x'.charCodeAt),
85
+ * instance 走近似方法表。诊断侧据此区分"部分成员可能有"与"确定没有"。 */
86
+ export function memberMayExistOn(tv: TypeValue, propName: string): boolean {
87
+ if (tv.kind === "primitive" || tv.kind === "literal") {
88
+ return wrapperPrototypeMember(tv, propName) !== undefined;
89
+ }
90
+ if (tv.kind === "instance") {
91
+ const className = tv.className;
92
+ const table = hasOwnProp(BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS, className)
93
+ ? BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS[className]
94
+ : BUILTIN_ERROR_CLASSES.has(className)
95
+ ? BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS.Error
96
+ : undefined;
97
+ if (table && hasOwnProp(table, propName)) return true;
98
+ // instance 的方法集是近似(未列出 ≠ 不存在)
99
+ return true;
100
+ }
101
+ return false;
102
+ }
103
+
104
+ function wrapperPrototypeMember(obj: TypeValue, propName: string): TypeValue | undefined { let className: string | undefined;
105
+ if (obj.kind === "primitive") {
106
+ className = obj.type === "bigint" ? undefined : obj.type[0].toUpperCase() + obj.type.slice(1);
107
+ } else if (obj.kind === "literal") {
108
+ if (typeof obj.value === "string") className = "String";
109
+ else if (typeof obj.value === "number") className = "Number";
110
+ else if (typeof obj.value === "boolean") className = "Boolean";
111
+ }
112
+ if (!className) return undefined;
113
+ if (propName === "constructor") return builtinClassValue(className);
114
+ const table = BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS[className];
115
+ if (table && hasOwnProp(table, propName)) return table[propName];
116
+ return undefined;
117
+ }
118
+
119
+
120
+
121
+
122
+
123
+
124
+ function arrayIsArrayLiteral(v: TypeValue): boolean | undefined {
125
+ let base: TypeValue = v;
126
+ while (base.kind === "refined") base = base.base;
127
+ switch (base.kind) {
128
+ case "array":
129
+ case "tuple": return true;
130
+ case "instance": return base.className === "Array";
131
+ case "object":
132
+ case "function":
133
+ case "promise":
134
+ case "literal":
135
+ case "primitive": return false;
136
+ default: return undefined;
137
+ }
138
+ }
139
+
140
+ // JavaScript truthiness decided statically. Object-ish kinds (object/array/
141
+ // tuple/function/instance/promise) are always truthy; literals decide on
142
+ // their value; never is unreachable (falsy); symbolic values are null.
143
+ function definiteBoolean(tv: TypeValue): boolean | null {
144
+ if (tv.kind === "union") {
145
+ let allTrue = true;
146
+ let allFalse = true;
147
+ for (const m of tv.members) {
148
+ const b = definiteBoolean(m);
149
+ if (b === null) return null;
150
+ allTrue = allTrue && b === true;
151
+ allFalse = allFalse && b === false;
152
+ }
153
+ if (allTrue) return true;
154
+ if (allFalse) return false;
155
+ return null;
156
+ }
157
+ switch (tv.kind) {
158
+ case "literal": return !!tv.value;
159
+ case "object":
160
+ case "array":
161
+ case "tuple":
162
+ case "function":
163
+ case "instance":
164
+ case "promise": return true;
165
+ case "never": return false;
166
+ default: return null;
167
+ }
168
+ }
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
+
182
+ // Receiver-binding for thisVal-dependent Object.prototype methods
183
+ // (toString/valueOf): `const v = obj.valueOf; v()` loses the member-access
184
+ // receiver, so accesses off a concrete receiver return a copy whose impl
185
+ // closes over it. An explicit `.call(x)`/`.apply(x)` thisVal still wins.
186
+ function bindObjectProtoMethod(fn: TypeValue, receiver: TypeValue): TypeValue {
187
+ const sig = getFnSig(fn);
188
+ if (!sig?.impl) return fn;
189
+ return T.fnSig(sig.paramTypes, sig.returnType, sig.throwsType, (args, thisVal) =>
190
+ sig.impl!(args, thisVal ?? receiver));
191
+ }
192
+
193
+ /** `.valueOf` on receivers that don't go through the object/instance
194
+ * property branches: non-nullish literals and primitives return their
195
+ * receiver (JS box-unbox semantics), structured values (array/tuple/
196
+ * function) return themselves, and null/undefined literals produce a call
197
+ * that throws TypeError — exactly like real JS, so an enclosing try/catch
198
+ * binds the error instead of the access poisoning the result with unknown. */
199
+ function valueOfBinding(receiver: TypeValue): TypeValue | undefined {
200
+ switch (receiver.kind) {
201
+ case "literal": {
202
+ const v = receiver.value;
203
+ if (v === null || v === undefined) {
204
+ return T.fnSig([], T.never, T.instanceOf("TypeError"));
205
+ }
206
+ return bindObjectProtoMethod(OBJECT_PROTOTYPE_METHODS["valueOf"], receiver);
207
+ }
208
+ case "primitive":
209
+ case "array":
210
+ case "tuple":
211
+ case "function":
212
+ return bindObjectProtoMethod(OBJECT_PROTOTYPE_METHODS["valueOf"], receiver);
213
+ default:
214
+ return undefined;
215
+ }
216
+ }
217
+
218
+ // Strict equality over prototype singletons (and instances compared against
219
+ // them): definite true/false only when every cross-pair agrees. null means
220
+ // "not decidable here" — the generic boolean fallback applies.
221
+ function builtinProtoIdentityEq(l: TypeValue, r: TypeValue): boolean | null {
222
+ const ls = l.kind === "union" ? l.members : [l];
223
+ const rs = r.kind === "union" ? r.members : [r];
224
+ let anyProto = false;
225
+ for (const m of ls.concat(rs)) {
226
+ if (m.kind !== "instance") return null;
227
+ if ((m as any)._builtinProto) anyProto = true;
228
+ }
229
+ if (!anyProto) return null;
230
+ let allEq = true;
231
+ let anyEq = false;
232
+ for (const lm of ls) {
233
+ for (const rm of rs) {
234
+ const eq = typeValueEquals(lm, rm);
235
+ allEq = allEq && eq;
236
+ anyEq = anyEq || eq;
237
+ }
238
+ }
239
+ if (allEq) return true;
240
+ if (!anyEq) return false;
241
+ return null;
242
+ }
243
+
244
+ // Strict equality over builtin class namespaces (`x.constructor === Array`
245
+ // where both sides are memoized builtinClassValue objects): definite
246
+ // true/false by class name. null means "not decidable here" — the generic
247
+ // boolean fallback applies.
248
+ function builtinClassIdentityEq(l: TypeValue, r: TypeValue): boolean | null {
249
+ const ls = l.kind === "union" ? l.members : [l];
250
+ const rs = r.kind === "union" ? r.members : [r];
251
+ const nameOf = (v: TypeValue): string | null =>
252
+ v.kind === "object" && typeof (v as any)._builtinName === "string" ? (v as any)._builtinName : null;
253
+ const lNames = ls.map(nameOf);
254
+ const rNames = rs.map(nameOf);
255
+ if (!lNames.some(Boolean) && !rNames.some(Boolean)) return null;
256
+ if (lNames.some((n) => n === null) || rNames.some((n) => n === null)) return null;
257
+ let allEq = true;
258
+ let anyEq = false;
259
+ for (const ln of lNames) {
260
+ for (const rn of rNames) {
261
+ const eq = ln === rn;
262
+ allEq = allEq && eq;
263
+ anyEq = anyEq || eq;
264
+ }
265
+ }
266
+ if (allEq) return true;
267
+ if (!anyEq) return false;
268
+ return null;
269
+ }
270
+
271
+ // Well-known symbols (Symbol.iterator, Symbol.asyncIterator, ...) as
272
+ // marked symbol primitives: plain T.symbol cannot name which symbol it
273
+ // is, but `Symbol.iterator in x` keys on exactly that identity.
274
+ const _wellKnownSymbols = new Map<string, TypeValue>();
275
+ function wellKnownSymbol(name: string): TypeValue {
276
+ let v = _wellKnownSymbols.get(name);
277
+ if (v === undefined) {
278
+ v = { ...(T.symbol as unknown as object), _symbolName: name } as unknown as TypeValue;
279
+ _wellKnownSymbols.set(name, v);
280
+ }
281
+ return v;
282
+ }
283
+
284
+ function wellKnownSymbolName(v: TypeValue): string | undefined {
285
+ let base = v;
286
+ while (base.kind === "refined") base = base.base;
287
+ if (base.kind === "primitive" && base.type === "symbol") {
288
+ return (base as any)._symbolName as string | undefined;
289
+ }
290
+ return undefined;
291
+ }
292
+
293
+ // `Symbol.iterator in receiver` iterability by receiver kind: true/false
294
+ // for statically-known receivers, undefined = uncertain (unknown class
295
+ // instances could define the protocol themselves).
296
+ function iteratorInReceiver(v: TypeValue): boolean | undefined {
297
+ switch (v.kind) {
298
+ case "array":
299
+ case "tuple":
300
+ return true;
301
+ case "instance":
302
+ if (v.className === "Set" || v.className === "Map" || v.className === "Buffer") return true;
303
+ if (BUILTIN_PROTOTYPE_CLASSES.has(v.className)) return false;
304
+ return undefined;
305
+ case "literal":
306
+ return typeof v.value === "string" ? true : false;
307
+ case "primitive":
308
+ return v.type === "string" ? true : false;
309
+ case "object":
310
+ case "promise":
311
+ case "function":
312
+ return false;
313
+ default:
314
+ return undefined;
315
+ }
316
+ }
317
+
318
+ // `key in obj` literal decisions for non-union operands:
319
+ // - Well-known symbols decide iterability (Symbol.iterator on
320
+ // arrays/sets/maps/strings → true, plain values → false, unknown-class
321
+ // instances → boolean).
322
+ // - String keys decide against the declared shape (own properties plus
323
+ // inherited Object.prototype/builtin-prototype members); a definite
324
+ // miss is false.
325
+ // - Symbolic keys or uncertain receivers degrade to boolean; unknown
326
+ // receivers stay unknown.
327
+ function evaluateInMember(key: TypeValue, container: TypeValue): TypeValue {
328
+ let k = key;
329
+ while (k.kind === "refined") k = k.base;
330
+ let c = container;
331
+ while (c.kind === "refined") c = c.base;
332
+
333
+ if (c.kind === "unknown") return T.unknown;
334
+
335
+ const symName = wellKnownSymbolName(k);
336
+ if (symName !== undefined) {
337
+ if (symName === "iterator") {
338
+ const iter = iteratorInReceiver(c);
339
+ return iter === undefined ? T.boolean : T.literal(iter);
340
+ }
341
+ // Other well-known symbols (asyncIterator, toStringTag, ...): none of
342
+ // the statically-known receivers carry them; unknown-class instances
343
+ // stay symbolic.
344
+ if (c.kind === "instance" && !BUILTIN_PROTOTYPE_CLASSES.has(c.className)) return T.boolean;
345
+ return T.literal(false);
346
+ }
347
+
348
+ const strKey = k.kind === "literal" && typeof k.value === "string" ? k.value : undefined;
349
+ if (strKey === undefined) {
350
+ // Symbolic key (string, or a symbol without a name): object-ish
351
+ // receivers still promise a definite boolean; primitives/nullish
352
+ // cannot hold any property.
353
+ switch (c.kind) {
354
+ case "object":
355
+ case "instance":
356
+ case "promise":
357
+ case "function":
358
+ case "array":
359
+ case "tuple":
360
+ return T.boolean;
361
+ case "literal":
362
+ case "primitive":
363
+ return T.literal(false);
364
+ default:
365
+ return T.unknown;
366
+ }
367
+ }
368
+
369
+ const isArrayIndex = (s: string) => /^\d+$/.test(s);
370
+ switch (c.kind) {
371
+ case "object":
372
+ return T.literal(hasOwnProp(c.properties, strKey) || hasOwnProp(OBJECT_PROTOTYPE_METHODS, strKey));
373
+ case "tuple":
374
+ if (strKey === "length") return T.literal(true);
375
+ if (isArrayIndex(strKey)) return T.literal(Number(strKey) < c.elements.length);
376
+ return T.literal(
377
+ hasOwnProp(OBJECT_PROTOTYPE_METHODS, strKey) ||
378
+ hasOwnProp(BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS.Array, strKey),
379
+ );
380
+ case "array":
381
+ return T.literal(
382
+ strKey === "length" ||
383
+ isArrayIndex(strKey) ||
384
+ hasOwnProp(OBJECT_PROTOTYPE_METHODS, strKey) ||
385
+ hasOwnProp(BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS.Array, strKey),
386
+ );
387
+ case "instance": {
388
+ if (hasOwnProp(c.properties, strKey)) return T.literal(true);
389
+ if (hasOwnProp(OBJECT_PROTOTYPE_METHODS, strKey)) return T.literal(true);
390
+ // hasOwnProp guard: plain [] access on the class key would leak
391
+ // native Object.prototype members.
392
+ const approx = hasOwnProp(BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS, c.className)
393
+ ? BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS[c.className]
394
+ : undefined;
395
+ if (approx && hasOwnProp(approx, strKey)) return T.literal(true);
396
+ if ((c.className === "Set" || c.className === "Map") && strKey === "size") return T.literal(true);
397
+ return T.literal(false);
398
+ }
399
+ case "function":
400
+ return T.literal(
401
+ strKey === "prototype" || strKey === "name" || strKey === "length" ||
402
+ strKey === "call" || strKey === "apply" || strKey === "bind" || strKey === "toString",
403
+ );
404
+ case "promise":
405
+ return T.literal(
406
+ strKey === "then" || strKey === "catch" || strKey === "finally" ||
407
+ hasOwnProp(OBJECT_PROTOTYPE_METHODS, strKey),
408
+ );
409
+ case "literal":
410
+ if (typeof c.value === "string") {
411
+ return T.literal(strKey === "length" || (isArrayIndex(strKey) && Number(strKey) < c.value.length));
412
+ }
413
+ return T.literal(false);
414
+ case "primitive":
415
+ if (c.type === "string") return T.literal(strKey === "length" || isArrayIndex(strKey));
416
+ return T.literal(false);
417
+ default:
418
+ return T.boolean;
419
+ }
420
+ }
22
421
 
23
422
  type SourceRange = { start: { line: number; column: number }; end: { line: number; column: number } };
24
423
 
424
+ let _currentSource = "";
425
+
426
+ export function setCurrentSource(source: string): void {
427
+ _currentSource = source;
428
+ }
429
+
430
+ type ActiveReplace = { targetSource: string; typeExpr: TypeValue };
431
+ let _activeReplacements: ActiveReplace[] = [];
432
+ let _activeAsOverride: TypeValue | null = null;
433
+
434
+ function nodeSourceText(node: Node): string | null {
435
+ if (node.start == null || node.end == null || !_currentSource) return null;
436
+ return _currentSource.slice(node.start, node.end);
437
+ }
438
+
439
+ function normalizeWhitespace(s: string): string {
440
+ return s.replace(/\s+/g, "");
441
+ }
442
+
443
+ function matchesReplacement(node: Node): TypeValue | null {
444
+ if (_activeReplacements.length === 0) return null;
445
+ const src = nodeSourceText(node);
446
+ if (!src) return null;
447
+ const normalized = normalizeWhitespace(src);
448
+ for (const r of _activeReplacements) {
449
+ if (normalizeWhitespace(r.targetSource) === normalized) return r.typeExpr;
450
+ }
451
+ return null;
452
+ }
453
+
25
454
  const RETURN_SIGNAL = Symbol("ReturnSignal");
26
455
  const BRANCH_SIGNAL = Symbol("BranchSignal");
27
456
  const THROW_SIGNAL = Symbol("ThrowSignal");
@@ -67,36 +496,107 @@ function isThrow(v: unknown): v is ThrowSignal {
67
496
  return typeof v === "object" && v !== null && THROW_SIGNAL in v;
68
497
  }
69
498
 
499
+ // --- break / continue ---
500
+ // 循环控制语句此前完全未求值(evaluate 无 case,信号丢失后守卫内
501
+ // `break` 之后的语句照常执行 —— reach.js 的 `if (!ref) { …; break }`
502
+ // 因此在 undefined 上求 `ref[key]`,产生假 no-method error)。实现上
503
+ // 借道 Throw 信号管线:break/continue 对象带 THROW_SYMBOL 标记,全部
504
+ // 既有 isThrow 检查点(语句列表、二元短路、调用边界)原样传播它们,
505
+ // 只有循环边界与 switch 吸收。真实 throw 与循环信号用 LOOP_SIGNAL_KIND
506
+ // 区分:前者继续走抛出语义,后者终止/跳过当轮迭代。
507
+ const LOOP_SIGNAL_KIND = Symbol("LoopSignalKind");
508
+
509
+ type LoopSignal = ThrowSignal & {
510
+ readonly [LOOP_SIGNAL_KIND]: "break" | "continue";
511
+ };
512
+
513
+ function makeBreak(): LoopSignal {
514
+ return { [THROW_SIGNAL]: true, [LOOP_SIGNAL_KIND]: "break", thrown: T.never };
515
+ }
516
+
517
+ function makeContinue(): LoopSignal {
518
+ return { [THROW_SIGNAL]: true, [LOOP_SIGNAL_KIND]: "continue", thrown: T.never };
519
+ }
520
+
521
+ function loopSignalKind(v: unknown): "break" | "continue" | null {
522
+ if (typeof v !== "object" || v === null) return null;
523
+ const kind = (v as Record<symbol, unknown>)[LOOP_SIGNAL_KIND];
524
+ return kind === "break" || kind === "continue" ? kind : null;
525
+ }
526
+
70
527
  type EvalResult = TypeValue | ReturnSignal | BranchSignal | ThrowSignal;
71
528
 
72
529
  const MEMO_IN_PROGRESS = Symbol("MemoInProgress");
73
530
  const callMemo = new Map<string, TypeValue | typeof MEMO_IN_PROGRESS>();
74
531
 
75
- function buildMemoKey(fn: TypeValue & { kind: "function" }, args: TypeValue[]): string | null {
532
+ function buildMemoKey(
533
+ fn: TypeValue & { kind: "function" },
534
+ args: TypeValue[],
535
+ thisVal?: TypeValue,
536
+ ): string | null {
76
537
  const fnName = (fn as any)._memoize as string | undefined;
77
538
  if (!fnName) return null;
78
539
  const argsKey = args.map(typeValueToString).join(",");
79
- return `${fnName}(${argsKey})`;
540
+ // `this` participates in the key: obj1.f() and obj2.f() must not share a
541
+ // memo entry when f reads `this`.
542
+ const thisKey = thisVal ? `this:${typeValueToString(thisVal)};` : "";
543
+ return `${fnName}(${thisKey}${argsKey})`;
80
544
  }
81
545
 
82
546
  const moduleCache = new Map<string, Environment>();
83
547
 
548
+ /** 模块加载链(import / require / @nudo:mock-module mock 文件)深度上限。
549
+ * 超限即截断为空环境并记录诊断——防御病态嵌套链,以及解析器返回未归一化
550
+ * 路径时(同一逻辑文件每次都是「新」路径、缓存永不命中)的真栈溢出。 */
551
+ const MAX_MODULE_LOAD_DEPTH = 16;
552
+
84
553
  export function resetMemo(): void {
85
554
  callMemo.clear();
86
555
  moduleCache.clear();
556
+ // 加载栈由 try/finally 保证配平,这里清空是双保险:任何中途逃逸的帧
557
+ // 都会让后续分析把正常文件误判为环。
558
+ _moduleEvalStack.length = 0;
559
+ _callDepth = 0;
560
+ _totalCalls = 0;
561
+ _activeCallKeys.length = 0;
87
562
  }
88
563
 
89
- export function setModuleResolver(resolver: ((source: string, fromDir: string) => { ast: Node; filePath: string } | null) | null): void {
564
+ export function setModuleResolver(resolver: ((source: string, fromDir: string) => { ast: Node; filePath: string; json?: unknown } | null) | null): void {
90
565
  currentModuleResolver = resolver;
91
566
  }
92
567
 
93
- let currentModuleResolver: ((source: string, fromDir: string) => { ast: Node; filePath: string } | null) | null = null;
568
+ let currentModuleResolver: ((source: string, fromDir: string) => { ast: Node; filePath: string; json?: unknown } | null) | null = null;
94
569
  let currentFileDir = "";
95
570
 
571
+ let envModules: Record<string, Record<string, TypeValue>> = {};
572
+ let mockModules: Map<string, { fromPath: string; names?: string[] }> = new Map();
573
+
574
+ export function setEnvModules(modules: Record<string, Record<string, TypeValue>>): void {
575
+ envModules = modules;
576
+ }
577
+
578
+ export function resetEnvModules(): void {
579
+ envModules = {};
580
+ }
581
+
582
+ export function setMockModules(mocks: Map<string, { fromPath: string; names?: string[] }>): void {
583
+ mockModules = mocks;
584
+ }
585
+
586
+ export function resetMockModules(): void {
587
+ mockModules = new Map();
588
+ }
589
+
96
590
  let _nodeTypeCollector: ((node: Node, tv: TypeValue) => void) | null = null;
97
591
  let _sampleCount = 3;
98
592
  let _maxConcreteIter = 1000;
99
593
 
594
+ let _onUnknownBuiltin: ((name: string, loc?: { start: { line: number; column: number }; end: { line: number; column: number } }) => void) | null = null;
595
+
596
+ export function setUnknownBuiltinHandler(handler: ((name: string, loc?: { start: { line: number; column: number }; end: { line: number; column: number } }) => void) | null) {
597
+ _onUnknownBuiltin = handler;
598
+ }
599
+
100
600
  export function setSampleCount(count: number): void {
101
601
  _sampleCount = count;
102
602
  }
@@ -115,6 +615,277 @@ function recordNodeType(node: Node, tv: TypeValue): void {
115
615
  }
116
616
  }
117
617
 
618
+ export type CallRecord = {
619
+ fnName: string;
620
+ argTypes: TypeValue[];
621
+ resultType: TypeValue;
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. */
626
+ callLoc?: { line: number; column: number };
627
+ targetModule?: string;
628
+ targetExport?: string;
629
+ /** export names the same function value was re-exported under after its
630
+ * defining module (barrel `index.js`, CJS forwarding shims); usage-site
631
+ * records stay name-matchable against them */
632
+ targetAliases?: string[];
633
+ /** module whose evaluation created the function value (definition site).
634
+ * Usage-site collection transitively evaluates the library under require(),
635
+ * so records of the library's INTERNAL calls (no export tag) can still be
636
+ * attributed to the defining file — while test-local functions and builtin
637
+ * statics stay unattributed and cannot pollute same-name candidates. */
638
+ fnModule?: string;
639
+ };
640
+
641
+ // --- Cross-module call attribution ---
642
+ // Module-exported function values get tagged with their (module, export)
643
+ // origin when the module environment is first built (moduleCache fill). The
644
+ // same object reference flows through import bindings to call sites, so
645
+ // recordCall can look the tag up from the evaluated callee value and mark the
646
+ // record as targeting another module. Non-function exports are not tagged.
647
+
648
+ const _exportTags = new WeakMap<object, { module: string; export: string; aliases?: string[] }>();
649
+
650
+ /** Tag a function value with its exporting (module, export). A re-export
651
+ * chain — a barrel `index.js` doing `parseChunked: require('./parse-chunked')`,
652
+ * a CJS shim doing `module.exports = require('../../src/index.js')` — flows
653
+ * ONE function value through several module scopes, and each scope's tagging
654
+ * pass used to overwrite the tag with its own (module, export), so usage-site
655
+ * records ended up pointing at the last intermediary instead of the defining
656
+ * file. Now the first (definition-site) tag sticks; later export names
657
+ * accumulate as aliases. */
658
+ // --- Function definition-site tags ---
659
+ // Which module's evaluation created a function value. Only AST function
660
+ // nodes route through tagFnModule — lazily built builtin tables never enter
661
+ // a module stack, so builtin statics stay unattributed.
662
+ const _fnModuleTags = new WeakMap<object, string>();
663
+ const _moduleEvalStack: string[] = [];
664
+
665
+ function tagFnModule(v: TypeValue): void {
666
+ const top = _moduleEvalStack[_moduleEvalStack.length - 1];
667
+ if (top !== undefined && v.kind === "function") {
668
+ if (!_fnModuleTags.has(v)) _fnModuleTags.set(v, top);
669
+ }
670
+ }
671
+
672
+ // 执行来源栈:当前正在执行的函数体来自哪个模块(fnModule tag;未 tag 的
673
+ // 函数继承栈顶——本地函数无 tag 时代码在本文件内执行)。recordUnknown 据此
674
+ // 给记录标 originModule,分析侧丢弃使用现场闭包体的泄漏错误。
675
+ const _execOriginStack: (string | undefined)[] = [];
676
+
677
+ /** usage-site 执行标记:外部调用记录的实参里携带的闭包(测试回调等)在
678
+ * case 合成重求值时被执行,其内部错误属于使用现场文件,与本文件分析无关。
679
+ * 分析侧在注入前对全部外部实参调用本函数;执行来源栈读到该标记时
680
+ * recordUnknown.originModule 记为标记值,分析侧据此丢弃。不按模块路径
681
+ * 比较——跨文件依赖(本文件 require 链上加载的模块)的执行记录是合法
682
+ * 的可达分析,不能误伤。 */
683
+ export const USAGE_SITE_MODULE = "__usage_site__";
684
+
685
+ export function setUsageSiteTag(tv: TypeValue, _seen: Set<object> = new Set()): void {
686
+ if (!tv || typeof tv !== "object" || _seen.has(tv)) return;
687
+ _seen.add(tv);
688
+ switch (tv.kind) {
689
+ case "function":
690
+ _fnModuleTags.set(tv, USAGE_SITE_MODULE);
691
+ return;
692
+ case "array":
693
+ return setUsageSiteTag(tv.element, _seen);
694
+ case "tuple":
695
+ for (const e of tv.elements) setUsageSiteTag(e, _seen);
696
+ return;
697
+ case "object":
698
+ for (const v of Object.values(tv.properties)) setUsageSiteTag(v, _seen);
699
+ return;
700
+ case "promise":
701
+ return setUsageSiteTag(tv.value, _seen);
702
+ case "union":
703
+ for (const m of tv.members) setUsageSiteTag(m, _seen);
704
+ return;
705
+ case "refined":
706
+ return setUsageSiteTag(tv.base, _seen);
707
+ default:
708
+ return;
709
+ }
710
+ }
711
+
712
+ function tagExport(v: TypeValue & { kind: "function" }, filePath: string, name: string): void { const existing = _exportTags.get(v);
713
+ if (existing) {
714
+ if (existing.module === filePath && existing.export === name) return;
715
+ if (!existing.aliases) existing.aliases = [];
716
+ if (!existing.aliases.includes(name)) existing.aliases.push(name);
717
+ return;
718
+ }
719
+ _exportTags.set(v, { module: filePath, export: name });
720
+ }
721
+
722
+ function tagModuleExports(env: Environment, filePath: string): void {
723
+ const bindings = env.getOwnBindings();
724
+ for (const [k, v] of Object.entries(bindings)) {
725
+ if (!k.startsWith("__export_")) continue;
726
+ if (v.kind !== "function") continue;
727
+ const name = k === "__export_default" ? "default" : k.slice("__export_".length);
728
+ tagExport(v, filePath, name);
729
+ }
730
+ }
731
+
732
+ // --- CommonJS module scope ---
733
+ // v1 does not distinguish module systems: every evaluated program (top-level
734
+ // or module load) gets the implicit CJS bindings, so `require` /
735
+ // `module.exports` / `exports.x` patterns analyze even inside ESM files.
736
+
737
+ const _cjsRequireValue = T.fn(["specifier"], { type: "BlockStatement", body: [] } as any, undefined as any);
738
+
739
+ function bindCommonJsGlobals(env: Environment): void {
740
+ if (env.has("module")) return; // already set up as a module scope
741
+ const exportsObj = T.object({});
742
+ // Marker so require() can tell "target never wrote CJS exports" apart from
743
+ // an explicit `module.exports = {...}` replacement.
744
+ (exportsObj as any).__cjsExportsRoot = true;
745
+ const moduleObj = T.object({ exports: exportsObj });
746
+ env.bind("exports", exportsObj);
747
+ env.bind("module", moduleObj);
748
+ env.bind("require", _cjsRequireValue);
749
+ env.bind("__dirname", T.string);
750
+ env.bind("__filename", T.string);
751
+ }
752
+
753
+ // CJS exports are plain values (`module.exports` itself, or its properties),
754
+ // not `__export_` bindings; tag the function values the same way so
755
+ // cross-module call attribution also works for require()d modules.
756
+ function tagCommonJsExports(env: Environment, filePath: string): void {
757
+ if (!env.has("module")) return;
758
+ const moduleVal = env.lookup("module");
759
+ if (moduleVal.kind !== "object") return;
760
+ const exp = moduleVal.properties["exports"];
761
+ if (!exp) return;
762
+ if (exp.kind === "function") {
763
+ tagExport(exp, filePath, "default");
764
+ return;
765
+ }
766
+ if (exp.kind === "object") {
767
+ for (const [k, v] of Object.entries(exp.properties)) {
768
+ if (v.kind === "function") tagExport(v, filePath, k);
769
+ }
770
+ }
771
+ }
772
+
773
+ let _callCollector: ((record: CallRecord) => void) | null = null;
774
+
775
+ export function setCallCollector(collector: ((record: CallRecord) => void) | null): void {
776
+ _callCollector = collector;
777
+ }
778
+
779
+ function recordCall(
780
+ fnName: string,
781
+ args: TypeValue[],
782
+ result: { value: TypeValue; throws: TypeValue },
783
+ loc: Node["loc"],
784
+ calleeVal?: TypeValue,
785
+ ): void {
786
+ if (!_callCollector) return;
787
+ // Synthetic Promise resolve/reject collectors are internal plumbing, not
788
+ // user-code call sites — never feed them into the callsite collector.
789
+ if (calleeVal && _promiseCollectors.has(calleeVal as unknown as object)) return;
790
+ const record: CallRecord = {
791
+ fnName,
792
+ argTypes: args,
793
+ resultType: result.value,
794
+ throws: result.throws,
795
+ callLoc: loc ? { line: loc.start.line, column: loc.start.column } : undefined,
796
+ };
797
+ const tag = calleeVal ? _exportTags.get(calleeVal) : undefined;
798
+ if (tag) {
799
+ record.targetModule = tag.module;
800
+ record.targetExport = tag.export;
801
+ if (tag.aliases && tag.aliases.length > 0) record.targetAliases = [...tag.aliases];
802
+ }
803
+ const fnModule = calleeVal ? _fnModuleTags.get(calleeVal) : undefined;
804
+ if (fnModule) record.fnModule = fnModule;
805
+ _callCollector(record);
806
+ }
807
+
808
+ export type UnknownRecord = {
809
+ kind: "method" | "property" | "global";
810
+ name: string;
811
+ receiverType?: TypeValue;
812
+ loc?: { line: number; column: number };
813
+ reason?: string;
814
+ origin?: { line: number; column: number };
815
+ /** 定义当前正在执行的函数的模块(执行来源栈顶)。case 合成重求值会执行
816
+ * 注入实参携带的使用现场闭包体,其内部错误记录属于使用现场文件——
817
+ * 分析侧据此丢弃(行数守卫会被同行号假阳性骗过)。 */
818
+ originModule?: string;
819
+ };
820
+
821
+ let _unknownCollector: ((record: UnknownRecord) => void) | null = null;
822
+
823
+ export function setUnknownCollector(collector: ((record: UnknownRecord) => void) | null): void {
824
+ _unknownCollector = collector;
825
+ }
826
+
827
+ // --- Provenance side table ---
828
+ // Maps an argument TypeValue (annotated at its call-site expression in
829
+ // evaluateArgs) back to the source location of the expression that produced it.
830
+ // The same object references flow into parameter bindings, so recordUnknown can
831
+ // reverse-lookup where a receiver value originated from. Disabled by default;
832
+ // zero overhead when off.
833
+
834
+ const _originMap = new WeakMap<TypeValue, { line: number; column: number }>();
835
+ let _provenanceEnabled = false;
836
+
837
+ export function setProvenanceTracking(enabled: boolean): void {
838
+ _provenanceEnabled = enabled;
839
+ }
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
+
848
+ function extractOrigin(
849
+ tv: TypeValue | undefined,
850
+ ): { line: number; column: number } | undefined {
851
+ if (!_provenanceEnabled || !tv) return undefined;
852
+ const direct = _originMap.get(tv);
853
+ if (direct) return direct;
854
+ if (tv.kind === "union") {
855
+ for (const member of tv.members) {
856
+ const found = _originMap.get(member);
857
+ if (found) return found;
858
+ }
859
+ } else if (tv.kind === "object") {
860
+ for (const propVal of Object.values(tv.properties)) {
861
+ const found = _originMap.get(propVal);
862
+ if (found) return found;
863
+ }
864
+ }
865
+ return undefined;
866
+ }
867
+
868
+ /** Own-property check for plain record tables. Plain `{}` records inherit
869
+ * Object.prototype, so `in` / bracket access would leak native members
870
+ * (e.g. the real JS `hasOwnProperty`, `toString`) into the type system as
871
+ * bogus values. */
872
+ function hasOwnProp(props: Record<string, unknown>, name: string): boolean {
873
+ return Object.prototype.hasOwnProperty.call(props, name);
874
+ }
875
+
876
+ function recordUnknown(r: Omit<UnknownRecord, "loc"> & { loc?: Node["loc"] }): void {
877
+ if (!_unknownCollector) return;
878
+ const origin = r.kind === "method" || r.kind === "property"
879
+ ? extractOrigin(r.receiverType)
880
+ : undefined;
881
+ _unknownCollector({
882
+ ...r,
883
+ origin,
884
+ originModule: _execOriginStack.length > 0 ? _execOriginStack[_execOriginStack.length - 1] : undefined,
885
+ loc: r.loc ? { line: r.loc.start.line, column: r.loc.start.column } : undefined,
886
+ });
887
+ }
888
+
118
889
  function distributeOverUnion(
119
890
  tv: TypeValue,
120
891
  fn: (member: TypeValue) => TypeValue,
@@ -125,23 +896,210 @@ function distributeOverUnion(
125
896
  return fn(tv);
126
897
  }
127
898
 
128
- function distributeBinaryOverUnion(
129
- left: TypeValue,
130
- right: TypeValue,
131
- fn: (l: TypeValue, r: TypeValue) => TypeValue,
899
+ // --- Computed member access with a non-literal key (key-set evaluation) ---
900
+ //
901
+ // `obj[key]` where the key is a string/number type rather than a known
902
+ // literal: the set of reachable values is exactly the object's own property
903
+ // values (or the tuple's elements). When they are structurally uniform one
904
+ // shared type answers every key; otherwise the union of all values does
905
+ // (bounded, so pathological records cannot blow up the type graph).
906
+ const MAX_KEYSET_PROPS = 16;
907
+ const MAX_KEYSET_MEMBERS = 12;
908
+ const MAX_KEYSET_TABLE_FNS = 8;
909
+
910
+ /** Structural fingerprint for key-set uniformity. Mirrors the analyzer's
911
+ * typeStructureKey semantics (literal values included, so {a:1,b:2} is NOT
912
+ * uniform) but keeps function values identity-distinct: two different
913
+ * closures (a method table's entries) must never count as "uniform", or
914
+ * `Table[f]()` would silently pick one function's result. */
915
+ const keySetObjIds = new WeakMap<object, number>();
916
+ let keySetObjIdSeq = 0;
917
+
918
+ function objectId(tv: object): number {
919
+ let id = keySetObjIds.get(tv);
920
+ if (id === undefined) {
921
+ id = ++keySetObjIdSeq;
922
+ keySetObjIds.set(tv, id);
923
+ }
924
+ return id;
925
+ }
926
+
927
+ const keySetKeyCache = new WeakMap<object, string>();
928
+ const KEYSET_KEY_NODE_BUDGET = 64;
929
+
930
+ function keySetStructureKeyUncached(tv: TypeValue, budget: { left: number }): string {
931
+ if (budget.left <= 0) return `oversized@${objectId(tv)}`;
932
+ budget.left--;
933
+ switch (tv.kind) {
934
+ case "literal":
935
+ return `lit(${typeof tv.value}:${String(tv.value)})`;
936
+ case "primitive":
937
+ return `prim(${tv.type})`;
938
+ case "array":
939
+ return `arr(${keySetStructureKeyUncached(tv.element, budget)})`;
940
+ case "tuple":
941
+ return `tup(${tv.elements.map((e) => keySetStructureKeyUncached(e, budget)).join(",")})`;
942
+ case "object":
943
+ return `obj(${Object.keys(tv.properties).sort().map((k) => `${k}:${keySetStructureKeyUncached(tv.properties[k], budget)}`).join(",")})`;
944
+ case "promise":
945
+ return `prom(${keySetStructureKeyUncached(tv.value, budget)})`;
946
+ case "instance":
947
+ return `inst(${tv.className})`;
948
+ case "refined":
949
+ return `ref(${keySetStructureKeyUncached(tv.base, budget)})`;
950
+ case "union":
951
+ return `uni(${tv.members.map((m) => keySetStructureKeyUncached(m, budget)).sort().join("|")})`;
952
+ case "function":
953
+ return `fn#${objectId(tv)}`;
954
+ default:
955
+ return tv.kind;
956
+ }
957
+ }
958
+
959
+ /** Structural fingerprint for key-set uniformity. Mirrors the analyzer's
960
+ * typeStructureKey semantics (literal values included, so {a:1,b:2} is NOT
961
+ * uniform) but keeps function values identity-distinct: two different
962
+ * closures (a method table's entries) must never count as "uniform", or
963
+ * `Table[f]()` would silently pick one function's result. Memoized per
964
+ * type value with a node budget — fixture-scale structures must not pay
965
+ * string building on every dynamic-key access. */
966
+ function keySetStructureKey(tv: TypeValue): string {
967
+ const cached = keySetKeyCache.get(tv);
968
+ if (cached !== undefined) return cached;
969
+ const key = keySetStructureKeyUncached(tv, { left: KEYSET_KEY_NODE_BUDGET });
970
+ keySetKeyCache.set(tv, key);
971
+ return key;
972
+ }
973
+
974
+ function objectKeySetAccess(obj: TypeValue & { kind: "object" }): TypeValue {
975
+ const keys = Object.keys(obj.properties);
976
+ if (keys.length === 0) return T.undefined;
977
+ if (keys.length > MAX_KEYSET_PROPS) return T.unknown;
978
+ const values = keys.map((k) => obj.properties[k]);
979
+ if (values.every((v) => v.kind === "unknown")) return T.unknown;
980
+ const firstKey = keySetStructureKey(values[0]);
981
+ if (values.every((v) => keySetStructureKey(v) === firstKey)) {
982
+ return values[0];
983
+ }
984
+ // Method tables (`Strings[f]()`) union their member functions; calling a
985
+ // union of N functions runs N evaluations, so keep that budget tighter
986
+ // than the data-member cap.
987
+ if (values.every((v) => v.kind === "function") && values.length > MAX_KEYSET_TABLE_FNS) {
988
+ return T.unknown;
989
+ }
990
+ const union = simplifyUnion(values);
991
+ if (union.kind === "union" && union.members.length > MAX_KEYSET_MEMBERS) return T.unknown;
992
+ return union;
993
+ }
994
+
995
+ /** Does this key type admit key-set evaluation? Literal keys (and unions
996
+ * that collapse to literals) are handled precisely; everything else falls
997
+ * back to the property/element set. */
998
+ function isDynamicStringKey(propVal: TypeValue): boolean {
999
+ if (propVal.kind === "unknown" || (propVal.kind === "primitive" && propVal.type === "string")) return true;
1000
+ if (propVal.kind === "union") {
1001
+ return propVal.members.some((m) => isDynamicStringKey(m) || (m.kind === "literal" && typeof m.value === "string"));
1002
+ }
1003
+ return false;
1004
+ }
1005
+
1006
+ function isDynamicNumberKey(propVal: TypeValue): boolean {
1007
+ if (propVal.kind === "unknown" || (propVal.kind === "primitive" && propVal.type === "number")) return true;
1008
+ if (propVal.kind === "union") {
1009
+ return propVal.members.some((m) => isDynamicNumberKey(m) || (m.kind === "literal" && typeof m.value === "number"));
1010
+ }
1011
+ return false;
1012
+ }
1013
+
1014
+ function unionOfLiteralKind(tv: TypeValue, pred: (v: unknown) => boolean): tv is TypeValue & { kind: "union" } {
1015
+ return tv.kind === "union" && tv.members.every((m) => m.kind === "literal" && pred(m.value));
1016
+ }
1017
+
1018
+ /** Computed `obj[key]` on one union member of the receiver. Shared by
1019
+ * MemberExpression / OptionalMemberExpression. `markMiss` records the
1020
+ * unknown-property diagnostic for literal string keys that miss. */
1021
+ function computedMemberOnMember(
1022
+ obj: TypeValue,
1023
+ propVal: TypeValue,
1024
+ markMiss: () => void,
132
1025
  ): TypeValue {
133
- if (left.kind === "union" && right.kind === "union") {
134
- return simplifyUnion(
135
- left.members.flatMap((l) => right.members.map((r) => fn(l, r))),
136
- );
1026
+ if (obj.kind === "object") {
1027
+ if (propVal.kind === "literal" && typeof propVal.value === "string") {
1028
+ return (hasOwnProp(obj.properties, propVal.value) ? obj.properties[propVal.value] : undefined) ?? T.undefined;
1029
+ }
1030
+ if (unionOfLiteralKind(propVal, (v) => typeof v === "string")) {
1031
+ return simplifyUnion(
1032
+ (propVal as TypeValue & { kind: "union" }).members.map((m) =>
1033
+ (m as TypeValue & { kind: "literal" }).value as string,
1034
+ ).map((k) => (hasOwnProp(obj.properties, k) ? obj.properties[k] : undefined) ?? T.undefined),
1035
+ );
1036
+ }
1037
+ if (isDynamicStringKey(propVal)) {
1038
+ return objectKeySetAccess(obj);
1039
+ }
1040
+ if (propVal.kind === "literal" && typeof propVal.value === "string") {
1041
+ markMiss();
1042
+ }
1043
+ return T.unknown;
1044
+ }
1045
+ if (obj.kind === "array" || obj.kind === "tuple") {
1046
+ if (propVal.kind === "literal" && typeof propVal.value === "number") {
1047
+ if (obj.kind === "tuple") return obj.elements[propVal.value] ?? T.undefined;
1048
+ return obj.element;
1049
+ }
1050
+ // A string-keyed read off an array/tuple hits no data property (real
1051
+ // JS yields undefined, same as a missed key on an object) — returning
1052
+ // unknown here would poison any union the read flows into.
1053
+ if (propVal.kind === "literal" && typeof propVal.value === "string") {
1054
+ return T.undefined;
1055
+ }
1056
+ if (unionOfLiteralKind(propVal, (v) => typeof v === "number")) {
1057
+ const indices = (propVal as TypeValue & { kind: "union" }).members.map(
1058
+ (m) => (m as TypeValue & { kind: "literal" }).value as number,
1059
+ );
1060
+ if (obj.kind === "tuple") {
1061
+ return simplifyUnion(indices.map((i) => obj.elements[i] ?? T.undefined));
1062
+ }
1063
+ return obj.element;
1064
+ }
1065
+ if (isDynamicNumberKey(propVal)) {
1066
+ // arr[anyNumber]: tuple → union of its elements, array → element type
1067
+ if (obj.kind === "tuple") {
1068
+ return obj.elements.length > 0 ? simplifyUnion(obj.elements) : T.undefined;
1069
+ }
1070
+ return obj.element;
1071
+ }
1072
+ return T.unknown;
1073
+ }
1074
+ if (obj.kind === "instance" && isDynamicStringKey(propVal)) {
1075
+ // Known instances (Map/Set/Buffer singletons...) carry no string-keyed
1076
+ // data properties; a dynamic key resolves to undefined rather than
1077
+ // poisoning enclosing unions with unknown.
1078
+ return T.undefined;
137
1079
  }
138
- if (left.kind === "union") {
139
- return simplifyUnion(left.members.map((l) => fn(l, right)));
1080
+ if (propVal.kind === "literal" && typeof propVal.value === "string") {
1081
+ markMiss();
140
1082
  }
141
- if (right.kind === "union") {
142
- return simplifyUnion(right.members.map((r) => fn(left, r)));
1083
+ return T.unknown;
1084
+ }
1085
+
1086
+ function evaluateComputedMemberAccess(objVal: TypeValue, propVal: TypeValue, node: Node): TypeValue {
1087
+ let propMissed = false;
1088
+ const result = distributeOverUnion(objVal, (obj) =>
1089
+ computedMemberOnMember(obj, propVal, () => {
1090
+ propMissed = true;
1091
+ }),
1092
+ );
1093
+ if (propMissed && propVal.kind === "literal" && typeof propVal.value === "string") {
1094
+ recordUnknown({
1095
+ kind: "property",
1096
+ name: propVal.value,
1097
+ receiverType: objVal,
1098
+ loc: node.loc,
1099
+ reason: `no property '${propVal.value}' on ${objVal.kind}`,
1100
+ });
143
1101
  }
144
- return fn(left, right);
1102
+ return result;
145
1103
  }
146
1104
 
147
1105
  let _unreachableRanges: SourceRange[] = [];
@@ -168,8 +1126,24 @@ function evaluateStatements(
168
1126
 
169
1127
  for (let i = 0; i < stmts.length; i++) {
170
1128
  const stmt = stmts[i];
1129
+
1130
+ const inlineDirectives = extractInlineDirectives(stmt);
1131
+ const savedAs = _activeAsOverride;
1132
+ const savedReplacements = _activeReplacements;
1133
+
1134
+ const asDir = inlineDirectives.find((d): d is InlineDirective & { kind: "as" } => d.kind === "as");
1135
+ const replaceDirs = inlineDirectives.filter((d): d is InlineDirective & { kind: "replace" } => d.kind === "replace");
1136
+
1137
+ _activeAsOverride = asDir?.typeExpr ?? null;
1138
+ _activeReplacements = replaceDirs.length > 0
1139
+ ? replaceDirs.map((d) => ({ targetSource: d.targetSource, typeExpr: d.typeExpr }))
1140
+ : [];
1141
+
171
1142
  const result = evaluate(stmt, currentEnv);
172
1143
 
1144
+ _activeAsOverride = savedAs;
1145
+ _activeReplacements = savedReplacements;
1146
+
173
1147
  if (isThrow(result)) {
174
1148
  collectUnreachable(stmts, i + 1);
175
1149
  return result;
@@ -218,6 +1192,9 @@ function describeParam(p: Node): string {
218
1192
  }
219
1193
 
220
1194
  export function evaluate(node: Node, env: Environment): EvalResult {
1195
+ const replacement = matchesReplacement(node);
1196
+ if (replacement) return replacement;
1197
+
221
1198
  const result = evaluateNode(node, env);
222
1199
  if (_nodeTypeCollector && node.loc && !isReturn(result) && !isBranch(result) && !isThrow(result)) {
223
1200
  recordNodeType(node, result);
@@ -234,6 +1211,7 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
234
1211
  return evaluateStatements(node.body, env);
235
1212
 
236
1213
  case "ExpressionStatement":
1214
+ if (_activeAsOverride) return _activeAsOverride;
237
1215
  return evaluate(node.expression, env);
238
1216
 
239
1217
  case "NumericLiteral":
@@ -248,13 +1226,69 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
248
1226
  case "NullLiteral":
249
1227
  return T.null;
250
1228
 
1229
+ case "RegExpLiteral":
1230
+ return createRegExpType();
1231
+
251
1232
  case "Identifier": {
252
1233
  if (node.name === "undefined") return T.undefined;
1234
+ // Check for built-in global objects (env-injected values take priority,
1235
+ // e.g. @nudo:env es binds fnSig-backed globals with precise impls).
1236
+ // hasOwnProp guard: `in` would hit the JS prototype chain and leak
1237
+ // native functions for names like `hasOwnProperty`.
1238
+ if (hasOwnProp(BUILTIN_STATIC_METHODS, node.name) && !env.has(node.name)) {
1239
+ const builtin = BUILTIN_STATIC_METHODS[node.name];
1240
+ if (typeof builtin === "object" && builtin !== null && !("kind" in builtin)) {
1241
+ // It's a namespace object (like Date, Math, JSON); memoized so
1242
+ // `'x'.constructor === String` hits reference equality.
1243
+ return builtinClassValue(node.name);
1244
+ }
1245
+ // It's a direct value (like parseInt, isNaN)
1246
+ return builtin as TypeValue;
1247
+ }
1248
+ // Constructible built-in classes without static-method coverage (Error,
1249
+ // Map, Set, ...) still resolve to a namespace object so `X.prototype`
1250
+ // can be typed as an instance.
1251
+ if (BUILTIN_PROTOTYPE_CLASSES.has(node.name) && !env.has(node.name)) {
1252
+ return builtinClassValue(node.name);
1253
+ }
1254
+ // Check if it looks like a built-in but isn't covered
1255
+ if (node.name[0] === node.name[0].toUpperCase() && !env.has(node.name)) {
1256
+ if (_onUnknownBuiltin) {
1257
+ _onUnknownBuiltin(node.name, node.loc as any);
1258
+ }
1259
+ recordUnknown({
1260
+ kind: "global",
1261
+ name: node.name,
1262
+ loc: node.loc,
1263
+ reason: `unknown global identifier '${node.name}'`,
1264
+ });
1265
+ // Unresolved builtins propagate unknown (not undefined) so property
1266
+ // access on them degrades to unknown instead of a false
1267
+ // "property does not exist on undefined" error.
1268
+ return T.unknown;
1269
+ }
1270
+ // Bare `hasOwnProperty(...)`-style references resolve through the
1271
+ // global object's prototype chain in real JS — Object.prototype
1272
+ // members act as implicit globals (own-guarded against proto leak).
1273
+ if (!env.has(node.name) && hasOwnProp(OBJECT_PROTOTYPE_METHODS, node.name)) {
1274
+ return OBJECT_PROTOTYPE_METHODS[node.name];
1275
+ }
253
1276
  return env.lookup(node.name);
254
1277
  }
255
1278
 
256
- case "ThisExpression":
257
- return env.lookup("this");
1279
+ case "ThisExpression": {
1280
+ // Un-bound `this` (plain function called without a receiver, e.g. a
1281
+ // callsite-synthesized case): the receiver is unknowable statically, so
1282
+ // degrade to unknown — `this.x` then warns (unknown-recv) instead of
1283
+ // erroring with "on type 'undefined'". Explicit bindings (constructors,
1284
+ // obj.f() receivers, .call(thisArg)) take precedence unchanged.
1285
+ const thisVal = env.lookup("this");
1286
+ // T.undefined is `{ kind: "literal", value: undefined }`.
1287
+ if (thisVal === undefined || (thisVal.kind === "literal" && thisVal.value === undefined)) {
1288
+ return T.unknown;
1289
+ }
1290
+ return thisVal;
1291
+ }
258
1292
 
259
1293
  case "TemplateLiteral": {
260
1294
  if (node.expressions.length === 0 && node.quasis.length === 1) {
@@ -291,22 +1325,39 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
291
1325
  if (node.operator === "instanceof") {
292
1326
  return evaluateInstanceof(leftVal, rightVal, node.right, env);
293
1327
  }
294
- return distributeBinaryOverUnion(leftVal, rightVal, (l, r) =>
295
- dispatchBinaryOp(node.operator, l, r),
296
- );
1328
+ if (node.operator === "in") {
1329
+ return distributeBinaryOverUnion(leftVal, rightVal, (l, r) => evaluateInMember(l, r));
1330
+ }
1331
+ if (node.operator === "===" || node.operator === "!==") {
1332
+ const identity = builtinProtoIdentityEq(leftVal, rightVal);
1333
+ if (identity !== null) {
1334
+ return T.literal(node.operator === "===" ? identity : !identity);
1335
+ }
1336
+ const classIdentity = builtinClassIdentityEq(leftVal, rightVal);
1337
+ if (classIdentity !== null) {
1338
+ return T.literal(node.operator === "===" ? classIdentity : !classIdentity);
1339
+ }
1340
+ }
1341
+
1342
+ // 统一二元:代数优先 → 宽松相等 → 外延 Ops
1343
+ const bin = evalBinaryValue(node.operator, leftVal, rightVal);
1344
+ if (bin !== undefined) return bin;
1345
+ return T.unknown;
297
1346
  }
298
1347
 
299
1348
  case "UnaryExpression": {
300
1349
  const argVal = evaluate(node.argument, env);
301
1350
  if (isReturn(argVal) || isBranch(argVal) || isThrow(argVal)) return argVal;
302
- if (node.operator === "typeof") {
303
- return distributeOverUnion(argVal, (v) => Ops.typeof_(v));
304
- }
1351
+ // 一元语言表面经代数(typeof / ! / -)
1352
+ const unary = tryAbsUnary(node.operator, argVal);
1353
+ if (unary !== undefined) return unary;
305
1354
  if (node.operator === "!") {
306
- return distributeOverUnion(argVal, (v) => Ops.not(v));
307
- }
308
- if (node.operator === "-") {
309
- return distributeOverUnion(argVal, (v) => Ops.neg(v));
1355
+ // 代数未接住:对象恒真等 host 知识
1356
+ return distributeOverUnion(argVal, (v) => {
1357
+ const decided = definiteBoolean(v);
1358
+ if (decided !== null) return T.literal(!decided);
1359
+ return Ops.not(v);
1360
+ });
310
1361
  }
311
1362
  return T.unknown;
312
1363
  }
@@ -321,6 +1372,13 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
321
1372
  const rv = evaluate(node.right, env);
322
1373
  return isReturn(rv) || isBranch(rv) || isThrow(rv) ? rv : rv;
323
1374
  }
1375
+ // Object-ish left operands are always truthy: `Buffer && f(x)` is f(x)
1376
+ const leftTruthy = definiteBoolean(leftVal);
1377
+ if (leftTruthy === true) {
1378
+ const rv = evaluate(node.right, env);
1379
+ return isReturn(rv) || isBranch(rv) || isThrow(rv) ? rv : rv;
1380
+ }
1381
+ if (leftTruthy === false) return leftVal;
324
1382
  const rv = evaluate(node.right, env);
325
1383
  const rightTV = isReturn(rv) || isBranch(rv) || isThrow(rv) ? T.unknown : rv;
326
1384
  return simplifyUnion([leftVal, rightTV]);
@@ -332,6 +1390,12 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
332
1390
  const rv = evaluate(node.right, env);
333
1391
  return isReturn(rv) || isBranch(rv) || isThrow(rv) ? rv : rv;
334
1392
  }
1393
+ const leftTruthy = definiteBoolean(leftVal);
1394
+ if (leftTruthy === true) return leftVal;
1395
+ if (leftTruthy === false) {
1396
+ const rv = evaluate(node.right, env);
1397
+ return isReturn(rv) || isBranch(rv) || isThrow(rv) ? rv : rv;
1398
+ }
335
1399
  const rv = evaluate(node.right, env);
336
1400
  const rightTV = isReturn(rv) || isBranch(rv) || isThrow(rv) ? T.unknown : rv;
337
1401
  return simplifyUnion([leftVal, rightTV]);
@@ -345,9 +1409,17 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
345
1409
  const rv = evaluate(node.right, env);
346
1410
  return isReturn(rv) || isBranch(rv) || isThrow(rv) ? rv : rv;
347
1411
  }
1412
+ // For non-literal types, narrow by removing null/undefined from the left side
1413
+ const narrowedLeft = subtractType(leftVal, (m) =>
1414
+ (m.kind === "literal" && (m.value === null || m.value === undefined))
1415
+ );
1416
+ if (narrowedLeft.kind !== "never") {
1417
+ return narrowedLeft;
1418
+ }
1419
+ // If all members were null/undefined, use the right side
348
1420
  const rv = evaluate(node.right, env);
349
1421
  const rightTV = isReturn(rv) || isBranch(rv) || isThrow(rv) ? T.unknown : rv;
350
- return simplifyUnion([leftVal, rightTV]);
1422
+ return rightTV;
351
1423
  }
352
1424
 
353
1425
  return T.unknown;
@@ -364,12 +1436,19 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
364
1436
  ? evaluate(node.consequent, trueEnv)
365
1437
  : evaluate(node.alternate, falseEnv);
366
1438
  }
1439
+ // Object-ish / otherwise-decidable tests pick their branch statically
1440
+ const decided = definiteBoolean(testVal);
1441
+ if (decided !== null) {
1442
+ return decided
1443
+ ? evaluate(node.consequent, trueEnv)
1444
+ : evaluate(node.alternate, falseEnv);
1445
+ }
367
1446
 
368
1447
  const cResult = evaluate(node.consequent, trueEnv);
369
1448
  const aResult = evaluate(node.alternate, falseEnv);
370
1449
  const cVal = isReturn(cResult) ? cResult.value : isBranch(cResult) ? cResult.returnedValue : isThrow(cResult) ? T.never : cResult;
371
1450
  const aVal = isReturn(aResult) ? aResult.value : isBranch(aResult) ? aResult.returnedValue : isThrow(aResult) ? T.never : aResult;
372
- return simplifyUnion([cVal, aVal]);
1451
+ return joinOrUnion(cVal, aVal);
373
1452
  }
374
1453
 
375
1454
  case "IfStatement": {
@@ -378,6 +1457,9 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
378
1457
  const testVal = evaluate(test, env);
379
1458
  if (isReturn(testVal) || isBranch(testVal) || isThrow(testVal)) return testVal;
380
1459
 
1460
+ // Kernel Φ:从测试表达式提取约束,真/假分支分别合取
1461
+ const phis = phiFromTest(test);
1462
+
381
1463
  if (testVal.kind === "literal") {
382
1464
  if (testVal.value) {
383
1465
  if (node.alternate?.loc) {
@@ -386,7 +1468,12 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
386
1468
  end: { line: node.alternate.loc.end.line, column: node.alternate.loc.end.column },
387
1469
  });
388
1470
  }
389
- return evaluate(node.consequent, trueEnv);
1471
+ pushPhi(combinePhi(currentPhi(), phis.whenTrue));
1472
+ try {
1473
+ return evaluate(node.consequent, trueEnv);
1474
+ } finally {
1475
+ popPhi();
1476
+ }
390
1477
  }
391
1478
  if (node.consequent.loc) {
392
1479
  _unreachableRanges.push({
@@ -394,15 +1481,52 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
394
1481
  end: { line: node.consequent.loc.end.line, column: node.consequent.loc.end.column },
395
1482
  });
396
1483
  }
397
- return node.alternate
398
- ? evaluate(node.alternate, falseEnv)
399
- : 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
+ }
1491
+ }
1492
+ // Object-ish / otherwise-decidable tests pick their branch statically
1493
+ // (e.g. `if (proto && proto.isImmutable)` once && resolves).
1494
+ const decided = definiteBoolean(testVal);
1495
+ if (decided !== null) {
1496
+ if (decided) {
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();
1510
+ }
400
1511
  }
401
1512
 
402
- const consequentResult = evaluate(node.consequent, trueEnv);
403
- const alternateResult = node.alternate
404
- ? evaluate(node.alternate, falseEnv)
405
- : 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
+ }
406
1530
 
407
1531
  const cReturns = isReturn(consequentResult);
408
1532
  const cBranches = isBranch(consequentResult);
@@ -427,21 +1551,21 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
427
1551
  }
428
1552
 
429
1553
  if (aThrows) {
430
- const cVal = cReturns ? consequentResult.value
431
- : cBranches ? consequentResult.returnedValue
1554
+ const cVal = cReturns ? (consequentResult as ReturnSignal).value
1555
+ : cBranches ? (consequentResult as BranchSignal).returnedValue
432
1556
  : consequentResult as TypeValue;
433
1557
  return makeBranch(cVal, trueEnv);
434
1558
  }
435
1559
 
436
- const cVal = cReturns ? consequentResult.value
437
- : cBranches ? consequentResult.returnedValue
1560
+ const cVal = cReturns ? (consequentResult as ReturnSignal).value
1561
+ : cBranches ? (consequentResult as BranchSignal).returnedValue
438
1562
  : consequentResult as TypeValue;
439
1563
  const aVal = aReturns ? (alternateResult as ReturnSignal).value
440
1564
  : aBranches ? (alternateResult as BranchSignal).returnedValue
441
1565
  : alternateResult as TypeValue | null;
442
1566
 
443
1567
  if (cReturns && aReturns) {
444
- return makeReturn(simplifyUnion([cVal, aVal!]));
1568
+ return makeReturn(joinOrUnion(cVal, aVal!));
445
1569
  }
446
1570
 
447
1571
  if (cReturns && !node.alternate) {
@@ -450,7 +1574,7 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
450
1574
 
451
1575
  if (cReturns && node.alternate) {
452
1576
  if (aReturns) {
453
- return makeReturn(simplifyUnion([cVal, aVal!]));
1577
+ return makeReturn(joinOrUnion(cVal, aVal!));
454
1578
  }
455
1579
  return makeBranch(cVal, falseEnv);
456
1580
  }
@@ -466,12 +1590,13 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
466
1590
  }
467
1591
 
468
1592
  case "BlockStatement": {
469
- const blockEnv = env.extend({});
1593
+ const blockEnv = env.fork();
470
1594
  const result = evaluateStatements(node.body, blockEnv);
471
1595
  return result;
472
1596
  }
473
1597
 
474
1598
  case "ReturnStatement": {
1599
+ if (_activeAsOverride) return makeReturn(_activeAsOverride);
475
1600
  const arg = node.argument;
476
1601
  if (!arg) return makeReturn(T.undefined);
477
1602
  const val = evaluate(arg, env);
@@ -481,8 +1606,13 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
481
1606
 
482
1607
  case "VariableDeclaration": {
483
1608
  for (const decl of node.declarations) {
484
- const init = decl.init ? evaluate(decl.init, env) : T.undefined;
1609
+ const init = _activeAsOverride ?? (decl.init ? evaluate(decl.init, env) : T.undefined);
485
1610
  if (isReturn(init) || isBranch(init) || isThrow(init)) return init;
1611
+ // Name anonymous function values after their binding so recursion
1612
+ // truncation records can cite e.g. `recursion:fn`.
1613
+ if (init.kind === "function" && decl.id.type === "Identifier" && !(init as any)._name) {
1614
+ (init as any)._name = decl.id.name;
1615
+ }
486
1616
  bindPattern(decl.id, init, env);
487
1617
  if (decl.id.type === "Identifier") {
488
1618
  recordNodeType(decl.id, init);
@@ -493,16 +1623,40 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
493
1623
 
494
1624
  case "AssignmentExpression": {
495
1625
  if (node.left.type === "Identifier") {
496
- const val = evaluate(node.right, env);
497
- if (isReturn(val) || isBranch(val) || isThrow(val)) return val;
1626
+ const rightVal = evaluate(node.right, env);
1627
+ if (isReturn(rightVal) || isBranch(rightVal) || isThrow(rightVal)) return rightVal;
1628
+
1629
+ // Handle compound assignment operators
1630
+ let val = rightVal;
1631
+ if (node.operator !== "=") {
1632
+ const leftVal = env.lookup(node.left.name);
1633
+ if (leftVal && leftVal.kind !== "unknown") {
1634
+ // Extract the binary operator (e.g., "+=" -> "+")
1635
+ const binaryOp = node.operator.slice(0, -1);
1636
+ val = evalBinaryValue(binaryOp, leftVal, rightVal) ?? T.unknown;
1637
+ }
1638
+ }
1639
+
498
1640
  if (!env.update(node.left.name, val)) {
499
1641
  env.bind(node.left.name, val);
500
1642
  }
1643
+ if (val.kind === "function" && !(val as any)._name) {
1644
+ (val as any)._name = node.left.name;
1645
+ }
501
1646
  return val;
502
1647
  }
503
1648
  if (node.left.type === "MemberExpression") {
504
1649
  const val = evaluate(node.right, env);
505
1650
  if (isReturn(val) || isBranch(val) || isThrow(val)) return val;
1651
+ // `obj.method = function () {...}` — name the function after the
1652
+ // assigned property (e.g. `recursion:internals.clone`).
1653
+ if (val.kind === "function" && !(val as any)._name) {
1654
+ const memberKey = getMemberKey(node.left, env);
1655
+ if (memberKey !== null) {
1656
+ const objKey = node.left.object.type === "Identifier" ? `${node.left.object.name}.` : "";
1657
+ (val as any)._name = `${objKey}${memberKey}`;
1658
+ }
1659
+ }
506
1660
  const objVal = evaluate(node.left.object, env);
507
1661
  if (isReturn(objVal) || isBranch(objVal) || isThrow(objVal)) return val;
508
1662
  if (objVal.kind === "object") {
@@ -570,7 +1724,10 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
570
1724
  const paramNames = node.params.map(describeParam);
571
1725
  const fnType = T.fn(paramNames, node.body, env);
572
1726
  (fnType as any)._paramPatterns = node.params;
1727
+ if (node.id) (fnType as any)._name = node.id.name;
573
1728
  if (node.async) (fnType as any)._async = true;
1729
+ (fnType as any)._hasArguments = true;
1730
+ tagFnModule(fnType);
574
1731
  env.bind(node.id.name, fnType);
575
1732
  return T.undefined;
576
1733
  }
@@ -581,7 +1738,10 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
581
1738
  const body = node.body;
582
1739
  const fnType = T.fn(paramNames, body, env);
583
1740
  (fnType as any)._paramPatterns = node.params;
1741
+ if ((node as any).id) (fnType as any)._name = (node as any).id.name;
584
1742
  if (node.async) (fnType as any)._async = true;
1743
+ (fnType as any)._hasArguments = true;
1744
+ tagFnModule(fnType);
585
1745
  return fnType;
586
1746
  }
587
1747
 
@@ -656,14 +1816,92 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
656
1816
  if (methodResult !== null) return methodResult;
657
1817
  }
658
1818
 
1819
+ // CommonJS require(specifier) — resolved through the same module
1820
+ // resolver/cache as ESM import. The identity check keeps user-defined
1821
+ // `require` bindings on the normal call path.
1822
+ if (
1823
+ callee.type === "Identifier" &&
1824
+ callee.name === "require" &&
1825
+ env.lookup("require") === _cjsRequireValue
1826
+ ) {
1827
+ return evaluateRequireCall(node, env);
1828
+ }
1829
+
1830
+ // Handle built-in global functions (only if not overridden in environment, e.g., by mocks)
1831
+ if (callee.type === "Identifier" && !env.has(callee.name)) {
1832
+ const builtinResult = evaluateBuiltinCall(callee.name, node.arguments as Node[], env);
1833
+ if (builtinResult !== null) return builtinResult;
1834
+ }
1835
+
1836
+ const calleeVal = evaluate(callee, env);
1837
+ if (isReturn(calleeVal) || isBranch(calleeVal) || isThrow(calleeVal)) return calleeVal;
1838
+
1839
+ const argVals = evaluateArgs(node.arguments as Node[], env);
1840
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
1841
+
1842
+ const thisVal = (callee.type === "MemberExpression" || callee.type === "OptionalMemberExpression")
1843
+ ? ((callee as any)._receiverVal as TypeValue | undefined)
1844
+ : undefined;
1845
+ if (calleeVal.kind === "function") {
1846
+ const full = callFunctionFull(calleeVal, argVals as TypeValue[], thisVal);
1847
+ if (_callCollector && callee.type === "Identifier") {
1848
+ recordCall(callee.name, argVals as TypeValue[], full, node.loc, calleeVal);
1849
+ } else if (_callCollector && (callee.type === "MemberExpression" || callee.type === "OptionalMemberExpression") && callee.property.type === "Identifier") {
1850
+ // 成员调用(ns.fn()/obj.fn()):跨文件调用点发现的来源——
1851
+ // 配合 _exportTags 还原 (module, export) 归属
1852
+ recordCall(callee.property.name, argVals as TypeValue[], full, node.loc, calleeVal);
1853
+ }
1854
+ if (full.value.kind === "never" && full.throws.kind !== "never") {
1855
+ const callLoc = node.loc ? {
1856
+ start: { line: node.loc.start.line, column: node.loc.start.column },
1857
+ end: { line: node.loc.end.line, column: node.loc.end.column },
1858
+ } : full.throwLoc;
1859
+ return makeThrow(full.throws, callLoc);
1860
+ }
1861
+ return full.value;
1862
+ }
1863
+
1864
+ return distributeOverUnion(calleeVal, (fn) => {
1865
+ if (fn.kind !== "function") return T.unknown;
1866
+ return callFunction(fn, argVals as TypeValue[], thisVal);
1867
+ });
1868
+ }
1869
+
1870
+ case "OptionalCallExpression": {
1871
+ const callee = node.callee as Node;
1872
+
1873
+ if (callee.type === "OptionalMemberExpression" || callee.type === "MemberExpression") {
1874
+ const objVal = evaluate(callee.object, env);
1875
+ if (isReturn(objVal) || isBranch(objVal) || isThrow(objVal)) return objVal;
1876
+ if (objVal.kind === "literal" && (objVal.value === null || objVal.value === undefined)) {
1877
+ return T.undefined;
1878
+ }
1879
+ const methodResult = evaluateMethodCall(callee as Node & { type: "MemberExpression" }, node.arguments as Node[], env);
1880
+ if (methodResult !== null) return methodResult;
1881
+ }
1882
+
659
1883
  const calleeVal = evaluate(callee, env);
660
1884
  if (isReturn(calleeVal) || isBranch(calleeVal) || isThrow(calleeVal)) return calleeVal;
661
1885
 
1886
+ if (calleeVal.kind === "literal" && (calleeVal.value === null || calleeVal.value === undefined)) {
1887
+ return T.undefined;
1888
+ }
1889
+
662
1890
  const argVals = evaluateArgs(node.arguments as Node[], env);
663
1891
  if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
664
1892
 
1893
+ const thisVal = (callee.type === "MemberExpression" || callee.type === "OptionalMemberExpression")
1894
+ ? ((callee as any)._receiverVal as TypeValue | undefined)
1895
+ : undefined;
665
1896
  if (calleeVal.kind === "function") {
666
- const full = callFunctionFull(calleeVal, argVals as TypeValue[]);
1897
+ const full = callFunctionFull(calleeVal, argVals as TypeValue[], thisVal);
1898
+ if (_callCollector && callee.type === "Identifier") {
1899
+ recordCall(callee.name, argVals as TypeValue[], full, node.loc, calleeVal);
1900
+ } else if (_callCollector && (callee.type === "MemberExpression" || callee.type === "OptionalMemberExpression") && callee.property.type === "Identifier") {
1901
+ // 成员调用(ns.fn()/obj.fn()):跨文件调用点发现的来源——
1902
+ // 配合 _exportTags 还原 (module, export) 归属
1903
+ recordCall(callee.property.name, argVals as TypeValue[], full, node.loc, calleeVal);
1904
+ }
667
1905
  if (full.value.kind === "never" && full.throws.kind !== "never") {
668
1906
  const callLoc = node.loc ? {
669
1907
  start: { line: node.loc.start.line, column: node.loc.start.column },
@@ -676,49 +1914,327 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
676
1914
 
677
1915
  return distributeOverUnion(calleeVal, (fn) => {
678
1916
  if (fn.kind !== "function") return T.unknown;
679
- return callFunction(fn, argVals as TypeValue[]);
1917
+ return callFunction(fn, argVals as TypeValue[], thisVal);
680
1918
  });
681
1919
  }
682
1920
 
683
1921
  case "MemberExpression": {
684
1922
  const objVal = evaluate(node.object, env);
685
1923
  if (isReturn(objVal) || isBranch(objVal) || isThrow(objVal)) return objVal;
1924
+ // Stash the receiver so an enclosing CallExpression (`obj.f()`) can
1925
+ // bind it as the callee's `this` without re-evaluating the object.
1926
+ (node as any)._receiverVal = objVal;
686
1927
 
687
1928
  if (node.computed) {
688
1929
  const propVal = evaluate(node.property, env);
689
1930
  if (isReturn(propVal) || isBranch(propVal) || isThrow(propVal)) return propVal;
690
- return distributeOverUnion(objVal, (obj) => {
691
- if (obj.kind === "object" && propVal.kind === "literal" && typeof propVal.value === "string") {
692
- return obj.properties[propVal.value] ?? T.undefined;
1931
+ return evaluateComputedMemberAccess(objVal, propVal, node);
1932
+ }
1933
+
1934
+ if (node.property.type === "Identifier") {
1935
+ const propName = node.property.name;
1936
+ let propMissed = false;
1937
+ const result = distributeOverUnion(objVal, (obj) => {
1938
+ // `x.valueOf` off non-object receivers (literals, primitives,
1939
+ // arrays, functions) — see valueOfBinding.
1940
+ if (propName === "valueOf") {
1941
+ const bound = valueOfBinding(obj);
1942
+ if (bound !== undefined) return bound;
1943
+ }
1944
+ // Check for built-in static methods (e.g., Date.now, Math.floor)
1945
+ const builtinName = (obj as any)._builtinName as string | undefined;
1946
+ if (builtinName && hasOwnProp(BUILTIN_STATIC_METHODS, builtinName)) {
1947
+ const builtin = BUILTIN_STATIC_METHODS[builtinName];
1948
+ // hasOwnProp guard: `in` would leak native Object.prototype
1949
+ // members (e.g. `JSON.toString` → the real JS function).
1950
+ if (typeof builtin === "object" && hasOwnProp(builtin, propName)) {
1951
+ // Well-known symbols get named copies so `Symbol.iterator
1952
+ // in x` can decide iterability.
1953
+ if (builtinName === "Symbol" && hasOwnProp(SYMBOL_STATIC_PROPS, propName)) {
1954
+ return wellKnownSymbol(propName);
1955
+ }
1956
+ return (builtin as Record<string, TypeValue>)[propName];
1957
+ }
1958
+ }
1959
+ // `X.prototype` on a constructible built-in resolves to the cached
1960
+ // singleton instance of X (stable identity across evaluations)
1961
+ if (propName === "prototype" && builtinName && BUILTIN_PROTOTYPE_CLASSES.has(builtinName)) {
1962
+ return builtinPrototype(builtinName);
693
1963
  }
694
- if ((obj.kind === "array" || obj.kind === "tuple") && propVal.kind === "literal" && typeof propVal.value === "number") {
695
- if (obj.kind === "tuple") return obj.elements[propVal.value] ?? T.undefined;
696
- return obj.element;
1964
+ // Check for Map.size property
1965
+ if (obj.kind === "instance" && obj.className === "Map" && propName === "size") {
1966
+ return T.number;
1967
+ }
1968
+ // Check for Set.size property
1969
+ if (obj.kind === "instance" && obj.className === "Set" && propName === "size") {
1970
+ return T.number;
1971
+ }
1972
+ if (obj.kind === "object") {
1973
+ const ownVal = hasOwnProp(obj.properties, propName) ? obj.properties[propName] : undefined;
1974
+ if (ownVal) return ownVal;
1975
+ // Every object inherits Object.prototype members (see table note).
1976
+ const protoFn = hasOwnProp(OBJECT_PROTOTYPE_METHODS, propName)
1977
+ ? OBJECT_PROTOTYPE_METHODS[propName]
1978
+ : undefined;
1979
+ if (protoFn) return bindObjectProtoMethod(protoFn, obj);
1980
+ return T.undefined;
1981
+ }
1982
+ if (obj.kind === "instance") {
1983
+ // Own-property lookup only: a plain `{}` properties record would
1984
+ // otherwise leak native Object.prototype members (e.g. the real
1985
+ // JS `toString` function) into the type system.
1986
+ const own = hasOwnProp(obj.properties, propName) ? obj.properties[propName] : undefined;
1987
+ if (own) return own;
1988
+ // Instances inherit Object.prototype members too: keeps
1989
+ // `Object.prototype.hasOwnProperty` (and the toString brand-check
1990
+ // idiom) typeable instead of degrading to undefined.
1991
+ const protoFn = hasOwnProp(OBJECT_PROTOTYPE_METHODS, propName)
1992
+ ? OBJECT_PROTOTYPE_METHODS[propName]
1993
+ : undefined;
1994
+ if (protoFn) return bindObjectProtoMethod(protoFn, obj);
1995
+ // `x.constructor` resolves to the class namespace: instances
1996
+ // and prototype singletons share the memoized builtinClassValue
1997
+ // (Array.prototype.constructor === Array), so reflection like
1998
+ // `new (Object.getPrototypeOf(x).constructor)(n)` keeps going.
1999
+ if (propName === "constructor") return builtinClassValue(obj.className);
2000
+ // instance 的方法集是声明的近似:未列出的方法返回 unknown-result
2001
+ // 函数值,让 `X.prototype.m.call(...)` 反射惯用法继续求值。
2002
+ const knownInstanceMethods: Record<string, TypeValue> = {
2003
+ values: T.fnSig([], T.array(T.unknown)),
2004
+ keys: T.fnSig([], T.array(T.unknown)),
2005
+ entries: T.fnSig([], T.array(T.tuple([T.unknown, T.unknown]))),
2006
+ get: T.fnSig([T.unknown], T.unknown),
2007
+ has: T.fnSig([T.unknown], T.boolean),
2008
+ add: T.fnSig([T.unknown], T.unknown),
2009
+ set: T.fnSig([T.unknown, T.unknown], T.unknown),
2010
+ delete: T.fnSig([T.unknown], T.boolean),
2011
+ forEach: T.fnSig([T.unknown], T.undefined),
2012
+ call: T.fnSig([T.unknown], T.unknown),
2013
+ apply: T.fnSig([T.unknown], T.unknown),
2014
+ bind: T.fnSig([T.unknown], T.unknown),
2015
+ };
2016
+ // hasOwnProp:普通 [] 访问会命中 JS 原型链(valueOf/hasOwnProperty
2017
+ // 等),把原生函数当 TypeValue 泄漏进类型系统
2018
+ const knownFn = hasOwnProp(knownInstanceMethods, propName)
2019
+ ? knownInstanceMethods[propName]
2020
+ : undefined;
2021
+ if (knownFn) return knownFn;
2022
+ return T.fnSig([T.unknown], T.unknown);
2023
+ }
2024
+ if (propName === "length" && (obj.kind === "array" || obj.kind === "tuple")) {
2025
+ return obj.kind === "tuple" ? T.literal(obj.elements.length) : T.number;
2026
+ }
2027
+ if (obj.kind === "array" || obj.kind === "tuple") {
2028
+ // Feature-detection probes (`if (value.toJSON) …`) read arrays
2029
+ // too: a missed named property is undefined (like objects),
2030
+ // never unknown — unknown here poisons guarded unions.
2031
+ return T.undefined;
697
2032
  }
2033
+ if (propName === "length" && obj.kind === "literal" && typeof obj.value === "string") {
2034
+ return T.literal(obj.value.length);
2035
+ }
2036
+ if (propName === "length" && obj.kind === "primitive" && obj.type === "string") {
2037
+ return T.number;
2038
+ }
2039
+ // Promise receivers carry Object.prototype members (valueOf
2040
+ // returns the receiver itself) plus then/catch/finally — the
2041
+ // same approximation the instance branch uses. Without this, a
2042
+ // Promise-typed union member poisons `x.valueOf` to unknown.
2043
+ if (obj.kind === "promise") {
2044
+ const protoFn = hasOwnProp(OBJECT_PROTOTYPE_METHODS, propName)
2045
+ ? OBJECT_PROTOTYPE_METHODS[propName]
2046
+ : undefined;
2047
+ if (protoFn) return bindObjectProtoMethod(protoFn, obj);
2048
+ const promiseMethods = BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS.Promise;
2049
+ if (promiseMethods && hasOwnProp(promiseMethods, propName)) {
2050
+ return promiseMethods[propName];
2051
+ }
2052
+ }
2053
+ // Primitive receivers auto-box through their wrapper prototype
2054
+ // ('x'.constructor === String): consult it before reporting a miss.
2055
+ // A wrapper miss on a wrapper-able primitive (string/number/
2056
+ // boolean/symbol) is definite absence — undefined, not unknown —
2057
+ // so `if (value.toJSON)` feature probes resolve their false
2058
+ // branch (mirrors the array treatment above).
2059
+ if (
2060
+ obj.kind === "primitive" ||
2061
+ (obj.kind === "literal" &&
2062
+ (typeof obj.value === "string" || typeof obj.value === "number" || typeof obj.value === "boolean"))
2063
+ ) {
2064
+ const wrapper = wrapperPrototypeMember(obj, propName);
2065
+ return wrapper !== undefined ? wrapper : T.undefined;
2066
+ }
2067
+ const wrapper = wrapperPrototypeMember(obj, propName);
2068
+ if (wrapper !== undefined) return wrapper;
2069
+ if (obj.kind === "refined") {
2070
+ const result = dispatchProperty(obj, propName);
2071
+ if (result !== undefined) return result;
2072
+ }
2073
+ propMissed = true;
698
2074
  return T.unknown;
699
2075
  });
2076
+ if (propMissed) {
2077
+ recordUnknown({
2078
+ kind: "property",
2079
+ name: propName,
2080
+ receiverType: objVal,
2081
+ loc: node.loc,
2082
+ reason: `no property '${propName}' on ${objVal.kind}`,
2083
+ });
2084
+ }
2085
+ return result;
2086
+ }
2087
+
2088
+ return T.unknown;
2089
+ }
2090
+
2091
+ case "OptionalMemberExpression": {
2092
+ const objVal = evaluate(node.object, env);
2093
+ if (isReturn(objVal) || isBranch(objVal) || isThrow(objVal)) return objVal;
2094
+
2095
+ // If object is null or undefined, short-circuit to undefined
2096
+ if (objVal.kind === "literal" && (objVal.value === null || objVal.value === undefined)) {
2097
+ return T.undefined;
2098
+ }
2099
+ (node as any)._receiverVal = objVal;
2100
+
2101
+ // Otherwise, evaluate like a normal member expression
2102
+ if (node.computed) {
2103
+ const propVal = evaluate(node.property, env);
2104
+ if (isReturn(propVal) || isBranch(propVal) || isThrow(propVal)) return propVal;
2105
+ return evaluateComputedMemberAccess(objVal, propVal, node);
700
2106
  }
701
2107
 
702
2108
  if (node.property.type === "Identifier") {
703
2109
  const propName = node.property.name;
704
- return distributeOverUnion(objVal, (obj) => {
705
- if (obj.kind === "object") return obj.properties[propName] ?? T.undefined;
706
- if (obj.kind === "instance") return obj.properties[propName] ?? T.undefined;
2110
+ let propMissed = false;
2111
+ const result = distributeOverUnion(objVal, (obj) => {
2112
+ // `x.valueOf` off non-object receivers (literals, primitives,
2113
+ // arrays, functions) — see valueOfBinding.
2114
+ if (propName === "valueOf") {
2115
+ const bound = valueOfBinding(obj);
2116
+ if (bound !== undefined) return bound;
2117
+ }
2118
+ // `X.prototype` on a constructible built-in resolves to the cached
2119
+ // singleton instance of X (stable identity across evaluations)
2120
+ const optBuiltinName = (obj as any)._builtinName as string | undefined;
2121
+ if (propName === "prototype" && optBuiltinName && BUILTIN_PROTOTYPE_CLASSES.has(optBuiltinName)) {
2122
+ return builtinPrototype(optBuiltinName);
2123
+ }
2124
+ if (obj.kind === "object") {
2125
+ const ownVal = hasOwnProp(obj.properties, propName) ? obj.properties[propName] : undefined;
2126
+ if (ownVal) return ownVal;
2127
+ // Every object inherits Object.prototype members (see table note).
2128
+ const protoFn = hasOwnProp(OBJECT_PROTOTYPE_METHODS, propName)
2129
+ ? OBJECT_PROTOTYPE_METHODS[propName]
2130
+ : undefined;
2131
+ if (protoFn) return bindObjectProtoMethod(protoFn, obj);
2132
+ return T.undefined;
2133
+ }
2134
+ if (obj.kind === "instance") {
2135
+ // Own-property lookup only (see MemberExpression note): a plain
2136
+ // `{}` properties record would otherwise leak native
2137
+ // Object.prototype members into the type system.
2138
+ const own = hasOwnProp(obj.properties, propName) ? obj.properties[propName] : undefined;
2139
+ if (own) return own;
2140
+ // Instances inherit Object.prototype members too: keeps
2141
+ // `Object.prototype.hasOwnProperty` (and the toString brand-check
2142
+ // idiom) typeable instead of degrading to undefined.
2143
+ const protoFn = hasOwnProp(OBJECT_PROTOTYPE_METHODS, propName)
2144
+ ? OBJECT_PROTOTYPE_METHODS[propName]
2145
+ : undefined;
2146
+ if (protoFn) return bindObjectProtoMethod(protoFn, obj);
2147
+ // `x.constructor` resolves to the class namespace: instances
2148
+ // and prototype singletons share the memoized builtinClassValue
2149
+ // (Array.prototype.constructor === Array), so reflection like
2150
+ // `new (Object.getPrototypeOf(x).constructor)(n)` keeps going.
2151
+ if (propName === "constructor") return builtinClassValue(obj.className);
2152
+ // instance 的方法集是声明的近似:未列出的方法返回 unknown-result
2153
+ // 函数值,让 `X.prototype.m.call(...)` 反射惯用法继续求值。
2154
+ const knownInstanceMethods: Record<string, TypeValue> = {
2155
+ values: T.fnSig([], T.array(T.unknown)),
2156
+ keys: T.fnSig([], T.array(T.unknown)),
2157
+ entries: T.fnSig([], T.array(T.tuple([T.unknown, T.unknown]))),
2158
+ get: T.fnSig([T.unknown], T.unknown),
2159
+ has: T.fnSig([T.unknown], T.boolean),
2160
+ add: T.fnSig([T.unknown], T.unknown),
2161
+ set: T.fnSig([T.unknown, T.unknown], T.unknown),
2162
+ delete: T.fnSig([T.unknown], T.boolean),
2163
+ forEach: T.fnSig([T.unknown], T.undefined),
2164
+ call: T.fnSig([T.unknown], T.unknown),
2165
+ apply: T.fnSig([T.unknown], T.unknown),
2166
+ bind: T.fnSig([T.unknown], T.unknown),
2167
+ };
2168
+ // hasOwnProp:普通 [] 访问会命中 JS 原型链(valueOf/hasOwnProperty
2169
+ // 等),把原生函数当 TypeValue 泄漏进类型系统
2170
+ const knownFn = hasOwnProp(knownInstanceMethods, propName)
2171
+ ? knownInstanceMethods[propName]
2172
+ : undefined;
2173
+ if (knownFn) return knownFn;
2174
+ return T.fnSig([T.unknown], T.unknown);
2175
+ }
707
2176
  if (propName === "length" && (obj.kind === "array" || obj.kind === "tuple")) {
708
2177
  return obj.kind === "tuple" ? T.literal(obj.elements.length) : T.number;
709
2178
  }
2179
+ if (obj.kind === "array" || obj.kind === "tuple") {
2180
+ // Feature-detection probes (`if (value.toJSON) …`) read arrays
2181
+ // too: a missed named property is undefined (like objects),
2182
+ // never unknown — unknown here poisons guarded unions.
2183
+ return T.undefined;
2184
+ }
710
2185
  if (propName === "length" && obj.kind === "literal" && typeof obj.value === "string") {
711
2186
  return T.literal(obj.value.length);
712
2187
  }
713
2188
  if (propName === "length" && obj.kind === "primitive" && obj.type === "string") {
714
2189
  return T.number;
715
2190
  }
2191
+ // Promise receivers carry Object.prototype members (valueOf
2192
+ // returns the receiver itself) plus then/catch/finally — the
2193
+ // same approximation the instance branch uses. Without this, a
2194
+ // Promise-typed union member poisons `x.valueOf` to unknown.
2195
+ if (obj.kind === "promise") {
2196
+ const protoFn = hasOwnProp(OBJECT_PROTOTYPE_METHODS, propName)
2197
+ ? OBJECT_PROTOTYPE_METHODS[propName]
2198
+ : undefined;
2199
+ if (protoFn) return bindObjectProtoMethod(protoFn, obj);
2200
+ const promiseMethods = BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS.Promise;
2201
+ if (promiseMethods && hasOwnProp(promiseMethods, propName)) {
2202
+ return promiseMethods[propName];
2203
+ }
2204
+ }
2205
+ // Primitive receivers auto-box through their wrapper prototype
2206
+ // ('x'.constructor === String): consult it before reporting a miss.
2207
+ // A wrapper miss on a wrapper-able primitive (string/number/
2208
+ // boolean/symbol) is definite absence — undefined, not unknown —
2209
+ // so `if (value.toJSON)` feature probes resolve their false
2210
+ // branch (mirrors the array treatment above).
2211
+ if (
2212
+ obj.kind === "primitive" ||
2213
+ (obj.kind === "literal" &&
2214
+ (typeof obj.value === "string" || typeof obj.value === "number" || typeof obj.value === "boolean"))
2215
+ ) {
2216
+ const wrapper = wrapperPrototypeMember(obj, propName);
2217
+ return wrapper !== undefined ? wrapper : T.undefined;
2218
+ }
2219
+ const wrapper = wrapperPrototypeMember(obj, propName);
2220
+ if (wrapper !== undefined) return wrapper;
716
2221
  if (obj.kind === "refined") {
717
2222
  const result = dispatchProperty(obj, propName);
718
2223
  if (result !== undefined) return result;
719
2224
  }
2225
+ propMissed = true;
720
2226
  return T.unknown;
721
2227
  });
2228
+ if (propMissed) {
2229
+ recordUnknown({
2230
+ kind: "property",
2231
+ name: propName,
2232
+ receiverType: objVal,
2233
+ loc: node.loc,
2234
+ reason: `no property '${propName}' on ${objVal.kind}`,
2235
+ });
2236
+ }
2237
+ return result;
722
2238
  }
723
2239
 
724
2240
  return T.unknown;
@@ -745,11 +2261,53 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
745
2261
  if (isReturn(val) || isBranch(val) || isThrow(val)) return val;
746
2262
  props[key] = val;
747
2263
  }
2264
+ } else if (prop.type === "ObjectMethod") {
2265
+ // Handle shorthand method syntax: { method() { ... } }
2266
+ const key = prop.key.type === "Identifier"
2267
+ ? prop.key.name
2268
+ : prop.key.type === "StringLiteral"
2269
+ ? prop.key.value
2270
+ : null;
2271
+ if (key) {
2272
+ const params = prop.params.map((p: Node) => {
2273
+ if (p.type === "Identifier") return p.name;
2274
+ if (p.type === "RestElement" && p.argument.type === "Identifier") return `...${p.argument.name}`;
2275
+ return `__param`;
2276
+ });
2277
+ props[key] = T.fn(params, prop.body, env);
2278
+ }
748
2279
  } else if (prop.type === "SpreadElement") {
749
2280
  const spreadVal = evaluate(prop.argument, env);
750
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
+ }
751
2291
  if (spreadVal.kind === "object") {
752
2292
  Object.assign(props, spreadVal.properties);
2293
+ } else if (spreadVal.kind === "union") {
2294
+ const objectMembers = spreadVal.members.filter((m: TypeValue) => m.kind === "object");
2295
+ if (objectMembers.length > 0) {
2296
+ const allKeys = new Set<string>();
2297
+ for (const m of objectMembers) {
2298
+ if (m.kind === "object") {
2299
+ for (const k of Object.keys(m.properties)) allKeys.add(k);
2300
+ }
2301
+ }
2302
+ for (const key of allKeys) {
2303
+ const values = objectMembers
2304
+ .filter((m: TypeValue) => m.kind === "object" && key in (m as any).properties)
2305
+ .map((m: TypeValue) => (m as any).properties[key]);
2306
+ if (values.length > 0) {
2307
+ props[key] = simplifyUnion(values);
2308
+ }
2309
+ }
2310
+ }
753
2311
  }
754
2312
  }
755
2313
  }
@@ -763,194 +2321,736 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
763
2321
  elements.push(T.undefined);
764
2322
  continue;
765
2323
  }
766
- if (elem.type === "SpreadElement") {
767
- const spreadVal = evaluate(elem.argument, env);
768
- if (isReturn(spreadVal) || isBranch(spreadVal) || isThrow(spreadVal)) return spreadVal;
769
- if (spreadVal.kind === "tuple") {
770
- elements.push(...spreadVal.elements);
771
- } else if (spreadVal.kind === "array") {
772
- return T.array(simplifyUnion([...elements, spreadVal.element]));
773
- } else {
774
- elements.push(T.unknown);
775
- }
776
- continue;
2324
+ if (elem.type === "SpreadElement") {
2325
+ const spreadVal = evaluate(elem.argument, env);
2326
+ if (isReturn(spreadVal) || isBranch(spreadVal) || isThrow(spreadVal)) return spreadVal;
2327
+ if (spreadVal.kind === "tuple") {
2328
+ elements.push(...spreadVal.elements);
2329
+ } else if (spreadVal.kind === "array") {
2330
+ return T.array(simplifyUnion([...elements, spreadVal.element]));
2331
+ } else {
2332
+ // Set/Map instances spread through their exact side tables
2333
+ // (`[...new Set([1,2])]` → [1, 2]) or their element typeArgs.
2334
+ const iterable = iterableSpreadValue(spreadVal);
2335
+ if (iterable) {
2336
+ if (iterable.kind === "tuple") {
2337
+ elements.push(...iterable.elements);
2338
+ } else if (iterable.kind === "array") {
2339
+ return T.array(simplifyUnion([...elements, iterable.element]));
2340
+ } else {
2341
+ elements.push(T.unknown);
2342
+ }
2343
+ } else {
2344
+ elements.push(T.unknown);
2345
+ }
2346
+ }
2347
+ continue;
2348
+ }
2349
+ const val = evaluate(elem as Node, env);
2350
+ if (isReturn(val) || isBranch(val) || isThrow(val)) return val;
2351
+ elements.push(val);
2352
+ }
2353
+ return T.tuple(elements);
2354
+ }
2355
+
2356
+ case "ThrowStatement": {
2357
+ const argVal = node.argument ? evaluate(node.argument, env) : T.undefined;
2358
+ if (isReturn(argVal) || isBranch(argVal) || isThrow(argVal)) return argVal;
2359
+ const throwLoc = node.loc ? {
2360
+ start: { line: node.loc.start.line, column: node.loc.start.column },
2361
+ end: { line: node.loc.end.line, column: node.loc.end.column },
2362
+ } : undefined;
2363
+ return makeThrow(argVal, throwLoc);
2364
+ }
2365
+
2366
+ case "BreakStatement":
2367
+ return makeBreak();
2368
+
2369
+ case "ContinueStatement":
2370
+ return makeContinue();
2371
+
2372
+ case "TryStatement": {
2373
+ return evaluateTryStatement(node, env);
2374
+ }
2375
+
2376
+ case "NewExpression": {
2377
+ return evaluateNewExpression(node, env);
2378
+ }
2379
+
2380
+ case "SwitchStatement": {
2381
+ return evaluateSwitchStatement(node, env);
2382
+ }
2383
+
2384
+ case "UpdateExpression": {
2385
+ if (node.argument.type === "Identifier") {
2386
+ const current = env.lookup(node.argument.name);
2387
+ if (current.kind === "literal" && typeof current.value === "number") {
2388
+ const newVal = node.operator === "++"
2389
+ ? T.literal(current.value + 1)
2390
+ : T.literal(current.value - 1);
2391
+ if (!env.update(node.argument.name, newVal)) {
2392
+ env.bind(node.argument.name, newVal);
2393
+ }
2394
+ return node.prefix ? newVal : current;
2395
+ }
2396
+ if (!env.update(node.argument.name, T.number)) {
2397
+ env.bind(node.argument.name, T.number);
2398
+ }
2399
+ return T.number;
2400
+ }
2401
+ return T.number;
2402
+ }
2403
+
2404
+ default:
2405
+ return T.unknown;
2406
+ }
2407
+ }
2408
+
2409
+ function getMemberKey(node: Node & { type: "MemberExpression" }, env: Environment): string | null {
2410
+ if (!node.computed && node.property.type === "Identifier") {
2411
+ return node.property.name;
2412
+ }
2413
+ if (node.computed) {
2414
+ const propVal = evaluate(node.property, env);
2415
+ if (!isReturn(propVal) && !isBranch(propVal) && !isThrow(propVal) && propVal.kind === "literal") {
2416
+ return String(propVal.value);
2417
+ }
2418
+ }
2419
+ return null;
2420
+ }
2421
+
2422
+ function bindPattern(pattern: Node, value: TypeValue, env: Environment): void {
2423
+ if (pattern.type === "Identifier") {
2424
+ env.bind(pattern.name, value);
2425
+ return;
2426
+ }
2427
+
2428
+ if (pattern.type === "RestElement") {
2429
+ // Handle rest parameters: (...args) => ...
2430
+ // The value should be a tuple of all remaining arguments
2431
+ if (pattern.argument.type === "Identifier") {
2432
+ env.bind(pattern.argument.name, value);
2433
+ }
2434
+ return;
2435
+ }
2436
+
2437
+ if (pattern.type === "AssignmentPattern") {
2438
+ const defaultVal = evaluate(pattern.right, env);
2439
+ const resolved = (value.kind === "literal" && value.value === undefined)
2440
+ ? (!isReturn(defaultVal) && !isBranch(defaultVal) && !isThrow(defaultVal) ? defaultVal : T.unknown)
2441
+ : value;
2442
+ bindPattern(pattern.left, resolved, env);
2443
+ return;
2444
+ }
2445
+
2446
+ if (pattern.type === "ObjectPattern") {
2447
+ const restKeys: string[] = [];
2448
+ for (const prop of pattern.properties) {
2449
+ if (prop.type === "RestElement") {
2450
+ if (value.kind === "object") {
2451
+ const remaining: Record<string, TypeValue> = {};
2452
+ for (const [k, v] of Object.entries(value.properties)) {
2453
+ if (!restKeys.includes(k)) remaining[k] = v;
2454
+ }
2455
+ bindPattern(prop.argument, T.object(remaining), env);
2456
+ } else {
2457
+ bindPattern(prop.argument, T.object({}), env);
2458
+ }
2459
+ continue;
2460
+ }
2461
+ if (prop.type !== "ObjectProperty") continue;
2462
+ const key = prop.key.type === "Identifier"
2463
+ ? prop.key.name
2464
+ : prop.key.type === "StringLiteral"
2465
+ ? prop.key.value
2466
+ : null;
2467
+ if (!key) continue;
2468
+ restKeys.push(key);
2469
+ // Own-property guard first (a plain record would leak native JS
2470
+ // functions for names like "hasOwnProperty"), then Object.prototype
2471
+ // fallback — real destructuring reads through the prototype chain.
2472
+ const propVal = value.kind === "object"
2473
+ ? (hasOwnProp(value.properties, key)
2474
+ ? value.properties[key]
2475
+ : (hasOwnProp(OBJECT_PROTOTYPE_METHODS, key) ? bindObjectProtoMethod(OBJECT_PROTOTYPE_METHODS[key], value) : T.undefined))
2476
+ : T.unknown;
2477
+ bindPattern(prop.value as Node, propVal, env);
2478
+ }
2479
+ return;
2480
+ }
2481
+
2482
+ if (pattern.type === "ArrayPattern") {
2483
+ for (let i = 0; i < pattern.elements.length; i++) {
2484
+ const elem = pattern.elements[i];
2485
+ if (!elem) continue;
2486
+ if (elem.type === "RestElement") {
2487
+ if (value.kind === "tuple") {
2488
+ bindPattern(elem.argument, T.tuple(value.elements.slice(i)), env);
2489
+ } else if (value.kind === "array") {
2490
+ bindPattern(elem.argument, value, env);
2491
+ } else {
2492
+ bindPattern(elem.argument, T.tuple([]), env);
2493
+ }
2494
+ continue;
2495
+ }
2496
+ const elemVal = value.kind === "tuple"
2497
+ ? (value.elements[i] ?? T.undefined)
2498
+ : value.kind === "array"
2499
+ ? value.element
2500
+ : T.unknown;
2501
+ bindPattern(elem, elemVal, env);
2502
+ }
2503
+ return;
2504
+ }
2505
+ }
2506
+
2507
+ function evaluateArgs(args: Node[], env: Environment): TypeValue[] | ReturnSignal | BranchSignal | ThrowSignal {
2508
+ const result: TypeValue[] = [];
2509
+ for (const arg of args) {
2510
+ if (arg.type === "SpreadElement") {
2511
+ const spreadVal = evaluate(arg.argument, env);
2512
+ if (isReturn(spreadVal) || isBranch(spreadVal) || isThrow(spreadVal)) return spreadVal;
2513
+ if (spreadVal.kind === "tuple") {
2514
+ result.push(...spreadVal.elements);
2515
+ } else if (spreadVal.kind === "array") {
2516
+ result.push(spreadVal.element);
2517
+ } else {
2518
+ // Set/Map instances spread through their exact side tables or
2519
+ // their element typeArgs (one element, like the array case).
2520
+ const iterable = iterableSpreadValue(spreadVal);
2521
+ if (iterable) {
2522
+ if (iterable.kind === "tuple") {
2523
+ result.push(...iterable.elements);
2524
+ } else if (iterable.kind === "array") {
2525
+ result.push(iterable.element);
2526
+ } else {
2527
+ result.push(T.unknown);
2528
+ }
2529
+ } else {
2530
+ result.push(T.unknown);
2531
+ }
2532
+ }
2533
+ continue;
2534
+ }
2535
+ const v = evaluate(arg, env);
2536
+ if (isReturn(v) || isBranch(v) || isThrow(v)) return v;
2537
+ if (_provenanceEnabled && arg.loc) {
2538
+ _originMap.set(v, { line: arg.loc.start.line, column: arg.loc.start.column });
2539
+ }
2540
+ result.push(v);
2541
+ }
2542
+ return result;
2543
+ }
2544
+
2545
+ function evaluateBuiltinCall(
2546
+ name: string,
2547
+ args: Node[],
2548
+ env: Environment,
2549
+ ): EvalResult | null {
2550
+ // Type conversion functions
2551
+ if (name === "String" || name === "Number" || name === "Boolean") {
2552
+ const argVals = evaluateArgs(args, env);
2553
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2554
+ if (argVals.length === 0) {
2555
+ if (name === "String") return T.literal("");
2556
+ if (name === "Number") return T.literal(0);
2557
+ if (name === "Boolean") return T.literal(false);
2558
+ }
2559
+ // For literals, try to convert
2560
+ if (argVals.length === 1) {
2561
+ const arg = argVals[0];
2562
+ if (name === "String") {
2563
+ if (arg.kind === "literal") return T.literal(String(arg.value));
2564
+ return T.string;
2565
+ }
2566
+ if (name === "Number") {
2567
+ if (arg.kind === "literal") {
2568
+ // Number() converts literals to their numeric value
2569
+ if (arg.value === null) return T.literal(0);
2570
+ if (arg.value === undefined) return T.literal(NaN);
2571
+ if (typeof arg.value === "boolean") return T.literal(arg.value ? 1 : 0);
2572
+ if (typeof arg.value === "number") return arg;
2573
+ if (typeof arg.value === "string") {
2574
+ const num = Number(arg.value);
2575
+ if (!isNaN(num)) return T.literal(num);
2576
+ return T.literal(NaN);
2577
+ }
2578
+ }
2579
+ return T.number;
2580
+ }
2581
+ if (name === "Boolean") {
2582
+ if (arg.kind === "literal") return T.literal(Boolean(arg.value));
2583
+ return T.boolean;
2584
+ }
2585
+ }
2586
+ if (name === "String") return T.string;
2587
+ if (name === "Number") return T.number;
2588
+ if (name === "Boolean") return T.boolean;
2589
+ }
2590
+
2591
+ // parseInt and parseFloat
2592
+ if (name === "parseInt" || name === "parseFloat") {
2593
+ const argVals = evaluateArgs(args, env);
2594
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2595
+ return T.number;
2596
+ }
2597
+
2598
+ // isNaN and isFinite
2599
+ if (name === "isNaN" || name === "isFinite") {
2600
+ const argVals = evaluateArgs(args, env);
2601
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2602
+ return T.boolean;
2603
+ }
2604
+
2605
+ // encodeURIComponent, decodeURIComponent, encodeURI, decodeURI
2606
+ if (name === "encodeURIComponent" || name === "decodeURIComponent" ||
2607
+ name === "encodeURI" || name === "decodeURI") {
2608
+ const argVals = evaluateArgs(args, env);
2609
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2610
+ return T.string;
2611
+ }
2612
+
2613
+ // Math functions (accessed via member expression, not here)
2614
+ if (name === "Math") {
2615
+ return null;
2616
+ }
2617
+
2618
+ // console functions (accessed via member expression, not here)
2619
+ if (name === "console") {
2620
+ return null;
2621
+ }
2622
+
2623
+ // fetch global function
2624
+ if (name === "fetch") {
2625
+ return T.promise(createResponseType());
2626
+ }
2627
+
2628
+ return null;
2629
+ }
2630
+
2631
+ // --- Function.prototype.call / apply / bind ---
2632
+ // When the receiver of a method call is itself a function value,
2633
+ // `f.call(thisArg, ...args)` / `f.apply(thisArg, argsArray)` re-invoke that
2634
+ // function with the args following the leading thisArg; `f.bind(...)`
2635
+ // approximates to the original function value. Non-function receivers keep
2636
+ // the existing fallback.
2637
+ function evaluateFunctionPrototypeMethod(
2638
+ fnVal: TypeValue & { kind: "function" },
2639
+ methodName: string,
2640
+ argVals: TypeValue[],
2641
+ ): EvalResult | null {
2642
+ if (methodName === "bind") return fnVal;
2643
+ if (methodName === "call") {
2644
+ const full = callFunctionFull(fnVal, argVals.slice(1), argVals[0]);
2645
+ // A definitely-throwing callee (e.g. `null.valueOf` binding) must raise
2646
+ // a ThrowSignal, not degrade to never — enclosing try/catch depends on it.
2647
+ if (full.value.kind === "never" && full.throws.kind !== "never") {
2648
+ return makeThrow(full.throws, full.throwLoc);
2649
+ }
2650
+ return full.value;
2651
+ }
2652
+ if (methodName === "apply") {
2653
+ const listArg = argVals[1];
2654
+ let spreadArgs: TypeValue[];
2655
+ if (listArg?.kind === "tuple") {
2656
+ spreadArgs = listArg.elements;
2657
+ } else {
2658
+ // Unknown-length array (or non-array): approximate with the element
2659
+ // type (or unknown) repeated to the callee's arity.
2660
+ const el = listArg?.kind === "array" ? listArg.element : T.unknown;
2661
+ spreadArgs = fnVal.params.map(() => el);
2662
+ }
2663
+ const full = callFunctionFull(fnVal, spreadArgs, argVals[0]);
2664
+ if (full.value.kind === "never" && full.throws.kind !== "never") {
2665
+ return makeThrow(full.throws, full.throwLoc);
2666
+ }
2667
+ return full.value;
2668
+ }
2669
+ return null;
2670
+ }
2671
+
2672
+ function evaluateMethodForMember(
2673
+ objVal: TypeValue,
2674
+ methodName: string,
2675
+ argVals: TypeValue[],
2676
+ callee: Node & { type: "MemberExpression" },
2677
+ env: Environment,
2678
+ ): EvalResult | null {
2679
+ // Function.prototype.call/apply/bind on function-valued union members
2680
+ if (objVal.kind === "function") {
2681
+ const fnProto = evaluateFunctionPrototypeMethod(objVal, methodName, argVals);
2682
+ if (fnProto !== null) return fnProto;
2683
+ }
2684
+
2685
+ // Promise instance methods
2686
+ if (objVal.kind === "promise") {
2687
+ if (methodName === "then") {
2688
+ const chained = applyPromiseThenCallback(objVal, argVals);
2689
+ if (chained !== null) return chained;
2690
+ }
2691
+ const result = evaluatePromiseInstanceMethod(objVal, methodName, argVals);
2692
+ if (result !== null) return result;
2693
+ }
2694
+
2695
+ // Instance methods (Map, Set, RegExp, etc.)
2696
+ if (objVal.kind === "instance") {
2697
+ const classMethods: Record<string, Record<string, (...args: TypeValue[]) => TypeValue>> = {
2698
+ Map: MAP_INSTANCE_METHODS,
2699
+ Set: SET_INSTANCE_METHODS,
2700
+ RegExp: REGEXP_INSTANCE_METHODS,
2701
+ };
2702
+ const methods = classMethods[objVal.className];
2703
+ if (methods) {
2704
+ const method = methods[methodName];
2705
+ if (method) return method(...argVals, objVal);
2706
+ }
2707
+ }
2708
+
2709
+ // Array/tuple methods(union 分布路径):与主路径共用 evaluateArrayMethodValues
2710
+ // ——实参在此已是 TypeValue,回调类 map/filter/reduce 等同样从函数值签名
2711
+ // 求值(mock 箭头 / 具名函数 / 内联箭头一致),不再保守降级 unknown。
2712
+ if (objVal.kind === "array" || objVal.kind === "tuple") {
2713
+ return evaluateArrayMethodValues(objVal, methodName, argVals);
2714
+ }
2715
+
2716
+ // String methods
2717
+ if (isStringLike(objVal)) {
2718
+ return evaluateStringMethod(objVal, methodName, argVals);
2719
+ }
2720
+
2721
+ return null;
2722
+ }
2723
+
2724
+ function evaluateMethodCall(
2725
+ callee: Node & { type: "MemberExpression" },
2726
+ args: Node[],
2727
+ env: Environment,
2728
+ ): EvalResult | null {
2729
+ const objVal = evaluate(callee.object, env);
2730
+ if (isReturn(objVal) || isBranch(objVal) || isThrow(objVal)) return objVal;
2731
+ const methodName = !callee.computed && callee.property.type === "Identifier"
2732
+ ? callee.property.name
2733
+ : null;
2734
+ if (!methodName) return null;
2735
+
2736
+ // Distribute method calls over union types
2737
+ if (objVal.kind === "union") {
2738
+ const argVals = evaluateArgs(args, env);
2739
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2740
+ let methodMissed = false;
2741
+ const result = distributeOverUnion(objVal, (member) => {
2742
+ // Create a temporary env with the member bound, then re-evaluate the method call
2743
+ const memberResult = evaluateMethodForMember(member, methodName, argVals as TypeValue[], callee, env);
2744
+ if (
2745
+ memberResult === null &&
2746
+ member.kind !== "object" && member.kind !== "instance" &&
2747
+ member.kind !== "refined" && member.kind !== "promise"
2748
+ ) {
2749
+ methodMissed = true;
2750
+ }
2751
+ // A throwing member contributes no value; return/branch signals
2752
+ // contribute their carried value (the potential throw is dropped
2753
+ // here; the single-callee path raises it properly).
2754
+ if (memberResult !== null && typeof memberResult === "object") {
2755
+ if (THROW_SIGNAL in memberResult) return T.never;
2756
+ if (RETURN_SIGNAL in memberResult) return memberResult.value;
2757
+ if (BRANCH_SIGNAL in memberResult) return memberResult.returnedValue;
2758
+ }
2759
+ return memberResult ?? T.unknown;
2760
+ });
2761
+ if (methodMissed) {
2762
+ recordUnknown({
2763
+ kind: "method",
2764
+ name: methodName,
2765
+ receiverType: objVal,
2766
+ loc: callee.loc,
2767
+ reason: `no method '${methodName}' on ${objVal.kind}`,
2768
+ });
2769
+ }
2770
+ return result;
2771
+ }
2772
+
2773
+ // Function.prototype.call/apply/bind re-invoking a function value
2774
+ if (objVal.kind === "function") {
2775
+ const argVals = evaluateArgs(args, env);
2776
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2777
+ const fnProto = evaluateFunctionPrototypeMethod(objVal, methodName, argVals as TypeValue[]);
2778
+ if (fnProto !== null) return fnProto;
2779
+ }
2780
+
2781
+ // Handle console methods (no return value)
2782
+ if (callee.object.type === "Identifier" && callee.object.name === "console" && !env.has("console")) {
2783
+ const argVals = evaluateArgs(args, env);
2784
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2785
+ return T.undefined;
2786
+ }
2787
+
2788
+ // Handle Math methods
2789
+ if (callee.object.type === "Identifier" && callee.object.name === "Math" && !env.has("Math")) {
2790
+ const argVals = evaluateArgs(args, env);
2791
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2792
+ // Math methods return numbers
2793
+ if (["abs", "ceil", "floor", "round", "sqrt", "pow", "min", "max",
2794
+ "random", "log", "log2", "log10", "exp", "sin", "cos", "tan",
2795
+ "asin", "acos", "atan", "atan2"].includes(methodName)) {
2796
+ return T.number;
2797
+ }
2798
+ // Math constants
2799
+ if (["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT1_2", "SQRT2"].includes(methodName)) {
2800
+ return T.number;
2801
+ }
2802
+ return T.number;
2803
+ }
2804
+
2805
+ // Handle Date methods
2806
+ if (callee.object.type === "Identifier" && callee.object.name === "Date" && !env.has("Date")) {
2807
+ const argVals = evaluateArgs(args, env);
2808
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2809
+ // Date static methods
2810
+ if (["now", "parse", "UTC"].includes(methodName)) {
2811
+ return T.number;
2812
+ }
2813
+ // Date constructor
2814
+ if (methodName === "constructor") {
2815
+ return T.instanceOf("Date", {
2816
+ getTime: T.fn(["...args"], { type: "BlockStatement", body: [] } as any, undefined as any),
2817
+ getFullYear: T.fn(["...args"], { type: "BlockStatement", body: [] } as any, undefined as any),
2818
+ toISOString: T.fn(["...args"], { type: "BlockStatement", body: [] } as any, undefined as any),
2819
+ });
2820
+ }
2821
+ return T.unknown;
2822
+ }
2823
+
2824
+ // Handle JSON methods
2825
+ if (callee.object.type === "Identifier" && callee.object.name === "JSON" && !env.has("JSON")) {
2826
+ const argVals = evaluateArgs(args, env);
2827
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2828
+ // JSON.parse: the table entry is a fnSig whose impl decodes literal
2829
+ // string arguments precisely (jsonToTypeValue); non-literal / invalid /
2830
+ // reviver-bearing calls fall back to its unknown return type.
2831
+ if (methodName === "parse") {
2832
+ return callFunction(
2833
+ (BUILTIN_STATIC_METHODS.JSON as Record<string, TypeValue>).parse as TypeValue & {
2834
+ kind: "function";
2835
+ },
2836
+ argVals as TypeValue[],
2837
+ );
2838
+ }
2839
+ // JSON.stringify returns string
2840
+ if (methodName === "stringify") {
2841
+ return T.string;
2842
+ }
2843
+ return T.unknown;
2844
+ }
2845
+
2846
+ // Handle Array methods (Array.from, Array.isArray, etc.)
2847
+ if (callee.object.type === "Identifier" && callee.object.name === "Array" && !env.has("Array")) {
2848
+ const argVals = evaluateArgs(args, env);
2849
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2850
+ if (methodName === "from") {
2851
+ if (argVals.length > 0) {
2852
+ const iterable = argVals[0];
2853
+ // Array.from(Set) -> array of Set's element type
2854
+ if (iterable.kind === "instance" && iterable.className === "Set") {
2855
+ const typeArgs = (iterable as any)._typeArgs;
2856
+ if (typeArgs?.T) return T.array(typeArgs.T);
2857
+ }
2858
+ // Array.from(tuple) -> tuple (preserve types)
2859
+ if (iterable.kind === "tuple") {
2860
+ return iterable;
2861
+ }
2862
+ // Array.from(array) -> array
2863
+ if (iterable.kind === "array") {
2864
+ return iterable;
777
2865
  }
778
- const val = evaluate(elem as Node, env);
779
- if (isReturn(val) || isBranch(val) || isThrow(val)) return val;
780
- elements.push(val);
781
2866
  }
782
- return T.tuple(elements);
2867
+ return T.array(T.unknown);
783
2868
  }
784
-
785
- case "ThrowStatement": {
786
- const argVal = node.argument ? evaluate(node.argument, env) : T.undefined;
787
- if (isReturn(argVal) || isBranch(argVal) || isThrow(argVal)) return argVal;
788
- const throwLoc = node.loc ? {
789
- start: { line: node.loc.start.line, column: node.loc.start.column },
790
- end: { line: node.loc.end.line, column: node.loc.end.column },
791
- } : undefined;
792
- return makeThrow(argVal, throwLoc);
2869
+ if (methodName === "isArray") {
2870
+ // Structurally-known receivers decide literally (arrays/tuples →
2871
+ // true, plain objects/instances-of-other-classes → false); unknown
2872
+ // receivers keep the symbolic boolean.
2873
+ return distributeOverUnion((argVals as TypeValue[])[0] ?? T.undefined, (v) => {
2874
+ const lit = arrayIsArrayLiteral(v);
2875
+ return lit === undefined ? T.boolean : T.literal(lit);
2876
+ });
793
2877
  }
2878
+ return T.unknown;
2879
+ }
794
2880
 
795
- case "TryStatement": {
796
- return evaluateTryStatement(node, env);
2881
+ // Handle Promise methods
2882
+ if (callee.object.type === "Identifier" && callee.object.name === "Promise" && !env.has("Promise")) {
2883
+ const argVals = evaluateArgs(args, env);
2884
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2885
+ const result = evaluatePromiseStaticMethod(methodName, argVals as TypeValue[]);
2886
+ if (result !== null) return result;
2887
+ return T.unknown;
2888
+ }
2889
+
2890
+ // Handle Symbol methods
2891
+ if (callee.object.type === "Identifier" && callee.object.name === "Symbol" && !env.has("Symbol")) {
2892
+ const argVals = evaluateArgs(args, env);
2893
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2894
+ if (methodName === "for") return T.symbol;
2895
+ if (methodName === "keyFor") return T.union(T.string, T.undefined);
2896
+ return T.unknown;
2897
+ }
2898
+
2899
+ // Handle Reflect methods
2900
+ if (callee.object.type === "Identifier" && callee.object.name === "Reflect" && !env.has("Reflect")) {
2901
+ const argVals = evaluateArgs(args, env);
2902
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2903
+ const method = (REFLECT_METHODS as Record<string, (...args: TypeValue[]) => TypeValue>)[methodName];
2904
+ if (method) return method(...(argVals as TypeValue[]));
2905
+ return T.unknown;
2906
+ }
2907
+
2908
+ // Handle Promise instance methods (.then, .catch, .finally)
2909
+ if (objVal.kind === "promise") {
2910
+ const argVals = evaluateArgs(args, env);
2911
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2912
+ if (methodName === "then") {
2913
+ const chained = applyPromiseThenCallback(objVal, argVals as TypeValue[]);
2914
+ if (chained !== null) return chained;
797
2915
  }
2916
+ const result = evaluatePromiseInstanceMethod(objVal, methodName, argVals as TypeValue[]);
2917
+ if (result !== null) return result;
2918
+ }
798
2919
 
799
- case "NewExpression": {
800
- return evaluateNewExpression(node, env);
2920
+ // Handle Map instance methods
2921
+ if (objVal.kind === "instance" && objVal.className === "Map") {
2922
+ const argVals = evaluateArgs(args, env);
2923
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2924
+ const method = MAP_INSTANCE_METHODS[methodName];
2925
+ if (method) {
2926
+ return method(...(argVals as TypeValue[]), objVal);
801
2927
  }
2928
+ }
802
2929
 
803
- case "SwitchStatement": {
804
- return evaluateSwitchStatement(node, env);
2930
+ // Handle Set instance methods
2931
+ if (objVal.kind === "instance" && objVal.className === "Set") {
2932
+ const argVals = evaluateArgs(args, env);
2933
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2934
+ const method = SET_INSTANCE_METHODS[methodName];
2935
+ if (method) {
2936
+ return method(...(argVals as TypeValue[]), objVal);
805
2937
  }
2938
+ }
806
2939
 
807
- case "UpdateExpression": {
808
- if (node.argument.type === "Identifier") {
809
- const current = env.lookup(node.argument.name);
810
- if (current.kind === "literal" && typeof current.value === "number") {
811
- const newVal = node.operator === "++"
812
- ? T.literal(current.value + 1)
813
- : T.literal(current.value - 1);
814
- if (!env.update(node.argument.name, newVal)) {
815
- env.bind(node.argument.name, newVal);
816
- }
817
- return node.prefix ? newVal : current;
818
- }
819
- if (!env.update(node.argument.name, T.number)) {
820
- env.bind(node.argument.name, T.number);
821
- }
822
- return T.number;
823
- }
824
- return T.number;
2940
+ // Handle RegExp instance methods
2941
+ if (objVal.kind === "instance" && objVal.className === "RegExp") {
2942
+ const argVals = evaluateArgs(args, env);
2943
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2944
+ const method = REGEXP_INSTANCE_METHODS[methodName];
2945
+ if (method) {
2946
+ return method(...(argVals as TypeValue[]), objVal);
825
2947
  }
2948
+ }
826
2949
 
827
- default:
828
- return T.unknown;
2950
+ // Handle URL instance methods
2951
+ if (objVal.kind === "instance" && objVal.className === "URL") {
2952
+ const argVals = evaluateArgs(args, env);
2953
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2954
+ const method = URL_INSTANCE_METHODS[methodName];
2955
+ if (method) {
2956
+ return method(...(argVals as TypeValue[]), objVal);
2957
+ }
829
2958
  }
830
- }
831
2959
 
832
- function getMemberKey(node: Node & { type: "MemberExpression" }, env: Environment): string | null {
833
- if (!node.computed && node.property.type === "Identifier") {
834
- return node.property.name;
2960
+ // Handle URLSearchParams instance methods
2961
+ if (objVal.kind === "instance" && objVal.className === "URLSearchParams") {
2962
+ const argVals = evaluateArgs(args, env);
2963
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2964
+ const method = URLSearchParams_INSTANCE_METHODS[methodName];
2965
+ if (method) {
2966
+ return method(...(argVals as TypeValue[]), objVal);
2967
+ }
835
2968
  }
836
- if (node.computed) {
837
- const propVal = evaluate(node.property, env);
838
- if (!isReturn(propVal) && !isBranch(propVal) && !isThrow(propVal) && propVal.kind === "literal") {
839
- return String(propVal.value);
2969
+
2970
+ // Handle Response instance methods
2971
+ if (objVal.kind === "instance" && objVal.className === "Response") {
2972
+ const argVals = evaluateArgs(args, env);
2973
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2974
+ const method = RESPONSE_INSTANCE_METHODS[methodName];
2975
+ if (method) {
2976
+ return method(...(argVals as TypeValue[]), objVal);
840
2977
  }
841
2978
  }
842
- return null;
843
- }
844
2979
 
845
- function bindPattern(pattern: Node, value: TypeValue, env: Environment): void {
846
- if (pattern.type === "Identifier") {
847
- env.bind(pattern.name, value);
848
- return;
2980
+ // Handle Headers instance methods
2981
+ if (objVal.kind === "instance" && objVal.className === "Headers") {
2982
+ const argVals = evaluateArgs(args, env);
2983
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2984
+ const method = HEADERS_INSTANCE_METHODS[methodName];
2985
+ if (method) {
2986
+ return method(...(argVals as TypeValue[]), objVal);
2987
+ }
849
2988
  }
850
2989
 
851
- if (pattern.type === "AssignmentPattern") {
852
- const defaultVal = evaluate(pattern.right, env);
853
- const resolved = (value.kind === "literal" && value.value === undefined)
854
- ? (!isReturn(defaultVal) && !isBranch(defaultVal) && !isThrow(defaultVal) ? defaultVal : T.unknown)
855
- : value;
856
- bindPattern(pattern.left, resolved, env);
857
- return;
2990
+ // Handle FormData instance methods
2991
+ if (objVal.kind === "instance" && objVal.className === "FormData") {
2992
+ const argVals = evaluateArgs(args, env);
2993
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
2994
+ const method = FORMDATA_INSTANCE_METHODS[methodName];
2995
+ if (method) {
2996
+ return method(...(argVals as TypeValue[]), objVal);
2997
+ }
858
2998
  }
859
2999
 
860
- if (pattern.type === "ObjectPattern") {
861
- const restKeys: string[] = [];
862
- for (const prop of pattern.properties) {
863
- if (prop.type === "RestElement") {
864
- if (value.kind === "object") {
865
- const remaining: Record<string, TypeValue> = {};
866
- for (const [k, v] of Object.entries(value.properties)) {
867
- if (!restKeys.includes(k)) remaining[k] = v;
868
- }
869
- bindPattern(prop.argument, T.object(remaining), env);
870
- } else {
871
- bindPattern(prop.argument, T.object({}), env);
872
- }
873
- continue;
874
- }
875
- if (prop.type !== "ObjectProperty") continue;
876
- const key = prop.key.type === "Identifier"
877
- ? prop.key.name
878
- : prop.key.type === "StringLiteral"
879
- ? prop.key.value
880
- : null;
881
- if (!key) continue;
882
- restKeys.push(key);
883
- const propVal = value.kind === "object"
884
- ? (value.properties[key] ?? T.undefined)
885
- : T.unknown;
886
- bindPattern(prop.value as Node, propVal, env);
3000
+ // Handle AbortController instance methods
3001
+ if (objVal.kind === "instance" && objVal.className === "AbortController") {
3002
+ const argVals = evaluateArgs(args, env);
3003
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
3004
+ const method = ABORTCONTROLLER_INSTANCE_METHODS[methodName];
3005
+ if (method) {
3006
+ return method(...(argVals as TypeValue[]), objVal);
887
3007
  }
888
- return;
889
3008
  }
890
3009
 
891
- if (pattern.type === "ArrayPattern") {
892
- for (let i = 0; i < pattern.elements.length; i++) {
893
- const elem = pattern.elements[i];
894
- if (!elem) continue;
895
- if (elem.type === "RestElement") {
896
- if (value.kind === "tuple") {
897
- bindPattern(elem.argument, T.tuple(value.elements.slice(i)), env);
898
- } else if (value.kind === "array") {
899
- bindPattern(elem.argument, value, env);
900
- } else {
901
- bindPattern(elem.argument, T.tuple([]), env);
902
- }
903
- continue;
904
- }
905
- const elemVal = value.kind === "tuple"
906
- ? (value.elements[i] ?? T.undefined)
907
- : value.kind === "array"
908
- ? value.element
909
- : T.unknown;
910
- bindPattern(elem, elemVal, env);
3010
+ // Handle WeakMap instance methods
3011
+ if (objVal.kind === "instance" && objVal.className === "WeakMap") {
3012
+ const argVals = evaluateArgs(args, env);
3013
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
3014
+ const method = WEAKMAP_INSTANCE_METHODS[methodName];
3015
+ if (method) {
3016
+ return method(...(argVals as TypeValue[]), objVal);
911
3017
  }
912
- return;
913
3018
  }
914
- }
915
3019
 
916
- function evaluateArgs(args: Node[], env: Environment): TypeValue[] | ReturnSignal | BranchSignal | ThrowSignal {
917
- const result: TypeValue[] = [];
918
- for (const arg of args) {
919
- if (arg.type === "SpreadElement") {
920
- const spreadVal = evaluate(arg.argument, env);
921
- if (isReturn(spreadVal) || isBranch(spreadVal) || isThrow(spreadVal)) return spreadVal;
922
- if (spreadVal.kind === "tuple") {
923
- result.push(...spreadVal.elements);
924
- } else if (spreadVal.kind === "array") {
925
- result.push(spreadVal.element);
926
- } else {
927
- result.push(T.unknown);
928
- }
929
- continue;
3020
+ // Handle WeakSet instance methods
3021
+ if (objVal.kind === "instance" && objVal.className === "WeakSet") {
3022
+ const argVals = evaluateArgs(args, env);
3023
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
3024
+ const method = WEAKSET_INSTANCE_METHODS[methodName];
3025
+ if (method) {
3026
+ return method(...(argVals as TypeValue[]), objVal);
930
3027
  }
931
- const v = evaluate(arg, env);
932
- if (isReturn(v) || isBranch(v) || isThrow(v)) return v;
933
- result.push(v);
934
3028
  }
935
- return result;
936
- }
937
3029
 
938
- function evaluateMethodCall(
939
- callee: Node & { type: "MemberExpression" },
940
- args: Node[],
941
- env: Environment,
942
- ): EvalResult | null {
943
- const objVal = evaluate(callee.object, env);
944
- if (isReturn(objVal) || isBranch(objVal) || isThrow(objVal)) return objVal;
3030
+ // Handle DateTimeFormat instance methods
3031
+ if (objVal.kind === "instance" && objVal.className === "DateTimeFormat") {
3032
+ const argVals = evaluateArgs(args, env);
3033
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
3034
+ const method = INTL_DATETIMEFORMAT_METHODS[methodName];
3035
+ if (method) {
3036
+ return method(...(argVals as TypeValue[]), objVal);
3037
+ }
3038
+ }
945
3039
 
946
- const methodName = !callee.computed && callee.property.type === "Identifier"
947
- ? callee.property.name
948
- : null;
949
- if (!methodName) return null;
3040
+ // Handle NumberFormat instance methods
3041
+ if (objVal.kind === "instance" && objVal.className === "NumberFormat") {
3042
+ const argVals = evaluateArgs(args, env);
3043
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
3044
+ const method = INTL_NUMBERFORMAT_METHODS[methodName];
3045
+ if (method) {
3046
+ return method(...(argVals as TypeValue[]), objVal);
3047
+ }
3048
+ }
950
3049
 
951
3050
  if (
952
3051
  callee.object.type === "Identifier" &&
953
3052
  callee.object.name === "Object" &&
3053
+ !env.has("Object") &&
954
3054
  args.length >= 1
955
3055
  ) {
956
3056
  const argVal = evaluate(args[0], env);
@@ -959,7 +3059,17 @@ function evaluateMethodCall(
959
3059
  }
960
3060
 
961
3061
  if (objVal.kind === "array" || objVal.kind === "tuple") {
962
- return evaluateArrayMethod(objVal, methodName, args, env);
3062
+ const arrResult = evaluateArrayMethod(objVal, methodName, args, env);
3063
+ if (arrResult === null) {
3064
+ recordUnknown({
3065
+ kind: "method",
3066
+ name: methodName,
3067
+ receiverType: objVal,
3068
+ loc: callee.loc,
3069
+ reason: `no method '${methodName}' on ${objVal.kind}`,
3070
+ });
3071
+ }
3072
+ return arrResult;
963
3073
  }
964
3074
 
965
3075
  if (isStringLike(objVal)) {
@@ -971,7 +3081,36 @@ function evaluateMethodCall(
971
3081
  if (refined !== undefined) return refined;
972
3082
  }
973
3083
 
974
- return evaluateStringMethod(objVal, methodName, argVals as TypeValue[]);
3084
+ const strResult = evaluateStringMethod(objVal, methodName, argVals as TypeValue[]);
3085
+ if (strResult === null) {
3086
+ recordUnknown({
3087
+ kind: "method",
3088
+ name: methodName,
3089
+ receiverType: objVal,
3090
+ loc: callee.loc,
3091
+ reason: `no method '${methodName}' on ${objVal.kind}`,
3092
+ });
3093
+ }
3094
+ return strResult;
3095
+ }
3096
+
3097
+ // Number receivers: toString/valueOf exist on every JS number. Without
3098
+ // this, deepEqual-style `obj.toString()` comparators reaching a numeric
3099
+ // union member record a false no-method error.
3100
+ if (isNumberLike(objVal)) {
3101
+ const argVals = evaluateArgs(args, env);
3102
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
3103
+ if (methodName === "toString") {
3104
+ return objVal.kind === "literal" && typeof objVal.value === "number"
3105
+ ? T.literal(String(objVal.value))
3106
+ : T.string;
3107
+ }
3108
+ if (methodName === "valueOf") {
3109
+ return objVal.kind === "literal" && typeof objVal.value === "number"
3110
+ ? objVal
3111
+ : T.number;
3112
+ }
3113
+ if (methodName === "toFixed" || methodName === "toPrecision") return T.string;
975
3114
  }
976
3115
 
977
3116
  if (objVal.kind === "refined") {
@@ -981,6 +3120,23 @@ function evaluateMethodCall(
981
3120
  if (result !== undefined) return result;
982
3121
  }
983
3122
 
3123
+ // Method dispatch fallback: no handler matched. For receivers whose property
3124
+ // lookup may still succeed (object/instance/refined/promise — resolved by the
3125
+ // MemberExpression evaluation downstream), stay silent; for everything else
3126
+ // this call will degrade to T.unknown, so record it.
3127
+ if (
3128
+ objVal.kind !== "object" && objVal.kind !== "instance" &&
3129
+ objVal.kind !== "refined" && objVal.kind !== "promise"
3130
+ ) {
3131
+ recordUnknown({
3132
+ kind: "method",
3133
+ name: methodName,
3134
+ receiverType: objVal,
3135
+ loc: callee.loc,
3136
+ reason: `no method '${methodName}' on ${objVal.kind}`,
3137
+ });
3138
+ }
3139
+
984
3140
  return null;
985
3141
  }
986
3142
 
@@ -991,6 +3147,13 @@ function isStringLike(tv: TypeValue): boolean {
991
3147
  return false;
992
3148
  }
993
3149
 
3150
+ function isNumberLike(tv: TypeValue): boolean {
3151
+ if (tv.kind === "literal" && typeof tv.value === "number") return true;
3152
+ if (tv.kind === "primitive" && tv.type === "number") return true;
3153
+ if (tv.kind === "refined") return isNumberLike(tv.base);
3154
+ return false;
3155
+ }
3156
+
994
3157
  function evaluateStringMethod(
995
3158
  receiver: TypeValue,
996
3159
  method: string,
@@ -1014,6 +3177,9 @@ function evaluateStringMethodLiteral(
1014
3177
  };
1015
3178
 
1016
3179
  switch (method) {
3180
+ // String.prototype.toString/valueOf return the receiver string itself
3181
+ case "toString":
3182
+ case "valueOf": return T.literal(str);
1017
3183
  case "toUpperCase": return T.literal(str.toUpperCase());
1018
3184
  case "toLowerCase": return T.literal(str.toLowerCase());
1019
3185
  case "trim": return T.literal(str.trim());
@@ -1125,6 +3291,9 @@ function evaluateStringMethodAbstract(
1125
3291
  _args: TypeValue[],
1126
3292
  ): TypeValue | null {
1127
3293
  switch (method) {
3294
+ case "toString":
3295
+ case "valueOf":
3296
+ return T.string;
1128
3297
  case "toUpperCase":
1129
3298
  case "toLowerCase":
1130
3299
  case "trim":
@@ -1160,6 +3329,58 @@ function evaluateObjectStaticMethod(
1160
3329
  method: string,
1161
3330
  obj: TypeValue,
1162
3331
  ): TypeValue | null {
3332
+ // Shape-aware receivers list their declared own-property names:
3333
+ // instances carry their properties record (usually empty for fresh
3334
+ // collections), tuples their indices. Key loops (`for (const key of
3335
+ // Object.keys(x))`) then iterate concrete keys instead of symbolic
3336
+ // strings.
3337
+ if (obj.kind === "instance") {
3338
+ if (method === "keys" || method === "getOwnPropertyNames") {
3339
+ return T.tuple(Object.keys(obj.properties).map((k) => T.literal(k)));
3340
+ }
3341
+ if (method === "values") return T.tuple(Object.values(obj.properties));
3342
+ if (method === "entries") {
3343
+ return T.tuple(
3344
+ Object.keys(obj.properties).map((k) => T.tuple([T.literal(k), obj.properties[k]])),
3345
+ );
3346
+ }
3347
+ return null;
3348
+ }
3349
+ if (obj.kind === "union") {
3350
+ // union 分发:逐成员 keys/values/entries。成员多时逐成员 tuple 的
3351
+ // union 会超预算——keys 恒为 string[](sound 下界),values/entries
3352
+ // 同理回退宽类型而非 unknown(hoek utils.keys 的 30+ 成员 union 曾
3353
+ // 在无分支路径上退化)。
3354
+ const per = obj.members.map((m) => evaluateObjectStaticMethod(method, m));
3355
+ if (method === "keys" || method === "getOwnPropertyNames") {
3356
+ const literals: TypeValue[] = [];
3357
+ for (const r of per) {
3358
+ if (r?.kind === "tuple") literals.push(...r.elements);
3359
+ else return T.array(T.string);
3360
+ }
3361
+ if (literals.length > 64) return T.array(T.string);
3362
+ return T.tuple(literals);
3363
+ }
3364
+ if (method === "values") {
3365
+ const vals = per.filter((r) => r && r.kind !== "unknown").flatMap((r) => (r!.kind === "tuple" ? r.elements : [r!]));
3366
+ if (per.some((r) => !r || r.kind === "unknown") || vals.length > 64) return T.array(T.unknown);
3367
+ return T.tuple(vals);
3368
+ }
3369
+ if (method === "entries") {
3370
+ return T.array(T.tuple([T.string, T.unknown]));
3371
+ }
3372
+ return null;
3373
+ }
3374
+ if (obj.kind === "tuple") {
3375
+ if (method === "keys" || method === "getOwnPropertyNames") {
3376
+ return T.tuple(obj.elements.map((_, i) => T.literal(String(i))));
3377
+ }
3378
+ if (method === "values") return T.tuple([...obj.elements]);
3379
+ if (method === "entries") {
3380
+ return T.tuple(obj.elements.map((el, i) => T.tuple([T.literal(String(i)), el])));
3381
+ }
3382
+ return null;
3383
+ }
1163
3384
  if (obj.kind !== "object") {
1164
3385
  if (method === "keys") return T.array(T.string);
1165
3386
  if (method === "values") return T.array(T.unknown);
@@ -1181,6 +3402,9 @@ function evaluateObjectStaticMethod(
1181
3402
  keys.map((k) => T.tuple([T.literal(k), obj.properties[k]])),
1182
3403
  );
1183
3404
  }
3405
+ if (method === "getOwnPropertyNames") {
3406
+ return T.tuple(keys.map((k) => T.literal(k)));
3407
+ }
1184
3408
  return null;
1185
3409
  }
1186
3410
 
@@ -1192,12 +3416,23 @@ function evaluateArrayMethod(
1192
3416
  ): EvalResult | null {
1193
3417
  const argVals = evaluateArgs(args, env);
1194
3418
  if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
3419
+ return evaluateArrayMethodValues(arr, method, argVals as TypeValue[]);
3420
+ }
1195
3421
 
1196
- const callbackFn = (argVals as TypeValue[])[0];
3422
+ /** Array/tuple 方法求值主体:实参已经是 TypeValue(AST 路径与 union 分布
3423
+ * 路径共用)。回调类方法(map/filter/reduce/...)拿到 kind "function" 的
3424
+ * 实参时经 callFunction 从其签名/闭包求值——mock 箭头、具名函数与内联
3425
+ * 箭头在此走同一机制。 */
3426
+ function evaluateArrayMethodValues(
3427
+ arr: TypeValue & { kind: "array" | "tuple" },
3428
+ method: string,
3429
+ argVals: TypeValue[],
3430
+ ): EvalResult | null {
3431
+ const callbackFn = argVals[0];
1197
3432
 
1198
3433
  if (method === "push") {
1199
3434
  if (arr.kind === "tuple") {
1200
- arr.elements.push(...(argVals as TypeValue[]));
3435
+ arr.elements.push(...argVals);
1201
3436
  return T.literal(arr.elements.length);
1202
3437
  }
1203
3438
  return T.number;
@@ -1207,13 +3442,29 @@ function evaluateArrayMethod(
1207
3442
  return arr.kind === "tuple" ? T.literal(arr.elements.length) : T.number;
1208
3443
  }
1209
3444
 
3445
+ if (method === "pop" || method === "shift") {
3446
+ if (arr.kind === "tuple") {
3447
+ // 抽象数组不模拟顺序语义:返回元素 union(空 tuple 理论返回 undefined,union 进去保持 sound)
3448
+ if (arr.elements.length === 0) return T.undefined;
3449
+ return T.union(...arr.elements);
3450
+ }
3451
+ return T.unknown;
3452
+ }
3453
+
3454
+ if (method === "unshift") {
3455
+ if (arr.kind === "tuple") {
3456
+ return T.literal(arr.elements.length + argVals.length);
3457
+ }
3458
+ return T.number;
3459
+ }
3460
+
1210
3461
  if (method === "indexOf" || method === "lastIndexOf") {
1211
3462
  return T.number;
1212
3463
  }
1213
3464
 
1214
3465
  if (method === "includes") {
1215
- if (arr.kind === "tuple" && (argVals as TypeValue[])[0]?.kind === "literal") {
1216
- const searchVal = (argVals as TypeValue[])[0];
3466
+ if (arr.kind === "tuple" && argVals[0]?.kind === "literal") {
3467
+ const searchVal = argVals[0];
1217
3468
  const found = arr.elements.some((e) => typeValueEquals(e, searchVal));
1218
3469
  return T.literal(found);
1219
3470
  }
@@ -1227,7 +3478,7 @@ function evaluateArrayMethod(
1227
3478
  if (method === "concat") {
1228
3479
  if (arr.kind === "tuple") {
1229
3480
  const otherElements: TypeValue[] = [];
1230
- for (const a of argVals as TypeValue[]) {
3481
+ for (const a of argVals) {
1231
3482
  if (a.kind === "tuple") otherElements.push(...a.elements);
1232
3483
  else if (a.kind === "array") return T.array(simplifyUnion([...arr.elements, a.element]));
1233
3484
  else otherElements.push(a);
@@ -1239,8 +3490,8 @@ function evaluateArrayMethod(
1239
3490
 
1240
3491
  if (method === "slice") {
1241
3492
  if (arr.kind === "tuple") {
1242
- const start = (argVals as TypeValue[])[0];
1243
- const end = (argVals as TypeValue[])[1];
3493
+ const start = argVals[0];
3494
+ const end = argVals[1];
1244
3495
  const startIdx = start?.kind === "literal" && typeof start.value === "number" ? start.value : 0;
1245
3496
  const endIdx = end?.kind === "literal" && typeof end.value === "number" ? end.value : arr.elements.length;
1246
3497
  return T.tuple(arr.elements.slice(startIdx, endIdx));
@@ -1248,12 +3499,23 @@ function evaluateArrayMethod(
1248
3499
  return T.array(arr.element);
1249
3500
  }
1250
3501
 
3502
+ // 回调实参是「函数 union」(cond ? f : g 透传给 HOF):对每个成员签名
3503
+ // 分别求值再 union 结果——与非函数守卫(unknown 回调)不同,这里每个
3504
+ // 成员都有完整的 TypeValue 信息,不该整体降级。
3505
+ if (callbackFn && callbackFn.kind === "union" && callbackFn.members.length > 0 &&
3506
+ callbackFn.members.every((m) => m.kind === "function")) {
3507
+ return distributeOverUnion(callbackFn, (member) => {
3508
+ const r = evaluateArrayMethodValues(arr, method, [member, ...argVals.slice(1)]);
3509
+ return r === null ? T.unknown : (r as TypeValue);
3510
+ });
3511
+ }
3512
+
1251
3513
  if (!callbackFn || callbackFn.kind !== "function") {
1252
3514
  if (method === "map") return arr.kind === "tuple" ? T.tuple(arr.elements.map(() => T.unknown)) : T.array(T.unknown);
1253
3515
  if (method === "filter") return arr.kind === "tuple" ? T.array(simplifyUnion(arr.elements)) : arr;
1254
3516
  if (method === "find") return arr.kind === "tuple" ? simplifyUnion([...arr.elements, T.undefined]) : simplifyUnion([arr.element, T.undefined]);
1255
3517
  if (method === "some" || method === "every") return T.boolean;
1256
- if (method === "reduce") return (argVals as TypeValue[])[1] ?? T.unknown;
3518
+ if (method === "reduce") return argVals[1] ?? T.unknown;
1257
3519
  if (method === "forEach") return T.undefined;
1258
3520
  if (method === "flatMap") return T.array(T.unknown);
1259
3521
  return null;
@@ -1261,23 +3523,36 @@ function evaluateArrayMethod(
1261
3523
 
1262
3524
  const fn = callbackFn as TypeValue & { kind: "function" };
1263
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
+
1264
3533
  if (method === "map") {
1265
3534
  if (arr.kind === "tuple") {
1266
3535
  const mapped = arr.elements.map((el, i) =>
1267
- callFunction(fn, [el, T.literal(i), arr]),
3536
+ callFunction(fn, [tagEl(el, fn.params[0] ?? "item"), T.literal(i), arr]),
1268
3537
  );
1269
3538
  return T.tuple(mapped);
1270
3539
  }
1271
- 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
+ );
1272
3543
  }
1273
3544
 
1274
3545
  if (method === "filter") {
1275
3546
  if (arr.kind === "tuple") {
1276
3547
  const kept: TypeValue[] = [];
1277
3548
  for (let i = 0; i < arr.elements.length; i++) {
1278
- 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
+ ]);
1279
3554
  if (result.kind === "literal" && !result.value) continue;
1280
- kept.push(arr.elements[i]);
3555
+ kept.push(arr.elements[i]!);
1281
3556
  }
1282
3557
  if (kept.length === 0) return T.tuple([]);
1283
3558
  return T.array(simplifyUnion(kept));
@@ -1286,17 +3561,35 @@ function evaluateArrayMethod(
1286
3561
  }
1287
3562
 
1288
3563
  if (method === "reduce") {
1289
- const init = (argVals as TypeValue[])[1];
3564
+ const init = argVals[1];
1290
3565
  if (arr.kind === "tuple") {
1291
3566
  let acc = init ?? arr.elements[0] ?? T.unknown;
1292
3567
  const startIdx = init ? 0 : 1;
1293
3568
  for (let i = startIdx; i < arr.elements.length; i++) {
1294
- 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
+ ]);
1295
3575
  }
1296
3576
  return acc;
1297
3577
  }
1298
- const acc = init ?? arr.element;
1299
- return callFunction(fn, [acc, arr.element, T.number, arr]);
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;
1300
3593
  }
1301
3594
 
1302
3595
  if (method === "find") {
@@ -1349,11 +3642,62 @@ function evaluateArrayMethod(
1349
3642
  return null;
1350
3643
  }
1351
3644
 
3645
+ // Iterable spread of Set/Map instances: exact side tables spread as
3646
+ // tuples ([...new Set([1,2])] → [1, 2]; Maps as [k, v] pairs), otherwise
3647
+ // the element typeArgs approximate like an array of that element type.
3648
+ // Oversized side tables degrade to the array approximation.
3649
+ const MAX_ITERABLE_SPREAD = 1000;
3650
+ function iterableSpreadValue(v: TypeValue): TypeValue | undefined {
3651
+ if (v.kind === "instance" && v.className === "Set") {
3652
+ const values = exactSetValues(v);
3653
+ if (values) {
3654
+ return values.length <= MAX_ITERABLE_SPREAD ? T.tuple([...values]) : setValuesIterable(v);
3655
+ }
3656
+ const t = (v as any)._typeArgs?.T;
3657
+ return t && t.kind !== "unknown" ? T.array(t) : undefined;
3658
+ }
3659
+ if (v.kind === "instance" && v.className === "Map") {
3660
+ const entries = exactMapEntries(v);
3661
+ if (entries && entries.length > MAX_ITERABLE_SPREAD) {
3662
+ const ta = (v as any)._typeArgs ?? {};
3663
+ return T.array(T.tuple([ta.K ?? T.unknown, ta.V ?? T.unknown]));
3664
+ }
3665
+ return mapEntriesIterable(v);
3666
+ }
3667
+ return undefined;
3668
+ }
3669
+
1352
3670
  function evaluateForOf(
1353
3671
  node: Node & { type: "ForOfStatement" },
1354
3672
  iterable: TypeValue,
1355
3673
  env: Environment,
1356
3674
  ): EvalResult {
3675
+ // A union iterable distributes: every member's elements flow through the
3676
+ // body in sequence (concatenated), not just the first member's. Branch
3677
+ // envs chain member-to-member exactly like element-to-element inside one
3678
+ // tuple iteration, so side effects on shared values (array push) and
3679
+ // outer-var reassignments accumulate across members. Non-iterable
3680
+ // members (number, …) fall through their own recursion harmlessly.
3681
+ if (iterable.kind === "union") {
3682
+ const returnValues: TypeValue[] = [];
3683
+ let currentEnv = env;
3684
+ for (const member of iterable.members) {
3685
+ const memberResult = evaluateForOf(node, member, currentEnv);
3686
+ if (isReturn(memberResult)) {
3687
+ returnValues.push(memberResult.value);
3688
+ return makeReturn(simplifyUnion(returnValues));
3689
+ }
3690
+ if (isBranch(memberResult)) {
3691
+ returnValues.push(memberResult.returnedValue);
3692
+ currentEnv = memberResult.fallthroughEnv;
3693
+ }
3694
+ }
3695
+ if (returnValues.length > 0) {
3696
+ return makeBranch(simplifyUnion(returnValues), currentEnv);
3697
+ }
3698
+ return T.undefined;
3699
+ }
3700
+
1357
3701
  if (iterable.kind === "tuple") {
1358
3702
  const returnValues: TypeValue[] = [];
1359
3703
  let currentEnv = env;
@@ -1369,6 +3713,7 @@ function evaluateForOf(
1369
3713
  returnValues.push(result.returnedValue);
1370
3714
  currentEnv = result.fallthroughEnv;
1371
3715
  }
3716
+ if (loopSignalKind(result) === "break") break;
1372
3717
  }
1373
3718
  if (returnValues.length > 0) {
1374
3719
  return makeBranch(simplifyUnion(returnValues), currentEnv);
@@ -1377,13 +3722,37 @@ function evaluateForOf(
1377
3722
  }
1378
3723
 
1379
3724
  if (iterable.kind === "array") {
1380
- const loopEnv = env.extend({});
3725
+ const loopEnv = env.fork();
1381
3726
  bindForLoopVar(node.left, iterable.element, loopEnv);
1382
3727
  const result = evaluate(node.body, loopEnv);
1383
3728
  if (isReturn(result)) return makeBranch(result.value, env);
1384
3729
  return T.undefined;
1385
3730
  }
1386
3731
 
3732
+ // Set instances iterate their exact value side table (`new Set([1,2])`
3733
+ // yields 1 then 2); no/partial table keeps the body unexecuted.
3734
+ if (iterable.kind === "instance" && iterable.className === "Set") {
3735
+ const values = exactSetValues(iterable);
3736
+ if (values && values.length > 0) {
3737
+ return evaluateForOf(node, T.tuple([...values]), env);
3738
+ }
3739
+ return T.undefined;
3740
+ }
3741
+
3742
+ // Map instances iterate their exact entry side table as [key, value]
3743
+ // tuples (`for (const [k, v] of map)`); same no/partial-table rule.
3744
+ if (iterable.kind === "instance" && iterable.className === "Map") {
3745
+ const entries = exactMapEntries(iterable);
3746
+ if (entries && entries.length > 0) {
3747
+ return evaluateForOf(
3748
+ node,
3749
+ T.tuple(entries.map((e) => T.tuple([e.key, e.value]))),
3750
+ env,
3751
+ );
3752
+ }
3753
+ return T.undefined;
3754
+ }
3755
+
1387
3756
  return T.undefined;
1388
3757
  }
1389
3758
 
@@ -1409,6 +3778,7 @@ function evaluateForIn(
1409
3778
  returnValues.push(result.returnedValue);
1410
3779
  currentEnv = result.fallthroughEnv;
1411
3780
  }
3781
+ if (loopSignalKind(result) === "break") break;
1412
3782
  }
1413
3783
  if (returnValues.length > 0) {
1414
3784
  return makeBranch(simplifyUnion(returnValues), currentEnv);
@@ -1417,7 +3787,7 @@ function evaluateForIn(
1417
3787
  }
1418
3788
  }
1419
3789
 
1420
- const loopEnv = env.extend({});
3790
+ const loopEnv = env.fork();
1421
3791
  bindForLoopVar(node.left, T.string, loopEnv);
1422
3792
  evaluate(node.body, loopEnv);
1423
3793
  return T.undefined;
@@ -1471,7 +3841,7 @@ function evaluateForStatement(
1471
3841
  node: Node & { type: "ForStatement" },
1472
3842
  env: Environment,
1473
3843
  ): EvalResult {
1474
- const loopEnv = env.extend({});
3844
+ const loopEnv = env.fork();
1475
3845
 
1476
3846
  if (node.init) {
1477
3847
  const initResult = evaluate(node.init, loopEnv);
@@ -1482,6 +3852,22 @@ function evaluateForStatement(
1482
3852
  const returnValues: TypeValue[] = [];
1483
3853
  let concreteCompleted = false;
1484
3854
 
3855
+ // let/const 声明的循环变量每轮迭代获得新绑定(真实 JS 的 per-iteration
3856
+ // scope):body 内创建的闭包捕获当轮副本而非共享 cell,var 保持共享
3857
+ // cell(闭包看到终值)。迭代结束后把副本值拷回 loopEnv,test/update/
3858
+ // widening 依旧能看到 body 对循环变量的修改(`for (let i = 0; i < n;)
3859
+ // { ...; i++ }` 这类无 update 子句的循环靠它推进)。
3860
+ const perIteration =
3861
+ node.init?.type === "VariableDeclaration" && node.init.kind !== "var";
3862
+ const runBody = (scope: Environment): EvalResult => {
3863
+ if (!perIteration) return evaluate(node.body, scope);
3864
+ const iterEnv = scope.fork();
3865
+ for (const name of varNames) iterEnv.bind(name, scope.lookup(name));
3866
+ const result = evaluate(node.body, iterEnv);
3867
+ for (const name of varNames) scope.update(name, iterEnv.lookup(name));
3868
+ return result;
3869
+ };
3870
+
1485
3871
  for (let i = 0; i < _maxConcreteIter; i++) {
1486
3872
  if (node.test) {
1487
3873
  const testVal = evaluate(node.test, loopEnv);
@@ -1490,7 +3876,7 @@ function evaluateForStatement(
1490
3876
  if (testVal.kind !== "literal") break;
1491
3877
  }
1492
3878
 
1493
- const bodyResult = evaluate(node.body, loopEnv);
3879
+ const bodyResult = runBody(loopEnv);
1494
3880
  if (isReturn(bodyResult)) {
1495
3881
  returnValues.push(bodyResult.value);
1496
3882
  concreteCompleted = true;
@@ -1499,6 +3885,12 @@ function evaluateForStatement(
1499
3885
  if (isBranch(bodyResult)) {
1500
3886
  returnValues.push(bodyResult.returnedValue);
1501
3887
  }
3888
+ // break 终止迭代(变量按 break 时的值流出循环);continue 落到
3889
+ // update 子句 —— 与真实 JS for 的 continue 语义一致。
3890
+ if (loopSignalKind(bodyResult) === "break") {
3891
+ concreteCompleted = true;
3892
+ break;
3893
+ }
1502
3894
 
1503
3895
  if (node.update) {
1504
3896
  const updateResult = evaluate(node.update, loopEnv);
@@ -1511,7 +3903,7 @@ function evaluateForStatement(
1511
3903
  const prevSnap = snapshotVars(varNames, loopEnv);
1512
3904
 
1513
3905
  for (let i = 0; i < 10; i++) {
1514
- evaluate(node.body, loopEnv);
3906
+ runBody(loopEnv);
1515
3907
  if (node.update) evaluate(node.update, loopEnv);
1516
3908
  widenVars(varNames, loopEnv);
1517
3909
  const currSnap = snapshotVars(varNames, loopEnv);
@@ -1550,6 +3942,7 @@ function evaluateWhileStatement(
1550
3942
  if (isBranch(bodyResult)) {
1551
3943
  returnValues.push(bodyResult.returnedValue);
1552
3944
  }
3945
+ if (loopSignalKind(bodyResult) === "break") break;
1553
3946
  }
1554
3947
 
1555
3948
  if (returnValues.length > 0) {
@@ -1573,6 +3966,7 @@ function evaluateDoWhileStatement(
1573
3966
  if (isBranch(bodyResult)) {
1574
3967
  returnValues.push(bodyResult.returnedValue);
1575
3968
  }
3969
+ if (loopSignalKind(bodyResult) === "break") break;
1576
3970
 
1577
3971
  const tv = evaluate(node.test, env);
1578
3972
  if (isReturn(tv) || isBranch(tv) || isThrow(tv)) return tv;
@@ -1586,23 +3980,293 @@ function evaluateDoWhileStatement(
1586
3980
  return T.undefined;
1587
3981
  }
1588
3982
 
1589
- function evaluateImportDeclaration(node: Node & { type: "ImportDeclaration" }, env: Environment): EvalResult {
1590
- const source = node.source.value;
1591
- if (!currentModuleResolver) return T.undefined;
3983
+ /** 诊断用的模块短名:UnknownRecord.name 会决定诊断 range 的列宽
3984
+ * (analyzer 按 name.length 折叠 end 列),完整路径会把列号撑爆。 */
3985
+ function moduleLabel(pathOrSpecifier: string): string {
3986
+ const base = pathOrSpecifier.replace(/^.*\//, "");
3987
+ return base || pathOrSpecifier;
3988
+ }
1592
3989
 
1593
- const resolved = currentModuleResolver(source, currentFileDir);
1594
- if (!resolved) return T.undefined;
3990
+ /** 相对说明符的完整候选路径(evaluator 不引 node:path,手工拼接并归一化
3991
+ * "./" 与 ".." 段,保证诊断里是干净的绝对路径)。 */
3992
+ function attemptedFullPath(fromDir: string, specifier: string): string {
3993
+ if (specifier.startsWith("/")) return specifier;
3994
+ const base = specifier.startsWith("./") ? specifier.slice(2) : specifier;
3995
+ const joined = fromDir ? `${fromDir.replace(/\/+$/, "")}/${base}` : base;
3996
+ const segments: string[] = [];
3997
+ for (const seg of joined.split("/")) {
3998
+ if (seg === "" || seg === ".") continue;
3999
+ if (seg === "..") segments.pop();
4000
+ else segments.push(seg);
4001
+ }
4002
+ return (joined.startsWith("/") ? "/" : "") + segments.join("/");
4003
+ }
1595
4004
 
4005
+ /** Load (or fetch from cache) the environment for a resolved module file.
4006
+ * Shared by ESM import and CJS require — one loading path, one cache.
4007
+ * loc 为触发本次加载的 import/require 结点,用于守卫诊断定位。 */
4008
+ function loadModuleEnv(resolved: { ast: Node; filePath: string }, loc?: Node["loc"]): Environment {
4009
+ // 循环加载守卫——必须先于缓存查询:进行中的文件在求值前就已预置进
4010
+ // moduleCache,缓存命中分支区分不了「已加载完成」与「环形依赖回到自身」。
4011
+ // 命中环时返回进行中的部分环境(ESM 环形依赖语义,类型不炸),同时记录
4012
+ // 指明环链的诊断(a -> b -> a)。
4013
+ const cycleIndex = _moduleEvalStack.indexOf(resolved.filePath);
4014
+ if (cycleIndex !== -1) {
4015
+ const chain = [..._moduleEvalStack.slice(cycleIndex), resolved.filePath];
4016
+ recordUnknown({
4017
+ kind: "global",
4018
+ name: `module-cycle:${moduleLabel(resolved.filePath)}`,
4019
+ loc,
4020
+ reason: `Circular module load: ${chain.join(" -> ")} (bindings inside the cycle resolve to their partially evaluated types)`,
4021
+ });
4022
+ return moduleCache.get(resolved.filePath) ?? freshStubModuleEnv(resolved);
4023
+ }
1596
4024
  let moduleEnv = moduleCache.get(resolved.filePath);
1597
4025
  if (!moduleEnv) {
4026
+ // 深度上限:每个真正新载入的文件占一帧,超过 MAX_MODULE_LOAD_DEPTH
4027
+ // 即截断为空环境(导出全为 unknown),报错指明链深与完整链。
4028
+ if (_moduleEvalStack.length >= MAX_MODULE_LOAD_DEPTH) {
4029
+ const chain = [..._moduleEvalStack, resolved.filePath];
4030
+ recordUnknown({
4031
+ kind: "global",
4032
+ name: `module-depth:${moduleLabel(resolved.filePath)}`,
4033
+ loc,
4034
+ reason: `Module load chain too deep (depth ${chain.length} > ${MAX_MODULE_LOAD_DEPTH} max): ${chain.join(" -> ")} (loading truncated, deeper modules typed as unknown)`,
4035
+ });
4036
+ return freshStubModuleEnv(resolved);
4037
+ }
1598
4038
  moduleEnv = createEnvironment();
1599
4039
  moduleCache.set(resolved.filePath, moduleEnv);
1600
4040
  const savedDir = currentFileDir;
1601
4041
  currentFileDir = resolved.filePath.replace(/\/[^/]+$/, "");
1602
- evaluateProgram(resolved.ast, moduleEnv);
4042
+ _moduleEvalStack.push(resolved.filePath);
4043
+ try {
4044
+ evaluateProgram(resolved.ast, moduleEnv);
4045
+ } finally {
4046
+ _moduleEvalStack.pop();
4047
+ }
1603
4048
  currentFileDir = savedDir;
4049
+ tagModuleExports(moduleEnv, resolved.filePath);
4050
+ tagCommonJsExports(moduleEnv, resolved.filePath);
4051
+ }
4052
+ return moduleEnv;
4053
+ }
4054
+
4055
+ /** 截断/环回退用的空环境:入缓存保证后续请求拿到同一份「全 unknown」结果。 */
4056
+ function freshStubModuleEnv(resolved: { ast: Node; filePath: string }): Environment {
4057
+ const stub = createEnvironment();
4058
+ moduleCache.set(resolved.filePath, stub);
4059
+ return stub;
4060
+ }
4061
+
4062
+ /** Build an exports namespace object from a module's `__export_` bindings
4063
+ * (ESM exports required from CJS). Null when the module exports nothing. */
4064
+ function namespaceFromExportBindings(env: Environment): TypeValue | null {
4065
+ const exports: Record<string, TypeValue> = {};
4066
+ const bindings = env.getOwnBindings();
4067
+ for (const [k, v] of Object.entries(bindings)) {
4068
+ if (!k.startsWith("__export_")) continue;
4069
+ exports[k === "__export_default" ? "default" : k.slice("__export_".length)] = v;
4070
+ }
4071
+ if (Object.keys(exports).length === 0) return null;
4072
+ return T.object(exports);
4073
+ }
4074
+
4075
+ /** The value of require() for an evaluated module: `module.exports` when the
4076
+ * target assigned it, the (possibly `exports.x`-mutated) exports object
4077
+ * otherwise, and the ESM namespace when the target is an ES module. */
4078
+ function commonJsExportsValue(moduleEnv: Environment): TypeValue {
4079
+ if (moduleEnv.has("module")) {
4080
+ const moduleVal = moduleEnv.lookup("module");
4081
+ if (moduleVal.kind === "object") {
4082
+ const exp = moduleVal.properties["exports"];
4083
+ if (exp) {
4084
+ if (
4085
+ exp.kind === "object" &&
4086
+ (exp as any).__cjsExportsRoot === true &&
4087
+ Object.keys(exp.properties).length === 0
4088
+ ) {
4089
+ // Target never wrote CJS exports — surface ESM exports instead.
4090
+ return namespaceFromExportBindings(moduleEnv) ?? exp;
4091
+ }
4092
+ return exp;
4093
+ }
4094
+ }
4095
+ }
4096
+ return T.unknown;
4097
+ }
4098
+
4099
+
4100
+ function evaluateRequireCall(node: Node & { type: "CallExpression" }, _env: Environment): EvalResult { const arg = (node.arguments as Node[])[0];
4101
+ if (!arg || arg.type !== "StringLiteral") {
4102
+ recordUnknown({
4103
+ kind: "global",
4104
+ name: "require",
4105
+ loc: node.loc,
4106
+ reason: "require() with non-literal specifier",
4107
+ });
4108
+ return T.unknown;
4109
+ }
4110
+ const specifier = arg.value as string;
4111
+ // Bare specifiers (npm packages) are not resolved in v1.
4112
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
4113
+ recordUnknown({
4114
+ kind: "global",
4115
+ name: `require('${specifier}')`,
4116
+ loc: node.loc,
4117
+ reason: `bare module specifier '${specifier}' is not resolved (npm packages unsupported)`,
4118
+ });
4119
+ return T.unknown;
4120
+ }
4121
+ if (!currentModuleResolver) return T.unknown;
4122
+ const resolved = currentModuleResolver(specifier, currentFileDir);
4123
+ if (!resolved) {
4124
+ // 缺失文件:报错带完整候选路径(module-missing 前缀走 analyzer 专属
4125
+ // 文案通道,reason 不会被通用 unknown-global 文案吞掉)。
4126
+ recordUnknown({
4127
+ kind: "global",
4128
+ name: `module-missing:require('${specifier}')`,
4129
+ loc: node.loc,
4130
+ reason: `Cannot resolve require('${specifier}'): no file at ${attemptedFullPath(currentFileDir, specifier)} (with or without .js/.ts/.mjs extension)`,
4131
+ });
4132
+ return T.unknown;
4133
+ }
4134
+ // .json 模块(如 require('../package.json')):JSON 值直接映射为字面量类型
4135
+ if ("json" in resolved && resolved.json !== undefined) {
4136
+ return jsonToTypeValue(resolved.json);
4137
+ }
4138
+ return commonJsExportsValue(loadModuleEnv(resolved, node.loc));
4139
+ }
4140
+
4141
+ function evaluateImportDeclaration(node: Node & { type: "ImportDeclaration" }, env: Environment): EvalResult {
4142
+ const source = node.source.value;
4143
+
4144
+ const mockModule = mockModules.get(source);
4145
+ if (mockModule && currentModuleResolver) {
4146
+ const mockResolved = currentModuleResolver(mockModule.fromPath, currentFileDir);
4147
+ if (mockResolved) {
4148
+ const mockEnv = loadModuleEnv(mockResolved, node.loc);
4149
+
4150
+ if (mockModule.names) {
4151
+ const originalResolved = currentModuleResolver(source, currentFileDir);
4152
+ let originalEnv: Environment | undefined;
4153
+ if (originalResolved) {
4154
+ originalEnv = loadModuleEnv(originalResolved, node.loc);
4155
+ }
4156
+
4157
+ for (const spec of node.specifiers) {
4158
+ if (spec.type === "ImportSpecifier") {
4159
+ const importedName = spec.imported.type === "Identifier" ? spec.imported.name : null;
4160
+ if (importedName && mockModule.names.includes(importedName)) {
4161
+ const val = mockEnv.has(`__export_${importedName}`) ? mockEnv.lookup(`__export_${importedName}`) : T.unknown;
4162
+ env.bind(spec.local.name, val);
4163
+ } else if (importedName && originalEnv) {
4164
+ const val = originalEnv.has(`__export_${importedName}`) ? originalEnv.lookup(`__export_${importedName}`) : T.unknown;
4165
+ env.bind(spec.local.name, val);
4166
+ }
4167
+ } else if (spec.type === "ImportDefaultSpecifier") {
4168
+ const sourceEnv = originalEnv ?? mockEnv;
4169
+ const val = sourceEnv.has(`__export_default`) ? sourceEnv.lookup(`__export_default`) : T.unknown;
4170
+ env.bind(spec.local.name, val);
4171
+ } else if (spec.type === "ImportNamespaceSpecifier") {
4172
+ const exports: Record<string, TypeValue> = {};
4173
+ const sourceEnv = originalEnv ?? mockEnv;
4174
+ const bindings = sourceEnv.getOwnBindings();
4175
+ for (const [k, v] of Object.entries(bindings)) {
4176
+ if (k.startsWith("__export_") && k !== "__export_default") {
4177
+ const name = k.slice("__export_".length);
4178
+ exports[name] = mockModule.names.includes(name)
4179
+ ? (mockEnv.has(k) ? mockEnv.lookup(k) : v)
4180
+ : v;
4181
+ }
4182
+ }
4183
+ const mockBindings = mockEnv.getOwnBindings();
4184
+ for (const [k, v] of Object.entries(mockBindings)) {
4185
+ if (k.startsWith("__export_") && k !== "__export_default") {
4186
+ const name = k.slice("__export_".length);
4187
+ if (mockModule.names.includes(name)) {
4188
+ exports[name] = v;
4189
+ }
4190
+ }
4191
+ }
4192
+ env.bind(spec.local.name, T.object(exports));
4193
+ }
4194
+ }
4195
+ return T.undefined;
4196
+ }
4197
+
4198
+ for (const spec of node.specifiers) {
4199
+ if (spec.type === "ImportDefaultSpecifier") {
4200
+ const val = mockEnv.has(`__export_default`) ? mockEnv.lookup(`__export_default`) : T.unknown;
4201
+ env.bind(spec.local.name, val);
4202
+ } else if (spec.type === "ImportSpecifier") {
4203
+ const importedName = spec.imported.type === "Identifier" ? spec.imported.name : null;
4204
+ if (importedName) {
4205
+ const val = mockEnv.has(`__export_${importedName}`) ? mockEnv.lookup(`__export_${importedName}`) : T.unknown;
4206
+ env.bind(spec.local.name, val);
4207
+ }
4208
+ } else if (spec.type === "ImportNamespaceSpecifier") {
4209
+ const exports: Record<string, TypeValue> = {};
4210
+ const bindings = mockEnv.getOwnBindings();
4211
+ for (const [k, v] of Object.entries(bindings)) {
4212
+ if (k.startsWith("__export_") && k !== "__export_default") {
4213
+ exports[k.slice("__export_".length)] = v;
4214
+ }
4215
+ }
4216
+ env.bind(spec.local.name, T.object(exports));
4217
+ }
4218
+ }
4219
+ return T.undefined;
4220
+ }
4221
+ // mock 文件本身解析失败:此前静默回落到原模块(mock 被无声忽略)。
4222
+ // 现在记录带完整候选路径的诊断后仍回落,分析不中断。
4223
+ recordUnknown({
4224
+ kind: "global",
4225
+ name: `module-missing:${moduleLabel(mockModule.fromPath)}`,
4226
+ loc: node.loc,
4227
+ reason: `@nudo:mock-module target for "${source}" not found: no file at ${attemptedFullPath(currentFileDir, mockModule.fromPath)} (with or without .js/.ts/.mjs extension); falling back to the original module`,
4228
+ });
4229
+ }
4230
+
4231
+ const envModule = envModules[source];
4232
+ if (envModule) {
4233
+ for (const spec of node.specifiers) {
4234
+ if (spec.type === "ImportDefaultSpecifier") {
4235
+ const defaultExport = envModule["default"];
4236
+ env.bind(spec.local.name, defaultExport ?? T.unknown);
4237
+ } else if (spec.type === "ImportSpecifier") {
4238
+ const importedName = spec.imported.type === "Identifier" ? spec.imported.name : null;
4239
+ if (importedName) {
4240
+ env.bind(spec.local.name, envModule[importedName] ?? T.unknown);
4241
+ }
4242
+ } else if (spec.type === "ImportNamespaceSpecifier") {
4243
+ const { default: _default, ...rest } = envModule;
4244
+ env.bind(spec.local.name, T.object(rest));
4245
+ }
4246
+ }
4247
+ return T.undefined;
4248
+ }
4249
+
4250
+ if (!currentModuleResolver) return T.undefined;
4251
+
4252
+ const resolved = currentModuleResolver(source, currentFileDir);
4253
+ if (!resolved) {
4254
+ // 相对/绝对说明符解析失败:此前完全静默(绑定凭空缺失,只剩下游
4255
+ // 「unknown global」噪音)。缺失文件报错带完整候选路径;裸说明符
4256
+ // (npm 包)保持静默,与 require() 的 bare 分支口径一致。
4257
+ if (source.startsWith(".") || source.startsWith("/")) {
4258
+ recordUnknown({
4259
+ kind: "global",
4260
+ name: `module-missing:${moduleLabel(source)}`,
4261
+ loc: node.loc,
4262
+ reason: `Cannot resolve import '${source}': no file at ${attemptedFullPath(currentFileDir, source)} (with or without .js/.ts/.mjs extension)`,
4263
+ });
4264
+ }
4265
+ return T.undefined;
1604
4266
  }
1605
4267
 
4268
+ const moduleEnv = loadModuleEnv(resolved, node.loc);
4269
+
1606
4270
  for (const spec of node.specifiers) {
1607
4271
  if (spec.type === "ImportDefaultSpecifier") {
1608
4272
  const val = moduleEnv.has(`__export_default`) ? moduleEnv.lookup(`__export_default`) : T.unknown;
@@ -1643,7 +4307,9 @@ function evaluateClassDeclaration(node: Node & { type: "ClassDeclaration" }, env
1643
4307
  );
1644
4308
  const fnType = T.fn(paramNames, member.body, env) as TypeValue & { kind: "function" };
1645
4309
  (fnType as any)._paramPatterns = member.params;
4310
+ (fnType as any)._name = `${className}.${methodName}`;
1646
4311
  if (member.async) (fnType as any)._async = true;
4312
+ tagFnModule(fnType);
1647
4313
 
1648
4314
  if (member.kind === "constructor") {
1649
4315
  constructorFn = fnType;
@@ -1671,26 +4337,320 @@ function evaluateInstanceof(left: TypeValue, _right: TypeValue, rightNode: Node,
1671
4337
  isSubtypeOf(lv, T.instanceOf(className));
1672
4338
  return T.literal(matches);
1673
4339
  }
4340
+ // Structurally-known receivers decide `x instanceof C` literally:
4341
+ // plain objects are never Date/Buffer/..., arrays are Array/Object, ...
4342
+ // (hoek getInternalProto's instanceof chain keys off this precision).
4343
+ const structural = builtinInstanceTest(lv, className);
4344
+ if (structural !== undefined) return T.literal(structural);
1674
4345
  return T.boolean;
1675
4346
  });
1676
4347
  }
1677
4348
 
1678
- const BUILTIN_ERROR_CLASSES = new Set([
1679
- "Error", "TypeError", "SyntaxError", "RangeError", "ReferenceError", "URIError", "EvalError",
1680
- ]);
4349
+ // --- new Promise(executor) ---
4350
+
4351
+ // Budgets for the static resolve-site scan: resolve calls hiding inside
4352
+ // never-executed callbacks are invisible to the executor run, so the
4353
+ // executor AST is walked for them. Bounded to keep large executors from
4354
+ // ballooning analysis time.
4355
+ const PROMISE_SCAN_MAX_SITES = 16;
4356
+ const PROMISE_SCAN_MAX_NODES = 2000;
4357
+
4358
+ // Synthetic resolve/reject collectors handed to executors (see
4359
+ // makePromiseCollector). Calls to them are internal plumbing; recordCall
4360
+ // consults this set to avoid emitting them as user call sites.
4361
+ const _promiseCollectors = new WeakSet<object>();
4362
+
4363
+ function makePromiseCollector(sink: TypeValue[]): TypeValue {
4364
+ const collector = T.fnSig([T.unknown], T.undefined, T.never, (args) => {
4365
+ sink.push(args[0] ?? T.undefined);
4366
+ return T.undefined;
4367
+ });
4368
+ _promiseCollectors.add(collector as unknown as object);
4369
+ return collector;
4370
+ }
4371
+
4372
+ // Resolve sites hiding in never-executed callbacks (`setTimeout(() =>
4373
+ // resolve(x), 1)`, `emitter.on('end', () => resolve(x))`) never fire during
4374
+ // the executor run; walk the executor subtree for `resolve(...)` calls —
4375
+ // matched by the executor's actual first-parameter name — and collect each
4376
+ // site's first argument node. Sites with spread/no arguments are skipped.
4377
+ function findResolveArgSites(root: Node, resolveName: string): Node[] {
4378
+ const sites: Node[] = [];
4379
+ let visited = 0;
4380
+ const stack: Node[] = [root];
4381
+ while (stack.length > 0 && sites.length < PROMISE_SCAN_MAX_SITES) {
4382
+ const n = stack.pop()!;
4383
+ if (++visited > PROMISE_SCAN_MAX_NODES) break;
4384
+ if (n.type === "CallExpression" || n.type === "OptionalCallExpression") {
4385
+ const c = (n as any).callee as Node | undefined;
4386
+ if (c?.type === "Identifier" && c.name === resolveName) {
4387
+ const first = (n as any).arguments?.[0] as Node | undefined;
4388
+ if (first && first.type !== "SpreadElement") sites.push(first);
4389
+ }
4390
+ }
4391
+ for (const key of Object.keys(n)) {
4392
+ if (key === "loc" || key === "leadingComments" || key === "trailingComments" || key === "innerComments") continue;
4393
+ const v = (n as any)[key];
4394
+ if (Array.isArray(v)) {
4395
+ for (const el of v) {
4396
+ if (el && typeof el === "object" && typeof (el as any).type === "string") stack.push(el as Node);
4397
+ }
4398
+ } else if (v && typeof v === "object" && typeof v.type === "string") {
4399
+ stack.push(v as Node);
4400
+ }
4401
+ }
4402
+ }
4403
+ return sites;
4404
+ }
4405
+
4406
+ // Returns the promise's fulfillment type. The executor call environment is
4407
+ // built here (not buried in callFunctionUnchecked) so the static scan can
4408
+ // evaluate resolve-site arguments in the post-execution executor scope,
4409
+ // where executor locals and the closure chain (outer params, e.g. hoek's
4410
+ // wait(timeout, returnValue)) are both visible.
4411
+ function evaluatePromiseExecutor(executor: TypeValue & { kind: "function" }): TypeValue {
4412
+ const resolveCollected: TypeValue[] = [];
4413
+ // reject is collected too (complete executor semantics) but only resolve
4414
+ // determines the fulfillment type.
4415
+ const rejectCollected: TypeValue[] = [];
4416
+ const callEnv = executor.closure.extend({});
4417
+ bindFunctionParams(executor, [makePromiseCollector(resolveCollected), makePromiseCollector(rejectCollected)], callEnv);
4418
+
4419
+ // try/finally 语义下 resolve 位点(位于 try 内)先于 finally 执行,但
4420
+ // 这里是先执行完整个 body 再扫描——finally 的副作用(如 finally
4421
+ // { parser = null })会毒化扫描求值(parser.finish() 变 no-method on
4422
+ // null)。快照执行前的绑定链;扫描时当前值被 null/undefined 化而快照
4423
+ // 值非空的绑定回退快照值(finally-nullification 的签名)。循环累加
4424
+ // (当前值更丰富)不受影响。
4425
+ const preBodySnapshot = callEnv.snapshot();
4426
+
4427
+ // Execute the executor body synchronously; direct resolve/reject calls
4428
+ // hit the collectors through normal parameter binding.
4429
+ if (_callDepth < MAX_CALL_DEPTH && _totalCalls < MAX_TOTAL_CALLS) {
4430
+ _callDepth++;
4431
+ _totalCalls++;
4432
+ const savedUnreachable = _unreachableRanges;
4433
+ _unreachableRanges = [];
4434
+ try {
4435
+ evaluate(executor.body, callEnv);
4436
+ } finally {
4437
+ _unreachableRanges = savedUnreachable;
4438
+ _callDepth--;
4439
+ }
4440
+ }
4441
+
4442
+ const isNullish = (v: TypeValue): boolean =>
4443
+ v.kind === "literal" && (v.value === null || v.value === undefined);
4444
+ const scanEnv: Environment = {
4445
+ lookup: (name: string) => {
4446
+ const cur = callEnv.lookup(name);
4447
+ if (isNullish(cur)) {
4448
+ const snap = preBodySnapshot.lookup(name);
4449
+ if (!isNullish(snap) && snap !== T.undefined) return snap;
4450
+ }
4451
+ return cur;
4452
+ },
4453
+ bind: (name, value) => callEnv.bind(name, value),
4454
+ update: (name, value) => callEnv.update(name, value),
4455
+ extend: (b) => callEnv.extend(b),
4456
+ fork: () => callEnv.fork(),
4457
+ has: (name) => callEnv.has(name),
4458
+ snapshot: () => callEnv.snapshot(),
4459
+ getOwnBindings: () => callEnv.getOwnBindings(),
4460
+ };
4461
+
4462
+ const firstPattern = ((executor as any)._paramPatterns as Node[] | undefined)?.[0];
4463
+ if (firstPattern?.type === "Identifier") {
4464
+ for (const argExpr of findResolveArgSites(executor.body, firstPattern.name)) {
4465
+ const argVal = evaluate(argExpr, scanEnv);
4466
+ if (isReturn(argVal) || isBranch(argVal) || isThrow(argVal)) continue;
4467
+ resolveCollected.push(argVal);
4468
+ }
4469
+ }
4470
+
4471
+ // No fulfillment value observed → never-resolving promise (hoek block()'s
4472
+ // `new Promise(Ignore)`); T.union flattens/dedups the collected values.
4473
+ return T.promise(T.union(...resolveCollected));
4474
+ }
4475
+
4476
+ // .then(cb): run the callback on the resolved value through the main call
4477
+ // path so the callback's return type flows into the chained promise; a
4478
+ // callback returning a promise flattens (runtime then-semantics). Returns
4479
+ // null when not applicable (non-then method, non-callable handler).
4480
+ function applyPromiseThenCallback(
4481
+ promiseValue: TypeValue & { kind: "promise" },
4482
+ argVals: TypeValue[],
4483
+ ): TypeValue | null {
4484
+ const cb = argVals[0];
4485
+ if (!cb || cb.kind !== "function") return null;
4486
+ const result = callFunctionFull(cb, [promiseValue.value]).value;
4487
+ return result.kind === "promise" ? result : T.promise(result);
4488
+ }
4489
+
4490
+ // Bind call arguments to a function value's parameters (rest parameters
4491
+ // collect the remaining arguments into a tuple; patterns delegate to
4492
+ // bindPattern). Shared by the direct call path, class construction, and
4493
+ // the Promise executor run.
4494
+ function bindFunctionParams(
4495
+ fn: TypeValue & { kind: "function" },
4496
+ args: TypeValue[],
4497
+ callEnv: Environment,
4498
+ ): void {
4499
+ const paramPatterns = (fn as any)._paramPatterns as Node[] | undefined;
4500
+ for (let i = 0; i < fn.params.length; i++) {
4501
+ const paramName = fn.params[i];
4502
+ // Check if this is a rest parameter (starts with ...)
4503
+ if (paramName.startsWith("...")) {
4504
+ const restValue = T.tuple(args.slice(i));
4505
+ if (paramPatterns && paramPatterns[i]) {
4506
+ bindPattern(paramPatterns[i], restValue, callEnv);
4507
+ } else {
4508
+ callEnv.bind(paramName.slice(3), restValue); // Remove "..." prefix
4509
+ }
4510
+ } else {
4511
+ let argVal = args[i] ?? T.undefined;
4512
+ {
4513
+ const tagged = tagParamArg(argVal, paramName);
4514
+ copyOrigin(argVal, tagged);
4515
+ argVal = tagged;
4516
+ }
4517
+ if (paramPatterns && paramPatterns[i]) {
4518
+ bindPattern(paramPatterns[i], argVal, callEnv);
4519
+ } else {
4520
+ callEnv.bind(paramName, argVal);
4521
+ }
4522
+ }
4523
+ }
4524
+ // 非箭头函数的 arguments 对象:tuple(实参) —— length/索引/迭代/展开
4525
+ // 经由既有 tuple 机制直接可用。箭头函数不绑(词法作用域回退外层,
4526
+ // 真实 JS 语义);_hasArguments 在函数值创建位点按节点类型标记。
4527
+ if ((fn as any)._hasArguments) {
4528
+ callEnv.bind("arguments", T.tuple(args));
4529
+ }
4530
+ }
1681
4531
 
1682
4532
  function evaluateNewExpression(node: Node & { type: "NewExpression" }, env: Environment): EvalResult {
1683
4533
  const callee = node.callee as Node;
1684
- if (callee.type === "Identifier" && BUILTIN_ERROR_CLASSES.has(callee.name)) {
4534
+ if (callee.type === "Identifier" && BUILTIN_ERROR_CLASSES.has(callee.name) && !env.has(callee.name)) {
1685
4535
  const argVals = evaluateArgs(node.arguments as Node[], env);
1686
4536
  if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
1687
4537
  const msgVal = (argVals as TypeValue[])[0] ?? T.undefined;
1688
4538
  return T.instanceOf(callee.name, { message: msgVal });
1689
4539
  }
1690
4540
 
4541
+ // Handle new Map()
4542
+ if (callee.type === "Identifier" && callee.name === "Map" && !env.has("Map")) {
4543
+ const argVals = evaluateArgs(node.arguments as Node[], env);
4544
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
4545
+ return createMapType(argVals as TypeValue[]);
4546
+ }
4547
+
4548
+ // Handle new Set()
4549
+ if (callee.type === "Identifier" && callee.name === "Set" && !env.has("Set")) {
4550
+ const argVals = evaluateArgs(node.arguments as Node[], env);
4551
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
4552
+ return createSetType(argVals as TypeValue[]);
4553
+ }
4554
+
4555
+ // Handle new RegExp()
4556
+ if (callee.type === "Identifier" && callee.name === "RegExp" && !env.has("RegExp")) {
4557
+ return createRegExpType();
4558
+ }
4559
+
4560
+ // Handle new URL()
4561
+ if (callee.type === "Identifier" && callee.name === "URL" && !env.has("URL")) {
4562
+ return createURLType();
4563
+ }
4564
+
4565
+ // Handle new URLSearchParams()
4566
+ if (callee.type === "Identifier" && callee.name === "URLSearchParams" && !env.has("URLSearchParams")) {
4567
+ return createURLSearchParamsType();
4568
+ }
4569
+
4570
+ // Handle new Response()
4571
+ if (callee.type === "Identifier" && callee.name === "Response" && !env.has("Response")) {
4572
+ return createResponseType();
4573
+ }
4574
+
4575
+ // Handle new Headers()
4576
+ if (callee.type === "Identifier" && callee.name === "Headers" && !env.has("Headers")) {
4577
+ return createHeadersType();
4578
+ }
4579
+
4580
+ // Handle new FormData()
4581
+ if (callee.type === "Identifier" && callee.name === "FormData" && !env.has("FormData")) {
4582
+ return createFormDataType();
4583
+ }
4584
+
4585
+ // Handle new AbortController()
4586
+ if (callee.type === "Identifier" && callee.name === "AbortController" && !env.has("AbortController")) {
4587
+ return createAbortControllerType();
4588
+ }
4589
+
4590
+ // Handle new WeakMap()
4591
+ if (callee.type === "Identifier" && callee.name === "WeakMap" && !env.has("WeakMap")) {
4592
+ return createWeakMapType();
4593
+ }
4594
+
4595
+ // Handle new WeakSet()
4596
+ if (callee.type === "Identifier" && callee.name === "WeakSet" && !env.has("WeakSet")) {
4597
+ return createWeakSetType();
4598
+ }
4599
+
4600
+ // Handle new Promise(executor): run the executor with resolve/reject
4601
+ // collectors, then statically scan for resolve sites hiding inside
4602
+ // never-executed callbacks (setTimeout / emitter.on) to recover the
4603
+ // fulfillment type. Unknown/non-function executor still yields an honest
4604
+ // Promise<unknown> instead of a bare unknown.
4605
+ if (callee.type === "Identifier" && callee.name === "Promise" && !env.has("Promise")) {
4606
+ const argVals = evaluateArgs(node.arguments as Node[], env);
4607
+ if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
4608
+ const executor = (argVals as TypeValue[])[0];
4609
+ if (executor && executor.kind === "function") {
4610
+ return evaluatePromiseExecutor(executor);
4611
+ }
4612
+ return T.promise(T.unknown);
4613
+ }
4614
+
4615
+ // Handle new Intl.DateTimeFormat() and new Intl.NumberFormat()
4616
+ if (callee.type === "MemberExpression" && !callee.computed) {
4617
+ const obj = callee.object as Node;
4618
+ const prop = callee.property as Node;
4619
+ if (obj.type === "Identifier" && obj.name === "Intl" && !env.has("Intl") && prop.type === "Identifier") {
4620
+ if (prop.name === "DateTimeFormat") {
4621
+ return createDateTimeFormatType();
4622
+ }
4623
+ if (prop.name === "NumberFormat") {
4624
+ return createNumberFormatType();
4625
+ }
4626
+ }
4627
+ }
4628
+
1691
4629
  const calleeVal = evaluate(callee, env);
1692
4630
  if (isReturn(calleeVal) || isBranch(calleeVal) || isThrow(calleeVal)) return calleeVal;
1693
4631
 
4632
+ // Reflection constructors: `new (X.prototype)()` (hoek clone's
4633
+ // `new Types.set()`) builds a fresh instance of the prototype's class;
4634
+ // collection classes get their side-table-carrying factories.
4635
+ const protoClassName = calleeVal.kind === "instance" ? (calleeVal as any)._builtinProto as string | undefined : undefined;
4636
+ if (protoClassName) {
4637
+ if (protoClassName === "Set") return createSetType();
4638
+ if (protoClassName === "Map") return createMapType();
4639
+ if (protoClassName === "Array") return T.array(T.unknown);
4640
+ return T.instanceOf(protoClassName);
4641
+ }
4642
+
4643
+ // Class-namespace constructors (`new (x.constructor)(n)` where the
4644
+ // constructor is a memoized builtinClassValue): Array approximates to
4645
+ // unknown[], Object to a plain object, everything else to its class
4646
+ // instance.
4647
+ const classNamespace = calleeVal.kind === "object" ? (calleeVal as any)._builtinName as string | undefined : undefined;
4648
+ if (classNamespace) {
4649
+ if (classNamespace === "Array") return T.array(T.unknown);
4650
+ if (classNamespace === "Object") return T.object({});
4651
+ return T.instanceOf(classNamespace);
4652
+ }
4653
+
1694
4654
  if (calleeVal.kind === "function") {
1695
4655
  const argVals = evaluateArgs(node.arguments as Node[], env);
1696
4656
  if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
@@ -1701,15 +4661,7 @@ function evaluateNewExpression(node: Node & { type: "NewExpression" }, env: Envi
1701
4661
  const constructEnv = calleeVal.closure.extend({});
1702
4662
  const thisObj = T.object(instanceProps);
1703
4663
  constructEnv.bind("this", thisObj);
1704
- const paramPatterns = (calleeVal as any)._paramPatterns as Node[] | undefined;
1705
- for (let i = 0; i < calleeVal.params.length; i++) {
1706
- const argVal = (argVals as TypeValue[])[i] ?? T.undefined;
1707
- if (paramPatterns?.[i]) {
1708
- bindPattern(paramPatterns[i], argVal, constructEnv);
1709
- } else {
1710
- constructEnv.bind(calleeVal.params[i], argVal);
1711
- }
1712
- }
4664
+ bindFunctionParams(calleeVal, argVals as TypeValue[], constructEnv);
1713
4665
  const result = evaluate(calleeVal.body, constructEnv);
1714
4666
  if (isThrow(result)) return result;
1715
4667
  const finalThis = constructEnv.lookup("this");
@@ -1727,7 +4679,7 @@ function evaluateNewExpression(node: Node & { type: "NewExpression" }, env: Envi
1727
4679
  }
1728
4680
 
1729
4681
  function evaluateTryStatement(node: Node & { type: "TryStatement" }, env: Environment): EvalResult {
1730
- const tryResult = evaluateStatements(node.block.body, env.extend({}));
4682
+ const tryResult = evaluateStatements(node.block.body, env.fork());
1731
4683
 
1732
4684
  const thrownType = isThrow(tryResult) ? tryResult.thrown : null;
1733
4685
 
@@ -1739,9 +4691,13 @@ function evaluateTryStatement(node: Node & { type: "TryStatement" }, env: Enviro
1739
4691
  ? tryResult
1740
4692
  : tryResult;
1741
4693
 
4694
+ // The handler only runs when the try block actually threw along the
4695
+ // evaluated paths. Evaluating it unconditionally unions the catch value
4696
+ // into every try/catch (with `err` bound to unknown when nothing threw),
4697
+ // which poisons guards like hoek's `try { return v.call(o) } catch …`.
1742
4698
  let catchResult: EvalResult | null = null;
1743
- if (node.handler && thrownType) {
1744
- const catchEnv = env.extend({});
4699
+ if (node.handler && thrownType !== null) {
4700
+ const catchEnv = env.fork();
1745
4701
  if (node.handler.param) {
1746
4702
  bindPattern(node.handler.param, thrownType, catchEnv);
1747
4703
  }
@@ -1749,7 +4705,7 @@ function evaluateTryStatement(node: Node & { type: "TryStatement" }, env: Enviro
1749
4705
  }
1750
4706
 
1751
4707
  if (node.finalizer) {
1752
- const finallyResult = evaluateStatements(node.finalizer.body, env.extend({}));
4708
+ const finallyResult = evaluateStatements(node.finalizer.body, env.fork());
1753
4709
  if (isThrow(finallyResult)) return finallyResult;
1754
4710
  if (isReturn(finallyResult)) return finallyResult;
1755
4711
  }
@@ -1798,7 +4754,11 @@ function evaluateSwitchStatement(node: Node & { type: "SwitchStatement" }, env:
1798
4754
  }
1799
4755
  if (matched) {
1800
4756
  const result = evaluateStatements(caseNode.consequent, env);
1801
- if (isThrow(result)) return result;
4757
+ if (isThrow(result)) {
4758
+ // switch 是 break 的边界:吸收后正常落穿
4759
+ if (loopSignalKind(result) === "break") break;
4760
+ return result;
4761
+ }
1802
4762
  if (isReturn(result)) {
1803
4763
  returnValues.push(result.value);
1804
4764
  break;
@@ -1817,7 +4777,28 @@ function evaluateSwitchStatement(node: Node & { type: "SwitchStatement" }, env:
1817
4777
 
1818
4778
  const returnValues: TypeValue[] = [];
1819
4779
  for (const caseNode of node.cases) {
1820
- const result = evaluateStatements(caseNode.consequent, env);
4780
+ let caseEnv = env;
4781
+ if (caseNode.test) {
4782
+ const testVal = evaluate(caseNode.test, env);
4783
+ if (isReturn(testVal) || isBranch(testVal) || isThrow(testVal)) return testVal;
4784
+
4785
+ // Try to narrow discriminant for this case using existing narrow()
4786
+ // Construct a synthetic BinaryExpression: discriminant === caseTest
4787
+ const syntheticTest = {
4788
+ type: "BinaryExpression",
4789
+ operator: "===",
4790
+ left: node.discriminant,
4791
+ right: caseNode.test,
4792
+ start: 0,
4793
+ end: 0,
4794
+ loc: null,
4795
+ } as unknown as Node;
4796
+ const [narrowedEnv] = narrow(syntheticTest, env);
4797
+ caseEnv = narrowedEnv;
4798
+ }
4799
+
4800
+ const result = evaluateStatements(caseNode.consequent, caseEnv);
4801
+ if (loopSignalKind(result) === "break") break;
1821
4802
  if (isThrow(result)) continue;
1822
4803
  if (isReturn(result)) {
1823
4804
  returnValues.push(result.value);
@@ -1840,19 +4821,210 @@ type CallResult = {
1840
4821
  throwLoc?: SourceRange;
1841
4822
  };
1842
4823
 
1843
- function callFunctionFull(fn: TypeValue & { kind: "function" }, args: TypeValue[]): CallResult {
1844
- const callEnv = fn.closure.extend({});
1845
- const paramPatterns = (fn as any)._paramPatterns as Node[] | undefined;
1846
- const isAsync = !!(fn as any)._async;
1847
- for (let i = 0; i < fn.params.length; i++) {
1848
- const argVal = args[i] ?? T.undefined;
1849
- if (paramPatterns && paramPatterns[i]) {
1850
- bindPattern(paramPatterns[i], argVal, callEnv);
1851
- } else {
1852
- callEnv.bind(fn.params[i], argVal);
4824
+ // --- Recursion termination: cycle detection + depth/work budgets ---
4825
+ // Abstract interpretation unrolls calls instead of iterating to a fixpoint,
4826
+ // so recursive functions invoked under unconstrained arguments (e.g. an
4827
+ // entry@ case where `seen.has(obj)` cannot concretely short-circuit) expand
4828
+ // forever. Three guards cut the expansion, all returning T.unknown — the
4829
+ // call-side dual of loop widening: the result simply drops the deeper
4830
+ // iterations' effects instead of guessing them, a sound over-approximation.
4831
+ //
4832
+ // 1. Cycle detection: a call whose (function value, abstract args) pair is
4833
+ // already being evaluated higher on the stack makes no abstract progress
4834
+ // (clone(obj, seen) re-invoking itself with identical types). Unrolling it
4835
+ // anyway fans out exponentially, so the re-entrant edge is cut at its
4836
+ // first recurrence — the same treatment the call memo applies on
4837
+ // MEMO_IN_PROGRESS. Accumulator-growing recursion (deepEqual's `seen`
4838
+ // tuple getting one entry longer per level) never repeats a key verbatim,
4839
+ // so cycle keys are *widened*: tuples longer than CYCLE_TUPLE_CAP and
4840
+ // objects with more than CYCLE_PROP_CAP properties all normalize to the
4841
+ // same key, making accumulator growth collapse after a few levels.
4842
+ // Args-changing recursion (factorial(5) -> factorial(4)) is unaffected
4843
+ // because each level still has a distinct widened key.
4844
+ // 2. Depth budget: backstop for recursion whose argument types keep morphing
4845
+ // without structurally repeating (f(n) -> f([n]) -> f([[n]]) ...) beyond
4846
+ // what the widening recognizes.
4847
+ // 3. Total call budget: absolute bound on the unrolled call tree so no
4848
+ // pathological branching shape can exhaust memory; analysis of any file
4849
+ // stays linear in this budget.
4850
+ const MAX_CALL_DEPTH = 64;
4851
+ const MAX_TOTAL_CALLS = 200_000;
4852
+ const CYCLE_TUPLE_CAP = 4;
4853
+ const CYCLE_PROP_CAP = 8;
4854
+ let _callDepth = 0;
4855
+ let _totalCalls = 0;
4856
+ const _activeCallKeys: string[] = [];
4857
+ const _fnCallIds = new WeakMap<object, string>();
4858
+ let _fnCallIdSeq = 0;
4859
+
4860
+ // Observed return values per function value: completed (non-truncated)
4861
+ // calls accumulate their results so a budget-truncated re-entry — deep
4862
+ // recursion or the re-evaluation fan-out heterogeneous union arguments
4863
+ // trigger — degrades to "what this function has actually returned" instead
4864
+ // of poisoning the caller's union with unknown. Keyed on the fn value, so
4865
+ // entries die with it; no reset hook needed.
4866
+ const _fnResultObs = new WeakMap<object, TypeValue[]>();
4867
+ const FN_RESULT_OBS_CAP = 16;
4868
+
4869
+ function recordFnResultObs(fn: TypeValue & { kind: "function" }, value: TypeValue): void {
4870
+ // never (always throws) and unknown carry no shape information; recording
4871
+ // unknown would just re-poison the fallback union.
4872
+ if (value.kind === "never" || value.kind === "unknown") return;
4873
+ let obs = _fnResultObs.get(fn);
4874
+ if (!obs) {
4875
+ obs = [];
4876
+ _fnResultObs.set(fn, obs);
4877
+ }
4878
+ // Cap keeps the table bounded; dedupe keeps one dominant result from
4879
+ // exhausting the cap before a second shape is ever seen.
4880
+ if (obs.length >= FN_RESULT_OBS_CAP) return;
4881
+ if (obs.some((o) => typeValueEquals(o, value))) return;
4882
+ obs.push(value);
4883
+ }
4884
+
4885
+ function observedResultFallback(fn: TypeValue & { kind: "function" }): TypeValue | undefined {
4886
+ const obs = _fnResultObs.get(fn);
4887
+ if (!obs || obs.length === 0) return undefined;
4888
+ // Union of observed results; >4 same-base literal members collapse to
4889
+ // the base type so long observation tails stay a primitive, not a
4890
+ // sixteen-way literal union.
4891
+ const union = collapseLiteralUnion(simplifyUnion(obs), 4);
4892
+ return union.kind === "never" ? undefined : union;
4893
+ }
4894
+
4895
+ // Memoized per type value: the same argument values are keyed on every
4896
+ // call of a loop (fixture-scale structures made this the dominant cost of
4897
+ // callsite harvesting). Stale entries after in-place property mutation can
4898
+ // only trigger earlier cycle-truncation (sound: result widens to unknown),
4899
+ // never a missed one that would over-run the depth budget.
4900
+ const cycleArgKeyCache = new WeakMap<object, string>();
4901
+
4902
+ function cycleArgKey(tv: TypeValue): string {
4903
+ if (tv.kind === "tuple" || tv.kind === "object" || tv.kind === "union") {
4904
+ const cached = cycleArgKeyCache.get(tv);
4905
+ if (cached !== undefined) return cached;
4906
+ const key = cycleArgKeyUncached(tv);
4907
+ cycleArgKeyCache.set(tv, key);
4908
+ return key;
4909
+ }
4910
+ return cycleArgKeyUncached(tv);
4911
+ }
4912
+
4913
+ function cycleArgKeyUncached(tv: TypeValue): string {
4914
+ if (tv.kind === "tuple") {
4915
+ if (tv.elements.length > CYCLE_TUPLE_CAP) {
4916
+ return `[${tv.elements.slice(0, CYCLE_TUPLE_CAP).map(cycleArgKey).join(",")},…widened]`;
4917
+ }
4918
+ return `[${tv.elements.map(cycleArgKey).join(",")}]`;
4919
+ }
4920
+ if (tv.kind === "object") {
4921
+ const entries = Object.entries(tv.properties);
4922
+ if (entries.length > CYCLE_PROP_CAP) {
4923
+ return `{${entries.slice(0, CYCLE_PROP_CAP).map(([k, v]) => `${k}:${cycleArgKey(v)}`).join(",")},…widened}`;
1853
4924
  }
4925
+ return `{${entries.map(([k, v]) => `${k}:${cycleArgKey(v)}`).join(",")}}`;
4926
+ }
4927
+ if (tv.kind === "union") {
4928
+ return tv.members.map(cycleArgKey).join("|");
4929
+ }
4930
+ // Cheap leaf tokens: full typeValueToString rendering of instances and
4931
+ // functions (method tables!) dominates key-building on fixture-scale args.
4932
+ // A coarser token can only merge distinct states into one cycle key —
4933
+ // earlier truncation, which widens to unknown (sound).
4934
+ if (tv.kind === "instance") {
4935
+ const propNames = Object.keys(tv.properties).slice(0, CYCLE_PROP_CAP).join(",");
4936
+ return `inst(${tv.className}{${propNames}})`;
4937
+ }
4938
+ if (tv.kind === "function") return `fn#${objectId(tv)}`;
4939
+ if (tv.kind === "refined") return cycleArgKey(tv.base);
4940
+ if (tv.kind === "promise") return `prom(${cycleArgKey(tv.value)})`;
4941
+ if (tv.kind === "literal") {
4942
+ const s = String(tv.value);
4943
+ return `lit(${typeof tv.value}:${s.length > 24 ? s.slice(0, 24) + "…" : s})`;
4944
+ }
4945
+ if (tv.kind === "primitive") return `prim(${tv.type})`;
4946
+ return tv.kind;
4947
+ }
4948
+
4949
+ function fnCallKey(fn: TypeValue & { kind: "function" }, args: TypeValue[], thisVal?: TypeValue): string {
4950
+ let id = _fnCallIds.get(fn);
4951
+ if (!id) {
4952
+ id = `fn#${++_fnCallIdSeq}`;
4953
+ _fnCallIds.set(fn, id);
4954
+ }
4955
+ const thisKey = thisVal ? `@${cycleArgKey(thisVal)}` : "";
4956
+ return `${id}${thisKey}(${args.map(cycleArgKey).join(",")})`;
4957
+ }
4958
+
4959
+ function callBudgetExhausted(fnName: string, loc: Node["loc"]): CallResult {
4960
+ // kind "global" reuses the existing UnknownRecord union (the analyzer
4961
+ // renders it as a warning-level record; no analyzer change needed).
4962
+ recordUnknown({
4963
+ kind: "global",
4964
+ name: `recursion:${fnName}`,
4965
+ loc,
4966
+ reason: `recursive call truncated to unknown (cycle, depth > ${MAX_CALL_DEPTH}, or work budget)`,
4967
+ });
4968
+ return { value: T.unknown, throws: T.never };
4969
+ }
4970
+
4971
+ function callFunctionFull(
4972
+ fn: TypeValue & { kind: "function" },
4973
+ args: TypeValue[],
4974
+ thisVal?: TypeValue,
4975
+ ): CallResult {
4976
+ const callKey = fnCallKey(fn, args, thisVal);
4977
+ const fnName = (fn as any)._name ?? "<anonymous>";
4978
+ if (_activeCallKeys.includes(callKey) || _callDepth >= MAX_CALL_DEPTH || _totalCalls >= MAX_TOTAL_CALLS) {
4979
+ // Diagnostic unchanged (truncation stays visible); the value degrades to
4980
+ // this function's observed results before unknown. The truncated call
4981
+ // shape itself stays unmemoized — this is not its true result.
4982
+ const exhausted = callBudgetExhausted(fnName, fn.body?.loc);
4983
+ const observed = observedResultFallback(fn);
4984
+ return observed !== undefined ? { value: observed, throws: exhausted.throws } : exhausted;
4985
+ }
4986
+ _totalCalls++;
4987
+ _callDepth++;
4988
+ _activeCallKeys.push(callKey);
4989
+ const fnOrigin = _fnModuleTags.get(fn);
4990
+ _execOriginStack.push(fnOrigin ?? _execOriginStack[_execOriginStack.length - 1]);
4991
+ try {
4992
+ const result = callFunctionUnchecked(fn, args, thisVal);
4993
+ recordFnResultObs(fn, result.value);
4994
+ return result;
4995
+ } finally {
4996
+ _execOriginStack.pop();
4997
+ _activeCallKeys.pop();
4998
+ _callDepth--;
4999
+ }
5000
+ }
5001
+
5002
+ function callFunctionUnchecked(
5003
+ fn: TypeValue & { kind: "function" },
5004
+ args: TypeValue[],
5005
+ thisVal?: TypeValue,
5006
+ ): CallResult {
5007
+ // Check for direct return value (used by sinon mocks)
5008
+ const directReturn = (fn as any)._directReturn as TypeValue | undefined;
5009
+ if (directReturn) {
5010
+ return { value: directReturn, throws: T.never };
5011
+ }
5012
+
5013
+ // Check for function signature impl (used by @nudo:env / @nudo:mock-module)
5014
+ const sig = getFnSig(fn);
5015
+ if (sig) {
5016
+ const implResult = sig.impl?.(args, thisVal);
5017
+ return { value: implResult ?? sig.returnType, throws: sig.throwsType };
1854
5018
  }
1855
5019
 
5020
+ const callEnv = fn.closure.extend({});
5021
+ // `obj.f()` binds the receiver as the callee's `this` before parameters.
5022
+ // Arrow callees ignore it in real JS; their bodies still resolve `this`
5023
+ // through the closure chain here — accepted approximation.
5024
+ if (thisVal) callEnv.bind("this", thisVal);
5025
+ const isAsync = !!(fn as any)._async;
5026
+ bindFunctionParams(fn, args, callEnv);
5027
+
1856
5028
  const savedUnreachable = _unreachableRanges;
1857
5029
  _unreachableRanges = [];
1858
5030
 
@@ -1892,8 +5064,12 @@ function callFunctionFull(fn: TypeValue & { kind: "function" }, args: TypeValue[
1892
5064
  return { value: wrapped, throws: T.never };
1893
5065
  }
1894
5066
 
1895
- function callFunction(fn: TypeValue & { kind: "function" }, args: TypeValue[]): TypeValue {
1896
- return callFunctionFull(fn, args).value;
5067
+ function callFunction(
5068
+ fn: TypeValue & { kind: "function" },
5069
+ args: TypeValue[],
5070
+ thisVal?: TypeValue,
5071
+ ): TypeValue {
5072
+ return callFunctionFull(fn, args, thisVal).value;
1897
5073
  }
1898
5074
 
1899
5075
  export function evaluateFunction(
@@ -1909,24 +5085,65 @@ export function evaluateFunctionFull(
1909
5085
  args: TypeValue[],
1910
5086
  env: Environment,
1911
5087
  ): CallResult {
1912
- if (fnNode.type === "FunctionDeclaration" || fnNode.type === "FunctionExpression") {
1913
- const callEnv = env.extend({});
1914
- const isAsync = !!(fnNode as any).async;
1915
- for (let i = 0; i < fnNode.params.length; i++) {
1916
- bindPattern(fnNode.params[i], args[i] ?? T.undefined, callEnv);
1917
- }
1918
- const result = evaluate(fnNode.body, callEnv);
1919
- if (isThrow(result)) return { value: T.never, throws: result.thrown, throwLoc: result.loc };
1920
- const value = isReturn(result) ? result.value
1921
- : isBranch(result) ? result.returnedValue
1922
- : result;
1923
- const wrapped = isAsync ? T.promise(value) : value;
1924
- return { value: wrapped, throws: T.never };
5088
+ // Unwrap export declarations to get the actual function
5089
+ let actualNode = fnNode;
5090
+ if (fnNode.type === "ExportNamedDeclaration" && fnNode.declaration) {
5091
+ actualNode = fnNode.declaration;
5092
+ } else if (fnNode.type === "ExportDefaultDeclaration") {
5093
+ actualNode = fnNode.declaration;
5094
+ }
5095
+
5096
+ if (actualNode.type === "FunctionDeclaration" || actualNode.type === "FunctionExpression" || actualNode.type === "ArrowFunctionExpression") {
5097
+ if (_callDepth >= MAX_CALL_DEPTH) {
5098
+ return callBudgetExhausted((actualNode as any).id?.name ?? "<anonymous>", actualNode.loc);
5099
+ }
5100
+ _callDepth++;
5101
+ try {
5102
+ const callEnv = env.fork();
5103
+ const isAsync = !!(actualNode as any).async;
5104
+ // Named functions can reference themselves; a bare case-evaluation
5105
+ // environment (analyzer's globalEnv normally has the binding, the
5106
+ // directive harness does not) still needs the self-reference for
5107
+ // recursion like `deepClone(obj[key])`.
5108
+ const fnName = (actualNode as any).id?.name as string | undefined;
5109
+ if (fnName) {
5110
+ const paramNames = (actualNode.params as Node[]).map(describeParam);
5111
+ const selfRef = T.fn(paramNames, actualNode.body, callEnv);
5112
+ (selfRef as any)._paramPatterns = actualNode.params;
5113
+ (selfRef as any)._name = fnName;
5114
+ if (isAsync) (selfRef as any)._async = true;
5115
+ callEnv.bind(fnName, selfRef);
5116
+ }
5117
+ for (let i = 0; i < actualNode.params.length; i++) {
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);
5126
+ }
5127
+ const result = evaluate(actualNode.body, callEnv);
5128
+ if (isThrow(result)) return { value: T.never, throws: result.thrown, throwLoc: result.loc };
5129
+ const value = isReturn(result) ? result.value
5130
+ : isBranch(result) ? result.returnedValue
5131
+ : result;
5132
+ // For async functions, unwrap Promise values to avoid double wrapping
5133
+ const wrapped = isAsync
5134
+ ? (value.kind === "promise" ? value : T.promise(value))
5135
+ : value;
5136
+ return { value: wrapped, throws: T.never };
5137
+ } finally {
5138
+ _callDepth--;
5139
+ }
1925
5140
  }
1926
5141
  return { value: T.unknown, throws: T.never };
1927
5142
  }
1928
5143
 
1929
5144
  export function evaluateProgram(node: Node, env: Environment): TypeValue {
5145
+ bindCommonJsGlobals(env);
5146
+ resetPhi();
1930
5147
  const result = evaluate(node, env);
1931
5148
  if (isReturn(result)) return result.value;
1932
5149
  if (isBranch(result)) return result.returnedValue;