@zzzen/pyright-internal 1.2.0-dev.20261101 → 1.2.0-dev.20261108

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 (159) hide show
  1. package/dist/analyzer/binder.js +9 -3
  2. package/dist/analyzer/binder.js.map +1 -1
  3. package/dist/analyzer/constructorTransform.js +12 -0
  4. package/dist/analyzer/constructorTransform.js.map +1 -1
  5. package/dist/analyzer/constructors.d.ts +1 -1
  6. package/dist/analyzer/constructors.js +1 -1
  7. package/dist/analyzer/constructors.js.map +1 -1
  8. package/dist/analyzer/decorators.d.ts +1 -1
  9. package/dist/analyzer/decorators.js.map +1 -1
  10. package/dist/analyzer/functionTransform.d.ts +5 -0
  11. package/dist/analyzer/functionTransform.js +6 -3
  12. package/dist/analyzer/functionTransform.js.map +1 -1
  13. package/dist/analyzer/missingBindingMetadata.d.ts +6 -0
  14. package/dist/analyzer/missingBindingMetadata.js +22 -0
  15. package/dist/analyzer/missingBindingMetadata.js.map +1 -0
  16. package/dist/analyzer/namedTuples.d.ts +4 -1
  17. package/dist/analyzer/namedTuples.js +1 -0
  18. package/dist/analyzer/namedTuples.js.map +1 -1
  19. package/dist/analyzer/patternMatching.d.ts +5 -1
  20. package/dist/analyzer/patternMatching.js +3 -0
  21. package/dist/analyzer/patternMatching.js.map +1 -1
  22. package/dist/analyzer/program.d.ts +3 -0
  23. package/dist/analyzer/program.js +71 -1
  24. package/dist/analyzer/program.js.map +1 -1
  25. package/dist/analyzer/tuples.d.ts +1 -1
  26. package/dist/analyzer/tuples.js.map +1 -1
  27. package/dist/analyzer/typeCacheUtils.d.ts +1 -0
  28. package/dist/analyzer/typeCacheUtils.js +10 -0
  29. package/dist/analyzer/typeCacheUtils.js.map +1 -1
  30. package/dist/analyzer/typeEvaluator/boundMethodSpecialization.d.ts +15 -0
  31. package/dist/analyzer/typeEvaluator/boundMethodSpecialization.js +98 -0
  32. package/dist/analyzer/typeEvaluator/boundMethodSpecialization.js.map +1 -0
  33. package/dist/analyzer/typeEvaluator/classConstruction.d.ts +25 -0
  34. package/dist/analyzer/typeEvaluator/classConstruction.js +251 -0
  35. package/dist/analyzer/typeEvaluator/classConstruction.js.map +1 -0
  36. package/dist/analyzer/typeEvaluator/collectionInference.d.ts +41 -0
  37. package/dist/analyzer/typeEvaluator/collectionInference.js +695 -0
  38. package/dist/analyzer/typeEvaluator/collectionInference.js.map +1 -0
  39. package/dist/analyzer/typeEvaluator/diagnostics.d.ts +33 -0
  40. package/dist/analyzer/typeEvaluator/diagnostics.js +198 -0
  41. package/dist/analyzer/typeEvaluator/diagnostics.js.map +1 -0
  42. package/dist/analyzer/typeEvaluator/evaluatorCore.d.ts +80 -0
  43. package/dist/analyzer/typeEvaluator/evaluatorCore.js +1126 -0
  44. package/dist/analyzer/typeEvaluator/evaluatorCore.js.map +1 -0
  45. package/dist/analyzer/typeEvaluator/evaluatorNarrowing.d.ts +17 -0
  46. package/dist/analyzer/typeEvaluator/evaluatorNarrowing.js +370 -0
  47. package/dist/analyzer/typeEvaluator/evaluatorNarrowing.js.map +1 -0
  48. package/dist/analyzer/typeEvaluator/expressionEvaluation.d.ts +81 -0
  49. package/dist/analyzer/typeEvaluator/expressionEvaluation.js +1802 -0
  50. package/dist/analyzer/typeEvaluator/expressionEvaluation.js.map +1 -0
  51. package/dist/analyzer/typeEvaluator/flowAnalysis.d.ts +33 -0
  52. package/dist/analyzer/typeEvaluator/flowAnalysis.js +217 -0
  53. package/dist/analyzer/typeEvaluator/flowAnalysis.js.map +1 -0
  54. package/dist/analyzer/typeEvaluator/internals.d.ts +137 -0
  55. package/dist/analyzer/typeEvaluator/internals.js +10 -0
  56. package/dist/analyzer/typeEvaluator/internals.js.map +1 -0
  57. package/dist/analyzer/typeEvaluator/literalTypes.d.ts +3 -0
  58. package/dist/analyzer/typeEvaluator/literalTypes.js +52 -0
  59. package/dist/analyzer/typeEvaluator/literalTypes.js.map +1 -0
  60. package/dist/analyzer/typeEvaluator/memberResolution.d.ts +79 -0
  61. package/dist/analyzer/typeEvaluator/memberResolution.js +1007 -0
  62. package/dist/analyzer/typeEvaluator/memberResolution.js.map +1 -0
  63. package/dist/analyzer/typeEvaluator/overrideValidation.d.ts +15 -0
  64. package/dist/analyzer/typeEvaluator/overrideValidation.js +422 -0
  65. package/dist/analyzer/typeEvaluator/overrideValidation.js.map +1 -0
  66. package/dist/analyzer/typeEvaluator/returnTypeInference.d.ts +33 -0
  67. package/dist/analyzer/typeEvaluator/returnTypeInference.js +226 -0
  68. package/dist/analyzer/typeEvaluator/returnTypeInference.js.map +1 -0
  69. package/dist/analyzer/typeEvaluator/specialFormCreation.d.ts +39 -0
  70. package/dist/analyzer/typeEvaluator/specialFormCreation.js +1728 -0
  71. package/dist/analyzer/typeEvaluator/specialFormCreation.js.map +1 -0
  72. package/dist/analyzer/typeEvaluator/tupleUnpacking.d.ts +2 -0
  73. package/dist/analyzer/typeEvaluator/tupleUnpacking.js +36 -0
  74. package/dist/analyzer/typeEvaluator/tupleUnpacking.js.map +1 -0
  75. package/dist/analyzer/typeEvaluator/typeCompatibility.d.ts +36 -0
  76. package/dist/analyzer/typeEvaluator/typeCompatibility.js +1320 -0
  77. package/dist/analyzer/typeEvaluator/typeCompatibility.js.map +1 -0
  78. package/dist/analyzer/typeEvaluator/typeGuardNarrowing.d.ts +186 -0
  79. package/dist/analyzer/typeEvaluator/typeGuardNarrowing.js +1683 -0
  80. package/dist/analyzer/typeEvaluator/typeGuardNarrowing.js.map +1 -0
  81. package/dist/analyzer/typeEvaluator/typeVarHandling.d.ts +48 -0
  82. package/dist/analyzer/typeEvaluator/typeVarHandling.js +756 -0
  83. package/dist/analyzer/typeEvaluator/typeVarHandling.js.map +1 -0
  84. package/dist/analyzer/typeEvaluator.d.ts +3 -31
  85. package/dist/analyzer/typeEvaluator.js +5033 -13761
  86. package/dist/analyzer/typeEvaluator.js.map +1 -1
  87. package/dist/analyzer/typeGuards.d.ts +2 -2
  88. package/dist/analyzer/typeGuards.js +212 -1567
  89. package/dist/analyzer/typeGuards.js.map +1 -1
  90. package/dist/analyzer/typeUtils.js +6 -2
  91. package/dist/analyzer/typeUtils.js.map +1 -1
  92. package/dist/analyzer/typedDicts.js +111 -0
  93. package/dist/analyzer/typedDicts.js.map +1 -1
  94. package/dist/browser/browserConnection.d.ts +12 -0
  95. package/dist/browser/browserConnection.js +23 -0
  96. package/dist/browser/browserConnection.js.map +1 -0
  97. package/dist/browser/browserFileSystem.d.ts +73 -0
  98. package/dist/browser/browserFileSystem.js +578 -0
  99. package/dist/browser/browserFileSystem.js.map +1 -0
  100. package/dist/browser/browserHost.d.ts +4 -0
  101. package/dist/browser/browserHost.js +10 -0
  102. package/dist/browser/browserHost.js.map +1 -0
  103. package/dist/browser/browserProcess.d.ts +1 -0
  104. package/dist/browser/browserProcess.js +17 -0
  105. package/dist/browser/browserProcess.js.map +1 -0
  106. package/dist/browser/browserServer.d.ts +8 -0
  107. package/dist/browser/browserServer.js +32 -0
  108. package/dist/browser/browserServer.js.map +1 -0
  109. package/dist/browser/cancellation/common.d.ts +10 -0
  110. package/dist/browser/cancellation/common.js +114 -0
  111. package/dist/browser/cancellation/common.js.map +1 -0
  112. package/dist/browser/cancellation/objectURLReceiverStrategy.d.ts +5 -0
  113. package/dist/browser/cancellation/objectURLReceiverStrategy.js +37 -0
  114. package/dist/browser/cancellation/objectURLReceiverStrategy.js.map +1 -0
  115. package/dist/browser/cancellation/objectURLSenderStrategy.d.ts +7 -0
  116. package/dist/browser/cancellation/objectURLSenderStrategy.js +32 -0
  117. package/dist/browser/cancellation/objectURLSenderStrategy.js.map +1 -0
  118. package/dist/browser/worker.d.ts +1 -0
  119. package/dist/browser/worker.js +33 -0
  120. package/dist/browser/worker.js.map +1 -0
  121. package/dist/pyrightServer.d.ts +34 -0
  122. package/dist/pyrightServer.js +239 -0
  123. package/dist/pyrightServer.js.map +1 -0
  124. package/dist/server.d.ts +3 -23
  125. package/dist/server.js +12 -232
  126. package/dist/server.js.map +1 -1
  127. package/dist/tests/benchmarks/typeInferenceComparison.d.ts +1 -0
  128. package/dist/tests/benchmarks/typeInferenceComparison.js +1235 -0
  129. package/dist/tests/benchmarks/typeInferenceComparison.js.map +1 -0
  130. package/dist/tests/benchmarks/typeInferencePyrightEntry.d.ts +1 -0
  131. package/dist/tests/benchmarks/typeInferencePyrightEntry.js +53 -0
  132. package/dist/tests/benchmarks/typeInferencePyrightEntry.js.map +1 -0
  133. package/dist/tests/browserCancellation.test.d.ts +1 -0
  134. package/dist/tests/browserCancellation.test.js +63 -0
  135. package/dist/tests/browserCancellation.test.js.map +1 -0
  136. package/dist/tests/browserServer.test.d.ts +1 -0
  137. package/dist/tests/browserServer.test.js +177 -0
  138. package/dist/tests/browserServer.test.js.map +1 -0
  139. package/dist/tests/constraintSolver.test.js +45 -0
  140. package/dist/tests/constraintSolver.test.js.map +1 -1
  141. package/dist/tests/mypyPrimerAnalysis.test.js +15 -1
  142. package/dist/tests/mypyPrimerAnalysis.test.js.map +1 -1
  143. package/dist/tests/typeCacheUtils.test.js +46 -0
  144. package/dist/tests/typeCacheUtils.test.js.map +1 -1
  145. package/dist/tests/typeEvaluator1.test.js +75 -0
  146. package/dist/tests/typeEvaluator1.test.js.map +1 -1
  147. package/dist/tests/typeEvaluator3.test.js +4 -0
  148. package/dist/tests/typeEvaluator3.test.js.map +1 -1
  149. package/dist/tests/typeEvaluator4.test.js +32 -0
  150. package/dist/tests/typeEvaluator4.test.js.map +1 -1
  151. package/dist/tests/typeEvaluator7.test.js +394 -0
  152. package/dist/tests/typeEvaluator7.test.js.map +1 -1
  153. package/dist/tests/typeUtils.test.js +28 -0
  154. package/dist/tests/typeUtils.test.js.map +1 -1
  155. package/dist/typeServer/stubGenerator.js +136 -3
  156. package/dist/typeServer/stubGenerator.js.map +1 -1
  157. package/dist/typeServer/typeServerConversionUtils.js +13 -0
  158. package/dist/typeServer/typeServerConversionUtils.js.map +1 -1
  159. package/package.json +3 -1
