@scriptc/compiler 0.0.0 → 0.0.2

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 (206) hide show
  1. package/LICENSE +202 -0
  2. package/ambient/package.json +3 -0
  3. package/ambient/scriptc-node-fallback.d.ts +3001 -0
  4. package/ambient/scriptc-overrides.d.ts +95 -0
  5. package/ambient/scriptc.d.ts +74 -0
  6. package/dist/backend/cc.d.ts +169 -0
  7. package/dist/backend/cc.js +897 -0
  8. package/dist/backend/cc.js.map +1 -0
  9. package/dist/backend/emission/emit-async.d.ts +145 -0
  10. package/dist/backend/emission/emit-async.js +996 -0
  11. package/dist/backend/emission/emit-async.js.map +1 -0
  12. package/dist/backend/emission/emit-exprs.d.ts +3 -0
  13. package/dist/backend/emission/emit-exprs.js +5949 -0
  14. package/dist/backend/emission/emit-exprs.js.map +1 -0
  15. package/dist/backend/emission/emit-island.d.ts +45 -0
  16. package/dist/backend/emission/emit-island.js +271 -0
  17. package/dist/backend/emission/emit-island.js.map +1 -0
  18. package/dist/backend/emission/emit-shapes.d.ts +142 -0
  19. package/dist/backend/emission/emit-shapes.js +575 -0
  20. package/dist/backend/emission/emit-shapes.js.map +1 -0
  21. package/dist/backend/emission/emit-stmts.d.ts +78 -0
  22. package/dist/backend/emission/emit-stmts.js +960 -0
  23. package/dist/backend/emission/emit-stmts.js.map +1 -0
  24. package/dist/backend/emission/emit-types.d.ts +65 -0
  25. package/dist/backend/emission/emit-types.js +652 -0
  26. package/dist/backend/emission/emit-types.js.map +1 -0
  27. package/dist/backend/emission/emit-walkers.d.ts +154 -0
  28. package/dist/backend/emission/emit-walkers.js +1587 -0
  29. package/dist/backend/emission/emit-walkers.js.map +1 -0
  30. package/dist/backend/emission/emitter.d.ts +452 -0
  31. package/dist/backend/emission/emitter.js +1249 -0
  32. package/dist/backend/emission/emitter.js.map +1 -0
  33. package/dist/backend/emission/may-throw.d.ts +15 -0
  34. package/dist/backend/emission/may-throw.js +250 -0
  35. package/dist/backend/emission/may-throw.js.map +1 -0
  36. package/dist/backend/llvm/blocks.d.ts +21 -0
  37. package/dist/backend/llvm/blocks.js +68 -0
  38. package/dist/backend/llvm/blocks.js.map +1 -0
  39. package/dist/backend/llvm/classes.d.ts +84 -0
  40. package/dist/backend/llvm/classes.js +438 -0
  41. package/dist/backend/llvm/classes.js.map +1 -0
  42. package/dist/backend/llvm/dyn.d.ts +123 -0
  43. package/dist/backend/llvm/dyn.js +2624 -0
  44. package/dist/backend/llvm/dyn.js.map +1 -0
  45. package/dist/backend/llvm/emitter.d.ts +3 -0
  46. package/dist/backend/llvm/emitter.js +10102 -0
  47. package/dist/backend/llvm/emitter.js.map +1 -0
  48. package/dist/backend/llvm/shapes.d.ts +78 -0
  49. package/dist/backend/llvm/shapes.js +754 -0
  50. package/dist/backend/llvm/shapes.js.map +1 -0
  51. package/dist/backend/llvm/unsupported.d.ts +6 -0
  52. package/dist/backend/llvm/unsupported.js +12 -0
  53. package/dist/backend/llvm/unsupported.js.map +1 -0
  54. package/dist/backend/llvm/walkers.d.ts +58 -0
  55. package/dist/backend/llvm/walkers.js +793 -0
  56. package/dist/backend/llvm/walkers.js.map +1 -0
  57. package/dist/backend/mangle.d.ts +124 -0
  58. package/dist/backend/mangle.js +232 -0
  59. package/dist/backend/mangle.js.map +1 -0
  60. package/dist/coverage/report.d.ts +65 -0
  61. package/dist/coverage/report.js +238 -0
  62. package/dist/coverage/report.js.map +1 -0
  63. package/dist/diagnostics/diagnostic.d.ts +140 -0
  64. package/dist/diagnostics/diagnostic.js +458 -0
  65. package/dist/diagnostics/diagnostic.js.map +1 -0
  66. package/dist/diagnostics/render.d.ts +11 -0
  67. package/dist/diagnostics/render.js +58 -0
  68. package/dist/diagnostics/render.js.map +1 -0
  69. package/dist/frontend/cjs-lexer.d.ts +20 -0
  70. package/dist/frontend/cjs-lexer.js +813 -0
  71. package/dist/frontend/cjs-lexer.js.map +1 -0
  72. package/dist/frontend/lowering/http2-constants.d.ts +1 -0
  73. package/dist/frontend/lowering/http2-constants.js +251 -0
  74. package/dist/frontend/lowering/http2-constants.js.map +1 -0
  75. package/dist/frontend/lowering/lib-boundary.d.ts +6 -0
  76. package/dist/frontend/lowering/lib-boundary.js +143 -0
  77. package/dist/frontend/lowering/lib-boundary.js.map +1 -0
  78. package/dist/frontend/lowering/lower-assert.d.ts +18 -0
  79. package/dist/frontend/lowering/lower-assert.js +1269 -0
  80. package/dist/frontend/lowering/lower-assert.js.map +1 -0
  81. package/dist/frontend/lowering/lower-builtins.d.ts +511 -0
  82. package/dist/frontend/lowering/lower-builtins.js +5249 -0
  83. package/dist/frontend/lowering/lower-builtins.js.map +1 -0
  84. package/dist/frontend/lowering/lower-calls.d.ts +581 -0
  85. package/dist/frontend/lowering/lower-calls.js +6286 -0
  86. package/dist/frontend/lowering/lower-calls.js.map +1 -0
  87. package/dist/frontend/lowering/lower-classes.d.ts +727 -0
  88. package/dist/frontend/lowering/lower-classes.js +4701 -0
  89. package/dist/frontend/lowering/lower-classes.js.map +1 -0
  90. package/dist/frontend/lowering/lower-comptime.d.ts +53 -0
  91. package/dist/frontend/lowering/lower-comptime.js +244 -0
  92. package/dist/frontend/lowering/lower-comptime.js.map +1 -0
  93. package/dist/frontend/lowering/lower-containers.d.ts +486 -0
  94. package/dist/frontend/lowering/lower-containers.js +6345 -0
  95. package/dist/frontend/lowering/lower-containers.js.map +1 -0
  96. package/dist/frontend/lowering/lower-dgram.d.ts +20 -0
  97. package/dist/frontend/lowering/lower-dgram.js +319 -0
  98. package/dist/frontend/lowering/lower-dgram.js.map +1 -0
  99. package/dist/frontend/lowering/lower-emitter.d.ts +10 -0
  100. package/dist/frontend/lowering/lower-emitter.js +688 -0
  101. package/dist/frontend/lowering/lower-emitter.js.map +1 -0
  102. package/dist/frontend/lowering/lower-enums.d.ts +22 -0
  103. package/dist/frontend/lowering/lower-enums.js +235 -0
  104. package/dist/frontend/lowering/lower-enums.js.map +1 -0
  105. package/dist/frontend/lowering/lower-expando.d.ts +35 -0
  106. package/dist/frontend/lowering/lower-expando.js +276 -0
  107. package/dist/frontend/lowering/lower-expando.js.map +1 -0
  108. package/dist/frontend/lowering/lower-exprs.d.ts +524 -0
  109. package/dist/frontend/lowering/lower-exprs.js +8111 -0
  110. package/dist/frontend/lowering/lower-exprs.js.map +1 -0
  111. package/dist/frontend/lowering/lower-generators.d.ts +50 -0
  112. package/dist/frontend/lowering/lower-generators.js +421 -0
  113. package/dist/frontend/lowering/lower-generators.js.map +1 -0
  114. package/dist/frontend/lowering/lower-inspect.d.ts +20 -0
  115. package/dist/frontend/lowering/lower-inspect.js +1074 -0
  116. package/dist/frontend/lowering/lower-inspect.js.map +1 -0
  117. package/dist/frontend/lowering/lower-island.d.ts +108 -0
  118. package/dist/frontend/lowering/lower-island.js +650 -0
  119. package/dist/frontend/lowering/lower-island.js.map +1 -0
  120. package/dist/frontend/lowering/lower-mixins.d.ts +96 -0
  121. package/dist/frontend/lowering/lower-mixins.js +534 -0
  122. package/dist/frontend/lowering/lower-mixins.js.map +1 -0
  123. package/dist/frontend/lowering/lower-modules.d.ts +135 -0
  124. package/dist/frontend/lowering/lower-modules.js +1613 -0
  125. package/dist/frontend/lowering/lower-modules.js.map +1 -0
  126. package/dist/frontend/lowering/lower-namespaces.d.ts +168 -0
  127. package/dist/frontend/lowering/lower-namespaces.js +706 -0
  128. package/dist/frontend/lowering/lower-namespaces.js.map +1 -0
  129. package/dist/frontend/lowering/lower-server.d.ts +90 -0
  130. package/dist/frontend/lowering/lower-server.js +3471 -0
  131. package/dist/frontend/lowering/lower-server.js.map +1 -0
  132. package/dist/frontend/lowering/lower-stmts.d.ts +361 -0
  133. package/dist/frontend/lowering/lower-stmts.js +5341 -0
  134. package/dist/frontend/lowering/lower-stmts.js.map +1 -0
  135. package/dist/frontend/lowering/lower-stream.d.ts +88 -0
  136. package/dist/frontend/lowering/lower-stream.js +1533 -0
  137. package/dist/frontend/lowering/lower-stream.js.map +1 -0
  138. package/dist/frontend/lowering/lower-test.d.ts +27 -0
  139. package/dist/frontend/lowering/lower-test.js +353 -0
  140. package/dist/frontend/lowering/lower-test.js.map +1 -0
  141. package/dist/frontend/lowering/lowerer.d.ts +1748 -0
  142. package/dist/frontend/lowering/lowerer.js +6257 -0
  143. package/dist/frontend/lowering/lowerer.js.map +1 -0
  144. package/dist/frontend/lowering/surfaces.d.ts +272 -0
  145. package/dist/frontend/lowering/surfaces.js +1096 -0
  146. package/dist/frontend/lowering/surfaces.js.map +1 -0
  147. package/dist/frontend/npm-static.d.ts +55 -0
  148. package/dist/frontend/npm-static.js +300 -0
  149. package/dist/frontend/npm-static.js.map +1 -0
  150. package/dist/frontend/npm.d.ts +330 -0
  151. package/dist/frontend/npm.js +1259 -0
  152. package/dist/frontend/npm.js.map +1 -0
  153. package/dist/frontend/program.d.ts +186 -0
  154. package/dist/frontend/program.js +2255 -0
  155. package/dist/frontend/program.js.map +1 -0
  156. package/dist/frontend/provenance-registry.d.ts +48 -0
  157. package/dist/frontend/provenance-registry.js +87 -0
  158. package/dist/frontend/provenance-registry.js.map +1 -0
  159. package/dist/frontend/provenance.d.ts +6 -0
  160. package/dist/frontend/provenance.js +459 -0
  161. package/dist/frontend/provenance.js.map +1 -0
  162. package/dist/frontend/resolve.d.ts +56 -0
  163. package/dist/frontend/resolve.js +682 -0
  164. package/dist/frontend/resolve.js.map +1 -0
  165. package/dist/frontend/shared.d.ts +67 -0
  166. package/dist/frontend/shared.js +241 -0
  167. package/dist/frontend/shared.js.map +1 -0
  168. package/dist/frontend/ts7/adapter.d.ts +7 -0
  169. package/dist/frontend/ts7/adapter.js +54 -0
  170. package/dist/frontend/ts7/adapter.js.map +1 -0
  171. package/dist/frontend/ts7/ast.d.ts +50 -0
  172. package/dist/frontend/ts7/ast.js +211 -0
  173. package/dist/frontend/ts7/ast.js.map +1 -0
  174. package/dist/frontend/ts7/census-check.d.ts +1 -0
  175. package/dist/frontend/ts7/census-check.js +12 -0
  176. package/dist/frontend/ts7/census-check.js.map +1 -0
  177. package/dist/frontend/ts7/checker.d.ts +140 -0
  178. package/dist/frontend/ts7/checker.js +544 -0
  179. package/dist/frontend/ts7/checker.js.map +1 -0
  180. package/dist/frontend/ts7/enums.d.ts +18 -0
  181. package/dist/frontend/ts7/enums.js +43 -0
  182. package/dist/frontend/ts7/enums.js.map +1 -0
  183. package/dist/frontend/ts7/program.d.ts +99 -0
  184. package/dist/frontend/ts7/program.js +278 -0
  185. package/dist/frontend/ts7/program.js.map +1 -0
  186. package/dist/frontend/ts7/world-check.d.ts +3 -0
  187. package/dist/frontend/ts7/world-check.js +34 -0
  188. package/dist/frontend/ts7/world-check.js.map +1 -0
  189. package/dist/frontend/types.d.ts +205 -0
  190. package/dist/frontend/types.js +2485 -0
  191. package/dist/frontend/types.js.map +1 -0
  192. package/dist/index.d.ts +85 -0
  193. package/dist/index.js +429 -0
  194. package/dist/index.js.map +1 -0
  195. package/dist/ir/nodes.d.ts +4334 -0
  196. package/dist/ir/nodes.js +1906 -0
  197. package/dist/ir/nodes.js.map +1 -0
  198. package/dist/ir/serialize.d.ts +4 -0
  199. package/dist/ir/serialize.js +34 -0
  200. package/dist/ir/serialize.js.map +1 -0
  201. package/dist/ir/validate.d.ts +36 -0
  202. package/dist/ir/validate.js +4885 -0
  203. package/dist/ir/validate.js.map +1 -0
  204. package/package.json +30 -6
  205. package/README.md +0 -3
  206. package/index.js +0 -2