@@ -0,0 +1,1235 @@
1
+ "use strict";
2
+ /*
3
+ * typeInferenceComparison.ts
4
+ * Copyright (c) Microsoft Corporation.
5
+ * Licensed under the MIT license.
6
+ *
7
+ * Generates deterministic reveal_type scenarios, runs Pyright and Pyrefly,
8
+ * and writes a Markdown report that highlights inference differences.
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ const child_process_1 = require("child_process");
45
+ const fs = __importStar(require("fs"));
46
+ const os = __importStar(require("os"));
47
+ const path = __importStar(require("path"));
48
+ const perf_hooks_1 = require("perf_hooks");
49
+ const DEFAULT_CASE_COUNT = 10000;
50
+ const DEFAULT_PYTHON_VERSION = '3.11';
51
+ const SCENARIO_TEMPLATE_COUNT = 32;
52
+ const GENERATED_DIRECTORY = path.resolve(process.cwd(), 'src', 'tests', 'benchmarks', '.generated', 'type-inference-comparison');
53
+ const commonHeader = `from __future__ import annotations
54
+
55
+ from collections.abc import Callable, Iterator, Sequence
56
+ from dataclasses import dataclass
57
+ from enum import Enum
58
+ from typing import Any, Generic, Literal, LiteralString, NamedTuple, ParamSpec, Protocol, Self, TypeGuard, TypedDict, TypeVar, overload, reveal_type
59
+
60
+ T = TypeVar("T")
61
+ C = TypeVar("C", int, str)
62
+ P = ParamSpec("P")
63
+ R = TypeVar("R")
64
+
65
+ def identity(value: T) -> T:
66
+ return value
67
+
68
+ def first(values: Sequence[T]) -> T:
69
+ return values[0]
70
+
71
+ def is_str(value: object) -> TypeGuard[str]:
72
+ return isinstance(value, str)
73
+
74
+ def preserve_constrained(value: C) -> C:
75
+ return value
76
+
77
+ def preserve_callable(callback: Callable[P, R]) -> Callable[P, R]:
78
+ return callback
79
+
80
+ def accepts_literal_string(value: LiteralString) -> None:
81
+ pass
82
+
83
+ class ValueProtocol(Protocol):
84
+ value: int
85
+
86
+ class ProtocolImpl:
87
+ value = 1
88
+
89
+ def read_protocol(value: ValueProtocol) -> int:
90
+ return value.value
91
+
92
+ class Fluent:
93
+ def clone(self) -> Self:
94
+ return self
95
+
96
+ class FluentChild(Fluent):
97
+ pass
98
+
99
+ class Color(Enum):
100
+ RED = 1
101
+ BLUE = 2
102
+
103
+ class Pair(NamedTuple):
104
+ left: int
105
+ right: str
106
+
107
+ class Manager:
108
+ def __enter__(self) -> int:
109
+ return 1
110
+
111
+ def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None:
112
+ pass
113
+
114
+ class Descriptor:
115
+ def __get__(self, instance: object, owner: type[object]) -> int:
116
+ return 1
117
+
118
+ class DescriptorOwner:
119
+ item = Descriptor()
120
+
121
+ JsonValue = int | str | list["JsonValue"]
122
+
123
+ @overload
124
+ def convert(value: Literal[0]) -> Literal["zero"]: ...
125
+ @overload
126
+ def convert(value: int) -> str: ...
127
+ @overload
128
+ def convert(value: str) -> int: ...
129
+ def convert(value: int | str) -> int | str:
130
+ return str(value) if isinstance(value, int) else len(value)
131
+
132
+ class Point:
133
+ __match_args__ = ("x", "y")
134
+
135
+ def __init__(self, x: int, y: int) -> None:
136
+ self.x = x
137
+ self.y = y
138
+
139
+ class BenchmarkIntResult:
140
+ def int_only(self) -> int:
141
+ return 1
142
+
143
+ class BenchmarkStrResult:
144
+ def str_only(self) -> str:
145
+ return ""
146
+
147
+ class BehaviorContainer(Generic[T]):
148
+ @overload
149
+ def copy(self: "BehaviorContainer[int]") -> "BehaviorContainer[int]": ...
150
+ @overload
151
+ def copy(self: "BehaviorContainer[str]") -> "BehaviorContainer[str]": ...
152
+ def copy(self) -> "BehaviorContainer[Any]":
153
+ return self
154
+
155
+ def item(self) -> T:
156
+ raise NotImplementedError
157
+
158
+ class BenchmarkRecord(TypedDict):
159
+ name: str
160
+ age: int
161
+
162
+ class BenchmarkA(TypedDict):
163
+ kind: Literal["a"]
164
+ value: int
165
+
166
+ class BenchmarkB(TypedDict):
167
+ kind: Literal["b"]
168
+ value: str
169
+ `;
170
+ function parseOptions(args) {
171
+ let caseCount = DEFAULT_CASE_COUNT;
172
+ let outputDirectory = GENERATED_DIRECTORY;
173
+ let pythonVersion = DEFAULT_PYTHON_VERSION;
174
+ let pyreflyCommand = process.env.PYREFLY_COMMAND || 'uvx';
175
+ for (let index = 0; index < args.length; index++) {
176
+ const arg = args[index];
177
+ const value = args[index + 1];
178
+ if (arg === '--cases' && value) {
179
+ caseCount = Number.parseInt(value, 10);
180
+ index++;
181
+ }
182
+ else if (arg === '--output' && value) {
183
+ outputDirectory = path.resolve(value);
184
+ index++;
185
+ }
186
+ else if (arg === '--python-version' && value) {
187
+ pythonVersion = value;
188
+ index++;
189
+ }
190
+ else if (arg === '--pyrefly-command' && value) {
191
+ pyreflyCommand = value;
192
+ index++;
193
+ }
194
+ else if (arg === '--help') {
195
+ printUsageAndExit();
196
+ }
197
+ else {
198
+ throw new Error(`Unknown or incomplete argument: ${arg}`);
199
+ }
200
+ }
201
+ if (!Number.isInteger(caseCount) || caseCount < 1) {
202
+ throw new Error('--cases must be a positive integer');
203
+ }
204
+ return { caseCount, outputDirectory, pythonVersion, pyreflyCommand };
205
+ }
206
+ function printUsageAndExit() {
207
+ console.log(`Usage: pnpm run benchmark:type-inference -- [options]
208
+
209
+ Options:
210
+ --cases <count> Number of generated reveal_type cases (default: ${DEFAULT_CASE_COUNT})
211
+ --output <directory> Report and corpus output directory
212
+ --python-version <version> Python version supplied to both checkers (default: ${DEFAULT_PYTHON_VERSION})
213
+ --pyrefly-command <command> Pyrefly executable, or uvx to run "uvx pyrefly" (default: uvx)
214
+ --help Show this help
215
+ `);
216
+ process.exit(0);
217
+ }
218
+ function createCase(index) {
219
+ const id = `case_${String(index + 1).padStart(5, '0')}`;
220
+ const integer = (index % 97) + 1;
221
+ const alternateInteger = ((index * 17) % 89) + 100;
222
+ const text = `value_${index % 53}`;
223
+ const template = index % SCENARIO_TEMPLATE_COUNT;
224
+ switch (template) {
225
+ case 0:
226
+ return {
227
+ id,
228
+ category: 'literal',
229
+ description: 'Literal integer inference',
230
+ lines: [`value = ${integer}`],
231
+ revealExpression: 'value',
232
+ };
233
+ case 1:
234
+ return {
235
+ id,
236
+ category: 'container',
237
+ description: 'Homogeneous list inference',
238
+ lines: [`value = [${integer}, ${alternateInteger}]`],
239
+ revealExpression: 'value',
240
+ };
241
+ case 2:
242
+ return {
243
+ id,
244
+ category: 'container',
245
+ description: 'Heterogeneous tuple inference',
246
+ lines: [`value = (${integer}, "${text}", True)`],
247
+ revealExpression: 'value',
248
+ };
249
+ case 3:
250
+ return {
251
+ id,
252
+ category: 'generic-call',
253
+ description: 'Generic identity function',
254
+ lines: [`value = identity(("${text}", ${integer}))`],
255
+ revealExpression: 'value',
256
+ };
257
+ case 4:
258
+ return {
259
+ id,
260
+ category: 'generic-call',
261
+ description: 'Generic sequence element extraction',
262
+ lines: [`value = first([${integer}, ${alternateInteger}])`],
263
+ revealExpression: 'value',
264
+ };
265
+ case 5:
266
+ return {
267
+ id,
268
+ category: 'comprehension',
269
+ description: 'Filtered list comprehension',
270
+ lines: [`value = [item * 2 for item in [${integer}, ${alternateInteger}] if item > 0]`],
271
+ revealExpression: 'value',
272
+ };
273
+ case 6:
274
+ return {
275
+ id,
276
+ category: 'narrowing',
277
+ description: 'None narrowing',
278
+ lines: [
279
+ `value: int | None = ${integer} if bool(${index % 2}) else None`,
280
+ 'if value is not None:',
281
+ ' narrowed = value',
282
+ 'else:',
283
+ ' narrowed = 0',
284
+ ],
285
+ revealExpression: 'narrowed',
286
+ };
287
+ case 7:
288
+ return {
289
+ id,
290
+ category: 'narrowing',
291
+ description: 'User-defined TypeGuard narrowing',
292
+ lines: [
293
+ `candidate: object = "${text}"`,
294
+ 'if is_str(candidate):',
295
+ ' narrowed = candidate',
296
+ 'else:',
297
+ ' narrowed = ""',
298
+ ],
299
+ revealExpression: 'narrowed',
300
+ };
301
+ case 8:
302
+ return {
303
+ id,
304
+ category: 'conditional',
305
+ description: 'Conditional expression union',
306
+ lines: [`value = ${integer} if bool(${index % 2}) else "${text}"`],
307
+ revealExpression: 'value',
308
+ };
309
+ case 9:
310
+ return {
311
+ id,
312
+ category: 'mapping',
313
+ description: 'Dictionary inference',
314
+ lines: [`value = {"left": ${integer}, "right": ${alternateInteger}}`],
315
+ revealExpression: 'value',
316
+ };
317
+ case 10:
318
+ return {
319
+ id,
320
+ category: 'callable',
321
+ description: 'Lambda inference',
322
+ lines: [`value = lambda item: item + ${integer}`],
323
+ revealExpression: 'value',
324
+ };
325
+ case 11:
326
+ return {
327
+ id,
328
+ category: 'flow',
329
+ description: 'Isinstance flow narrowing',
330
+ lines: [
331
+ `candidate: int | str = ${integer} if bool(${index % 2}) else "${text}"`,
332
+ 'if isinstance(candidate, str):',
333
+ ' value = candidate.upper()',
334
+ 'else:',
335
+ ' value = candidate + 1',
336
+ ],
337
+ revealExpression: 'value',
338
+ };
339
+ case 12:
340
+ return {
341
+ id,
342
+ category: 'unpacking',
343
+ description: 'Tuple unpacking inference',
344
+ lines: [`left, *middle, right = (${integer}, "${text}", True, ${alternateInteger})`],
345
+ revealExpression: 'middle',
346
+ };
347
+ case 13:
348
+ return {
349
+ id,
350
+ category: 'iteration',
351
+ description: 'Generator expression inference',
352
+ lines: [`value = (item for item in [${integer}, ${alternateInteger}])`],
353
+ revealExpression: 'value',
354
+ };
355
+ case 14:
356
+ return {
357
+ id,
358
+ category: 'generic-class',
359
+ description: 'Generic dataclass construction',
360
+ lines: [
361
+ '@dataclass',
362
+ 'class Box(Generic[T]):',
363
+ ' value: T',
364
+ '',
365
+ `value = Box(("${text}", ${integer}))`,
366
+ ],
367
+ revealExpression: 'value',
368
+ };
369
+ case 15:
370
+ return {
371
+ id,
372
+ category: 'higher-order',
373
+ description: 'Higher-order callable return inference',
374
+ lines: [
375
+ 'def apply(callback: Callable[[int], T], value: int) -> T:',
376
+ ' return callback(value)',
377
+ '',
378
+ `value = apply(lambda item: (item, "${text}"), ${integer})`,
379
+ ],
380
+ revealExpression: 'value',
381
+ };
382
+ case 16:
383
+ return {
384
+ id,
385
+ category: 'overload',
386
+ description: 'Literal overload selection',
387
+ lines: ['value = convert(0)'],
388
+ revealExpression: 'value',
389
+ };
390
+ case 17:
391
+ return {
392
+ id,
393
+ category: 'constrained-typevar',
394
+ description: 'Constrained TypeVar preservation',
395
+ lines: [`value = preserve_constrained(${integer})`],
396
+ revealExpression: 'value',
397
+ };
398
+ case 18:
399
+ return {
400
+ id,
401
+ category: 'paramspec',
402
+ description: 'ParamSpec callable preservation',
403
+ lines: [
404
+ 'def callback(left: int, right: str) -> tuple[int, str]:',
405
+ ' return left, right',
406
+ '',
407
+ 'value = preserve_callable(callback)',
408
+ ],
409
+ revealExpression: 'value',
410
+ };
411
+ case 19:
412
+ return {
413
+ id,
414
+ category: 'protocol',
415
+ description: 'Structural protocol argument',
416
+ lines: ['value = read_protocol(ProtocolImpl())'],
417
+ revealExpression: 'value',
418
+ };
419
+ case 20:
420
+ return {
421
+ id,
422
+ category: 'self',
423
+ description: 'Self return specialization',
424
+ lines: ['value = FluentChild().clone()'],
425
+ revealExpression: 'value',
426
+ };
427
+ case 21:
428
+ return {
429
+ id,
430
+ category: 'typed-dict',
431
+ description: 'Direct TypedDict literal key',
432
+ lines: [`record: BenchmarkRecord = {"name": "${text}", "age": ${integer}}`, 'value = record["name"]'],
433
+ revealExpression: 'value',
434
+ };
435
+ case 22:
436
+ return {
437
+ id,
438
+ category: 'enum',
439
+ description: 'Enum member inference',
440
+ lines: ['value = Color.RED'],
441
+ revealExpression: 'value',
442
+ };
443
+ case 23:
444
+ return {
445
+ id,
446
+ category: 'named-tuple',
447
+ description: 'NamedTuple field inference',
448
+ lines: [`value = Pair(${integer}, "${text}").right`],
449
+ revealExpression: 'value',
450
+ };
451
+ case 24:
452
+ return {
453
+ id,
454
+ category: 'match',
455
+ description: 'Literal match result inference',
456
+ parameters: 'tag: Literal["a", "b"]',
457
+ lines: ['match tag:', ' case "a":', ' value = 1', ' case "b":', ' value = "b"'],
458
+ revealExpression: 'value',
459
+ };
460
+ case 25:
461
+ return {
462
+ id,
463
+ category: 'match',
464
+ description: 'Class pattern inference',
465
+ parameters: 'point: Point',
466
+ lines: ['match point:', ' case Point(x, y):', ' value = x + y'],
467
+ revealExpression: 'value',
468
+ };
469
+ case 26:
470
+ return {
471
+ id,
472
+ category: 'generator',
473
+ description: 'Generator function return inference',
474
+ lines: ['def values() -> Iterator[int]:', ` yield ${integer}`, '', 'value = values()'],
475
+ revealExpression: 'value',
476
+ };
477
+ case 27:
478
+ return {
479
+ id,
480
+ category: 'async',
481
+ description: 'Awaited async function inference',
482
+ isAsync: true,
483
+ lines: ['async def get_value() -> int:', ` return ${integer}`, '', 'value = await get_value()'],
484
+ revealExpression: 'value',
485
+ };
486
+ case 28:
487
+ return {
488
+ id,
489
+ category: 'context-manager',
490
+ description: 'Context manager enter type',
491
+ lines: ['with Manager() as item:', ' value = item'],
492
+ revealExpression: 'value',
493
+ };
494
+ case 29:
495
+ return {
496
+ id,
497
+ category: 'descriptor',
498
+ description: 'Descriptor access inference',
499
+ lines: ['value = DescriptorOwner().item'],
500
+ revealExpression: 'value',
501
+ };
502
+ case 30:
503
+ return {
504
+ id,
505
+ category: 'recursive-alias',
506
+ description: 'Recursive type alias assignment',
507
+ lines: [`value: JsonValue = [${integer}, "${text}"]`],
508
+ revealExpression: 'value',
509
+ };
510
+ default:
511
+ return {
512
+ id,
513
+ category: 'assignment-expression',
514
+ description: 'Assignment expression flow inference',
515
+ lines: [`if value := identity("${text}"):`, ' result = value.upper()', 'else:', ' result = ""'],
516
+ revealExpression: 'result',
517
+ };
518
+ }
519
+ }
520
+ function createBehaviorCases() {
521
+ return [
522
+ {
523
+ id: 'behavior_generic_call',
524
+ category: 'generic-call',
525
+ description: 'Literal-sensitive assignment after generic identity',
526
+ lines: [
527
+ 'value = identity(("tag", 4))',
528
+ 'exact: tuple[Literal["tag"], Literal[4]] = value # benchmark: require-accept',
529
+ ],
530
+ revealExpression: 'value',
531
+ },
532
+ {
533
+ id: 'behavior_narrowing',
534
+ category: 'narrowing',
535
+ description: 'Literal union retained across None narrowing',
536
+ lines: [
537
+ 'value: int | None = 7 if bool(0) else None',
538
+ 'if value is not None:',
539
+ ' narrowed = value',
540
+ 'else:',
541
+ ' narrowed = 0',
542
+ 'exact: Literal[7, 0] = narrowed # benchmark: require-accept',
543
+ ],
544
+ revealExpression: 'narrowed',
545
+ },
546
+ {
547
+ id: 'behavior_literal_string',
548
+ category: 'flow',
549
+ description: 'LiteralString retained through branch flow',
550
+ lines: [
551
+ 'candidate: int | str = 11 if bool(1) else "value"',
552
+ 'if isinstance(candidate, str):',
553
+ ' value = candidate.upper()',
554
+ 'else:',
555
+ ' value = candidate + 1',
556
+ 'if isinstance(value, str):',
557
+ ' accepts_literal_string(value) # benchmark: require-accept',
558
+ ],
559
+ revealExpression: 'value',
560
+ },
561
+ {
562
+ id: 'behavior_generic_class',
563
+ category: 'generic-class',
564
+ description: 'Literal-sensitive generic class construction',
565
+ lines: [
566
+ '@dataclass',
567
+ 'class Box(Generic[T]):',
568
+ ' value: T',
569
+ '',
570
+ 'value = Box(("tag", 15))',
571
+ 'exact: Box[tuple[Literal["tag"], Literal[15]]] = value # benchmark: require-accept',
572
+ ],
573
+ revealExpression: 'value',
574
+ },
575
+ {
576
+ id: 'behavior_higher_order',
577
+ category: 'higher-order',
578
+ description: 'Literal retained through callback return inference',
579
+ lines: [
580
+ 'def apply(callback: Callable[[int], T], value: int) -> T:',
581
+ ' return callback(value)',
582
+ '',
583
+ 'value = apply(lambda item: (item, "tag"), 16)',
584
+ 'exact: tuple[int, Literal["tag"]] = value # benchmark: require-accept',
585
+ ],
586
+ revealExpression: 'value',
587
+ },
588
+ {
589
+ id: 'behavior_ambiguous_overload',
590
+ category: 'ambiguous-overload',
591
+ description: 'Ambiguous overload with invariant Any argument',
592
+ parameters: 'source: list[Any]',
593
+ lines: [
594
+ '@overload',
595
+ 'def choose(value: list[int]) -> list[int]: ...',
596
+ '@overload',
597
+ 'def choose(value: list[str]) -> list[str]: ...',
598
+ 'def choose(value: Any) -> Any:',
599
+ ' return value',
600
+ '',
601
+ 'value = choose(source)',
602
+ 'value.append(1) # benchmark: require-accept',
603
+ 'value.append(3.14) # benchmark: expect-error',
604
+ 'value.nonexistent() # benchmark: expect-error',
605
+ ],
606
+ revealExpression: 'value',
607
+ },
608
+ {
609
+ id: 'behavior_disjoint_overload',
610
+ category: 'disjoint-overload',
611
+ description: 'Ambiguous overload with disjoint result members',
612
+ parameters: 'source: list[Any]',
613
+ lines: [
614
+ '@overload',
615
+ 'def choose(value: list[int]) -> BenchmarkIntResult: ...',
616
+ '@overload',
617
+ 'def choose(value: list[str]) -> BenchmarkStrResult: ...',
618
+ 'def choose(value: Any) -> Any:',
619
+ ' return BenchmarkIntResult()',
620
+ '',
621
+ 'value = choose(source)',
622
+ 'value.int_only() # benchmark: require-accept',
623
+ 'value.str_only() # benchmark: require-accept',
624
+ 'value.missing() # benchmark: expect-error',
625
+ ],
626
+ revealExpression: 'value',
627
+ },
628
+ {
629
+ id: 'behavior_typed_dict_key',
630
+ category: 'typed-dict-key',
631
+ description: 'Literal key retained through a generic tuple',
632
+ lines: [
633
+ 'record: BenchmarkRecord = {"name": "tag", "age": 19}',
634
+ 'pair = identity(("name", 19))',
635
+ 'key = pair[0]',
636
+ 'value = record[key] # benchmark: require-accept',
637
+ 'value.upper() # benchmark: require-accept',
638
+ ],
639
+ revealExpression: 'value',
640
+ },
641
+ {
642
+ id: 'behavior_discriminated_union',
643
+ category: 'discriminated-union',
644
+ description: 'TypedDict discriminated union narrowing',
645
+ parameters: 'item: BenchmarkA | BenchmarkB',
646
+ lines: ['if item["kind"] == "a":', ' value = item["value"]', 'else:', ' value = item["value"]'],
647
+ revealExpression: 'value',
648
+ },
649
+ {
650
+ id: 'behavior_literal_overload',
651
+ category: 'overload',
652
+ description: 'Literal-sensitive overload selection after generic inference',
653
+ lines: [
654
+ 'pair = identity((0, "tag"))',
655
+ 'value = convert(pair[0])',
656
+ 'exact: Literal["zero"] = value # benchmark: require-accept',
657
+ ],
658
+ revealExpression: 'value',
659
+ },
660
+ {
661
+ id: 'behavior_default_overload',
662
+ category: 'ambiguous-overload',
663
+ description: 'Ambiguous overload with a default argument',
664
+ parameters: 'source: list[Any]',
665
+ lines: [
666
+ '@overload',
667
+ 'def choose(value: list[int], second: int = 0) -> list[int]: ...',
668
+ '@overload',
669
+ 'def choose(value: list[str], second: int = 0) -> list[str]: ...',
670
+ 'def choose(value: Any, second: int = 0) -> Any:',
671
+ ' return value',
672
+ '',
673
+ 'value = choose(source)',
674
+ 'value.append(1) # benchmark: require-accept',
675
+ 'value.append(3.14) # benchmark: expect-error',
676
+ ],
677
+ revealExpression: 'value',
678
+ },
679
+ {
680
+ id: 'behavior_method_overload',
681
+ category: 'ambiguous-overload',
682
+ description: 'Ambiguous overload on a generic receiver',
683
+ parameters: 'source: BehaviorContainer[Any]',
684
+ lines: [
685
+ 'value = source.copy()',
686
+ 'value.item().bit_length() # benchmark: require-accept',
687
+ 'value.item().upper() # benchmark: require-accept',
688
+ 'value.item().missing() # benchmark: expect-error',
689
+ ],
690
+ revealExpression: 'value',
691
+ },
692
+ {
693
+ id: 'behavior_paramspec',
694
+ category: 'paramspec',
695
+ description: 'ParamSpec callable assignment preservation',
696
+ lines: [
697
+ 'def callback(left: int, right: str) -> tuple[int, str]:',
698
+ ' return left, right',
699
+ '',
700
+ 'value = preserve_callable(callback)',
701
+ 'exact: Callable[[int, str], tuple[int, str]] = value # benchmark: require-accept',
702
+ ],
703
+ revealExpression: 'value',
704
+ },
705
+ ];
706
+ }
707
+ function renderCases(generatedCases) {
708
+ const output = [...commonHeader.trimEnd().split('\n'), ''];
709
+ const casesByRevealLine = new Map();
710
+ const expectedErrorsByLine = new Map();
711
+ const requiredAcceptsByLine = new Map();
712
+ for (const generatedCase of generatedCases) {
713
+ output.push(`# ${generatedCase.id}: ${generatedCase.description}`);
714
+ output.push(`${generatedCase.isAsync ? 'async ' : ''}def ${generatedCase.id}(${generatedCase.parameters || ''}) -> None:`);
715
+ for (const line of generatedCase.lines) {
716
+ const renderedLine = line ? ` ${line}` : '';
717
+ output.push(renderedLine);
718
+ const location = { generatedCase, source: line.replace(/\s+# benchmark: .+$/, '').trim() };
719
+ if (line.includes('# benchmark: expect-error')) {
720
+ expectedErrorsByLine.set(output.length, location);
721
+ }
722
+ else if (line.includes('# benchmark: require-accept')) {
723
+ requiredAcceptsByLine.set(output.length, location);
724
+ }
725
+ }
726
+ output.push(` reveal_type(${generatedCase.revealExpression})`);
727
+ casesByRevealLine.set(output.length, generatedCase);
728
+ output.push('');
729
+ }
730
+ return {
731
+ source: `${output.join('\n')}\n`,
732
+ casesByRevealLine,
733
+ expectedErrorsByLine,
734
+ requiredAcceptsByLine,
735
+ };
736
+ }
737
+ function renderCorpus(caseCount) {
738
+ return renderCases(Array.from({ length: caseCount }, (_unused, index) => createCase(index)));
739
+ }
740
+ function renderBehaviorCorpus() {
741
+ return renderCases(createBehaviorCases());
742
+ }
743
+ function runProcess(command, args, cwd) {
744
+ const start = perf_hooks_1.performance.now();
745
+ const result = (0, child_process_1.spawnSync)(command, args, {
746
+ cwd,
747
+ encoding: 'utf8',
748
+ maxBuffer: 64 * 1024 * 1024,
749
+ shell: false,
750
+ });
751
+ if (result.error) {
752
+ throw new Error(`Failed to run ${command}: ${result.error.message}`);
753
+ }
754
+ return {
755
+ exitCode: result.status,
756
+ stdout: result.stdout || '',
757
+ stderr: result.stderr || '',
758
+ durationMs: perf_hooks_1.performance.now() - start,
759
+ };
760
+ }
761
+ function runPyright(corpusPath, pythonVersion) {
762
+ const pyrightPath = path.resolve(__dirname, 'typeInferencePyrightEntry.js');
763
+ const workingDirectory = path.dirname(corpusPath);
764
+ const versionResult = runProcess(process.execPath, [pyrightPath, '--version'], workingDirectory);
765
+ const result = runProcess(process.execPath, [pyrightPath, corpusPath, '--outputjson', '--pythonversion', pythonVersion], workingDirectory);
766
+ if (!result.stdout.trim()) {
767
+ throw new Error(`Pyright produced no JSON output. stderr: ${result.stderr || '<empty>'}`);
768
+ }
769
+ const parsed = JSON.parse(result.stdout);
770
+ const diagnostics = (parsed.generalDiagnostics || []).map((diagnostic) => ({
771
+ line: (diagnostic.range?.start.line ?? -1) + 1,
772
+ severity: diagnostic.severity,
773
+ message: diagnostic.message,
774
+ rule: diagnostic.rule,
775
+ }));
776
+ return {
777
+ name: 'Pyright',
778
+ version: versionResult.stdout.trim() || versionResult.stderr.trim() || 'unknown',
779
+ durationMs: result.durationMs,
780
+ exitCode: result.exitCode,
781
+ reveals: extractReveals(diagnostics, /^Type of ".+" is "(.+)"$/),
782
+ diagnostics: diagnostics.filter((diagnostic) => !diagnostic.message.startsWith('Type of "')),
783
+ stderr: result.stderr.trim(),
784
+ };
785
+ }
786
+ function runPyrefly(corpusPath, pythonVersion, command) {
787
+ const workingDirectory = path.dirname(corpusPath);
788
+ const commandName = path.basename(command).toLowerCase();
789
+ const usesPackageRunner = commandName === 'uvx' || commandName === 'uvx.exe';
790
+ const prefixArgs = usesPackageRunner ? ['pyrefly'] : [];
791
+ const versionResult = runProcess(command, [...prefixArgs, '--version'], workingDirectory);
792
+ const result = runProcess(command, [
793
+ ...prefixArgs,
794
+ 'check',
795
+ corpusPath,
796
+ '--output-format=json',
797
+ '--output=-',
798
+ '--summary=none',
799
+ '--progress-bar=no',
800
+ '--preset=default',
801
+ '--color=never',
802
+ '--threads=1',
803
+ '--python-version',
804
+ pythonVersion,
805
+ ], workingDirectory);
806
+ if (!result.stdout.trim()) {
807
+ throw new Error(`Pyrefly produced no JSON output. stderr: ${result.stderr || '<empty>'}`);
808
+ }
809
+ const parsed = JSON.parse(result.stdout);
810
+ const diagnostics = (parsed.errors || []).map((diagnostic) => ({
811
+ line: diagnostic.line,
812
+ severity: diagnostic.severity,
813
+ message: diagnostic.description,
814
+ rule: diagnostic.name,
815
+ }));
816
+ return {
817
+ name: 'Pyrefly',
818
+ version: versionResult.stdout.trim() || versionResult.stderr.trim() || 'unknown',
819
+ durationMs: result.durationMs,
820
+ exitCode: result.exitCode,
821
+ reveals: extractReveals(diagnostics, /^revealed type: (.+)$/i),
822
+ diagnostics: diagnostics.filter((diagnostic) => diagnostic.rule !== 'reveal-type'),
823
+ stderr: result.stderr.trim(),
824
+ };
825
+ }
826
+ function extractReveals(diagnostics, pattern) {
827
+ const reveals = new Map();
828
+ for (const diagnostic of diagnostics) {
829
+ const match = pattern.exec(diagnostic.message);
830
+ if (match) {
831
+ reveals.set(diagnostic.line, match[1]);
832
+ }
833
+ }
834
+ return reveals;
835
+ }
836
+ function normalizeType(type) {
837
+ const normalized = type
838
+ .replace(/\btyping\./g, '')
839
+ .replace(/\btyping_extensions\./g, '')
840
+ .replace(/\bcase_\d+\./g, '')
841
+ .replace(/\s+/g, '')
842
+ .toLowerCase();
843
+ return canonicalizeType(normalized);
844
+ }
845
+ function canonicalizeType(type) {
846
+ const unionMembers = splitTopLevel(type, '|');
847
+ if (unionMembers.length > 1) {
848
+ return unionMembers.map(canonicalizeType).sort().join('|');
849
+ }
850
+ const openingBracket = type.indexOf('[');
851
+ if (openingBracket < 0 || !type.endsWith(']')) {
852
+ return type;
853
+ }
854
+ const name = type.slice(0, openingBracket);
855
+ const argumentsText = type.slice(openingBracket + 1, -1);
856
+ let typeArguments = splitTopLevel(argumentsText, ',').map(canonicalizeType);
857
+ if (name === 'literal') {
858
+ typeArguments = typeArguments.sort();
859
+ }
860
+ else if (name === 'generator' && typeArguments.length === 1) {
861
+ typeArguments.push('none', 'none');
862
+ }
863
+ else if (name === 'union') {
864
+ return typeArguments.sort().join('|');
865
+ }
866
+ return `${name}[${typeArguments.join(',')}]`;
867
+ }
868
+ function splitTopLevel(value, separator) {
869
+ const parts = [];
870
+ let bracketDepth = 0;
871
+ let quote;
872
+ let start = 0;
873
+ for (let index = 0; index < value.length; index++) {
874
+ const character = value[index];
875
+ if (quote) {
876
+ if (character === quote && value[index - 1] !== '\\') {
877
+ quote = undefined;
878
+ }
879
+ }
880
+ else if (character === '"' || character === "'") {
881
+ quote = character;
882
+ }
883
+ else if (character === '[') {
884
+ bracketDepth++;
885
+ }
886
+ else if (character === ']') {
887
+ bracketDepth--;
888
+ }
889
+ else if (character === separator && bracketDepth === 0) {
890
+ parts.push(value.slice(start, index));
891
+ start = index + 1;
892
+ }
893
+ }
894
+ parts.push(value.slice(start));
895
+ return parts;
896
+ }
897
+ function imprecisionScore(type) {
898
+ const normalized = normalizeType(type);
899
+ const matches = normalized.match(/\b(any|unknown|object)\b/g);
900
+ return matches?.length ?? 0;
901
+ }
902
+ function literalScore(type) {
903
+ return (normalizeType(type).match(/literal\[/g) || []).length;
904
+ }
905
+ function compareTypes(pyrightType, pyreflyType) {
906
+ if (!pyrightType && !pyreflyType) {
907
+ return { kind: 'missing-both', reason: 'Neither checker reported a reveal type.' };
908
+ }
909
+ if (!pyrightType) {
910
+ return { kind: 'missing-pyright', reason: 'Only Pyrefly reported a reveal type.' };
911
+ }
912
+ if (!pyreflyType) {
913
+ return { kind: 'missing-pyrefly', reason: 'Only Pyright reported a reveal type.' };
914
+ }
915
+ if (normalizeType(pyrightType) === normalizeType(pyreflyType)) {
916
+ return { kind: 'equivalent', reason: 'Normalized type strings are equivalent.' };
917
+ }
918
+ const pyrightImprecision = imprecisionScore(pyrightType);
919
+ const pyreflyImprecision = imprecisionScore(pyreflyType);
920
+ if (pyrightImprecision !== pyreflyImprecision) {
921
+ return pyrightImprecision < pyreflyImprecision
922
+ ? { kind: 'pyright-narrower', reason: 'Pyrefly contains more Any, Unknown, or object components.' }
923
+ : { kind: 'pyrefly-narrower', reason: 'Pyright contains more Any, Unknown, or object components.' };
924
+ }
925
+ const pyrightLiterals = literalScore(pyrightType);
926
+ const pyreflyLiterals = literalScore(pyreflyType);
927
+ if (pyrightLiterals !== pyreflyLiterals) {
928
+ return pyrightLiterals > pyreflyLiterals
929
+ ? { kind: 'pyright-narrower', reason: 'Pyright preserves more Literal components.' }
930
+ : { kind: 'pyrefly-narrower', reason: 'Pyrefly preserves more Literal components.' };
931
+ }
932
+ return {
933
+ kind: 'different',
934
+ reason: 'The concrete types differ and require semantic adjudication.',
935
+ };
936
+ }
937
+ function compareCases(corpus, pyright, pyrefly) {
938
+ const comparisons = [];
939
+ for (const [line, generatedCase] of corpus.casesByRevealLine) {
940
+ const pyrightType = pyright.reveals.get(line);
941
+ const pyreflyType = pyrefly.reveals.get(line);
942
+ const result = compareTypes(pyrightType, pyreflyType);
943
+ comparisons.push({ generatedCase, pyrightType, pyreflyType, ...result });
944
+ }
945
+ return comparisons;
946
+ }
947
+ function isBlockingDiagnostic(diagnostic) {
948
+ return diagnostic.severity === 'error' || diagnostic.severity === 'warning' || diagnostic.severity === 'warn';
949
+ }
950
+ function getBehavioralProbeResults(probesByLine, pyright, pyrefly) {
951
+ return [...probesByLine].map(([line, location]) => ({
952
+ line,
953
+ location,
954
+ pyrightDiagnostics: pyright.diagnostics.filter((diagnostic) => diagnostic.line === line && isBlockingDiagnostic(diagnostic)),
955
+ pyreflyDiagnostics: pyrefly.diagnostics.filter((diagnostic) => diagnostic.line === line && isBlockingDiagnostic(diagnostic)),
956
+ }));
957
+ }
958
+ function getOrdinaryDiagnostics(checker, corpus) {
959
+ return checker.diagnostics.filter((diagnostic) => isBlockingDiagnostic(diagnostic) &&
960
+ !corpus.expectedErrorsByLine.has(diagnostic.line) &&
961
+ !corpus.requiredAcceptsByLine.has(diagnostic.line));
962
+ }
963
+ function escapeTableCell(value) {
964
+ return value.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
965
+ }
966
+ function formatDuration(durationMs) {
967
+ return `${(durationMs / 1000).toFixed(2)}s`;
968
+ }
969
+ function getPyreflyArgument(comparison) {
970
+ switch (comparison.generatedCase.category) {
971
+ case 'generic-call':
972
+ return 'Widening literal arguments while solving a type variable produces a stable reusable type. Retaining incidental values can make generic return types and overload selection depend on constants that callers did not annotate as Literal.';
973
+ case 'narrowing':
974
+ return 'The explicit annotation is an abstraction boundary. After both branches produce an int, reporting int is more stable than retaining the initializer and fallback constants as a literal union.';
975
+ case 'flow':
976
+ return 'The variable was declared int | str, and the branch operations return those public types. str accurately describes upper(), whose result is not a known literal value, while the integer literal comes from optional constant folding rather than a typing guarantee.';
977
+ case 'generic-class':
978
+ return 'Constructor inference for a mutable generic class should generally widen incidental literal values. A literal-specialized type argument can be too narrow if the attribute is reassigned later and can make otherwise equivalent instances appear incompatible.';
979
+ case 'higher-order':
980
+ return 'Widening constants in an inferred callback return keeps the callable reusable and prevents captured literal values from leaking into the public result type. The tuple shape is preserved even though its elements are widened.';
981
+ case 'ambiguous-overload':
982
+ case 'disjoint-overload':
983
+ return 'A gradual fallback avoids committing to one overload result, but it also removes member information and suppresses downstream argument and attribute diagnostics. This is a capability tradeoff rather than display-only widening.';
984
+ case 'typed-dict-key':
985
+ return 'Widening the generic tuple element to str avoids value-sensitive inference, but the widened key can no longer index a TypedDict. The policy therefore creates an observable rejection in otherwise valid code.';
986
+ case 'overload':
987
+ return 'Widening the argument selects a broader overload and produces a stable public type, but it can lose literal-specific return information that downstream assignments or overloads rely on.';
988
+ case 'paramspec':
989
+ return 'Equivalent callable signatures may be printed differently. The important behavior is whether calls, assignments, parameter names, and return types remain checked.';
990
+ case 'async':
991
+ return 'The checkers use different coroutine representations and gradual parameter spellings. This should be adjudicated through await behavior and assignments rather than display text alone.';
992
+ case 'recursive-alias':
993
+ return 'Recursive aliases can be displayed either by expansion or by retaining the alias. The comparison should focus on assignability, narrowing, and member checking rather than textual compactness.';
994
+ default:
995
+ if (comparison.reason.includes('Any, Unknown, or object')) {
996
+ return 'A gradual component may reflect an intentional boundary, unsupported feature, or soundness fallback. It is weaker evidence than a concrete-type disagreement but still needs validation before being called a loss.';
997
+ }
998
+ return "A narrower display is not automatically a better type. The difference should be checked against the typing specification, mutation behavior, and the checker's documented inference policy.";
999
+ }
1000
+ }
1001
+ function getDifferenceFamilies(comparisons) {
1002
+ const families = new Map();
1003
+ for (const comparison of comparisons) {
1004
+ if (comparison.kind === 'equivalent') {
1005
+ continue;
1006
+ }
1007
+ const key = `${comparison.generatedCase.category}:${comparison.kind}:${comparison.reason}`;
1008
+ const existing = families.get(key);
1009
+ if (existing) {
1010
+ existing.count++;
1011
+ }
1012
+ else {
1013
+ families.set(key, {
1014
+ category: comparison.generatedCase.category,
1015
+ count: 1,
1016
+ example: comparison,
1017
+ pyreflyArgument: getPyreflyArgument(comparison),
1018
+ });
1019
+ }
1020
+ }
1021
+ return [...families.values()].sort((left, right) => right.count - left.count);
1022
+ }
1023
+ function describeDiagnostics(diagnostics) {
1024
+ if (diagnostics.length === 0) {
1025
+ return '';
1026
+ }
1027
+ return diagnostics.map((diagnostic) => diagnostic.rule || diagnostic.message).join('; ');
1028
+ }
1029
+ function writeMarkdownReport(outputPath, options, comparisons, precisionRun, behaviorRun) {
1030
+ const counts = new Map();
1031
+ for (const comparison of comparisons) {
1032
+ counts.set(comparison.kind, (counts.get(comparison.kind) || 0) + 1);
1033
+ }
1034
+ const equivalent = counts.get('equivalent') || 0;
1035
+ const pyrightWins = counts.get('pyright-narrower') || 0;
1036
+ const pyreflyWins = counts.get('pyrefly-narrower') || 0;
1037
+ const different = counts.get('different') || 0;
1038
+ const missing = (counts.get('missing-pyright') || 0) + (counts.get('missing-pyrefly') || 0) + (counts.get('missing-both') || 0);
1039
+ const expectedErrorResults = getBehavioralProbeResults(behaviorRun.corpus.expectedErrorsByLine, behaviorRun.pyright, behaviorRun.pyrefly);
1040
+ const requiredAcceptResults = getBehavioralProbeResults(behaviorRun.corpus.requiredAcceptsByLine, behaviorRun.pyright, behaviorRun.pyrefly);
1041
+ const pyrightCaughtErrors = expectedErrorResults.filter((result) => result.pyrightDiagnostics.length > 0).length;
1042
+ const pyreflyCaughtErrors = expectedErrorResults.filter((result) => result.pyreflyDiagnostics.length > 0).length;
1043
+ const pyrightAccepted = requiredAcceptResults.filter((result) => result.pyrightDiagnostics.length === 0).length;
1044
+ const pyreflyAccepted = requiredAcceptResults.filter((result) => result.pyreflyDiagnostics.length === 0).length;
1045
+ const ordinaryPyrightDiagnostics = [
1046
+ ...getOrdinaryDiagnostics(precisionRun.pyright, precisionRun.corpus),
1047
+ ...getOrdinaryDiagnostics(behaviorRun.pyright, behaviorRun.corpus),
1048
+ ];
1049
+ const ordinaryPyreflyDiagnostics = [
1050
+ ...getOrdinaryDiagnostics(precisionRun.pyrefly, precisionRun.corpus),
1051
+ ...getOrdinaryDiagnostics(behaviorRun.pyrefly, behaviorRun.corpus),
1052
+ ];
1053
+ const ordinaryDiagnosticCount = ordinaryPyrightDiagnostics.length + ordinaryPyreflyDiagnostics.length;
1054
+ const assessment = missing > 0 || ordinaryDiagnosticCount > 0
1055
+ ? 'Not ready for an automatic switch decision: missing reveals or unclassified diagnostics need triage.'
1056
+ : pyrightCaughtErrors > pyreflyCaughtErrors || pyrightAccepted > pyreflyAccepted
1057
+ ? 'Pyright preserved more downstream checking or narrow-type capabilities in this corpus; review the behavioral probes separately from display precision.'
1058
+ : pyreflyCaughtErrors > pyrightCaughtErrors || pyreflyAccepted > pyrightAccepted
1059
+ ? 'Pyrefly preserved more downstream checking or narrow-type capabilities in this corpus; review the behavioral probes separately from display precision.'
1060
+ : different > 0
1061
+ ? 'No automatic switch recommendation: concrete disagreements require typing-spec or runtime adjudication.'
1062
+ : pyreflyWins > pyrightWins
1063
+ ? 'Pyrefly produced more narrow outputs under this heuristic; this is not by itself a switch recommendation.'
1064
+ : pyrightWins > pyreflyWins
1065
+ ? 'Pyright produced more narrow outputs under this heuristic; this is not by itself evidence that its inference policy is better.'
1066
+ : 'The checkers are tied on the prototype specificity heuristic.';
1067
+ const differences = comparisons.filter((comparison) => comparison.kind !== 'equivalent');
1068
+ const differenceFamilies = getDifferenceFamilies(comparisons);
1069
+ const lines = [
1070
+ '# Pyright vs. Pyrefly type inference comparison',
1071
+ '',
1072
+ `Generated: ${new Date().toISOString()}`,
1073
+ '',
1074
+ '## Switching assessment',
1075
+ '',
1076
+ `**${assessment}**`,
1077
+ '',
1078
+ 'This report measures the specificity and availability of inferred `reveal_type` results. It does not by itself measure soundness, diagnostic quality, compatibility with a production codebase, IDE behavior, or typing-spec compliance. A narrower type can be wrong, so every concrete disagreement remains a review item rather than an automatic win.',
1079
+ '',
1080
+ '## Run configuration',
1081
+ '',
1082
+ '| Setting | Value |',
1083
+ '|---|---|',
1084
+ `| Cases | ${options.caseCount} |`,
1085
+ `| Scenario templates | ${SCENARIO_TEMPLATE_COUNT} |`,
1086
+ `| Behavioral cases | ${behaviorRun.corpus.casesByRevealLine.size} |`,
1087
+ `| Python version | ${escapeTableCell(options.pythonVersion)} |`,
1088
+ `| Precision corpus | \`${escapeTableCell(path.relative(path.dirname(outputPath), precisionRun.corpusPath))}\` |`,
1089
+ `| Behavioral corpus | \`${escapeTableCell(path.relative(path.dirname(outputPath), behaviorRun.corpusPath))}\` |`,
1090
+ `| Pyright | ${escapeTableCell(precisionRun.pyright.version)} |`,
1091
+ `| Pyrefly | ${escapeTableCell(precisionRun.pyrefly.version)} |`,
1092
+ '',
1093
+ '## Summary',
1094
+ '',
1095
+ '| Result | Count | Meaning |',
1096
+ '|---|---:|---|',
1097
+ `| Equivalent | ${equivalent} | Normalized reveal strings match |`,
1098
+ `| Pyright narrower outputs | ${pyrightWins} | Fewer gradual components or more literal detail |`,
1099
+ `| Pyrefly narrower outputs | ${pyreflyWins} | Fewer gradual components or more literal detail |`,
1100
+ `| Different concrete types | ${different} | Needs semantic adjudication |`,
1101
+ `| Missing reveal results | ${missing} | At least one checker did not produce a result |`,
1102
+ '',
1103
+ '| Checker | Reveals | Non-reveal diagnostics | Time | Exit code |',
1104
+ '|---|---:|---:|---:|---:|',
1105
+ `| Pyright | ${precisionRun.pyright.reveals.size + behaviorRun.pyright.reveals.size} | ${precisionRun.pyright.diagnostics.length + behaviorRun.pyright.diagnostics.length} | ${formatDuration(precisionRun.pyright.durationMs + behaviorRun.pyright.durationMs)} | ${precisionRun.pyright.exitCode ?? 'none'} / ${behaviorRun.pyright.exitCode ?? 'none'} |`,
1106
+ `| Pyrefly | ${precisionRun.pyrefly.reveals.size + behaviorRun.pyrefly.reveals.size} | ${precisionRun.pyrefly.diagnostics.length + behaviorRun.pyrefly.diagnostics.length} | ${formatDuration(precisionRun.pyrefly.durationMs + behaviorRun.pyrefly.durationMs)} | ${precisionRun.pyrefly.exitCode ?? 'none'} / ${behaviorRun.pyrefly.exitCode ?? 'none'} |`,
1107
+ '',
1108
+ 'Timing is a single cold process run and is included only as context, not as a stable performance benchmark.',
1109
+ '',
1110
+ '## Behavioral probes',
1111
+ '',
1112
+ 'Behavioral probes distinguish display-only widening from observable checker behavior. `expect-error` probes are intentionally invalid operations that should remain diagnosed. `require-accept` probes exercise capabilities made available by a narrower inferred type; disagreement here can reflect a deliberate inference policy rather than a correctness bug.',
1113
+ '',
1114
+ '| Probe kind | Total | Pyright | Pyrefly |',
1115
+ '|---|---:|---:|---:|',
1116
+ `| Expected errors caught | ${expectedErrorResults.length} | ${pyrightCaughtErrors} | ${pyreflyCaughtErrors} |`,
1117
+ `| Required accepts accepted | ${requiredAcceptResults.length} | ${pyrightAccepted} | ${pyreflyAccepted} |`,
1118
+ '',
1119
+ '### Behavioral differences',
1120
+ '',
1121
+ '| Kind | Case | Probe | Pyright | Pyrefly |',
1122
+ '|---|---|---|---|---|',
1123
+ ];
1124
+ const expectedErrorDifferences = expectedErrorResults.filter((result) => result.pyrightDiagnostics.length > 0 !== result.pyreflyDiagnostics.length > 0);
1125
+ const requiredAcceptDifferences = requiredAcceptResults.filter((result) => (result.pyrightDiagnostics.length === 0) !== (result.pyreflyDiagnostics.length === 0));
1126
+ if (expectedErrorDifferences.length === 0 && requiredAcceptDifferences.length === 0) {
1127
+ lines.push('| _None_ | | | | |');
1128
+ }
1129
+ else {
1130
+ for (const result of expectedErrorDifferences) {
1131
+ lines.push(`| expect-error | ${result.location.generatedCase.id} | \`${escapeTableCell(result.location.source)}\` | ${result.pyrightDiagnostics.length > 0
1132
+ ? `caught (${escapeTableCell(describeDiagnostics(result.pyrightDiagnostics))})`
1133
+ : 'missed'} | ${result.pyreflyDiagnostics.length > 0
1134
+ ? `caught (${escapeTableCell(describeDiagnostics(result.pyreflyDiagnostics))})`
1135
+ : 'missed'} |`);
1136
+ }
1137
+ for (const result of requiredAcceptDifferences) {
1138
+ lines.push(`| require-accept | ${result.location.generatedCase.id} | \`${escapeTableCell(result.location.source)}\` | ${result.pyrightDiagnostics.length === 0
1139
+ ? 'accepted'
1140
+ : `rejected (${escapeTableCell(describeDiagnostics(result.pyrightDiagnostics))})`} | ${result.pyreflyDiagnostics.length === 0
1141
+ ? 'accepted'
1142
+ : `rejected (${escapeTableCell(describeDiagnostics(result.pyreflyDiagnostics))})`} |`);
1143
+ }
1144
+ }
1145
+ lines.push('', '## Pyrefly-side interpretation', '', 'The narrower-output count is intentionally adversarially reviewed below. These arguments do not prove that Pyrefly is correct; they explain why widening can be a deliberate and useful inference policy rather than a loss of capability.', '', '| Family | Count | Example | Strongest Pyrefly argument |', '|---|---:|---|---|');
1146
+ if (differenceFamilies.length === 0) {
1147
+ lines.push('| _None_ | 0 | | |');
1148
+ }
1149
+ else {
1150
+ for (const family of differenceFamilies) {
1151
+ lines.push(`| ${escapeTableCell(family.category)} | ${family.count} | ${family.example.generatedCase.id}: \`${escapeTableCell(family.example.pyrightType || 'missing')}\` vs. \`${escapeTableCell(family.example.pyreflyType || 'missing')}\` | ${escapeTableCell(family.pyreflyArgument)} |`);
1152
+ }
1153
+ }
1154
+ lines.push('', '## Differences', '', '| Case | Category | Result | Pyright | Pyrefly | Reason |', '|---|---|---|---|---|---|');
1155
+ if (differences.length === 0) {
1156
+ lines.push('| _None_ | | | | | |');
1157
+ }
1158
+ else {
1159
+ for (const comparison of differences) {
1160
+ lines.push(`| ${comparison.generatedCase.id} | ${escapeTableCell(comparison.generatedCase.category)} | ${comparison.kind} | \`${escapeTableCell(comparison.pyrightType || 'missing')}\` | \`${escapeTableCell(comparison.pyreflyType || 'missing')}\` | ${escapeTableCell(comparison.reason)} |`);
1161
+ }
1162
+ }
1163
+ lines.push('', '## Unclassified diagnostics', '');
1164
+ for (const [checkerName, diagnostics] of [
1165
+ ['Pyright', ordinaryPyrightDiagnostics],
1166
+ ['Pyrefly', ordinaryPyreflyDiagnostics],
1167
+ ]) {
1168
+ lines.push(`### ${checkerName}`, '');
1169
+ if (diagnostics.length === 0) {
1170
+ lines.push('_None._', '');
1171
+ }
1172
+ else {
1173
+ lines.push('| Line | Severity | Rule | Message |', '|---:|---|---|---|');
1174
+ for (const diagnostic of diagnostics) {
1175
+ lines.push(`| ${diagnostic.line} | ${escapeTableCell(diagnostic.severity)} | ${escapeTableCell(diagnostic.rule || '')} | ${escapeTableCell(diagnostic.message)} |`);
1176
+ }
1177
+ lines.push('');
1178
+ }
1179
+ }
1180
+ lines.push('## Interpretation rules', '', '- Equivalent types are compared after removing whitespace and `typing` qualification.', '- `Any`, `Unknown`, and `object` are treated as gradual/imprecise components.', '- Preserving more `Literal[...]` components is classified as a narrower output, not an automatic quality win.', '- `expect-error` probes measure whether invalid operations remain diagnosed after inference.', '- `require-accept` probes measure whether a narrower result enables an operation; these are capability comparisons rather than automatic correctness judgments.', '- All other concrete differences are intentionally left unresolved.', '- A production switch should additionally run both checkers over representative repositories and adjudicate error false positives, false negatives, suppression behavior, configuration parity, and IDE features.', '');
1181
+ const pyrightStderr = [precisionRun.pyright.stderr, behaviorRun.pyright.stderr].filter(Boolean).join('\n');
1182
+ const pyreflyStderr = [precisionRun.pyrefly.stderr, behaviorRun.pyrefly.stderr].filter(Boolean).join('\n');
1183
+ if (pyrightStderr) {
1184
+ lines.push('## Pyright stderr', '', '```text', pyrightStderr, '```', '');
1185
+ }
1186
+ if (pyreflyStderr) {
1187
+ lines.push('## Pyrefly stderr', '', '```text', pyreflyStderr, '```', '');
1188
+ }
1189
+ fs.writeFileSync(outputPath, lines.join('\n'), 'utf8');
1190
+ }
1191
+ function main() {
1192
+ const options = parseOptions(process.argv.slice(2));
1193
+ fs.mkdirSync(options.outputDirectory, { recursive: true });
1194
+ const precisionCorpus = renderCorpus(options.caseCount);
1195
+ const behaviorCorpus = renderBehaviorCorpus();
1196
+ const precisionCorpusPath = path.join(options.outputDirectory, 'generated_type_inference_corpus.py');
1197
+ const behaviorCorpusPath = path.join(options.outputDirectory, 'generated_behavior_corpus.py');
1198
+ const reportPath = path.join(options.outputDirectory, 'type-inference-comparison.md');
1199
+ fs.writeFileSync(precisionCorpusPath, precisionCorpus.source, 'utf8');
1200
+ fs.writeFileSync(behaviorCorpusPath, behaviorCorpus.source, 'utf8');
1201
+ console.log(`Generated ${options.caseCount} precision cases in ${precisionCorpusPath}`);
1202
+ console.log(`Generated ${behaviorCorpus.casesByRevealLine.size} behavioral cases in ${behaviorCorpusPath}`);
1203
+ const workingDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'type-inference-comparison-'));
1204
+ const workingPrecisionCorpusPath = path.join(workingDirectory, path.basename(precisionCorpusPath));
1205
+ const workingBehaviorCorpusPath = path.join(workingDirectory, path.basename(behaviorCorpusPath));
1206
+ fs.writeFileSync(workingPrecisionCorpusPath, precisionCorpus.source, 'utf8');
1207
+ fs.writeFileSync(workingBehaviorCorpusPath, behaviorCorpus.source, 'utf8');
1208
+ try {
1209
+ const precisionRun = {
1210
+ corpusPath: precisionCorpusPath,
1211
+ corpus: precisionCorpus,
1212
+ pyright: runPyright(workingPrecisionCorpusPath, options.pythonVersion),
1213
+ pyrefly: runPyrefly(workingPrecisionCorpusPath, options.pythonVersion, options.pyreflyCommand),
1214
+ };
1215
+ const behaviorRun = {
1216
+ corpusPath: behaviorCorpusPath,
1217
+ corpus: behaviorCorpus,
1218
+ pyright: runPyright(workingBehaviorCorpusPath, options.pythonVersion),
1219
+ pyrefly: runPyrefly(workingBehaviorCorpusPath, options.pythonVersion, options.pyreflyCommand),
1220
+ };
1221
+ console.log(`Pyright: ${precisionRun.pyright.reveals.size + behaviorRun.pyright.reveals.size} reveals in ${formatDuration(precisionRun.pyright.durationMs + behaviorRun.pyright.durationMs)}`);
1222
+ console.log(`Pyrefly: ${precisionRun.pyrefly.reveals.size + behaviorRun.pyrefly.reveals.size} reveals in ${formatDuration(precisionRun.pyrefly.durationMs + behaviorRun.pyrefly.durationMs)}`);
1223
+ const comparisons = [
1224
+ ...compareCases(precisionRun.corpus, precisionRun.pyright, precisionRun.pyrefly),
1225
+ ...compareCases(behaviorRun.corpus, behaviorRun.pyright, behaviorRun.pyrefly),
1226
+ ];
1227
+ writeMarkdownReport(reportPath, options, comparisons, precisionRun, behaviorRun);
1228
+ console.log(`Markdown report written to ${reportPath}`);
1229
+ }
1230
+ finally {
1231
+ fs.rmSync(workingDirectory, { recursive: true });
1232
+ }
1233
+ }
1234
+ main();
1235
+ //# sourceMappingURL=typeInferenceComparison.js.map