@@ -0,0 +1,2485 @@
1
+ import * as ts from "./ts7/adapter.js";
2
+ import { arrayOf, BOOL, bytesOf, CHILD_T, DYN, F64, funcOf, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, JSVAL, mapOf, NULL_T, PROCSTREAM_T, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, setOf, STRING, SYMBOL_T, typeEquals, typeKey, UNDEFINED_T, VOID } from "../ir/nodes.js";
3
+ import { isJsSourceFile, isNodeTypesPath } from "./program.js";
4
+ import { accessorSlotProp } from "../ir/nodes.js";
5
+ // typeKey moved to ir/nodes.ts (the backend needs it too, for per-type
6
+ // helper interning); re-exported here so frontend call sites keep their
7
+ // import path.
8
+ export { typeKey };
9
+ /** The island-backed ambient TYPE names of the fetch slice: standard-
10
+ * library interfaces whose VALUES live in the embedded engine (the
11
+ * engine's fetch mints the Response; AbortSignal.timeout mints the
12
+ * signal), so under --dynamic they map to island handles (jsval) exactly
13
+ * like npm-declared types. Checked with declaration provenance in mapType;
14
+ * consumed by the lowerer's badType for the static-build wording. */
15
+ export const ISLAND_AMBIENT_TYPES = ["Response", "RequestInit", "AbortSignal"];
16
+ /** The frontend's record-shape interner. Records are monomorphic structural
17
+ * shapes: fields sorted by name form the canonical identity, and two types
18
+ * with the same canonical field list share one shapeId (and later one C
19
+ * struct). Owned by the Lowerer; `mapType` needs it to intern the object
20
+ * types it encounters, which is why it is threaded through as a parameter. */
21
+ export class ShapeRegistry {
22
+ byKey = new Map();
23
+ byId = new Map();
24
+ /** All interned shapes in first-seen (`r0`, `r1`, ...) order. */
25
+ shapes = [];
26
+ /** ts.Types currently being mapped — breaks recursive shape types
27
+ * (`type T = { next: T }`), which mapType rejects by returning null. */
28
+ inProgress = new Set();
29
+ /** Interns a canonical (name-sorted) field list, returning its shapeId.
30
+ * `tuple` shapes (fields "0".."n-1" from a tuple type) intern SEPARATELY
31
+ * from structurally identical numeric-keyed object records: the flag is
32
+ * part of the identity because the two serialize differently (a tuple is
33
+ * a JSON array, an object a JSON object). A string index signature's
34
+ * value type (`indexValue`) is part of the identity too: the same
35
+ * declared fields with and without an overflow portion — or with
36
+ * differently-typed ones — are different structs. */
37
+ intern(fields, tuple = false, indexValue, declaredOrder) {
38
+ const key = (tuple ? "tuple!" : "") +
39
+ (indexValue ? `idx<${typeKey(indexValue)}>!` : "") +
40
+ JSON.stringify(fields.map((f) => [f.name, typeKey(f.type)]));
41
+ let id = this.byKey.get(key);
42
+ if (id === undefined) {
43
+ id = `r${this.shapes.length}`;
44
+ const shape = {
45
+ id,
46
+ fields,
47
+ ...(tuple ? { tuple: true } : {}),
48
+ ...(indexValue ? { indexValue } : {}),
49
+ // First-seen declaration order — metadata, NOT identity (see the
50
+ // IrRecordShape doc): a later structurally-equal type keeps the
51
+ // first one's order.
52
+ ...(declaredOrder ? { declaredOrder } : {}),
53
+ };
54
+ this.byKey.set(key, id);
55
+ this.byId.set(id, shape);
56
+ this.shapes.push(shape);
57
+ }
58
+ return id;
59
+ }
60
+ get(shapeId) {
61
+ return this.byId.get(shapeId);
62
+ }
63
+ }
64
+ /** The frontend's union interner — mirrors ShapeRegistry. A union's
65
+ * canonical identity is its typeKey-sorted arm list; two ts unions whose
66
+ * arms map to the same IR types share one unionId (and later one runtime
67
+ * tag numbering: an arm's index in the canonical list IS its tag). Owned by
68
+ * the Lowerer; threaded through mapType exactly like ShapeRegistry. */
69
+ export class UnionRegistry {
70
+ byKey = new Map();
71
+ byId = new Map();
72
+ /** All interned unions in first-seen (`u0`, `u1`, ...) order. */
73
+ unions = [];
74
+ /** ts.Types currently being mapped — breaks recursive union types
75
+ * (`type T = A | { next: T }`), which mapType rejects by returning null. */
76
+ inProgress = new Set();
77
+ /** Interns a canonical (typeKey-sorted, deduplicated) arm list, returning
78
+ * its unionId. */
79
+ intern(arms) {
80
+ const key = JSON.stringify(arms.map(typeKey));
81
+ let id = this.byKey.get(key);
82
+ if (id === undefined) {
83
+ id = `u${this.unions.length}`;
84
+ const def = { id, arms };
85
+ this.byKey.set(key, id);
86
+ this.byId.set(id, def);
87
+ this.unions.push(def);
88
+ }
89
+ return id;
90
+ }
91
+ get(unionId) {
92
+ return this.byId.get(unionId);
93
+ }
94
+ }
95
+ /** Human-readable rendering of an IrType for diagnostics (records expand to
96
+ * their canonical field list, unions to their arms; `checker.typeToString`
97
+ * can't — it never sees IR types). */
98
+ export function formatIrType(t, shapes, unions) {
99
+ switch (t.kind) {
100
+ case "f64":
101
+ return "number";
102
+ case "string":
103
+ return "string";
104
+ case "bool":
105
+ return "boolean";
106
+ case "dyn":
107
+ return "unknown";
108
+ case "caught":
109
+ // What tsc calls the binding; the fence messages carry the real story.
110
+ return "unknown";
111
+ case "void":
112
+ return "void";
113
+ case "undefinedT":
114
+ return "undefined";
115
+ case "nullT":
116
+ return "null";
117
+ case "array": {
118
+ // Union/func elements need the parens TS syntax would ("(number |
119
+ // string)[]" — without them the [] reads as binding to the last arm).
120
+ const elem = formatIrType(t.elem, shapes, unions);
121
+ return t.elem.kind === "union" || t.elem.kind === "func" ? `(${elem})[]` : `${elem}[]`;
122
+ }
123
+ case "bytes":
124
+ // The u8 kind reads as Uint8Array (Buffer maps here too — one
125
+ // runtime representation; the message stays honest either way).
126
+ return t.elem === "u8" ? "Uint8Array" : t.elem === "u32" ? "Uint32Array" : t.elem === "i32" ? "Int32Array" : "Float32Array";
127
+ case "map":
128
+ return `Map<${formatIrType(t.key, shapes, unions)}, ${formatIrType(t.value, shapes, unions)}>`;
129
+ case "set":
130
+ return `Set<${formatIrType(t.elem, shapes, unions)}>`;
131
+ case "func":
132
+ return `(${t.params.map((p) => formatIrType(p, shapes, unions)).join(", ")}) => ${formatIrType(t.ret, shapes, unions)}`;
133
+ case "object":
134
+ // Runtime-provided error classes carry '%'-prefixed IR names
135
+ // ("%Error") so user classes can never collide; diagnostics show the
136
+ // source-level name.
137
+ return t.className.startsWith("%") ? t.className.slice(1) : t.className;
138
+ case "classval":
139
+ // The static side, in TS's own spelling.
140
+ return `typeof ${t.className.startsWith("%") ? t.className.slice(1) : t.className}`;
141
+ case "record": {
142
+ const shape = shapes.get(t.shapeId);
143
+ if (!shape)
144
+ return `{ /* unknown shape ${t.shapeId} */ }`;
145
+ if (shape.tuple) {
146
+ const byIndex = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
147
+ return `[${byIndex.map((f) => formatIrType(f.type, shapes, unions)).join(", ")}]`;
148
+ }
149
+ const members = shape.fields.map((f) => {
150
+ // Accessor slots print in TS's accessor spelling, not the
151
+ // reserved '%'-field encoding.
152
+ const slot = accessorSlotProp(f.name);
153
+ if (slot && f.type.kind === "func") {
154
+ return slot.kind === "get"
155
+ ? `get ${slot.prop}(): ${formatIrType(f.type.ret, shapes, unions)}`
156
+ : `set ${slot.prop}(${formatIrType(f.type.params[0] ?? VOID, shapes, unions)})`;
157
+ }
158
+ return `${f.name}: ${formatIrType(f.type, shapes, unions)}`;
159
+ });
160
+ if (shape.indexValue) {
161
+ members.push(`[key: string]: ${formatIrType(shape.indexValue, shapes, unions)}`);
162
+ }
163
+ if (members.length === 0)
164
+ return "{}";
165
+ return `{ ${members.join("; ")} }`;
166
+ }
167
+ case "union": {
168
+ const def = unions.get(t.unionId);
169
+ if (!def)
170
+ return `/* union ${t.unionId} */`;
171
+ return def.arms.map((a) => formatIrType(a, shapes, unions)).join(" | ");
172
+ }
173
+ case "jsval":
174
+ return "any";
175
+ case "regex":
176
+ return "RegExp";
177
+ case "url":
178
+ return "URL";
179
+ case "searchParams":
180
+ return "URLSearchParams";
181
+ case "symbol":
182
+ return "symbol";
183
+ case "stats":
184
+ return "Stats";
185
+ case "spawnRes":
186
+ return "SpawnSyncReturns";
187
+ case "child":
188
+ return "ChildProcess";
189
+ case "netServer":
190
+ return "Server";
191
+ case "netSocket":
192
+ return "Socket";
193
+ case "http2Session":
194
+ return "Http2Session";
195
+ case "http2Stream":
196
+ return "Http2Stream";
197
+ case "dgramSocket":
198
+ return "dgram.Socket";
199
+ case "testCtx":
200
+ return "TestContext";
201
+ case "httpReq":
202
+ return "IncomingMessage";
203
+ case "httpRes":
204
+ return "ServerResponse";
205
+ case "httpClientReq":
206
+ return "ClientRequest";
207
+ case "secureCtx":
208
+ return "SecureContext";
209
+ case "fsWatcher":
210
+ return "FSWatcher";
211
+ case "childStream":
212
+ return "Readable";
213
+ case "procStream":
214
+ return "WriteStream";
215
+ case "promise":
216
+ return `Promise<${formatIrType(t.inner, shapes, unions)}>`;
217
+ case "generator":
218
+ return `Generator<${formatIrType(t.yieldT, shapes, unions)}, ${formatIrType(t.retT, shapes, unions)}, ${formatIrType(t.nextT, shapes, unions)}>`;
219
+ default: {
220
+ const _exhaustive = t;
221
+ void _exhaustive;
222
+ throw new Error("unreachable");
223
+ }
224
+ }
225
+ }
226
+ /** True when the declaration sits inside `declare module "<name>"` (or
227
+ * its node:-prefixed twin) — the disambiguator for builtin-module type
228
+ * names that repeat across modules (net.Server vs http.Server). Both
229
+ * @types/node and the shipped fallback declare builtins this way. */
230
+ /** ES OrdinaryOwnPropertyKeys order over string keys: canonical array
231
+ * indices (the exact string spelling of an integer in [0, 2^32-1)) come
232
+ * first in ascending numeric order, everything else follows in the given
233
+ * (insertion/declaration) order. This is JS's enumeration order for the
234
+ * objects records model — Object.keys, JSON.stringify, spread, inspect. */
235
+ export function esOwnKeyOrder(names) {
236
+ const isArrayIndex = (name) => {
237
+ const n = Number(name);
238
+ return Number.isInteger(n) && n >= 0 && n < 4294967295 && String(n) === name;
239
+ };
240
+ const indices = names.filter(isArrayIndex).sort((a, b) => Number(a) - Number(b));
241
+ return indices.length === 0 ? names : [...indices, ...names.filter((n) => !isArrayIndex(n))];
242
+ }
243
+ function isDeclaredInAmbientModule(d, name) {
244
+ let node = d.parent;
245
+ while (node) {
246
+ if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
247
+ const spec = node.name.text;
248
+ return spec === name || spec === `node:${name}`;
249
+ }
250
+ node = node.parent;
251
+ }
252
+ return false;
253
+ }
254
+ /** True when the declaration's nearest enclosing namespace is `name`
255
+ * (`declare global { namespace NodeJS { ... } }` — the NodeJS-global
256
+ * interfaces @types/node declares outside any ambient module). */
257
+ function isDeclaredInAmbientNamespace(d, name) {
258
+ let node = d.parent;
259
+ while (node) {
260
+ if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name)) {
261
+ return node.name.text === name;
262
+ }
263
+ node = node.parent;
264
+ }
265
+ return false;
266
+ }
267
+ /** True when a type contains a record anywhere (the trigger for the
268
+ * shape-mismatch diagnostic SC2002 — non-record mismatches are tsc's or
269
+ * the validator's business). */
270
+ export function containsRecord(t) {
271
+ switch (t.kind) {
272
+ case "record":
273
+ return true;
274
+ case "array":
275
+ return containsRecord(t.elem);
276
+ case "func":
277
+ return t.params.some(containsRecord) || containsRecord(t.ret);
278
+ default:
279
+ // Unions deliberately return false: a union-involving mismatch has its
280
+ // own diagnostic (SC2003 via containsUnion), not the record one.
281
+ return false;
282
+ }
283
+ }
284
+ /** True when a type contains a union anywhere reachable without a registry
285
+ * (the trigger for the union-mismatch diagnostic SC2003 — e.g. a value
286
+ * narrowed to a SUB-union flowing into the full union's slot, which would
287
+ * need a runtime re-tag). */
288
+ export function containsUnion(t) {
289
+ switch (t.kind) {
290
+ case "union":
291
+ return true;
292
+ case "array":
293
+ return containsUnion(t.elem);
294
+ case "func":
295
+ return t.params.some(containsUnion) || containsUnion(t.ret);
296
+ default:
297
+ return false;
298
+ }
299
+ }
300
+ /** lower-calls.ts's bodyReadsArguments, duplicated here (types.ts must not
301
+ * import from lowering/ — that edge is a module cycle): does the function's
302
+ * OWN body read `arguments`? Nested plain functions/methods own theirs
303
+ * (skipped); arrows see the enclosing one (descended). */
304
+ function bodyReadsArgumentsLocal(fn) {
305
+ let found = false;
306
+ if (fn.body === undefined)
307
+ return false;
308
+ // Iterative walk (walkPreorder): function bodies can hold pathologically
309
+ // deep expression chains that a recursive visit would die on.
310
+ ts.walkPreorder(fn.body, (n) => {
311
+ if (ts.isIdentifier(n) && n.text === "arguments" && !(ts.isPropertyAccessExpression(n.parent) && n.parent.name === n)) {
312
+ found = true;
313
+ return "stop";
314
+ }
315
+ if ((ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n) || ts.isMethodDeclaration(n)) && n !== fn) {
316
+ return "skip"; // own `arguments` scope
317
+ }
318
+ return undefined;
319
+ });
320
+ return found;
321
+ }
322
+ /** A generator type's normalized value channels (Generator<T, TReturn,
323
+ * TNext> and the IteratorResult alias share this):
324
+ * - yield: `never` (a generator that never yields) rides the VOID
325
+ * sentinel; any/unknown ride dyn; undefined-only yields have no C value
326
+ * form (null); else the mapped type.
327
+ * - return: void/undefined/never carry no value (VOID — the done-value is
328
+ * the undefined arm); any/unknown are DEFAULTS (`Generator<number>` is
329
+ * Generator<number, any, any>), and a defaulted return channel means
330
+ * "no modeled return value" — VOID, not dyn (a dyn return channel would
331
+ * force the yield channel dyn too; bodies that `return v` under a
332
+ * defaulted TReturn keep their fence at the return site).
333
+ * - next: void/undefined/never mean valueless resumes (the undefined
334
+ * unit); any/unknown ride dyn; else the mapped type (`.next(v)` then
335
+ * requires its argument — fenced at the call site).
336
+ * Mixed dyn/concrete value channels stay unmapped (the shared result
337
+ * record's value slot is one representation). */
338
+ function genChannels(yieldTs, retTs, nextTs, ctx) {
339
+ if (!yieldTs)
340
+ return null;
341
+ const UNITISH = ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Never;
342
+ const ANYISH = ts.TypeFlags.Any | ts.TypeFlags.Unknown;
343
+ let yieldT;
344
+ if (yieldTs.flags & ts.TypeFlags.Never)
345
+ yieldT = VOID;
346
+ else if (yieldTs.flags & ANYISH)
347
+ yieldT = DYN;
348
+ else {
349
+ yieldT = mapType(yieldTs, ctx);
350
+ if (yieldT?.kind === "void" || (yieldT && isUnitType(yieldT)))
351
+ yieldT = null;
352
+ }
353
+ if (!yieldT)
354
+ return null;
355
+ const retT = retTs === undefined || retTs.flags & (UNITISH | ANYISH)
356
+ ? VOID
357
+ : mapType(retTs, ctx);
358
+ if (!retT || (retT.kind !== "void" && isUnitType(retT)))
359
+ return null;
360
+ const nextT = nextTs === undefined || nextTs.flags & UNITISH
361
+ ? UNDEFINED_T
362
+ : nextTs.flags & ANYISH ? DYN : mapType(nextTs, ctx);
363
+ if (!nextT || nextT.kind === "void" || nextT.kind === "nullT")
364
+ return null;
365
+ const dynMix = (yieldT.kind === "dyn" && retT.kind !== "dyn" && retT.kind !== "void") ||
366
+ (retT.kind === "dyn" && yieldT.kind !== "dyn" && yieldT.kind !== "void");
367
+ if (dynMix)
368
+ return null;
369
+ return { yieldT, retT, nextT };
370
+ }
371
+ /** SELF-REFERENTIAL types recurse through mapType with no structural floor:
372
+ * `function somefn() { return somefn; }` maps the return type by mapping the
373
+ * function's own type again (the 15-stack-overflow-self-return signature),
374
+ * and `new.target` in a class-field function expression builds the same
375
+ * loop through the containing class. Legitimate types bottom out fast —
376
+ * anything this deep is cyclic (or pathological), and unmappable (null) is
377
+ * the honest answer: every call site already owns a fence or checked-dyn
378
+ * fallback for it. */
379
+ const MAP_TYPE_MAX_DEPTH = 64;
380
+ let mapTypeDepth = 0;
381
+ export function mapType(type, ctx) {
382
+ if (mapTypeDepth >= MAP_TYPE_MAX_DEPTH)
383
+ return null;
384
+ mapTypeDepth++;
385
+ try {
386
+ return mapTypeInner(type, ctx);
387
+ }
388
+ finally {
389
+ mapTypeDepth--;
390
+ }
391
+ }
392
+ /** True when a class expression can NEVER register a lowering: one
393
+ * enclosed by a function-like body or a class static block mints a
394
+ * DISTINCT class per evaluation, which lowerClassExpressionInfo always
395
+ * fences. Its instance/static types must stay UNMAPPED — an object type
396
+ * naming a struct that will never be emitted is the invalid-C escape
397
+ * family (every SITE using the value already carries its own fence). */
398
+ function classExprNeverRegisters(decl) {
399
+ if (!ts.isClassExpression(decl))
400
+ return false;
401
+ for (let p = decl.parent; !ts.isSourceFile(p); p = p.parent) {
402
+ if (ts.isFunctionLike(p) || ts.isClassStaticBlockDeclaration(p))
403
+ return true;
404
+ }
405
+ return false;
406
+ }
407
+ function mapTypeInner(type, ctx) {
408
+ const { checker, unions, classNamer, resolveTypeParam } = ctx;
409
+ if (resolveTypeParam && type.flags & ts.TypeFlags.TypeParameter) {
410
+ const bound = resolveTypeParam(type);
411
+ if (bound)
412
+ return bound;
413
+ }
414
+ // Narrowed type parameters: `!== undefined` / `!== null` / truthiness on
415
+ // a `T | undefined`-flavored value inside a generic body leaves the
416
+ // SYMBOLIC forms `T & {}`, `T & ({} | null)`, `(T & {}) | (T & null)`
417
+ // (concrete types compute the intersection away; only generic bodies see
418
+ // them). Each companion is an ALLOWANCE over the binding's arms — `{}`
419
+ // admits every non-unit arm, a null/undefined companion its unit — and
420
+ // the mapping is the binding filtered to what the narrowing kept.
421
+ if (resolveTypeParam) {
422
+ const viaNarrowedParam = mapNarrowedTypeParam(type, ctx);
423
+ if (viaNarrowedParam !== undefined)
424
+ return viaNarrowedParam;
425
+ }
426
+ const widened = checker.getBaseTypeOfLiteralType(type);
427
+ const flags = widened.flags;
428
+ if (flags & ts.TypeFlags.Number)
429
+ return F64;
430
+ if (flags & ts.TypeFlags.String)
431
+ return STRING;
432
+ if (flags & ts.TypeFlags.Boolean || flags & ts.TypeFlags.BooleanLiteral)
433
+ return BOOL;
434
+ // The lib's BOXED wrapper interfaces used as TYPES (`const n: Number =
435
+ // 5`): every value such a slot can hold IS the primitive — `new
436
+ // Number(...)` construction is fenced, so no box object ever exists —
437
+ // and the interface members are exactly the primitive's lowered surface.
438
+ // Provenance-gated: a user's own `interface Number` maps as a record.
439
+ {
440
+ const boxSym = widened.getSymbol();
441
+ if (boxSym &&
442
+ (boxSym.name === "Number" || boxSym.name === "String" || boxSym.name === "Boolean") &&
443
+ checker.declarationsOf(boxSym).some((d) => ctx.isStdlibFile(d.getSourceFile()))) {
444
+ return boxSym.name === "Number" ? F64 : boxSym.name === "String" ? STRING : BOOL;
445
+ }
446
+ }
447
+ // `symbol` and `unique symbol` (the type of `const k = Symbol(...)` —
448
+ // getBaseTypeOfLiteralType widens unique symbols, but check both flags)
449
+ // map to the symbol identity kind.
450
+ if (flags & (ts.TypeFlags.ESSymbol | ts.TypeFlags.UniqueESSymbol))
451
+ return SYMBOL_T;
452
+ // STANDALONE undefined (and void) map to VOID: return-type mapping flows
453
+ // through here, and `(): void | undefined` functions must stay void.
454
+ // Inside a union, undefined becomes the undefinedT unit ARM instead (see
455
+ // the union branch below). VALUE positions that today receive VOID
456
+ // (record fields, tuple/array elements, variable slots) substitute the
457
+ // unit-only union themselves (isUnitOnlyTsType + unitOnlyUnion) — the
458
+ // position knows it wants a value; this mapping cannot.
459
+ if (flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined))
460
+ return VOID;
461
+ // Standalone `null` (a `const x = null` binding, a `{ value: null }`
462
+ // field, a `(): null` return): the unit-only union — the value is always
463
+ // THE interned null instance, comparisons are tag tests, JSON serializes
464
+ // null. Inside a union, null keeps becoming the nullT ARM (the union
465
+ // branch below never recurses here for null parts).
466
+ if (flags & ts.TypeFlags.Null)
467
+ return unitOnlyUnion(unions);
468
+ // `never` VALUES are uninhabited — the checker only types dead reads
469
+ // this way (`for (const v of [])`'s loop var, the empty literal's
470
+ // `never[]` element) — so any representation is unobservable; f64 is the
471
+ // cheapest slot. Return positions never reach here (declaredReturnType
472
+ // and the signature branches map never returns to VOID first), and
473
+ // construction sites cannot exist (nothing has type never to feed them).
474
+ if (flags & ts.TypeFlags.Never)
475
+ return F64;
476
+ // `unknown` is a VALUE with a runtime representation (the dyn JSON DOM —
477
+ // JSON.parse results and unknown-typed locals/params/returns).
478
+ if (flags & ts.TypeFlags.Unknown)
479
+ return DYN;
480
+ // `object` (the NonPrimitive intrinsic) is a TOP type over non-primitive
481
+ // values — tsc admits every record, array, function, or class instance
482
+ // into an `object` slot, so no exact record shape can hold it. It lowers
483
+ // like `unknown` (the dyn DOM): assignments convert at the site (sources
484
+ // outside the DOM fence there, same as unknown), reads narrow back out
485
+ // through the same checked casts. Member accesses tsc allows on `object`
486
+ // (the Object.prototype surface) fence at their use sites.
487
+ if (flags & ts.TypeFlags.NonPrimitive)
488
+ return DYN;
489
+ // `any` is the island handle type under --dynamic (the type-hole becomes
490
+ // dynamically-executed code); without the flag it stays unmapped and the
491
+ // per-site diagnostic tells the user about --dynamic and the static
492
+ // alternative (`unknown` + a checked cast).
493
+ if (flags & ts.TypeFlags.Any)
494
+ return ctx.dynamic ? JSVAL : null;
495
+ // Enum types and enum member literal types: an enum VALUE at runtime IS
496
+ // its underlying primitive (tsc's own emit stores plain numbers/strings
497
+ // on the enum object; const enums inline them), so the enum type maps to
498
+ // number/string — comparisons, switch dispatch, arithmetic, and template
499
+ // formatting all take their ordinary primitive lowerings. A member
500
+ // literal type (`E.A` in type position) widens to the whole enum first
501
+ // (getBaseTypeOfLiteralType), so both spellings land here. Heterogeneous
502
+ // enums map to the number|string union (each member READ produces one
503
+ // exact arm; the slot re-tags like any literal-into-union assignment).
504
+ // A union MIXING enum literals with non-enum arms (`E | undefined`)
505
+ // falls through to the general union branch below, whose per-part
506
+ // recursion re-enters here with the pure enum parts.
507
+ if (flags & ts.TypeFlags.EnumLike) {
508
+ const parts = widened.isUnionType() ? widened.getTypes() : [widened];
509
+ if (parts.every((p) => p.flags & ts.TypeFlags.EnumLike)) {
510
+ let hasNum = false;
511
+ let hasStr = false;
512
+ for (const p of parts) {
513
+ if (p.flags & ts.TypeFlags.StringLiteral)
514
+ hasStr = true;
515
+ // NumberLiteral members, and the non-literal `Enum` flavor (an
516
+ // enum with computed members is a NUMERIC enum type — string
517
+ // members would make it a literal enum by definition).
518
+ else
519
+ hasNum = true;
520
+ }
521
+ if (hasNum && hasStr) {
522
+ const arms = [F64, STRING];
523
+ arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
524
+ return { kind: "union", unionId: unions.intern(arms) };
525
+ }
526
+ return hasStr ? STRING : F64;
527
+ }
528
+ }
529
+ // Types DECLARED by a shipped .d.ts — an npm package's, or a LOCAL
530
+ // declaration file describing sibling JS the program loads dynamically
531
+ // (an Emscripten factory's .d.mts): the .d.ts is trusted as the type
532
+ // surface, but the values behind it live in the embedded engine — under
533
+ // --dynamic they are island handles (jsval), and every operation on them
534
+ // rides the engine ops with validated exits at typed boundaries.
535
+ // Primitives/arrays/etc. REACHED THROUGH such types keep their
536
+ // structural mapping (they were handled above or recurse normally); this
537
+ // rule fires only when the type's own identity is declaration-file-
538
+ // declared — interfaces, classes, type literals, and aliases from the
539
+ // .d.ts. The standard library's declaration files are carved out (their
540
+ // surfaces have static lowerings), and the program's own compiled
541
+ // modules are never declaration files. Without --dynamic it stays
542
+ // unmapped; badType reports the per-package requires-dynamic diagnostic
543
+ // for node_modules types and the generic story otherwise.
544
+ const npmSym = widened.getAliasSymbol() ?? widened.getSymbol();
545
+ const npmDecls = npmSym ? checker.declarationsOf(npmSym) : undefined;
546
+ if (npmDecls &&
547
+ npmDecls.length > 0 &&
548
+ npmDecls.every((d) => {
549
+ const sf = d.getSourceFile();
550
+ return sf.isDeclarationFile && !ctx.isStdlibFile(sf);
551
+ })) {
552
+ return ctx.dynamic ? JSVAL : null;
553
+ }
554
+ // NOTE on module NAMESPACE types (`typeof import("./x.mjs")` — what a
555
+ // dynamic import resolves to): non-stdlib ones fall under the rule
556
+ // above (their declarations are the .d.ts SourceFiles themselves).
557
+ // Stdlib `declare module` namespaces ("fs", "path") deliberately stay
558
+ // unmapped: their members have STATIC lowerings keyed off import
559
+ // bindings (builtinImportOf), and a handle mapping would reroute
560
+ // `import * as path` member calls into the engine — dynamic imports of
561
+ // builtins get their handles from the import lowering's IR type
562
+ // instead.
563
+ // T[]: monomorphic arrays, recursively (number[][] works). An element type
564
+ // that doesn't map (never from a context-free `[]`) makes the whole array
565
+ // unsupported — null propagates. Record/object/union elements ride the
566
+ // runtime's REF element kind (per-array RC entry points, the map-value
567
+ // technique), and PROMISE elements ride it too (Promise<T>[] is
568
+ // Promise.all's food: promises are refcounted, cycle-headered values
569
+ // with `_v` adapters like any other ref element — they just never
570
+ // JSON-serialize or cross the island boundary, which the safety
571
+ // predicates already refuse). FUNCTION elements ride REF too (closure
572
+ // `_v` adapters + scr_closure_trace_v — closures are cycle-headered):
573
+ // `f === f` identity through indexOf/includes is sound because a
574
+ // function VALUE is one ScrClosure for its whole life — top-level
575
+ // declarations intern one immortal closure, and inner closures are
576
+ // allocated once at their definition's evaluation and flow by
577
+ // reference, exactly JS's function identity. map/set/regex/url/dyn and
578
+ // the other opaque handles stay unsupported.
579
+ if (checker.isArrayType(widened)) {
580
+ const elemTs = checker.getTypeArguments(widened)[0];
581
+ if (!elemTs)
582
+ return null;
583
+ let elem = mapType(elemTs, ctx);
584
+ // `undefined[]` (sparse literals — `[,]` — and explicit annotations):
585
+ // the element rides the unit-only union like a record field would; the
586
+ // VOID mapping is a return-position artifact, not a value.
587
+ if (elem?.kind === "void" && isUnitOnlyTsType(elemTs))
588
+ elem = unitOnlyUnion(unions);
589
+ // A jsval element ABSORBS the array — with one carve-out. An array
590
+ // type entangled with package-declared ('npm-jsval') elements
591
+ // (`GeneratedFile[]`, `ModelMessage[]`) describes ISLAND values: the
592
+ // handles those APIs produce are engine arrays, never the static
593
+ // ScrArr this mapping would otherwise claim, so the whole array is
594
+ // one handle and every operation on it is an engine op. The carve-out
595
+ // is literal `any[]`: an EXPLICIT static array of dynamic values —
596
+ // the messages-building pattern (`const content: any[] = []`) — which
597
+ // keeps its static array-of-handles representation.
598
+ if (elem?.kind === "jsval" && !(elemTs.flags & ts.TypeFlags.Any))
599
+ return JSVAL;
600
+ if (!elem ||
601
+ elem.kind === "void" ||
602
+ elem.kind === "map" ||
603
+ elem.kind === "set" ||
604
+ elem.kind === "regex" ||
605
+ elem.kind === "url" ||
606
+ elem.kind === "searchParams" ||
607
+ elem.kind === "stats" ||
608
+ elem.kind === "spawnRes" ||
609
+ elem.kind === "netSocket" ||
610
+ elem.kind === "dgramSocket" ||
611
+ elem.kind === "testCtx" ||
612
+ elem.kind === "httpReq" ||
613
+ elem.kind === "httpRes" ||
614
+ elem.kind === "httpClientReq" ||
615
+ elem.kind === "secureCtx" ||
616
+ elem.kind === "fsWatcher" ||
617
+ elem.kind === "childStream" ||
618
+ elem.kind === "procStream" ||
619
+ elem.kind === "generator" ||
620
+ elem.kind === "dyn") {
621
+ return null; // ScrArr has no closure/map/regex/url/dyn element kinds yet
622
+ }
623
+ // ChildProcess[] (the running-apps list) and Server[] (the [...set]
624
+ // drain of the auxiliary-server registries): handles are ordinary
625
+ // refcounted REF elements (their `_v` adapters, no trace — both drop
626
+ // their closures at their terminal event).
627
+ return arrayOf(elem);
628
+ }
629
+ // Tuples: `[string, string]` maps to an interned RECORD shape flagged
630
+ // `tuple`, with one field per position named by its index ("0", "1", ...)
631
+ // — fixed shape, literal-index access, length = the arity constant
632
+ // (SEMANTICS.md). The flag keeps tuple shapes distinct from numeric-keyed
633
+ // object records (JSON must serialize a tuple as an ARRAY) and is why
634
+ // interning happens through the tuple-aware path. Optional/rest elements
635
+ // have no fixed shape and stay unmapped; element types follow record-field
636
+ // rules (no void/dyn).
637
+ if (checker.isTupleType(widened)) {
638
+ const ref = widened;
639
+ // elementFlags live on the tuple SHAPE: a direct tuple type carries
640
+ // them itself; a REFERENCE to one (isTupleType still answers true —
641
+ // the facade's 5.9.3 contract) reads them off its target.
642
+ const tupleShape = ref.elementFlags !== undefined
643
+ ? ref
644
+ : ref.getTarget();
645
+ // The empty tuple `[]` — a declared annotation, or the facade edge
646
+ // where isTupleType answers true with NO element flags on the shape or
647
+ // its target (the empty-array arm of `''.match(/x/) || []`; the
648
+ // declared `[]` reads the same way, 0 type arguments and no flags). A
649
+ // zero-field tuple SHAPE would violate the IR's tuple invariant
650
+ // (positional fields "0".."n-1", n >= 1), but the type needs no shape
651
+ // at all — its only inhabitant is the empty array. It rides the ARRAY
652
+ // representation over the unit-only element (the `undefined[]` rule):
653
+ // length reads the runtime 0 and no element is ever written (push
654
+ // takes `never`; indexed reads are out of range for tsc). Element-
655
+ // FACING surfaces (JSON.stringify, spread, for-of) keep their
656
+ // unit-element fences: no element exists, but the type-directed checks
657
+ // see the unit arm.
658
+ const args = checker.getTypeArguments(ref);
659
+ if (args.length === 0)
660
+ return arrayOf(unitOnlyUnion(unions));
661
+ if (tupleShape?.elementFlags === undefined)
662
+ return null;
663
+ // Optional/rest elements (`[string?, number?]`, `[string, ...number[]]`)
664
+ // have no fixed shape — the ARITY is a runtime fact. Under --dynamic
665
+ // the value lives in the engine, a real JS array carrying its true
666
+ // length (reads, destructuring, JSON, .length all exact); static
667
+ // builds stay unmapped and badType tells the dynamic-tier story.
668
+ if (tupleShape.elementFlags.some((f) => !(f & ts.ElementFlags.Required))) {
669
+ return ctx.dynamic ? JSVAL : null;
670
+ }
671
+ const fields = [];
672
+ for (let i = 0; i < args.length; i++) {
673
+ let et = mapType(args[i], ctx);
674
+ // A unit-only element (`[number, undefined]`) rides the unit-only
675
+ // union, the record-field rule.
676
+ if (et?.kind === "void" && isUnitOnlyTsType(args[i]))
677
+ et = unitOnlyUnion(unions);
678
+ // dyn ELEMENTS map now — `[string, unknown]`, the Object.entries
679
+ // tuple over an `unknown`-valued index signature (the pricing-table
680
+ // normalizer shape): the slot has the overflow map's RC/JSON/DOM
681
+ // plumbing, values arrive dyn or convert via dynFrom.
682
+ if (!et || et.kind === "void")
683
+ return null;
684
+ // A jsval MEMBER absorbs the tuple (bare jsval fields have no shape
685
+ // slot — the record-field rule), exactly like record fields below.
686
+ if (et.kind === "jsval")
687
+ return JSVAL;
688
+ fields.push({ name: String(i), type: et });
689
+ }
690
+ fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
691
+ return { kind: "record", shapeId: ctx.shapes.intern(fields, true) };
692
+ }
693
+ // Class instances: the type's symbol is a class declared in the user's
694
+ // file. The class NAME as a value has the *constructor* type — same
695
+ // REFINED handle intersections — @types/node's idioms: `ServerResponse<
696
+ // IncomingMessage> & { req: IncomingMessage }` (RequestListener's
697
+ // inferred res param — every unannotated http.createServer handler) and
698
+ // the cast shape `Socket & { encrypted?: boolean }`. The VALUE is the
699
+ // handle; the refinement's extra members are type-level decoration whose
700
+ // uses fence per site (they are not stdlib members). Exactly one
701
+ // constituent must map to an opaque builtin-handle kind; every other
702
+ // part must be a plain object refinement — no call/construct signatures,
703
+ // no class identity of its own.
704
+ if (widened.isIntersectionType()) {
705
+ const HANDLE_KINDS = new Set([
706
+ "netServer", "netSocket", "httpReq", "httpRes", "httpClientReq", "dgramSocket",
707
+ // process.stdout's own type IS the refined intersection
708
+ // `WriteStream & { fd: 1 }` — the scalar stream kind rides the same
709
+ // refinement rule.
710
+ "procStream",
711
+ ]);
712
+ let handle = null;
713
+ let refined = true;
714
+ for (const part of widened.getTypes()) {
715
+ const mapped = mapType(part, ctx);
716
+ if (mapped && HANDLE_KINDS.has(mapped.kind)) {
717
+ if (handle !== null && handle.kind !== mapped.kind) {
718
+ refined = false;
719
+ break;
720
+ }
721
+ handle = mapped;
722
+ continue;
723
+ }
724
+ const partSym = part.getSymbol();
725
+ if ((part.flags & ts.TypeFlags.Object) === 0 ||
726
+ checker.getCallSignatures(part).length > 0 ||
727
+ checker.getConstructSignatures(part).length > 0 ||
728
+ (partSym !== undefined && (partSym.flags & ts.SymbolFlags.Class) !== 0)) {
729
+ refined = false;
730
+ break;
731
+ }
732
+ }
733
+ if (refined && handle !== null)
734
+ return handle;
735
+ // MIXIN instance intersections (`Tagged.C & Derived` — values built
736
+ // through a mixin result): the chain structure names the unique
737
+ // pinned instantiation; ambiguity stays unmapped (the hook's rules).
738
+ {
739
+ const viaMixin = ctx.mixinIntersectionInstance?.(widened);
740
+ if (viaMixin)
741
+ return viaMixin;
742
+ }
743
+ }
744
+ // symbol, but with construct signatures — that is the STATIC side, and
745
+ // it maps to classval below.
746
+ const widenedSym = widened.getSymbol();
747
+ const classDecl = widenedSym ? checker.valueDeclarationOf(widenedSym) : undefined;
748
+ if (classDecl &&
749
+ (ts.isClassDeclaration(classDecl) || ts.isClassExpression(classDecl)) &&
750
+ // Nameless class DECLARATIONS are legal only as `export default class
751
+ // {}` — classNamer spells them "%anon" (unique per file), the same
752
+ // name their collection registered.
753
+ !classDecl.getSourceFile().isDeclarationFile &&
754
+ checker.getConstructSignatures(widened).length === 0) {
755
+ // Declared OUTSIDE the lowered program (reached through the type world
756
+ // alone — jsdoc `typeof import()` over a never-required module): the
757
+ // class will never register, so the instance type is unmappable, not
758
+ // an object type naming a struct that cannot exist.
759
+ if (!ctx.isProgramFile(classDecl.getSourceFile()))
760
+ return null;
761
+ // The class inside a MIXIN function: per-call-site instantiations
762
+ // share this one node — the CURRENT instantiation (collecting or
763
+ // lowering) is the answer; outside any mixin context the node's type
764
+ // stays unmapped (nothing names a call site).
765
+ {
766
+ const viaMixin = ctx.mixinClassInstance?.(classDecl);
767
+ if (viaMixin)
768
+ return viaMixin;
769
+ }
770
+ // A class expression inside a function/static block never registers
771
+ // (a distinct class per evaluation): unmappable for the same reason.
772
+ if (classExprNeverRegisters(classDecl))
773
+ return null;
774
+ // A GENERIC class's instance type (`Box<number>`) maps to the concrete
775
+ // INSTANTIATION's class (`Box%0`), registered on demand — the Lowerer
776
+ // hook owns the instance table (monomorphization by flow).
777
+ if (classDecl.typeParameters) {
778
+ return ctx.genericClassInstance ? ctx.genericClassInstance(classDecl, widened) : null;
779
+ }
780
+ return { kind: "object", className: classNamer(classDecl) };
781
+ }
782
+ // The class STATIC side (`typeof C` — the class name as a value): the
783
+ // same symbol WITH its construct signatures. Program classes only; lib
784
+ // and package statics keep their fences.
785
+ if (classDecl &&
786
+ (ts.isClassDeclaration(classDecl) || ts.isClassExpression(classDecl)) &&
787
+ !classDecl.getSourceFile().isDeclarationFile &&
788
+ checker.getConstructSignatures(widened).length > 0) {
789
+ if (!ctx.isProgramFile(classDecl.getSourceFile()))
790
+ return null;
791
+ // The MIXIN class node's static side (`typeof C` inside the mixin):
792
+ // the current instantiation's classval, like the instance type above.
793
+ {
794
+ const viaMixin = ctx.mixinClassInstance?.(classDecl);
795
+ if (viaMixin?.kind === "object")
796
+ return { kind: "classval", className: viaMixin.className };
797
+ }
798
+ if (classExprNeverRegisters(classDecl))
799
+ return null;
800
+ // A GENERIC class's static side: only an INSTANTIATED one maps — an
801
+ // instantiation expression's type (`Box<number>` as a value) carries a
802
+ // construct signature returning the concrete instance, which maps to
803
+ // the instantiation's classval. Uninstantiated `typeof Box` keeps the
804
+ // type parameter in the construct signature's return — unmapped (the
805
+ // family has no thunk and no single constructor ABI).
806
+ if (classDecl.typeParameters) {
807
+ const ctorSigs = checker.getConstructSignatures(widened);
808
+ if (ctorSigs.length !== 1)
809
+ return null;
810
+ const inst = mapType(checker.getReturnTypeOfSignature(ctorSigs[0]), ctx);
811
+ return inst?.kind === "object" ? { kind: "classval", className: inst.className } : null;
812
+ }
813
+ return { kind: "classval", className: classNamer(classDecl) };
814
+ }
815
+ // Constructor-signature TYPES (`new (…) => T`, an interface with one
816
+ // construct signature): slots typed as constructables hold class
817
+ // values whose instance type is T — classval of T's class. The spelled
818
+ // signature is not part of the IR type: every value legally in the slot
819
+ // shares the class's own completed constructor ABI (the upcast rule),
820
+ // which is what newValue completes against. Only single-signature
821
+ // constructables over PROGRAM class instances map; overload sets and
822
+ // lib/package constructables stay unmapped (their fences name them).
823
+ {
824
+ const ctorSigs = checker.getConstructSignatures(widened);
825
+ if (ctorSigs.length === 1 && checker.getCallSignatures(widened).length === 0) {
826
+ const inst = mapType(checker.getReturnTypeOfSignature(ctorSigs[0]), ctx);
827
+ if (inst?.kind === "object" &&
828
+ !RUNTIME_ERROR_CLASSES.has(inst.className) &&
829
+ inst.className !== RUNTIME_EMITTER_CLASS &&
830
+ !RUNTIME_STREAM_CLASSES.has(inst.className)) {
831
+ return { kind: "classval", className: inst.className };
832
+ }
833
+ }
834
+ }
835
+ // Map<K, V>: a reference to the standard library's Map interface —
836
+ // provenance, not the name (a user's own `interface Map` maps as a record
837
+ // like any other). Keys are fenced to f64/string (SameValueZero hashing),
838
+ // values to the supported kinds (see isSupportedMapValue); anything
839
+ // outside stays unmapped — callers report SC2001 with the full type
840
+ // text, and the `new Map` lowering names the offending half specifically.
841
+ const psym = widened.getSymbol();
842
+ const isStdlibInterface = (name) => psym?.name === name &&
843
+ checker.declarationsOf(psym).some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()));
844
+ // The builtin Error classes: references to the LIB's Error/TypeError/
845
+ // RangeError/SyntaxError interfaces map to the runtime-provided class
846
+ // hierarchy (provenance, not the name — a user's own `class Error`
847
+ // resolved through the class-instance branch above, and a user
848
+ // `interface Error` maps as a record). The '%'-prefixed IR names are the
849
+ // RUNTIME_ERROR_CLASSES table's; the Lowerer registers the matching
850
+ // ClassInfos eagerly, so every consumer of the class name finds them.
851
+ for (const [irName, rec] of RUNTIME_ERROR_CLASSES) {
852
+ if (isStdlibInterface(rec.lib))
853
+ return { kind: "object", className: irName };
854
+ }
855
+ // The lib's `Object` interface is a TOP type: tsc admits every
856
+ // non-nullish value into an `Object` slot (`const x: Object = "s"`), so
857
+ // it lowers like `unknown` (the dyn DOM) — the same rule as `object` and
858
+ // the empty object type, provenance-checked so a user's own `interface
859
+ // Object` still maps as a record. Its Object.prototype member surface
860
+ // (`x.toString()`) fences at each use site, never silently.
861
+ if (isStdlibInterface("Object"))
862
+ return DYN;
863
+ // events.EventEmitter: the runtime-provided emitter base class (the
864
+ // Error-hierarchy precedent — user subclasses resolved through the
865
+ // class-instance branch above). Both type layers declare it in the
866
+ // "events" ambient module — the fallback as a class, @types/node as a
867
+ // (generic) class plus a same-named interface — and @types/node's
868
+ // NodeJS.EventEmitter interface (the structural surface net.Socket
869
+ // et al. extend) is the same runtime value. Provenance-checked.
870
+ if (psym?.name === "EventEmitter" &&
871
+ checker.declarationsOf(psym).some((d) => (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) &&
872
+ ctx.isStdlibFile(d.getSourceFile()) &&
873
+ (isDeclaredInAmbientModule(d, "events") || isDeclaredInAmbientNamespace(d, "NodeJS")))) {
874
+ return { kind: "object", className: RUNTIME_EMITTER_CLASS };
875
+ }
876
+ // readline.Interface: the interface value is an f64 handle into the
877
+ // runtime's registry (the Timeout-id precedent). The NAME is generic,
878
+ // so the provenance check adds the enclosing ambient module, like
879
+ // net.Server (@types/node also declares Interface in
880
+ // readline/promises — that one stays unmapped and fences).
881
+ if (psym?.name === "Interface" &&
882
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
883
+ ctx.isStdlibFile(d.getSourceFile()) &&
884
+ isDeclaredInAmbientModule(d, "readline"))) {
885
+ return F64;
886
+ }
887
+ // diagnostics_channel.Channel: the channel value is an f64 handle into
888
+ // the runtime's channel registry (the readline.Interface precedent —
889
+ // Node's channels are process-lived too, its WeakRef machinery aside).
890
+ // Provenance: the name is specific enough that stdlib-file provenance
891
+ // plus the ambient module suffices for both the fallback declarations
892
+ // and @types/node's class Channel<StoreType, ContextType>.
893
+ if (psym?.name === "Channel" &&
894
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
895
+ ctx.isStdlibFile(d.getSourceFile()) &&
896
+ isDeclaredInAmbientModule(d, "diagnostics_channel"))) {
897
+ return F64;
898
+ }
899
+ // async_hooks.AsyncLocalStorage: the store value is an f64 handle into
900
+ // the runtime's store id space (the Channel story — stores are
901
+ // process-lived; contexts ride the fiber machinery).
902
+ if (psym?.name === "AsyncLocalStorage" &&
903
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
904
+ ctx.isStdlibFile(d.getSourceFile()) &&
905
+ isDeclaredInAmbientModule(d, "async_hooks"))) {
906
+ return F64;
907
+ }
908
+ // diagnostics_channel.TracingChannel: the same f64-handle story over the
909
+ // runtime's tracing registry (five event channels per entry).
910
+ if (psym?.name === "TracingChannel" &&
911
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
912
+ ctx.isStdlibFile(d.getSourceFile()) &&
913
+ isDeclaredInAmbientModule(d, "diagnostics_channel"))) {
914
+ return F64;
915
+ }
916
+ // string_decoder.StringDecoder: the decoder value is a two-field record
917
+ // — the CANONICAL encoding name (construction normalizes aliases; the
918
+ // `.encoding` property reads it) and the f64 packing the pending
919
+ // partial sequence (the %strdec helpers' state cell). Provenance-
920
+ // checked like Stats; construction and the write/end methods are
921
+ // special-cased in lowerNew / lowerStringDecoderMethodCall, so the
922
+ // record shape never surfaces.
923
+ if (psym?.name === "StringDecoder" &&
924
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
925
+ ctx.isStdlibFile(d.getSourceFile()))) {
926
+ return {
927
+ kind: "record",
928
+ shapeId: ctx.shapes.intern([
929
+ { name: "%enc", type: STRING },
930
+ { name: "%pending", type: F64 },
931
+ ]),
932
+ };
933
+ }
934
+ // Promise.withResolvers's return type — `{ promise, resolve, reject }`
935
+ // from the overrides declaration (or es2024's PromiseWithResolvers,
936
+ // the same shape): a real data record, but its anonymous literal lives
937
+ // in a declaration file, which recordProvenanceOk rightly refuses for
938
+ // lib type worlds. Map it manually with the executor's own scriptc
939
+ // field shapes (plain-value resolve — () => void for Promise<void> —
940
+ // and the Error-pinned reject), so both the destructured and the
941
+ // record-holding binding forms compile. User-declared literals with
942
+ // these member names keep the ordinary record path (their declarations
943
+ // are not in declaration files).
944
+ if (flags & ts.TypeFlags.Object &&
945
+ (psym ? checker.declarationsOf(psym).length : 0) > 0 &&
946
+ (psym ? checker.declarationsOf(psym) : []).every((d) => d.getSourceFile().isDeclarationFile)) {
947
+ const props = checker.getPropertiesOfType(widened);
948
+ if (props.length === 3 &&
949
+ ["promise", "resolve", "reject"].every((n) => props.some((p) => p.name === n))) {
950
+ const promProp = props.find((p) => p.name === "promise");
951
+ const promT = mapType(checker.getTypeOfSymbol(promProp), ctx);
952
+ if (promT?.kind === "promise") {
953
+ const inner = promT.inner;
954
+ return {
955
+ kind: "record",
956
+ shapeId: ctx.shapes.intern([
957
+ { name: "promise", type: promT },
958
+ {
959
+ name: "reject",
960
+ type: { kind: "func", params: [{ kind: "object", className: "%Error" }], ret: VOID },
961
+ },
962
+ {
963
+ name: "resolve",
964
+ type: { kind: "func", params: inner.kind === "void" ? [] : [inner], ret: VOID },
965
+ },
966
+ ], false, undefined, ["promise", "resolve", "reject"]),
967
+ };
968
+ }
969
+ }
970
+ }
971
+ // NodeJS.ErrnoException (@types/node): an Error with optional
972
+ // code/errno/syscall/path members. The VALUE is a plain runtime error
973
+ // (fs/exec throw sites stamp `code`), so the type maps to the %Error
974
+ // root — `err as NodeJS.ErrnoException` from an Error-typed value is a
975
+ // no-op cast, error-typed listener params accept it, and the `.code`
976
+ // read has its own lowering (errno/syscall/path stay per-member fences).
977
+ if (isStdlibInterface("ErrnoException"))
978
+ return { kind: "object", className: "%Error" };
979
+ // The fetch ambient slice (Response, RequestInit, AbortSignal): island-
980
+ // backed ambient TYPES — the values behind them live in the embedded
981
+ // engine (fetch's Response, AbortSignal.timeout's signal), so under
982
+ // --dynamic they map to island handles exactly like npm-declared types,
983
+ // and every operation on them rides the engine ops with validated exits
984
+ // at typed boundaries. Provenance, not names: a user's own `interface
985
+ // Response` maps as a record like any other. Without the flag they stay
986
+ // unmapped; the use sites report the per-site requires-dynamic story
987
+ // (the fetch/AbortSignal.timeout lowerings' SC2012, badType's naming
988
+ // for a bare value of the type).
989
+ // Interface OR class declarations (the shipped fallback declares
990
+ // interfaces; @types/node's undici-types declares Response as a class).
991
+ if (psym &&
992
+ ISLAND_AMBIENT_TYPES.includes(psym.name) &&
993
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
994
+ ctx.isStdlibFile(d.getSourceFile()))) {
995
+ return ctx.dynamic ? JSVAL : null;
996
+ }
997
+ // ReadonlyMap/ReadonlySet are the SAME runtime values as Map/Set — the
998
+ // readonly-ness is a checker-only view (no mutating members on the
999
+ // interface), so both map to the identical IR kinds and the read-side
1000
+ // method lowerings (.has/.get/.size/iteration) dispatch unchanged.
1001
+ if (isStdlibInterface("Map") || isStdlibInterface("ReadonlyMap")) {
1002
+ const args = checker.getTypeArguments(widened);
1003
+ if (args.length !== 2)
1004
+ return null;
1005
+ const key = mapType(args[0], ctx);
1006
+ if (!key || !isSupportedMapKey(key))
1007
+ return null;
1008
+ const value = mapType(args[1], ctx);
1009
+ if (!value || !isSupportedMapValue(value))
1010
+ return null;
1011
+ return mapOf(key, value);
1012
+ }
1013
+ // Set<T>: Map's sibling — same provenance rule, elements fenced to Map's
1014
+ // KEY kinds (f64/string, SameValueZero). Anything else stays unmapped;
1015
+ // the `new Set` lowering names the offending element type specifically.
1016
+ if (isStdlibInterface("Set") || isStdlibInterface("ReadonlySet")) {
1017
+ const args = checker.getTypeArguments(widened);
1018
+ if (args.length !== 1)
1019
+ return null;
1020
+ const elem = mapType(args[0], ctx);
1021
+ if (!elem || !isSupportedSetElem(elem))
1022
+ return null;
1023
+ return setOf(elem);
1024
+ }
1025
+ // RegExp: a reference to the lib RegExp interface — provenance, not the
1026
+ // name (a user's own `interface RegExp` maps as a record). Regex values
1027
+ // are literals interned as immortal statics; the kind is fenced out of
1028
+ // array elements, union arms, and Map keys/values above/below.
1029
+ if (isStdlibInterface("RegExp")) {
1030
+ return { kind: "regex" };
1031
+ }
1032
+ // Typed arrays: references to the lib's Uint8Array/Uint32Array/
1033
+ // Float32Array interfaces (provenance, not names). The es2022+ lib
1034
+ // declares them generic over the backing buffer (`Uint8Array<ArrayBuffer>`
1035
+ // in error text) — the type argument is irrelevant here: no views exist,
1036
+ // every value owns its storage. The other TypedArray flavors stay
1037
+ // unmapped (the record path's index-signature check rejects them).
1038
+ // RegExpMatchArray (s.match's result) IS a string[] here — the honest
1039
+ // slice: [whole match, ...captures] (a nonparticipating capture reads
1040
+ // "" — SEMANTICS.md). The `.index`/`.input`/`.groups` extras fence per
1041
+ // member like any other unlowered array property.
1042
+ if (isStdlibInterface("RegExpMatchArray"))
1043
+ return arrayOf(STRING);
1044
+ // TemplateStringsArray (a tag function's first parameter): a string[] —
1045
+ // the cooked spans the templateStrings site value carries. The `.raw`
1046
+ // extra fences per member like RegExpMatchArray's `.index`/`.input`.
1047
+ if (isStdlibInterface("TemplateStringsArray"))
1048
+ return arrayOf(STRING);
1049
+ // RegExpExecArray (matchAll's row type): the same honest slice.
1050
+ if (isStdlibInterface("RegExpExecArray"))
1051
+ return arrayOf(STRING);
1052
+ // RegExpStringIterator<RegExpExecArray> (matchAll's checker result): the
1053
+ // VALUE-position spelling of the eager drain — the intrinsic already
1054
+ // materializes string[][] (lazy vs eager is unobservable: strings are
1055
+ // immutable and the spec clones the regex at the call), so a stored
1056
+ // `const urlMatches = output.matchAll(re)` types as exactly that array.
1057
+ if (isStdlibInterface("RegExpStringIterator"))
1058
+ return arrayOf(arrayOf(STRING));
1059
+ // NodeJS.Timeout / the fallback `Timeout` interface (setTimeout's return)
1060
+ // maps to F64 — the numeric timer handle. The `.ref`/`.unref`/`.hasRef`
1061
+ // methods lower over that handle (loop-liveness bookkeeping); `Timeout |
1062
+ // null` becomes `number | null`, and clearTimeout/clearInterval take the
1063
+ // handle. Provenance-checked like the other named interfaces.
1064
+ if (isStdlibInterface("Timeout"))
1065
+ return F64;
1066
+ // NodeJS.Immediate / the fallback `Immediate` (setImmediate's return):
1067
+ // the same numeric-handle story over the check-phase queue — its own id
1068
+ // space, so clearTimeout of an Immediate no-ops like Node.
1069
+ if (isStdlibInterface("Immediate"))
1070
+ return F64;
1071
+ if (isStdlibInterface("Uint8Array"))
1072
+ return bytesOf("u8");
1073
+ if (isStdlibInterface("Uint32Array"))
1074
+ return bytesOf("u32");
1075
+ // Int32Array: the signed 32-bit kind (element reads sign-extend, writes
1076
+ // ToInt32-wrap) — the Atomics.wait sleep idiom constructs one over a
1077
+ // SharedArrayBuffer, and the i32 semantics hold for every other use.
1078
+ if (isStdlibInterface("Int32Array"))
1079
+ return bytesOf("i32");
1080
+ if (isStdlibInterface("Float32Array"))
1081
+ return bytesOf("f32");
1082
+ // DataView: the ONE view kind — a u8 bytes value whose runtime
1083
+ // representation borrows (aliases) its owner's storage, so reads through
1084
+ // it see writes to the source exactly like JS. The checker keeps the
1085
+ // DataView and typed-array member surfaces apart; at the IR level both
1086
+ // are bytes<u8>.
1087
+ if (isStdlibInterface("DataView"))
1088
+ return bytesOf("u8");
1089
+ // Node's Buffer IS a Uint8Array subclass — ONE runtime representation
1090
+ // (the u8 bytes kind). Declared `interface Buffer` by the shipped
1091
+ // fallback and by @types/node (whose NonSharedBuffer alias resolves to
1092
+ // the same symbol); provenance-checked like URL.
1093
+ if (psym?.name === "Buffer" &&
1094
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1095
+ ctx.isStdlibFile(d.getSourceFile()))) {
1096
+ return bytesOf("u8");
1097
+ }
1098
+ // URL: the WHATWG URL instance type — declared as a global `interface
1099
+ // URL` (the shipped fallback, and @types/node's globals) AND as `class
1100
+ // URL` in @types/node's "url" module (`new URL(x)` types as the class's
1101
+ // instance type). Provenance, not the name: a user's own URL maps as a
1102
+ // record/class like any other.
1103
+ if (psym?.name === "URL" &&
1104
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1105
+ ctx.isStdlibFile(d.getSourceFile()))) {
1106
+ return { kind: "url" };
1107
+ }
1108
+ // URLSearchParams: the WHATWG live query view — declared as a global
1109
+ // interface by the shipped fallback and by @types/node (plus `class
1110
+ // URLSearchParams` in the "url" module). Provenance-checked like URL.
1111
+ if (psym?.name === "URLSearchParams" &&
1112
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1113
+ ctx.isStdlibFile(d.getSourceFile()))) {
1114
+ return { kind: "searchParams" };
1115
+ }
1116
+ // fs.Stats: @types/node's `class Stats` (module "fs") or the fallback
1117
+ // declarations' interface. Provenance-checked like URL.
1118
+ if (psym?.name === "Stats" &&
1119
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1120
+ ctx.isStdlibFile(d.getSourceFile()))) {
1121
+ return { kind: "stats" };
1122
+ }
1123
+ // child_process.SpawnSyncReturns: @types/node's generic interface (the
1124
+ // Buffer/string split is re-checked at the stdout/stderr READ — status
1125
+ // reads work either way) or the fallback declarations' plain interface.
1126
+ // Provenance-checked like Stats.
1127
+ if (psym?.name === "SpawnSyncReturns" &&
1128
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1129
+ ctx.isStdlibFile(d.getSourceFile()))) {
1130
+ return { kind: "spawnRes" };
1131
+ }
1132
+ // child_process.ChildProcess: @types/node's class or the fallback
1133
+ // declarations' interface. Provenance-checked like Stats.
1134
+ if (psym?.name === "ChildProcess" &&
1135
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1136
+ ctx.isStdlibFile(d.getSourceFile()))) {
1137
+ return { kind: "child" };
1138
+ }
1139
+ // net.Server / net.Socket: @types/node's classes or the fallback
1140
+ // declarations' interfaces. The NAME is ambiguous across builtin
1141
+ // modules (http.Server, tls.Socket share it), so the provenance check
1142
+ // adds the ENCLOSING AMBIENT MODULE: both @types/node and the fallback
1143
+ // declare these inside `declare module "net"` — a declaration outside
1144
+ // that module (http's Server) stays unmapped and fences at its use
1145
+ // site.
1146
+ if ((psym?.name === "Server" || psym?.name === "Socket" || psym?.name === "TLSSocket") &&
1147
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1148
+ ctx.isStdlibFile(d.getSourceFile()) &&
1149
+ (isDeclaredInAmbientModule(d, "net") ||
1150
+ // http.Server extends net.Server in @types/node — and tls.Server
1151
+ // / https.Server extend it transitively: same lowered surface
1152
+ // (listen/close/address/'error'), same handle kind. tls.TLSSocket
1153
+ // extends net.Socket the same way: post-handshake it IS a net
1154
+ // socket to every lowered member.
1155
+ (psym.name === "Server" &&
1156
+ (isDeclaredInAmbientModule(d, "http") ||
1157
+ isDeclaredInAmbientModule(d, "tls") ||
1158
+ isDeclaredInAmbientModule(d, "https"))) ||
1159
+ (psym.name === "TLSSocket" && isDeclaredInAmbientModule(d, "tls"))))) {
1160
+ return psym.name === "Server" ? { kind: "netServer" } : { kind: "netSocket" };
1161
+ }
1162
+ // tls.SecureContext: the opaque parsed-cert/key handle SNI callbacks
1163
+ // answer with (tls.createSecureContext({ cert, key }) mints it; the TLS
1164
+ // server's handshake consumes it). Module-checked like net.Server —
1165
+ // @types/node declares `interface SecureContext` inside `declare module
1166
+ // "tls"`, and the shipped fallback mirrors it.
1167
+ if (psym?.name === "SecureContext" &&
1168
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1169
+ ctx.isStdlibFile(d.getSourceFile()) &&
1170
+ isDeclaredInAmbientModule(d, "tls"))) {
1171
+ return { kind: "secureCtx" };
1172
+ }
1173
+ // fs.FSWatcher: the fs.watch handle (scr_watch.c). Module-checked like
1174
+ // net.Server — @types/node declares `interface FSWatcher extends
1175
+ // EventEmitter` inside `declare module "fs"`, and the shipped fallback
1176
+ // mirrors it.
1177
+ if (psym?.name === "FSWatcher" &&
1178
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1179
+ ctx.isStdlibFile(d.getSourceFile()) &&
1180
+ isDeclaredInAmbientModule(d, "fs"))) {
1181
+ return { kind: "fsWatcher" };
1182
+ }
1183
+ // The piped child-output stream, under BOTH spellings @types/node uses
1184
+ // for it: stream.Readable (ChildProcess.stdout's declared type — the
1185
+ // class inside `declare module "stream"`) and NodeJS.ReadableStream
1186
+ // (the global-namespace interface real code writes in its own
1187
+ // signatures — the portless prefixStream/NgrokChildProcess idiom). In
1188
+ // a static program every value of either type ARISES from
1189
+ // child.stdout/stderr — anything else producing one fences at its
1190
+ // producing site. The NodeJS namespace check keeps the web
1191
+ // ReadableStream (undici-types' whatwg stream, generic, no ambient
1192
+ // namespace) unmapped.
1193
+ // The process output streams as first-class VALUES, under the two
1194
+ // spellings @types/node uses: tty.WriteStream (process.stdout's own
1195
+ // intersection base — `WriteStream & { fd: 1 }` resolves through the
1196
+ // handle-refinement path below) and NodeJS.WritableStream (the
1197
+ // global-namespace interface real code writes in its own signatures —
1198
+ // prefixStream's `output` param). The representation is the raw fd
1199
+ // scalar; the lowered surface is write(string).
1200
+ if ((psym?.name === "WriteStream" &&
1201
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1202
+ ctx.isStdlibFile(d.getSourceFile()) &&
1203
+ isDeclaredInAmbientModule(d, "tty"))) ||
1204
+ (psym?.name === "WritableStream" &&
1205
+ checker.declarationsOf(psym).some((d) => ts.isInterfaceDeclaration(d) &&
1206
+ ctx.isStdlibFile(d.getSourceFile()) &&
1207
+ isDeclaredInAmbientNamespace(d, "NodeJS")))) {
1208
+ return PROCSTREAM_T;
1209
+ }
1210
+ // The static node:stream classes (the shipped fallback declarations —
1211
+ // NOT @types/node, whose stream.Readable also types child stdio; under
1212
+ // @types/node the childStream mapping below keeps priority and the
1213
+ // static stream classes stand down). Checked BEFORE the childStream
1214
+ // branch: the fallback's Readable must map to the runtime class, and
1215
+ // under the fallback child.stdout is NodeJS.ReadableStream (its own
1216
+ // branch below), so the two never collide.
1217
+ for (const [irName, rec] of RUNTIME_STREAM_CLASSES) {
1218
+ if (psym?.name === rec.lib &&
1219
+ checker.declarationsOf(psym).some((d) => ts.isClassDeclaration(d) &&
1220
+ ctx.isStdlibFile(d.getSourceFile()) &&
1221
+ !isNodeTypesPath(d.getSourceFile().fileName) &&
1222
+ isDeclaredInAmbientModule(d, "stream"))) {
1223
+ return { kind: "object", className: irName };
1224
+ }
1225
+ }
1226
+ if ((psym?.name === "Readable" &&
1227
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1228
+ ctx.isStdlibFile(d.getSourceFile()) &&
1229
+ isDeclaredInAmbientModule(d, "stream"))) ||
1230
+ (psym?.name === "ReadableStream" &&
1231
+ checker.declarationsOf(psym).some((d) => ts.isInterfaceDeclaration(d) &&
1232
+ ctx.isStdlibFile(d.getSourceFile()) &&
1233
+ isDeclaredInAmbientNamespace(d, "NodeJS")))) {
1234
+ return { kind: "childStream" };
1235
+ }
1236
+ // dgram.Socket: the same ambiguous "Socket" name disambiguated by its
1237
+ // enclosing ambient module — @types/node's `class Socket extends
1238
+ // EventEmitter` and the fallback declarations' interface both live
1239
+ // inside `declare module "dgram"`.
1240
+ if (psym?.name === "Socket" &&
1241
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1242
+ ctx.isStdlibFile(d.getSourceFile()) &&
1243
+ isDeclaredInAmbientModule(d, "dgram"))) {
1244
+ return { kind: "dgramSocket" };
1245
+ }
1246
+ // node:test's TestContext — the test-body parameter (`test('x', (t) =>
1247
+ // ...)`). @types/node's `class TestContext` and the fallback
1248
+ // declarations' interface both live inside `declare module "node:test"`
1249
+ // (isDeclaredInAmbientModule answers for both spellings).
1250
+ if (psym?.name === "TestContext" &&
1251
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1252
+ ctx.isStdlibFile(d.getSourceFile()) &&
1253
+ isDeclaredInAmbientModule(d, "test"))) {
1254
+ return { kind: "testCtx" };
1255
+ }
1256
+ // NodeJS.ErrnoException: @types/node's `interface ErrnoException extends
1257
+ // Error` (dns/fs callback error types). It IS an Error at runtime — map
1258
+ // it to the runtime %Error hierarchy like the lib Error interfaces
1259
+ // above; the optional errno/code/path/syscall extras fence per member
1260
+ // (the .stack stance). Provenance-checked like the rest.
1261
+ if (psym?.name === "ErrnoException" &&
1262
+ checker.declarationsOf(psym).some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()))) {
1263
+ return { kind: "object", className: "%Error" };
1264
+ }
1265
+ // http.IncomingMessage / http.ServerResponse — module-checked like
1266
+ // net.Server (the names repeat nowhere today, but provenance stays the
1267
+ // rule).
1268
+ if ((psym?.name === "IncomingMessage" || psym?.name === "ServerResponse") &&
1269
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1270
+ ctx.isStdlibFile(d.getSourceFile()) &&
1271
+ isDeclaredInAmbientModule(d, "http"))) {
1272
+ return psym.name === "IncomingMessage" ? { kind: "httpReq" } : { kind: "httpRes" };
1273
+ }
1274
+ // http2's compatibility surface (the allowHTTP1 lowering): the secure
1275
+ // server IS a net server (listen/close/address/emit ride the same
1276
+ // handle), and Http2ServerRequest/Response ARE the http req/res kinds —
1277
+ // every connection the lowering accepts is HTTP/1.1, exactly the case
1278
+ // where Node hands the compatibility objects the http.IncomingMessage /
1279
+ // ServerResponse surface. h2-only members (stream, session) fence per
1280
+ // member at their use sites (lower-server.ts). Module-checked like the
1281
+ // rest.
1282
+ if ((psym?.name === "Http2SecureServer" ||
1283
+ psym?.name === "Http2ServerRequest" ||
1284
+ psym?.name === "Http2ServerResponse") &&
1285
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1286
+ ctx.isStdlibFile(d.getSourceFile()) &&
1287
+ isDeclaredInAmbientModule(d, "http2"))) {
1288
+ return psym.name === "Http2SecureServer"
1289
+ ? { kind: "netServer" }
1290
+ : psym.name === "Http2ServerRequest"
1291
+ ? { kind: "httpReq" }
1292
+ : { kind: "httpRes" };
1293
+ }
1294
+ // The REAL h2 surface (scr_http2.c): the h2c server is a net server
1295
+ // like the secure one; sessions and streams are first-class handle
1296
+ // kinds (client and server flavors share a kind — the runtime handle
1297
+ // carries the role). Module-checked like the rest.
1298
+ if ((psym?.name === "Http2Server" || psym?.name === "Http2Session" ||
1299
+ psym?.name === "ClientHttp2Session" || psym?.name === "ServerHttp2Session" ||
1300
+ psym?.name === "ClientHttp2Stream" || psym?.name === "ServerHttp2Stream") &&
1301
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1302
+ ctx.isStdlibFile(d.getSourceFile()) &&
1303
+ isDeclaredInAmbientModule(d, "http2"))) {
1304
+ return psym.name === "Http2Server"
1305
+ ? { kind: "netServer" }
1306
+ : psym.name === "ClientHttp2Stream" || psym.name === "ServerHttp2Stream"
1307
+ ? { kind: "http2Stream" }
1308
+ : { kind: "http2Session" };
1309
+ }
1310
+ // http.ClientRequest (http.request/http.get results) — module-checked
1311
+ // like IncomingMessage.
1312
+ if (psym?.name === "ClientRequest" &&
1313
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1314
+ ctx.isStdlibFile(d.getSourceFile()) &&
1315
+ isDeclaredInAmbientModule(d, "http"))) {
1316
+ return { kind: "httpClientReq" };
1317
+ }
1318
+ // NodeJS.ProcessEnv: @types/node's interface (Dict<string> plus a TZ?
1319
+ // member) maps to the PURE index-signature shape it structurally is —
1320
+ // `{ [k: string]: string | undefined }` (TZ is just another
1321
+ // string-or-undefined key and reads through the overflow like the rest).
1322
+ // What makes `process.env` as a VALUE representable (the snapshot
1323
+ // record) and ProcessEnv-typed parameters mappable. The fallback
1324
+ // declarations' anonymous env type takes the pureIndexShape path below
1325
+ // and interns the SAME shape.
1326
+ if (psym?.name === "ProcessEnv" &&
1327
+ checker.declarationsOf(psym).some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()))) {
1328
+ const value = withUndefinedArm(STRING, ctx.unions);
1329
+ if (!value)
1330
+ return null;
1331
+ return { kind: "record", shapeId: ctx.shapes.intern([], false, value, []) };
1332
+ }
1333
+ // Promise<T>: a reference to the lib Promise interface. The inner type
1334
+ // maps recursively; promise-of-unmappable stays unmappable.
1335
+ if (isStdlibInterface("Promise")) {
1336
+ const arg = checker.getTypeArguments(widened)[0];
1337
+ if (!arg)
1338
+ return null;
1339
+ // Promise<never> — a throw-only async function's inference. Like sync
1340
+ // `never` in return position (declaredReturnType), the honest reading
1341
+ // is "void with a stronger guarantee": the promise only ever rejects,
1342
+ // so no await can observe a fulfillment value. `never` VALUES outside
1343
+ // a promise stay unmapped.
1344
+ if (arg.flags & ts.TypeFlags.Never)
1345
+ return { kind: "promise", inner: VOID };
1346
+ // Promise<any> in a STATIC build (untyped JS entries: `new
1347
+ // Promise((resolve) => ...)` infers any): the settled value rides the
1348
+ // dyn arm exactly like Promise<unknown> — every use of the awaited
1349
+ // value is checked-dynamic, the same honesty `unknown` gets. Dynamic
1350
+ // builds keep the jsval mapping below (an engine promise is one
1351
+ // handle).
1352
+ if (arg.flags & ts.TypeFlags.Any && !ctx.dynamic)
1353
+ return { kind: "promise", inner: DYN };
1354
+ const inner = mapType(arg, ctx);
1355
+ if (!inner)
1356
+ return null;
1357
+ return { kind: "promise", inner };
1358
+ }
1359
+ // Generator<T, TReturn, TNext> (and the lib's IterableIterator<T, ...>,
1360
+ // the older annotation spelling — Generator extends it): the sync
1361
+ // generator kind. Channel normalization keeps the runtime honest:
1362
+ // yield channel — T must be a real value type (a generator that could
1363
+ // only yield undefined has no C value form); `never`
1364
+ // (a generator that never yields) rides the VOID
1365
+ // sentinel — the suspended branch is unreachable.
1366
+ // return channel — void/undefined/never carry no value (VOID: the
1367
+ // done-value is the undefined arm); any/unknown ride
1368
+ // dyn; else the mapped type.
1369
+ // next channel — void/undefined/never mean valueless resumes (the
1370
+ // undefined UNIT: `.next()` sends nothing, yields are
1371
+ // statement-position); any/unknown ride dyn (`.next(v)`
1372
+ // boxes; the yield expression reads checked-dynamic);
1373
+ // else the mapped type (`.next(v)` requires its
1374
+ // argument — fenced at the call).
1375
+ // Mixed dyn/concrete channels stay unmapped: the shared result record's
1376
+ // value slot would need a dyn union arm, which does not exist.
1377
+ if (isStdlibInterface("Generator") || isStdlibInterface("IterableIterator")) {
1378
+ const args = checker.getTypeArguments(widened);
1379
+ const channels = genChannels(args[0], args[1], args[2], ctx);
1380
+ if (!channels)
1381
+ return null;
1382
+ // The result record must exist too (its union must be legal), or
1383
+ // `.next()` could never answer — mapped generators always resume.
1384
+ if (!genResultRecord(channels.yieldT, channels.retT, ctx.shapes, unions))
1385
+ return null;
1386
+ return { kind: "generator", ...channels };
1387
+ }
1388
+ // IteratorResult<T, TReturn> — the checker's type of `g.next()` (an
1389
+ // alias for IteratorYieldResult<T> | IteratorReturnResult<TReturn>):
1390
+ // maps to the SAME interned record genResume answers, so `const r =
1391
+ // g.next()` binds without an annotation and reads flow.
1392
+ {
1393
+ const alias = widened.getAliasSymbol();
1394
+ if (alias?.name === "IteratorResult" &&
1395
+ checker.declarationsOf(alias).some((d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()))) {
1396
+ const targs = widened.getAliasTypeArguments() ?? [];
1397
+ const channels = genChannels(targs[0], targs[1], undefined, ctx);
1398
+ if (!channels)
1399
+ return null;
1400
+ return genResultRecord(channels.yieldT, channels.retT, ctx.shapes, unions);
1401
+ }
1402
+ }
1403
+ // PromiseSettledResult<T>'s parts: the lib's PromiseFulfilledResult<T> /
1404
+ // PromiseRejectedResult interfaces map to the documented HONEST SUBSET
1405
+ // record { status: string } — `value` (T) and `reason` (any) have no
1406
+ // record-field representation here (void fields don't exist; a jsval
1407
+ // field would absorb the record), and the manual-allSettled pattern this
1408
+ // mapping exists for (pMap) discards them. Construction literals still
1409
+ // EVALUATE a dropped initializer for its effects — the awaited mapper —
1410
+ // via the object-literal lowering's drop path. Both interfaces intern
1411
+ // the SAME shape, so the settled union collapses to one record arm and
1412
+ // status keeps its string reads/comparisons. SEMANTICS.md 46.
1413
+ // net/dgram AddressInfo and dgram RemoteInfo: stdlib interfaces over
1414
+ // data properties, interned EXPLICITLY (the record path's provenance
1415
+ // fence keeps .d.ts shapes out of the structural walk — the
1416
+ // PromiseFulfilledResult precedent). RemoteInfo's 'IPv4' | 'IPv6'
1417
+ // family widens to string here; the runtime only ever answers "IPv4"
1418
+ // (udp4 is the one lowered socket type).
1419
+ // child_process.ExecFileSyncOptionsWithStringEncoding: the exec-options
1420
+ // interface interned explicitly on the AddressInfo pattern, so a TYPED
1421
+ // options value (`const commandOptions: ExecFileSyncOptions... = {...}`,
1422
+ // a runner function's options parameter) flows to execFileSync/execSync
1423
+ // as a runtime record — the windows-ca command-runner idiom. The fields
1424
+ // are the members the exec lowering honestly implements: encoding
1425
+ // (required — the "WithStringEncoding" refinement), cwd/input/timeout,
1426
+ // stdio (the "pipe"/"ignore" string or 3-tuple forms — a runtime array
1427
+ // is string[]; unsupported runtime modes throw at the call), and the
1428
+ // accepted-but-inert maxBuffer/windowsHide. Members outside this set
1429
+ // (env, killSignal, shell, uid, ...) keep the shape unmatched — their
1430
+ // literals fence at the shape check instead of silently dropping.
1431
+ if (isStdlibInterface("ExecFileSyncOptionsWithStringEncoding")) {
1432
+ const optStr = withUndefinedArm(STRING, ctx.unions);
1433
+ const optNum = withUndefinedArm(F64, ctx.unions);
1434
+ const optBool = withUndefinedArm(BOOL, ctx.unions);
1435
+ const stdioArms = [arrayOf(STRING), STRING, UNDEFINED_T].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
1436
+ const stdio = { kind: "union", unionId: ctx.unions.intern(stdioArms) };
1437
+ if (!optStr || !optNum || !optBool)
1438
+ return null;
1439
+ return {
1440
+ kind: "record",
1441
+ shapeId: ctx.shapes.intern([
1442
+ { name: "cwd", type: optStr },
1443
+ { name: "encoding", type: STRING },
1444
+ { name: "input", type: optStr },
1445
+ { name: "maxBuffer", type: optNum },
1446
+ { name: "stdio", type: stdio },
1447
+ { name: "timeout", type: optNum },
1448
+ { name: "windowsHide", type: optBool },
1449
+ ], false, undefined, ["encoding", "cwd", "input", "timeout", "stdio", "maxBuffer", "windowsHide"]),
1450
+ };
1451
+ }
1452
+ if (isStdlibInterface("AddressInfo")) {
1453
+ return {
1454
+ kind: "record",
1455
+ shapeId: ctx.shapes.intern([
1456
+ { name: "address", type: STRING },
1457
+ { name: "family", type: STRING },
1458
+ { name: "port", type: F64 },
1459
+ ], false, undefined, ["address", "family", "port"]),
1460
+ };
1461
+ }
1462
+ // fs.Dirent (readdirSync's withFileTypes rows): interned explicitly on
1463
+ // the AddressInfo pattern. The VISIBLE fields are Node's own keys (name,
1464
+ // parentPath — Object.keys order matches Node via declaredOrder); the
1465
+ // hidden %dtype field carries the entry kind (libuv's UV_DIRENT
1466
+ // encoding) that the isFile/isDirectory/isSymbolicLink call lowerings
1467
+ // read (lowerDirentMethodCall — the StringDecoder pattern, so the
1468
+ // %-field never surfaces through the lowered Dirent surface itself;
1469
+ // JSON.stringify of a Dirent would show it — the documented
1470
+ // internal-shape edge). @types/node declares Dirent<Name> generic over
1471
+ // the name's encoding — only the string instantiation maps (the
1472
+ // encoding:'buffer' readdir form has no lowering); the shipped fallback
1473
+ // declares it concrete (an interface; @types/node uses a class — both
1474
+ // provenance-checked, the Buffer/URL pattern).
1475
+ if (psym?.name === "Dirent" &&
1476
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1477
+ ctx.isStdlibFile(d.getSourceFile()))) {
1478
+ const args = checker.getTypeArguments(widened);
1479
+ if (args.length > 1)
1480
+ return null;
1481
+ if (args.length === 1) {
1482
+ const t = args[0] && mapType(args[0], ctx);
1483
+ if (!t || t.kind !== "string")
1484
+ return null;
1485
+ }
1486
+ return {
1487
+ kind: "record",
1488
+ shapeId: ctx.shapes.intern([
1489
+ { name: "%dtype", type: F64 },
1490
+ { name: "name", type: STRING },
1491
+ { name: "parentPath", type: STRING },
1492
+ ], false, undefined, ["name", "parentPath"]),
1493
+ };
1494
+ }
1495
+ // os.UserInfo (os.userInfo()'s result): a stdlib data interface interned
1496
+ // explicitly, the AddressInfo pattern. The shipped fallback declares it
1497
+ // concrete; @types/node declares UserInfo<T>, where only the string
1498
+ // instantiation maps (UserInfo<Buffer> — the encoding:'buffer' option —
1499
+ // stays unmapped; the call-site lowering fences the option form anyway).
1500
+ // shell is the declared `string | null` union; POSIX always answers the
1501
+ // string arm. Key order is Node's runtime insertion order (node_os.cc
1502
+ // builds uid, gid, username, homedir, shell), so Object.keys over the
1503
+ // record answers exactly Node.
1504
+ if (isStdlibInterface("UserInfo")) {
1505
+ const args = checker.getTypeArguments(widened);
1506
+ if (args.length > 1)
1507
+ return null;
1508
+ if (args.length === 1) {
1509
+ const t = args[0] && mapType(args[0], ctx);
1510
+ if (!t || t.kind !== "string")
1511
+ return null;
1512
+ }
1513
+ const shellArms = [NULL_T, STRING].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
1514
+ const shell = { kind: "union", unionId: ctx.unions.intern(shellArms) };
1515
+ return {
1516
+ kind: "record",
1517
+ shapeId: ctx.shapes.intern([
1518
+ { name: "gid", type: F64 },
1519
+ { name: "homedir", type: STRING },
1520
+ { name: "shell", type: shell },
1521
+ { name: "uid", type: F64 },
1522
+ { name: "username", type: STRING },
1523
+ ], false, undefined, ["uid", "gid", "username", "homedir", "shell"]),
1524
+ };
1525
+ }
1526
+ // crypto.X509Certificate — the Dirent-style data record: THREE visible
1527
+ // fields (fingerprint and the validFrom/validTo validity window, all
1528
+ // computed at construction by the new-expression lowering). Module-
1529
+ // checked like SecureContext; @types/node declares the class inside
1530
+ // `declare module "crypto"`, the shipped fallback mirrors it.
1531
+ // JSON.stringify of one shows the three fields where Node's
1532
+ // internal-slot object shows {} — the Dirent %dtype edge's cousin.
1533
+ if (psym?.name === "X509Certificate" &&
1534
+ checker.declarationsOf(psym).some((d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
1535
+ ctx.isStdlibFile(d.getSourceFile()) &&
1536
+ isDeclaredInAmbientModule(d, "crypto"))) {
1537
+ return {
1538
+ kind: "record",
1539
+ shapeId: ctx.shapes.intern([
1540
+ { name: "fingerprint", type: STRING },
1541
+ { name: "validFrom", type: STRING },
1542
+ { name: "validTo", type: STRING },
1543
+ ], false, undefined, ["fingerprint", "validFrom", "validTo"]),
1544
+ };
1545
+ }
1546
+ // process.cpuUsage()/threadCpuUsage()'s {user, system} microsecond
1547
+ // record and resourceUsage()'s getrusage record — stdlib data
1548
+ // interfaces interned explicitly (the RemoteInfo pattern; declared
1549
+ // order is Node's own construction order, so Object.keys agrees).
1550
+ if (isStdlibInterface("CpuUsage")) {
1551
+ return {
1552
+ kind: "record",
1553
+ shapeId: ctx.shapes.intern([
1554
+ { name: "system", type: F64 },
1555
+ { name: "user", type: F64 },
1556
+ ], false, undefined, ["user", "system"]),
1557
+ };
1558
+ }
1559
+ if (isStdlibInterface("ResourceUsage")) {
1560
+ const names = [
1561
+ "userCPUTime", "systemCPUTime", "maxRSS", "sharedMemorySize",
1562
+ "unsharedDataSize", "unsharedStackSize", "minorPageFault",
1563
+ "majorPageFault", "swappedOut", "fsRead", "fsWrite", "ipcSent",
1564
+ "ipcReceived", "signalsCount", "voluntaryContextSwitches",
1565
+ "involuntaryContextSwitches",
1566
+ ];
1567
+ return {
1568
+ kind: "record",
1569
+ shapeId: ctx.shapes.intern([...names].sort().map((name) => ({ name, type: F64 })), false, undefined, names),
1570
+ };
1571
+ }
1572
+ if (isStdlibInterface("RemoteInfo")) {
1573
+ return {
1574
+ kind: "record",
1575
+ shapeId: ctx.shapes.intern([
1576
+ { name: "address", type: STRING },
1577
+ { name: "family", type: STRING },
1578
+ { name: "port", type: F64 },
1579
+ { name: "size", type: F64 },
1580
+ ], false, undefined, ["address", "family", "port", "size"]),
1581
+ };
1582
+ }
1583
+ // os.NetworkInterfaceInfoIPv4/IPv6 (the arms of networkInterfaces()'s
1584
+ // NetworkInterfaceInfo union): stdlib data interfaces interned explicitly,
1585
+ // the AddressInfo pattern. The family literal types widen to string
1586
+ // (RemoteInfo's rule); IPv4's optional scopeid is the undefined-armed
1587
+ // union (holding undefined at runtime — Node omits the key), IPv6's is a
1588
+ // plain number. Declared order is Node's runtime insertion order
1589
+ // (lib/os.js builds address..cidr then appends scopeid), so Object.keys
1590
+ // over a row answers exactly Node.
1591
+ if (isStdlibInterface("NetworkInterfaceInfoIPv4") || isStdlibInterface("NetworkInterfaceInfoIPv6")) {
1592
+ const v6 = psym.name === "NetworkInterfaceInfoIPv6";
1593
+ const cidrArms = [NULL_T, STRING].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
1594
+ const cidr = { kind: "union", unionId: ctx.unions.intern(cidrArms) };
1595
+ const scopeid = v6 ? F64 : withUndefinedArm(F64, ctx.unions);
1596
+ if (!scopeid)
1597
+ return null;
1598
+ const fields = [
1599
+ { name: "address", type: STRING },
1600
+ { name: "cidr", type: cidr },
1601
+ { name: "family", type: STRING },
1602
+ { name: "internal", type: BOOL },
1603
+ { name: "mac", type: STRING },
1604
+ { name: "netmask", type: STRING },
1605
+ { name: "scopeid", type: scopeid },
1606
+ ];
1607
+ return {
1608
+ kind: "record",
1609
+ shapeId: ctx.shapes.intern(fields, false, undefined, [
1610
+ "address", "netmask", "family", "mac", "internal", "cidr", "scopeid",
1611
+ ]),
1612
+ };
1613
+ }
1614
+ if (isStdlibInterface("PromiseFulfilledResult") ||
1615
+ isStdlibInterface("PromiseRejectedResult")) {
1616
+ return {
1617
+ kind: "record",
1618
+ shapeId: ctx.shapes.intern([{ name: "status", type: STRING }], false, undefined, ["status"]),
1619
+ };
1620
+ }
1621
+ // Function types: exactly one call signature, no generics, and every
1622
+ // parameter positional — the supported closure surface. OPTIONAL
1623
+ // parameters (`ctx?: T` — no initializer, no rest) map as their
1624
+ // checker-given `T | undefined` union: optionality lives entirely in the
1625
+ // parameter TYPE, exactly the optional-record-field rule, and a call
1626
+ // that omits the argument completes it with the undefined arm (the
1627
+ // lowerer's trailing completion). DEFAULTED parameters (`x: T = e`) map
1628
+ // the same way — the completed-signature contract: the TYPE spells
1629
+ // `T | undefined`, the default's VALUE lives in the closure body's own
1630
+ // prologue (declareParams), and the undefined arm is what triggers it.
1631
+ // Rest parameters keep the fence (a rest signature is never spellable
1632
+ // as one completed arity).
1633
+ // The chalk-style FUNCTION-WITH-PROPERTIES hybrid: an intersection of
1634
+ // exactly one callable part with plain data-property parts (`F & { bold:
1635
+ // F }` — Object.assign(fn, {...})'s type) maps to a RECORD carrying the
1636
+ // callable in a reserved `%call` field ('%' cannot appear in a TS
1637
+ // property name, so no user field collides). Calls through the value
1638
+ // read the slot (lowerRecordFieldCall / the call-value path), extraction
1639
+ // into a plain F slot reads it too (coerceToExpected), and Object.assign
1640
+ // constructs the record. Intersections that don't fit fall through to
1641
+ // the plain-func mapping below (historic behavior: the properties drop
1642
+ // from the type).
1643
+ if (widened.isIntersectionType() && checker.getConstructSignatures(widened).length === 0) {
1644
+ const hybrid = mapHybridCallableIntersection(widened, ctx);
1645
+ if (hybrid)
1646
+ return hybrid;
1647
+ }
1648
+ const callSigs = checker.getCallSignatures(widened);
1649
+ if (callSigs.length === 1) {
1650
+ const sig = callSigs[0];
1651
+ if (sig.getTypeParameters().length)
1652
+ return null;
1653
+ // A SYNTHESIZED rest param (tsc's JS inference for a function body
1654
+ // reading `arguments` — `(...args: any[]) => any` whose args symbol
1655
+ // has no declaration): the dotDotDot check below can't see it, and a
1656
+ // fixed-arity func mapping would LIE about the value's calling
1657
+ // convention. Detected by the param-count mismatch against the
1658
+ // signature's own declaration; unmappable like spelled rest.
1659
+ {
1660
+ const sigDecl = checker.signatureDeclaration(sig);
1661
+ const declParams = sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters : undefined;
1662
+ if (declParams !== undefined && declParams.length !== sig.getParameters().length) {
1663
+ return null;
1664
+ }
1665
+ // tsgo never SYNTHESIZES that rest param into the inferred signature
1666
+ // (5.9.3 did — the count mismatch above was the whole detector
1667
+ // there), so the declaration's own body answers directly.
1668
+ if (sigDecl !== undefined && ts.isFunctionLike(sigDecl) && bodyReadsArgumentsLocal(sigDecl)) {
1669
+ return null;
1670
+ }
1671
+ }
1672
+ const params = [];
1673
+ for (const p of sig.getParameters()) {
1674
+ const decl = checker.valueDeclarationOf(p);
1675
+ if (decl && ts.isParameter(decl) && decl.dotDotDotToken) {
1676
+ return null;
1677
+ }
1678
+ const optional = decl !== undefined &&
1679
+ ts.isParameter(decl) &&
1680
+ (decl.questionToken !== undefined || decl.initializer !== undefined);
1681
+ let pt = mapType(checker.getTypeOfSymbol(p), ctx);
1682
+ // Belt and braces for non-strict type worlds: an optional param's
1683
+ // ABI slot is always the undefined-armed union (strictNullChecks
1684
+ // already spells it that way; arm it here if the world didn't).
1685
+ if (optional && pt && pt.kind !== "void" && pt.kind !== "jsval") {
1686
+ const armed = withUndefinedArm(pt, ctx.unions);
1687
+ if (!armed)
1688
+ return null;
1689
+ pt = armed;
1690
+ }
1691
+ if (!pt)
1692
+ return null;
1693
+ // `(value: void) => void` (Promise<void>'s resolve) is callable with
1694
+ // no arguments — a void param is dropped, not a mapping failure.
1695
+ if (pt.kind === "void")
1696
+ continue;
1697
+ params.push(pt);
1698
+ }
1699
+ const retT = checker.getReturnTypeOfSignature(sig);
1700
+ // `() => never` (a throw-only lambda's inferred type) is assignable to
1701
+ // `() => void` and its calls never produce a value — map the return
1702
+ // like declaredReturnType does for declarations.
1703
+ const ret = retT.flags & ts.TypeFlags.Never ? VOID : mapType(retT, ctx);
1704
+ if (!ret)
1705
+ return null;
1706
+ return funcOf(params, ret);
1707
+ }
1708
+ // Records: object types whose members are all data properties (shorthand
1709
+ // methods in type position count — they're func-typed fields) with
1710
+ // mappable types; no call/construct signatures, no index signatures, not
1711
+ // a tuple, and declared in USER code (lib interfaces like
1712
+ // Function/Object/Iterable stay unmapped — the standard-library types are
1713
+ // a type world, not data shapes). Optional properties (`a?: T`) need no special handling: under
1714
+ // strictNullChecks the checker types them `T | undefined`, so the field
1715
+ // maps to an undefined-armed union — optionality lives entirely in the
1716
+ // field TYPE, which is also what keeps `{a: string}` and `{a?: string}`
1717
+ // distinct interned shapes (and makes `{a?: string}` and
1718
+ // `{a: string | undefined}` deliberately the SAME shape, matching tsc's
1719
+ // mutual assignability without exactOptionalPropertyTypes). Two
1720
+ // structurally identical types intern to one shapeId.
1721
+ //
1722
+ // Mapped-type results (`Partial<Config>`, `Record<"a" | "b", number>`,
1723
+ // Pick/Omit spellings) and intersections of object types resolve through
1724
+ // the SAME path: the member walk below asks the checker for the resolved
1725
+ // property list, so a utility type interns the identical shape its
1726
+ // longhand spelling would. Intersections are a distinct TypeFlag, hence
1727
+ // the widened condition.
1728
+ // A USER-declared child-process-shaped interface (the ngrok
1729
+ // NgrokChildProcess idiom — `spawn(...) as NgrokChildProcess`, declared
1730
+ // so tests can hand in mocks): every member is named on the lowered
1731
+ // ChildProcess surface AND at least two are the handle-defining ones
1732
+ // (kill/on/stdout/stderr/unref/exitCode — a data record like
1733
+ // `{ pid: number }` never qualifies). The TYPE maps to the child
1734
+ // handle: in a compiled program the only VALUE producer is spawn
1735
+ // itself (a mock object literal in this slot fences at its
1736
+ // construction site), and every member the interface may declare is
1737
+ // exactly a lowered child member, so uses typecheck against the
1738
+ // interface and lower against the handle.
1739
+ if (flags & ts.TypeFlags.Object &&
1740
+ callSigs.length === 0 &&
1741
+ psym !== undefined &&
1742
+ checker.declarationsOf(psym).length > 0 &&
1743
+ checker.declarationsOf(psym).every((d) => ts.isInterfaceDeclaration(d) && !ctx.isStdlibFile(d.getSourceFile()))) {
1744
+ const CHILD_SURFACE = new Set([
1745
+ "pid", "exitCode", "killed", "kill", "on", "once", "off",
1746
+ "removeListener", "unref", "ref", "stdout", "stderr",
1747
+ ]);
1748
+ const CHILD_CORE = new Set(["kill", "on", "stdout", "stderr", "unref", "exitCode"]);
1749
+ const props = checker.getPropertiesOfType(widened);
1750
+ let core = 0;
1751
+ const childShaped = props.length > 0 &&
1752
+ props.every((p) => {
1753
+ if (!CHILD_SURFACE.has(p.name))
1754
+ return false;
1755
+ if (CHILD_CORE.has(p.name))
1756
+ core++;
1757
+ return true;
1758
+ });
1759
+ if (childShaped && core >= 2)
1760
+ return CHILD_T;
1761
+ }
1762
+ if (flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection) && callSigs.length === 0) {
1763
+ const viaAlias = mapGenericUtilityAlias(widened, ctx);
1764
+ if (viaAlias)
1765
+ return viaAlias;
1766
+ const record = mapRecordType(widened, ctx);
1767
+ if (record)
1768
+ return record;
1769
+ }
1770
+ // Unions. `boolean` is internally `true | false`, and narrowing produces
1771
+ // literal unions like `"a" | "b"` — parts that map to the SAME IR type
1772
+ // collapse (deduplicated by typeKey), so a single surviving arm is just
1773
+ // that type. Two or more distinct arms become a TAGGED union: the
1774
+ // typeKey-sorted arm list is interned (its identity), and an arm's index
1775
+ // in that list is its runtime tag. `undefined` and `null` PARTS become
1776
+ // the unit arms undefinedT/nullT — strictNullChecks spells optionality
1777
+ // as exactly these unions (`string | undefined`, `number | null`), and
1778
+ // union membership is the only position where the units are
1779
+ // representable. Deliberately unmapped: any other unmappable part
1780
+ // (poisons the union), void parts, func arms (closure identity vs tag
1781
+ // interplay, later), and recursive unions (`type T = A | { next: T }` —
1782
+ // an RC cycle anyway).
1783
+ if (widened.isUnionType()) {
1784
+ if (unions.inProgress.has(widened))
1785
+ return null;
1786
+ unions.inProgress.add(widened);
1787
+ try {
1788
+ const byKey = new Map();
1789
+ for (const part of widened.getTypes()) {
1790
+ // A `void` PART is inhabited only by undefined (`Promise<void> |
1791
+ // void` return types, `string | void`): it becomes the undefinedT
1792
+ // unit arm, exactly like an undefined part — the value either
1793
+ // exists or is undefined, and the union tag is the test. Standalone
1794
+ // void stays VOID (the return-position mapping above).
1795
+ if (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) {
1796
+ byKey.set(typeKey(UNDEFINED_T), UNDEFINED_T);
1797
+ continue;
1798
+ }
1799
+ if (part.flags & ts.TypeFlags.Null) {
1800
+ byKey.set(typeKey(NULL_T), NULL_T);
1801
+ continue;
1802
+ }
1803
+ const mapped = mapType(part, ctx);
1804
+ // A jsval PART absorbs the union: `GeneratedFile | undefined` (a
1805
+ // package-declared arm) is one island handle — the engine's
1806
+ // undefined/null ARE handle values, and any data sibling is
1807
+ // representable in the engine too, so the handle is the one
1808
+ // runtime representation. Exactly tsc's own collapse for `any | T`
1809
+ // (which never even reaches here as a union). Sibling arms that
1810
+ // already mapped don't matter — and ones that would NOT map on
1811
+ // their own don't block the absorb either: a STATIC value of such
1812
+ // an arm flowing in still fences per-site at the marshal
1813
+ // (jsvalIn's boundary message).
1814
+ if (mapped?.kind === "jsval")
1815
+ return JSVAL;
1816
+ if (!mapped) {
1817
+ // Before failing the whole union, let a LATER jsval part absorb.
1818
+ for (const rest of widened.getTypes()) {
1819
+ if (mapType(rest, ctx)?.kind === "jsval")
1820
+ return JSVAL;
1821
+ }
1822
+ return null;
1823
+ }
1824
+ byKey.set(typeKey(mapped), mapped);
1825
+ }
1826
+ const arms = [...byKey.values()];
1827
+ // A single surviving UNIT arm cannot stand alone (degenerate — the
1828
+ // checker collapsed everything else away); anything else single is
1829
+ // just that type.
1830
+ if (arms.length === 1 && isUnitType(arms[0]))
1831
+ return null;
1832
+ if (arms.length === 1)
1833
+ return arms[0];
1834
+ if (arms.some((a) => a.kind === "void" || a.kind === "union" ||
1835
+ // Map/Set and regex arms stay out (like func against data
1836
+ // arms: no narrowing test — no discriminant fields on them).
1837
+ a.kind === "map" || a.kind === "set" || a.kind === "regex" || a.kind === "dyn" ||
1838
+ // Generator arms follow the map/set rule: no narrowing test.
1839
+ a.kind === "generator" ||
1840
+ // A func arm has no narrowing test against sibling DATA arms —
1841
+ // but against pure unit companions the unit TAG tests are the
1842
+ // narrowing (cb !== null, cb ?? f, cb?.()), so the nullable-
1843
+ // callback shape `(() => void) | null | undefined` maps.
1844
+ (a.kind === "func" && !arms.every((b) => b === a || isUnitType(b))) ||
1845
+ // Promise arms follow the func rule (typeof gives no test
1846
+ // against sibling data arms): only the promise-or-absent shape
1847
+ // maps — `Promise<T> | undefined`, and `Promise<T> | void`
1848
+ // return types whose void part became the undefined arm above.
1849
+ (a.kind === "promise" && !arms.every((b) => b === a || isUnitType(b))))) {
1850
+ return null;
1851
+ }
1852
+ arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
1853
+ return { kind: "union", unionId: unions.intern(arms) };
1854
+ }
1855
+ finally {
1856
+ unions.inProgress.delete(widened);
1857
+ }
1858
+ }
1859
+ return null;
1860
+ }
1861
+ /** The narrowed-type-parameter recognizer behind mapType's early return:
1862
+ * a union/intersection over exactly ONE type parameter whose companions
1863
+ * are all `{}`-empty shapes or the null/undefined units. Returns the
1864
+ * bound type filtered to the arms the companions allow, null when the
1865
+ * pattern matches but cannot map (unbound parameter, nothing left), and
1866
+ * undefined when the type is not this pattern at all (the caller falls
1867
+ * through to the ordinary mapping). */
1868
+ function mapNarrowedTypeParam(type, ctx) {
1869
+ const { checker, unions, resolveTypeParam } = ctx;
1870
+ if (!resolveTypeParam)
1871
+ return undefined;
1872
+ const isEmptyShape = (t) => (t.flags & ts.TypeFlags.Object) !== 0 &&
1873
+ checker.getPropertiesOfType(t).length === 0 &&
1874
+ checker.getCallSignatures(t).length === 0 &&
1875
+ checker.getConstructSignatures(t).length === 0 &&
1876
+ checker.getIndexInfosOfType(t).length === 0;
1877
+ let tp = null;
1878
+ let allowNonUnit = false;
1879
+ let allowUndefined = false;
1880
+ let allowNull = false;
1881
+ const visitCompanion = (c) => {
1882
+ if (isEmptyShape(c))
1883
+ return (allowNonUnit = true);
1884
+ if (c.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void))
1885
+ return (allowUndefined = true);
1886
+ if (c.flags & ts.TypeFlags.Null)
1887
+ return (allowNull = true);
1888
+ // A companion UNION (`T & ({} | null)`): each part is an allowance.
1889
+ if (c.isUnionType())
1890
+ return c.getTypes().every(visitCompanion);
1891
+ return false;
1892
+ };
1893
+ const visitPart = (part) => {
1894
+ if (!part.isIntersectionType())
1895
+ return false;
1896
+ for (const p of part.getTypes()) {
1897
+ if (p.flags & ts.TypeFlags.TypeParameter) {
1898
+ if (tp && tp !== p)
1899
+ return false; // two different parameters: not this pattern
1900
+ tp = p;
1901
+ continue;
1902
+ }
1903
+ if (!visitCompanion(p))
1904
+ return false;
1905
+ }
1906
+ return true;
1907
+ };
1908
+ const parts = type.isUnionType() ? type.getTypes() : [type];
1909
+ if (!parts.every(visitPart) || !tp)
1910
+ return undefined;
1911
+ const bound = resolveTypeParam(tp);
1912
+ if (!bound)
1913
+ return null;
1914
+ if (bound.kind !== "union")
1915
+ return allowNonUnit ? bound : null;
1916
+ const def = unions.get(bound.unionId);
1917
+ if (!def)
1918
+ return null;
1919
+ const arms = def.arms.filter((a) => a.kind === "undefinedT" ? allowUndefined : a.kind === "nullT" ? allowNull : allowNonUnit);
1920
+ if (arms.length === 0)
1921
+ return null;
1922
+ if (arms.length === 1)
1923
+ return isUnitType(arms[0]) ? null : arms[0];
1924
+ // Filtering preserves the canonical (typeKey-sorted) arm order.
1925
+ return { kind: "union", unionId: unions.intern(arms) };
1926
+ }
1927
+ /** `Partial<T>` / `Readonly<T>` where T is a STILL-GENERIC type parameter —
1928
+ * the one mapped-type spelling a monomorphized body can contain that the
1929
+ * checker cannot resolve for us (`keyof T` is unknown inside the body, so
1930
+ * getPropertiesOfType returns nothing). The lowerer's binding can: Readonly
1931
+ * maps exactly like T (writes are tsc errors, trusted), and Partial arms
1932
+ * every field of T's record with undefined — precisely what the longhand
1933
+ * optional spelling produces, so the result interns to the same shape as
1934
+ * the concrete `Partial<Config>` at the call boundary. Other utility
1935
+ * aliases (Pick, Omit, Record, Required) need literal KEYS or the
1936
+ * optional/required distinction, which IR types deliberately do not carry
1937
+ * (`{a?: string}` and `{a: string | undefined}` are ONE shape) — those stay
1938
+ * unmapped inside generic bodies; their concrete uses resolve structurally
1939
+ * through mapRecordType. */
1940
+ function mapGenericUtilityAlias(widened, ctx) {
1941
+ const { resolveTypeParam, shapes, unions } = ctx;
1942
+ if (!resolveTypeParam)
1943
+ return null;
1944
+ const alias = widened.getAliasSymbol();
1945
+ if (!alias || (alias.name !== "Partial" && alias.name !== "Readonly"))
1946
+ return null;
1947
+ const aliasArgs = widened.getAliasTypeArguments();
1948
+ const arg = aliasArgs[0];
1949
+ if (aliasArgs.length !== 1 || !arg)
1950
+ return null;
1951
+ if (!(arg.flags & ts.TypeFlags.TypeParameter))
1952
+ return null;
1953
+ // Provenance: the STANDARD LIBRARY's alias, not a user's shadowing one.
1954
+ const isLibAlias = ctx.checker.declarationsOf(alias).some((d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()));
1955
+ if (!isLibAlias)
1956
+ return null;
1957
+ const bound = resolveTypeParam(arg);
1958
+ if (!bound || bound.kind !== "record")
1959
+ return null;
1960
+ if (alias.name === "Readonly")
1961
+ return bound;
1962
+ const shape = shapes.get(bound.shapeId);
1963
+ if (!shape)
1964
+ return null;
1965
+ // Partial over an index-signature shape would need `V | undefined`
1966
+ // overflow arming; no generic body needs it yet — stay unmapped.
1967
+ if (shape.indexValue)
1968
+ return null;
1969
+ const fields = [];
1970
+ for (const f of shape.fields) {
1971
+ const armed = withUndefinedArm(f.type, unions);
1972
+ if (!armed)
1973
+ return null;
1974
+ fields.push({ name: f.name, type: armed });
1975
+ }
1976
+ // Fields inherit the source shape's canonical (name-sorted) order — and
1977
+ // its declaration order (mapped types preserve property order in TS).
1978
+ return { kind: "record", shapeId: shapes.intern(fields, false, undefined, shape.declaredOrder) };
1979
+ }
1980
+ /** The UNIT-ONLY slot type: the interned `null | undefined` union, the one
1981
+ * representation the IR already has for values that are nothing but units.
1982
+ * Bindings and fields whose CHECKER type is a bare unit (`null`,
1983
+ * `undefined`, `void` in value position, or unions of only those) ride it:
1984
+ * the runtime value is always one of the two interned unit instances, the
1985
+ * tag is the narrowing test (`x === null`, `typeof x === "undefined"`),
1986
+ * and JSON keeps Node's split (null serializes, undefined omits). The type
1987
+ * admits an arm the checker may not spell (a `null`-typed slot gets an
1988
+ * undefined arm too) — the representation is coarser than the TS type,
1989
+ * exactly like literal widening, and tsc has already rejected every write
1990
+ * that could inhabit the extra arm. */
1991
+ export function unitOnlyUnion(unions) {
1992
+ const arms = [NULL_T, UNDEFINED_T];
1993
+ arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
1994
+ return { kind: "union", unionId: unions.intern(arms) };
1995
+ }
1996
+ /** The IteratorResult record of a generator's value channels: `{ done:
1997
+ * boolean, value: V }` where V is dyn (an any/unknown channel) or the
1998
+ * canonical union of the yield arms, the return arms (when the return
1999
+ * channel carries a value), and `undefined` (an exhausted `.next()`
2000
+ * answers undefined). ONE shape per channel pair — `g.next()`'s lowering,
2001
+ * `.return()`, `.throw()`, the for-of desugar, and mapType's
2002
+ * IteratorResult alias mapping all intern through here, so reads agree.
2003
+ * Null when the combined union would be illegal (a func/set arm beside
2004
+ * data arms, a map/regex arm — kinds with no narrowing test): such
2005
+ * generators stay unmapped. */
2006
+ export function genResultRecord(yieldT, retT, shapes, unions) {
2007
+ let valueT;
2008
+ if (yieldT.kind === "dyn" || retT.kind === "dyn") {
2009
+ valueT = DYN;
2010
+ }
2011
+ else {
2012
+ const byKey = new Map();
2013
+ const add = (t) => {
2014
+ if (t.kind === "void")
2015
+ return true; // no value on this channel
2016
+ if (t.kind === "union") {
2017
+ for (const a of unions.get(t.unionId)?.arms ?? [])
2018
+ byKey.set(typeKey(a), a);
2019
+ return true;
2020
+ }
2021
+ if (t.kind === "map" || t.kind === "regex" || t.kind === "jsval" || t.kind === "generator") {
2022
+ return false; // no legal union arm exists for these kinds
2023
+ }
2024
+ byKey.set(typeKey(t), t);
2025
+ return true;
2026
+ };
2027
+ if (!add(yieldT) || !add(retT))
2028
+ return null;
2029
+ byKey.set(typeKey(UNDEFINED_T), UNDEFINED_T);
2030
+ const arms = [...byKey.values()];
2031
+ // func/set arms are legal only beside unit arms (no narrowing test
2032
+ // against data siblings — the union rule).
2033
+ if (arms.some((a) => (a.kind === "func" || a.kind === "set") && !arms.every((b) => b === a || isUnitType(b)))) {
2034
+ return null;
2035
+ }
2036
+ if (arms.every(isUnitType) && arms.length < 2) {
2037
+ // The degenerate no-value generator (never yields, void return):
2038
+ // the slot still needs a readable undefined — the unit-only union.
2039
+ valueT = unitOnlyUnion(unions);
2040
+ }
2041
+ else {
2042
+ arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
2043
+ valueT = { kind: "union", unionId: unions.intern(arms) };
2044
+ }
2045
+ }
2046
+ return {
2047
+ kind: "record",
2048
+ shapeId: shapes.intern([
2049
+ { name: "done", type: BOOL },
2050
+ { name: "value", type: valueT },
2051
+ ]),
2052
+ };
2053
+ }
2054
+ /** True when a checker type is inhabited ONLY by the unit values —
2055
+ * undefined (or `void`, whose runtime inhabitant is undefined) and null,
2056
+ * or a union of just those. The positions that today fence VOID (record
2057
+ * fields, tuple/array elements, variable slots) substitute unitOnlyUnion
2058
+ * for these; RETURN positions keep the VOID mapping (a void return is not
2059
+ * a value). */
2060
+ export function isUnitOnlyTsType(t) {
2061
+ const UNIT = ts.TypeFlags.Undefined | ts.TypeFlags.Void | ts.TypeFlags.Null;
2062
+ const parts = t.isUnionType() ? t.getTypes() : [t];
2063
+ return parts.every((p) => (p.flags & UNIT) !== 0);
2064
+ }
2065
+ /** IR-level `t | undefined`, canonicalized and fenced exactly like the
2066
+ * ts-union branch of mapType (typeKey-sorted arms, deduplicated; map/
2067
+ * regex/dyn/void arms unrepresentable; a func arm IS representable — the
2068
+ * result is exactly the nullable-callback shape mapType's union branch
2069
+ * admits, `(() => void) | undefined`) so the interned union is IDENTICAL
2070
+ * to what mapping the checker's own `T | undefined` produces. */
2071
+ export function withUndefinedArm(t, unions) {
2072
+ if (t.kind === "union") {
2073
+ const def = unions.get(t.unionId);
2074
+ if (!def)
2075
+ return null;
2076
+ if (def.arms.some((a) => a.kind === "undefinedT"))
2077
+ return t;
2078
+ const arms = [...def.arms, UNDEFINED_T];
2079
+ arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
2080
+ return { kind: "union", unionId: unions.intern(arms) };
2081
+ }
2082
+ if (t.kind === "void" || t.kind === "map" ||
2083
+ t.kind === "regex" || t.kind === "dyn" ||
2084
+ // A bare unit field type cannot occur (units live only inside unions),
2085
+ // but guard against constructing a single-arm union from one.
2086
+ isUnitType(t)) {
2087
+ return null;
2088
+ }
2089
+ const arms = [t, UNDEFINED_T];
2090
+ arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
2091
+ return { kind: "union", unionId: unions.intern(arms) };
2092
+ }
2093
+ /** The chalk-shape recognizer behind mapType's hybrid branch: an
2094
+ * intersection with ONE call signature whose callable part maps to a func
2095
+ * and whose other parts are plain object refinements over data properties
2096
+ * (no construct signatures, no class identity, no index signatures). The
2097
+ * result interns a record of the data properties plus the reserved
2098
+ * `%call` slot holding the callable. `declaredOrder` lists only the DATA
2099
+ * properties — Object.keys over the hybrid answers the enumerable
2100
+ * assigned keys, which is also Node's answer for Object.assign(fn, obj)
2101
+ * (a function's own length/name are non-enumerable). Null whenever the
2102
+ * shape doesn't fit — the caller falls through to the plain-func mapping. */
2103
+ function mapHybridCallableIntersection(widened, ctx) {
2104
+ const { checker, shapes } = ctx;
2105
+ if (!widened.isIntersectionType())
2106
+ return null;
2107
+ if (checker.getCallSignatures(widened).length !== 1)
2108
+ return null;
2109
+ let funcPart = null;
2110
+ for (const part of widened.getTypes()) {
2111
+ if (checker.getCallSignatures(part).length > 0) {
2112
+ if (funcPart)
2113
+ return null; // exactly one callable part
2114
+ funcPart = part;
2115
+ continue;
2116
+ }
2117
+ const partSym = part.getSymbol();
2118
+ if ((part.flags & ts.TypeFlags.Object) === 0 ||
2119
+ checker.getConstructSignatures(part).length > 0 ||
2120
+ (partSym !== undefined && (partSym.flags & ts.SymbolFlags.Class) !== 0) ||
2121
+ checker.getIndexInfosOfType(part).length > 0) {
2122
+ return null;
2123
+ }
2124
+ }
2125
+ if (!funcPart)
2126
+ return null;
2127
+ const fn = mapType(funcPart, ctx);
2128
+ if (!fn || fn.kind !== "func")
2129
+ return null;
2130
+ const fields = [{ name: "%call", type: fn }];
2131
+ const declaredOrder = [];
2132
+ for (const p of checker.getPropertiesOfType(widened)) {
2133
+ if (p.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor))
2134
+ return null;
2135
+ const pt = mapType(checker.getTypeOfSymbol(p), ctx);
2136
+ // No jsval absorb here: an island-entangled hybrid is not this shape.
2137
+ if (!pt || pt.kind === "void" || pt.kind === "jsval")
2138
+ return null;
2139
+ fields.push({ name: p.name, type: pt });
2140
+ declaredOrder.push(p.name);
2141
+ }
2142
+ if (fields.length === 1)
2143
+ return null; // no properties — it is just F
2144
+ fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
2145
+ return { kind: "record", shapeId: shapes.intern(fields, false, undefined, declaredOrder) };
2146
+ }
2147
+ /** True for MAPPED-type results — `Partial<Config>`, `Record<"a", n>`,
2148
+ * whatever `Pick`/`Omit` reduce to. Their shape is computed by the checker,
2149
+ * not declared anywhere: the only declaration behind them is the utility
2150
+ * type's `{ [P in keyof T]: ... }` machinery in lib.es5.d.ts. */
2151
+ function isMappedShape(t) {
2152
+ return ((t.flags & ts.TypeFlags.Object) !== 0 &&
2153
+ (t.objectFlags & ts.ObjectFlags.Mapped) !== 0);
2154
+ }
2155
+ /** The record path's provenance fence. Declared shapes (object literals,
2156
+ * interfaces, type literals) must come from user code, never a .d.ts — the
2157
+ * empty ambient interfaces (Object, Function, Boolean, ...) exist only to
2158
+ * satisfy tsc and must not become zero-field records. Checker-COMPUTED
2159
+ * shapes (mapped-type results, intersections) have no user declaration to
2160
+ * point at: mapped types pass here and get per-MEMBER provenance in the
2161
+ * field walk instead; an intersection passes when every part is itself an
2162
+ * ordinary provenance-passing object type (class parts keep their nominal
2163
+ * identity and never flatten into a struct). */
2164
+ function recordProvenanceOk(t, checker) {
2165
+ if (t.isIntersectionType()) {
2166
+ return t.getTypes().every((part) => {
2167
+ const partSym = part.getSymbol();
2168
+ return (part.flags & ts.TypeFlags.Object) !== 0 &&
2169
+ !(partSym && partSym.flags & ts.SymbolFlags.Class) &&
2170
+ checker.getCallSignatures(part).length === 0 &&
2171
+ checker.getConstructSignatures(part).length === 0 &&
2172
+ recordProvenanceOk(part, checker);
2173
+ });
2174
+ }
2175
+ if (isMappedShape(t))
2176
+ return true;
2177
+ const tSym = t.getSymbol();
2178
+ const decls = tSym ? checker.declarationsOf(tSym) : undefined;
2179
+ if (!decls || decls.length === 0)
2180
+ return false;
2181
+ return !decls.some((d) => d.getSourceFile().isDeclarationFile);
2182
+ }
2183
+ /** A GENERIC-callable member type (`m<T>(x: T): T` / `f: <T>(x: T) => T` in
2184
+ * an object type): pure function-shaped — call signatures only, every one
2185
+ * carrying its own type parameters — with no data properties, no construct
2186
+ * signatures, no index signatures. Such members are EXCLUDED from record
2187
+ * shapes instead of failing them: no single closure slot can hold a generic
2188
+ * function, so the shape keeps its data fields and calls of the member
2189
+ * monomorphize per call site against the defining declaration (see
2190
+ * lower-calls.ts, lowerObjLitGenericMethodCall). Node's JSON.stringify drops
2191
+ * function-valued properties, so serialization of the shape stays exact;
2192
+ * Object.keys over such a shape omits the member (a pin — every excluded
2193
+ * member is a function the key walk would name). */
2194
+ export function isGenericCallableMemberType(t, checker) {
2195
+ const sigs = checker.getCallSignatures(t);
2196
+ if (sigs.length === 0 || !sigs.every((s) => s.getTypeParameters().length > 0))
2197
+ return false;
2198
+ return (checker.getConstructSignatures(t).length === 0 &&
2199
+ checker.getPropertiesOfType(t).length === 0 &&
2200
+ checker.getIndexInfosOfType(t).length === 0);
2201
+ }
2202
+ function mapRecordType(widened, ctx) {
2203
+ const { checker, shapes } = ctx;
2204
+ if (checker.getConstructSignatures(widened).length > 0)
2205
+ return null;
2206
+ if (checker.isTupleType(widened) || checker.isArrayLikeType(widened))
2207
+ return null;
2208
+ // ENUM OBJECTS (`typeof e` — the enum used as a first-class value) look
2209
+ // exactly like a number-keyed hybrid (member fields + the reverse-map
2210
+ // index signature), but the VALUE has no lowering: an enum identifier in
2211
+ // value position resolves to nothing. Mapping the type would strand the
2212
+ // reference sites, so the enum-object type stays unmapped and badType
2213
+ // names the construct.
2214
+ const widenedSym = widened.getSymbol();
2215
+ if (widenedSym !== undefined && (widenedSym.flags & ts.SymbolFlags.Enum) !== 0)
2216
+ return null;
2217
+ // An index signature maps to a hybrid shape: declared fields keep
2218
+ // struct slots, undeclared keys ride the shape's overflow map, valued
2219
+ // uniformly by the signature's value type (`unknown` → dyn — a model-pricing table's
2220
+ // ModelPricing; `Record<string, T>` resolves here too, declared-field-
2221
+ // free). STRING and NUMBER key domains both compile — JS object keys ARE
2222
+ // strings (`o[1]` reads `o["1"]`), so a number-keyed signature is the
2223
+ // same string-keyed store with every access canonicalized through the
2224
+ // JS-exact number formatter (the access lowerings own that step). The
2225
+ // `string & {}` key — the special-intersection idiom mapped types use to
2226
+ // keep a literal-key union wide (`Record<(string & {}) | "left", T>`) —
2227
+ // is a string key: every part is string-flavored or the empty refinement.
2228
+ // BOTH signatures at once (string + number) collapse into the one store
2229
+ // when their value types intern identically (tsc requires only
2230
+ // assignability; unequal slots would need a per-key answer the store
2231
+ // cannot give). Value types outside the map-value world stay unmapped
2232
+ // (isSupportedIndexValue).
2233
+ let indexValue;
2234
+ const indexInfos = checker.getIndexInfosOfType(widened);
2235
+ if (indexInfos.length > 0) {
2236
+ const stringKey = (k) => (k.flags & ts.TypeFlags.String) !== 0 ||
2237
+ (k.isIntersectionType() &&
2238
+ k.getTypes().every((p) => (p.flags & ts.TypeFlags.String) !== 0 ||
2239
+ ((p.flags & ts.TypeFlags.Object) !== 0 &&
2240
+ checker.getPropertiesOfType(p).length === 0 &&
2241
+ checker.getCallSignatures(p).length === 0 &&
2242
+ checker.getConstructSignatures(p).length === 0 &&
2243
+ checker.getIndexInfosOfType(p).length === 0)));
2244
+ if (indexInfos.length > 2)
2245
+ return null;
2246
+ let v = null;
2247
+ for (const info of indexInfos) {
2248
+ if (!stringKey(info.keyType) && !(info.keyType.flags & ts.TypeFlags.Number))
2249
+ return null;
2250
+ const iv = mapType(info.valueType, ctx);
2251
+ // A jsval-valued signature absorbs the shape: `Record<string,
2252
+ // JSONValue>` (a package's own JSON alias) and `Record<string, any>`
2253
+ // are one island object — the overflow map has no handle slot, and
2254
+ // island objects hold arbitrary engine values natively.
2255
+ if (iv?.kind === "jsval")
2256
+ return JSVAL;
2257
+ if (!iv || !isSupportedIndexValue(iv))
2258
+ return null;
2259
+ if (v !== null && !typeEquals(v, iv))
2260
+ return null;
2261
+ v = iv;
2262
+ }
2263
+ indexValue = v;
2264
+ }
2265
+ // The HEADER-FAMILY canonicalization: an index-signature shape whose
2266
+ // slot carries a `string[]` arm (alongside string/number/undefined —
2267
+ // nothing else) is the http header world: @types/node's
2268
+ // IncomingHttpHeaders and OutgoingHttpHeaders both declare ~60 OPTIONAL
2269
+ // well-known members over a Dict slot, and every literal SPREAD of them
2270
+ // (`{ ...req.headers, host }`) inherits those phantom members — struct
2271
+ // slots they are not; they are well-known KEYS of the slot. All such
2272
+ // shapes intern as ONE pure index-signature record over the canonical
2273
+ // OUTGOING slot `number | string | string[] | undefined` (numbers join
2274
+ // type-level only — parsed header values are strings), provided every
2275
+ // declared member's type fits inside the slot. Identical shapes make
2276
+ // the proxy's forwarded-header build (`{ ...req.headers }` into an
2277
+ // outgoing literal) a plain copy instead of an arm-wise re-tag the
2278
+ // merge machinery cannot do.
2279
+ if (indexValue?.kind === "union") {
2280
+ const slotDef = ctx.unions.get(indexValue.unionId);
2281
+ const armOk = (a) => a.kind === "f64" || a.kind === "string" || a.kind === "undefinedT" ||
2282
+ (a.kind === "array" && a.elem.kind === "string");
2283
+ if (slotDef !== undefined &&
2284
+ slotDef.arms.some((a) => a.kind === "array" && a.elem.kind === "string") &&
2285
+ slotDef.arms.every(armOk)) {
2286
+ const armsRaw = [F64, STRING, arrayOf(STRING), UNDEFINED_T];
2287
+ armsRaw.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
2288
+ const canonical = { kind: "union", unionId: ctx.unions.intern(armsRaw) };
2289
+ const fits = (t) => {
2290
+ if (!t)
2291
+ return false;
2292
+ if (t.kind === "union") {
2293
+ const def = ctx.unions.get(t.unionId);
2294
+ return def !== undefined && def.arms.every(armOk);
2295
+ }
2296
+ return armOk(t);
2297
+ };
2298
+ if (checker.getPropertiesOfType(widened).every((p) => fits(mapType(checker.getTypeOfSymbol(p), ctx)))) {
2299
+ return { kind: "record", shapeId: shapes.intern([], false, canonical, []) };
2300
+ }
2301
+ }
2302
+ }
2303
+ // Provenance keeps LIB type worlds out — but a PURE string index
2304
+ // signature with no declared members is structurally Record<string, V>
2305
+ // wherever it is declared (the lib's Object.fromEntries return type
2306
+ // `{ [k: string]: T }` — a data shape, not an API surface). Members, if
2307
+ // any, still walk the per-member provenance below.
2308
+ const pureIndexShape = indexValue !== undefined && checker.getPropertiesOfType(widened).length === 0;
2309
+ // The ANONYMOUS empty object type (`var v: {}` — the checker's shared
2310
+ // `{}` intrinsic carries a declaration-less `__type` symbol, so
2311
+ // declaration provenance has nothing to inspect): structurally the empty
2312
+ // record. tsc admits ANY non-nullish assignment into a `{}` slot;
2313
+ // non-record values fence at their assignment site (the exact-shape
2314
+ // stance), records of other shapes at theirs — the declaration itself is
2315
+ // representable.
2316
+ const anonSym = widened.getSymbol();
2317
+ const anonymousEmpty = (anonSym === undefined || checker.declarationsOf(anonSym).length === 0) &&
2318
+ !widened.isIntersectionType() &&
2319
+ indexValue === undefined &&
2320
+ checker.getPropertiesOfType(widened).length === 0;
2321
+ if (!recordProvenanceOk(widened, checker) && !pureIndexShape && !anonymousEmpty)
2322
+ return null;
2323
+ // Checker-computed shapes (no user declaration) need two extra fences in
2324
+ // the member walk below; see the comments there.
2325
+ const computed = widened.isIntersectionType() || isMappedShape(widened);
2326
+ // Recursive shapes (`type T = { next: T }`) cannot intern (and would be RC
2327
+ // cycles anyway): break the recursion by failing the inner reference.
2328
+ if (shapes.inProgress.has(widened))
2329
+ return null;
2330
+ shapes.inProgress.add(widened);
2331
+ try {
2332
+ const props = checker.getPropertiesOfType(widened);
2333
+ // A computed shape with NO members is not a real empty record: inside a
2334
+ // generic body `Partial<T>` resolves to no members at all (keyof T is
2335
+ // still unknown there) — interning `{}` would be silently wrong. The
2336
+ // degenerate empties (`Partial<{}>`, `Omit<C, keyof C>`) go with it.
2337
+ // An INDEX-SIGNATURE shape is exempt: `Record<string, T>` legitimately
2338
+ // has zero declared members — the signature is the shape.
2339
+ if (computed && props.length === 0 && !indexValue)
2340
+ return null;
2341
+ // A DECLARED empty object type — `{}` (spelled or the checker's shared
2342
+ // intrinsic), `interface Empty {}` — is tsc's TOP type over non-nullish
2343
+ // values: every number, string, record, array, function, or class
2344
+ // instance is assignable into the slot, so the exact-record stance
2345
+ // cannot hold it. It lowers like `unknown` (the dyn DOM), the same rule
2346
+ // as `object` and the lib's `Object`: assignments convert at the site
2347
+ // (sources outside the DOM fence there, exactly as unknown does), reads
2348
+ // narrow back out through the same checked casts. The type INFERRED
2349
+ // from an empty object literal (`const o = {}`) stays the empty record
2350
+ // — it describes a value the program built, not an annotation that
2351
+ // admits everything — and computed empties (`Partial<T>` in a generic
2352
+ // body) returned null above. Empty shapes WITH an index signature
2353
+ // (`Record<string, T>`) are not empty: the signature is the shape.
2354
+ if (props.length === 0 &&
2355
+ indexValue === undefined &&
2356
+ checker.getCallSignatures(widened).length === 0 &&
2357
+ checker.getConstructSignatures(widened).length === 0 &&
2358
+ !(anonSym !== undefined && checker.declarationsOf(anonSym).some((d) => ts.isObjectLiteralExpression(d)))) {
2359
+ return DYN;
2360
+ }
2361
+ const fields = [];
2362
+ for (const p of props) {
2363
+ if (p.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)) {
2364
+ // OBJECT-LITERAL get/set accessors (TS sources): the property has
2365
+ // no data slot — the shape carries reserved closure fields instead
2366
+ // (`%get:x` invoked per read, `%set:x` per write; see
2367
+ // accessorSlotProp in ir/nodes.ts). Type-literal and interface
2368
+ // accessor MEMBERS map the same way so accessor records cross
2369
+ // annotated boundaries (`function f(p: { get x(): number })`) —
2370
+ // tsc lets a plain DATA property satisfy such a member, but the
2371
+ // exact-shape stance turns that into a loud shape-mismatch fence
2372
+ // at the literal, never a silent split. JS-file literals keep
2373
+ // their existing paths (the CJS export-table narrowing and the
2374
+ // checked-dynamic fallback), and .d.ts declarations stay out with
2375
+ // the rest of the lib's type worlds.
2376
+ const decls = checker.declarationsOf(p);
2377
+ const getDecl = decls.find((d) => ts.isGetAccessorDeclaration(d));
2378
+ const setDecl = decls.find((d) => ts.isSetAccessorDeclaration(d));
2379
+ const accessorOwned = decls.length > 0 &&
2380
+ decls.every((d) => (ts.isGetAccessorDeclaration(d) || ts.isSetAccessorDeclaration(d)) &&
2381
+ d.parent !== undefined &&
2382
+ (ts.isObjectLiteralExpression(d.parent) ||
2383
+ ts.isInterfaceDeclaration(d.parent) ||
2384
+ d.parent.kind === ts.SyntaxKind.TypeLiteral) &&
2385
+ !d.getSourceFile().isDeclarationFile &&
2386
+ !isJsSourceFile(d.getSourceFile()));
2387
+ if (!accessorOwned || (!getDecl && !setDecl))
2388
+ return null;
2389
+ // Symbol-keyed accessors have no foldable literal name to fill at
2390
+ // the literal — no slot to make.
2391
+ if (p.name.startsWith("__@"))
2392
+ return null;
2393
+ // An index-signature shape stores accessor NAMES nowhere the keyed
2394
+ // read/walk machinery can answer (the overflow would miss where
2395
+ // Node dispatches the getter) — those shapes stay unmapped.
2396
+ if (indexValue !== undefined)
2397
+ return null;
2398
+ let readT = null;
2399
+ if (getDecl) {
2400
+ readT = mapType(checker.getTypeOfSymbol(p), ctx);
2401
+ if (readT === null || readT.kind === "jsval")
2402
+ return null;
2403
+ }
2404
+ let writeT = null;
2405
+ if (setDecl) {
2406
+ const param = setDecl.parameters[0];
2407
+ if (!param)
2408
+ return null;
2409
+ writeT = mapType(checker.getTypeAtLocation(param), ctx);
2410
+ // A void/unit-typed write slot has no value form; DIVERGENT
2411
+ // getter/setter types stay out too (one property, one type —
2412
+ // the class-accessor stance).
2413
+ if (writeT === null || writeT.kind === "void" || writeT.kind === "jsval" || isUnitType(writeT))
2414
+ return null;
2415
+ if (readT !== null && typeKey(readT) !== typeKey(writeT))
2416
+ return null;
2417
+ }
2418
+ if (readT !== null || getDecl)
2419
+ fields.push({ name: `%get:${p.name}`, type: funcOf([], readT ?? VOID) });
2420
+ if (writeT !== null)
2421
+ fields.push({ name: `%set:${p.name}`, type: funcOf([writeT], VOID) });
2422
+ continue;
2423
+ }
2424
+ // Computed shapes carry provenance per MEMBER: a utility type over a
2425
+ // lib interface (`Readonly<Date>`) is still the lib's type world, not
2426
+ // a data shape. Synthesized members (a literal-key Record's) have no
2427
+ // declarations and pass.
2428
+ if (computed && checker.declarationsOf(p).some((d) => d.getSourceFile().isDeclarationFile)) {
2429
+ return null;
2430
+ }
2431
+ const fieldTs = checker.getTypeOfSymbol(p);
2432
+ // GENERIC-callable members leave the shape (no single closure slot
2433
+ // can hold them — see isGenericCallableMemberType): the shape keeps
2434
+ // its data fields, and calls of the member monomorphize per call
2435
+ // site against the defining object literal's declaration.
2436
+ if (isGenericCallableMemberType(fieldTs, checker))
2437
+ continue;
2438
+ let pt = mapType(fieldTs, ctx);
2439
+ // Unit-only FIELDS (`{ msg?: undefined }` — the discriminated-union
2440
+ // absent-field idiom — and `{ p: undefined }` spellings): the
2441
+ // unit-only union. The runtime value is the interned unit, JSON
2442
+ // omits the undefined arm exactly like an omitted optional.
2443
+ if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs))
2444
+ pt = unitOnlyUnion(ctx.unions);
2445
+ // dyn FIELDS map now (`{ v: unknown }`, `[string, unknown]` tuples):
2446
+ // the slot carries a DOM value exactly like an `unknown`-valued
2447
+ // overflow entry — same RC adapters, same dynFrom conversion on the
2448
+ // way in, same checked casts on the way out. (JSON.stringify of a
2449
+ // dyn-field-bearing shape keeps its fence: jsonSafe stays false.)
2450
+ if (!pt || pt.kind === "void")
2451
+ return null;
2452
+ // A DATA property spelled like a reserved accessor slot (`{ "%get:x":
2453
+ // v }` — a string-literal key): mapping it would collide with the
2454
+ // accessor dispatch, so the shape stays unmapped.
2455
+ if (accessorSlotProp(p.name) !== null)
2456
+ return null;
2457
+ // A bare jsval FIELD absorbs the record: shapes have no handle slot
2458
+ // (the IR forbids jsval fields — no JSON story), while an island
2459
+ // OBJECT holds engine values natively — `{ model: gateway(id),
2460
+ // prompt }` is one island object, built field by field (jsval
2461
+ // members as the same handle, static members marshaled). jsval-
2462
+ // BEARING composite fields (`content: any[]`) keep their static
2463
+ // shape — the lift covers them.
2464
+ if (pt.kind === "jsval")
2465
+ return JSVAL;
2466
+ fields.push({ name: p.name, type: pt });
2467
+ }
2468
+ // getPropertiesOfType yields DECLARATION order (interface/alias/literal
2469
+ // source order) — captured before the canonical sort so Object.keys/
2470
+ // entries/values can emit it (SEMANTICS.md 36). Two rules compose:
2471
+ // accessor slots stay out like every '%'-field (key-order surfaces
2472
+ // over accessor-carrying shapes fence by name at their lowerings),
2473
+ // and JS's own-key order applies — integer-like keys (canonical array
2474
+ // indices, from folded numeric computed keys and quoted numeric keys)
2475
+ // enumerate FIRST ascending, then string keys in insertion order
2476
+ // (OrdinaryOwnPropertyKeys) — Object.keys/JSON/inspect all read it.
2477
+ const declaredOrder = esOwnKeyOrder(fields.filter((f) => accessorSlotProp(f.name) === null).map((f) => f.name));
2478
+ fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
2479
+ return { kind: "record", shapeId: shapes.intern(fields, false, indexValue, declaredOrder) };
2480
+ }
2481
+ finally {
2482
+ shapes.inProgress.delete(widened);
2483
+ }
2484
+ }
2485
+ //# sourceMappingURL=types.js.map