@scriptc/compiler 0.0.5 → 0.0.6

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.
@@ -19,9 +19,49 @@ export declare class ShapeRegistry {
19
19
  private readonly byId;
20
20
  /** All interned shapes in first-seen (`r0`, `r1`, ...) order. */
21
21
  readonly shapes: IrRecordShape[];
22
- /** ts.Types currently being mapped — breaks recursive shape types
23
- * (`type T = { next: T }`), which mapType rejects by returning null. */
22
+ /** ts.Types currently being mapped — a BACK-REFERENCE to one of these is
23
+ * the recursive knot (`interface TreeNode { children: TreeNode[] }`):
24
+ * mapType answers a NAMED RECURSIVE SHAPE (recursiveRef) whose fields
25
+ * fill in when the outer frame completes. */
24
26
  readonly inProgress: Set<ts.Type>;
27
+ /** RECURSIVE shape ids, keyed by CHECKER TYPE identity: one shapeId per
28
+ * declaration site (the checker canonicalizes interface/alias types per
29
+ * declaration, and caches generic instantiations, so the same spelling
30
+ * is the same ts.Type). Cross-declaration structural unification exists
31
+ * only through the byKey registration finalizeRecursive performs — a
32
+ * one-level unfolding that references the same knot (`{ next: T }` where
33
+ * `type T = { next: T }`) folds into the knot's id; two INDEPENDENT
34
+ * structurally identical recursive declarations intern as distinct
35
+ * shapes, so values of one fence at the other's slots with the ordinary
36
+ * shape-mismatch diagnostic (tsc admits the assignment; the exact-shape
37
+ * stance reports it — the documented v1 width/assignability consequence
38
+ * of per-declaration identity). */
39
+ private readonly recIds;
40
+ /** Placeholder ids minted but not yet finalized: mid-construction, or
41
+ * permanently pending when the outer mapping failed (such shapes are
42
+ * referenced by nothing reachable and prune at module assembly). */
43
+ private readonly pendingRec;
44
+ /** The interning key of a canonical field list — shared by intern and
45
+ * finalizeRecursive so the two registration paths can never disagree. */
46
+ private keyOf;
47
+ /** The shape id a back-reference to an in-progress type resolves to:
48
+ * reuses the type's persistent recursive id or mints a PLACEHOLDER
49
+ * entry (empty fields) the outer frame finalizes. */
50
+ recursiveRef(t: ts.Type): string;
51
+ /** The FINALIZED recursive shape for a checker type — undefined while
52
+ * never mapped, mid-construction, or permanently failed. */
53
+ recursiveShapeFor(t: ts.Type): string | undefined;
54
+ /** True when a back-reference minted a placeholder for `t` that the
55
+ * outer frame has not (yet) finalized. */
56
+ recursivePending(t: ts.Type): boolean;
57
+ /** Completes a recursive placeholder with its computed field list and
58
+ * registers the structural key (first writer wins — the key may already
59
+ * belong to a structurally identical shape; the recursive id stays
60
+ * authoritative for its own checker type either way). */
61
+ finalizeRecursive(t: ts.Type, fields: {
62
+ name: string;
63
+ type: IrType;
64
+ }[], indexValue?: IrType, declaredOrder?: string[]): string;
25
65
  /** Interns a canonical (name-sorted) field list, returning its shapeId.
26
66
  * `tuple` shapes (fields "0".."n-1" from a tuple type) intern SEPARATELY
27
67
  * from structurally identical numeric-keyed object records: the flag is
@@ -46,9 +86,30 @@ export declare class UnionRegistry {
46
86
  private readonly byId;
47
87
  /** All interned unions in first-seen (`u0`, `u1`, ...) order. */
48
88
  readonly unions: IrUnionDef[];
49
- /** ts.Types currently being mapped — breaks recursive union types
50
- * (`type T = A | { next: T }`), which mapType rejects by returning null. */
89
+ /** ts.Types currently being mapped — a back-reference to one is the
90
+ * recursive knot passing through a union (`type Tree = Leaf | Branch`
91
+ * whose Branch arm carries `Tree[]`, the optional field `a?: A` of a
92
+ * mutually recursive pair): mapType answers a NAMED RECURSIVE UNION
93
+ * (recursiveRef) whose arms fill in when the outer frame completes. */
51
94
  readonly inProgress: Set<ts.Type>;
95
+ /** Recursive union ids, keyed by checker type identity — the
96
+ * ShapeRegistry.recIds story exactly (per-declaration identity, byKey
97
+ * folding one-level unfoldings in). */
98
+ private readonly recIds;
99
+ private readonly pendingRec;
100
+ /** The union id a back-reference to an in-progress union resolves to:
101
+ * reuses the type's persistent recursive id or mints a PLACEHOLDER
102
+ * entry (empty arms) the outer frame finalizes. */
103
+ recursiveRef(t: ts.Type): string;
104
+ /** The FINALIZED recursive union for a checker type — undefined while
105
+ * never mapped, mid-construction, or permanently failed. */
106
+ recursiveUnionFor(t: ts.Type): string | undefined;
107
+ /** True when a back-reference minted a placeholder for `t` that the
108
+ * outer frame has not (yet) finalized. */
109
+ recursivePending(t: ts.Type): boolean;
110
+ /** Completes a recursive placeholder with its canonical arm list and
111
+ * registers the structural key (first writer wins, like shapes). */
112
+ finalizeRecursive(t: ts.Type, arms: IrType[]): string;
52
113
  /** Interns a canonical (typeKey-sorted, deduplicated) arm list, returning
53
114
  * its unionId. */
54
115
  intern(arms: IrType[]): string;
@@ -56,8 +117,9 @@ export declare class UnionRegistry {
56
117
  }
57
118
  /** Human-readable rendering of an IrType for diagnostics (records expand to
58
119
  * their canonical field list, unions to their arms; `checker.typeToString`
59
- * can't — it never sees IR types). */
60
- export declare function formatIrType(t: IrType, shapes: ShapeRegistry, unions: UnionRegistry): string;
120
+ * can't — it never sees IR types). `seen` breaks recursive shapes/unions:
121
+ * a back-reference renders as "..." instead of expanding forever. */
122
+ export declare function formatIrType(t: IrType, shapes: ShapeRegistry, unions: UnionRegistry, seen?: Set<string>): string;
61
123
  /** True when the declaration sits inside `declare module "<name>"` (or
62
124
  * its node:-prefixed twin) — the disambiguator for builtin-module type
63
125
  * names that repeat across modules (net.Server vs http.Server). Both
@@ -23,9 +23,84 @@ export class ShapeRegistry {
23
23
  byId = new Map();
24
24
  /** All interned shapes in first-seen (`r0`, `r1`, ...) order. */
25
25
  shapes = [];
26
- /** ts.Types currently being mapped — breaks recursive shape types
27
- * (`type T = { next: T }`), which mapType rejects by returning null. */
26
+ /** ts.Types currently being mapped — a BACK-REFERENCE to one of these is
27
+ * the recursive knot (`interface TreeNode { children: TreeNode[] }`):
28
+ * mapType answers a NAMED RECURSIVE SHAPE (recursiveRef) whose fields
29
+ * fill in when the outer frame completes. */
28
30
  inProgress = new Set();
31
+ /** RECURSIVE shape ids, keyed by CHECKER TYPE identity: one shapeId per
32
+ * declaration site (the checker canonicalizes interface/alias types per
33
+ * declaration, and caches generic instantiations, so the same spelling
34
+ * is the same ts.Type). Cross-declaration structural unification exists
35
+ * only through the byKey registration finalizeRecursive performs — a
36
+ * one-level unfolding that references the same knot (`{ next: T }` where
37
+ * `type T = { next: T }`) folds into the knot's id; two INDEPENDENT
38
+ * structurally identical recursive declarations intern as distinct
39
+ * shapes, so values of one fence at the other's slots with the ordinary
40
+ * shape-mismatch diagnostic (tsc admits the assignment; the exact-shape
41
+ * stance reports it — the documented v1 width/assignability consequence
42
+ * of per-declaration identity). */
43
+ recIds = new Map();
44
+ /** Placeholder ids minted but not yet finalized: mid-construction, or
45
+ * permanently pending when the outer mapping failed (such shapes are
46
+ * referenced by nothing reachable and prune at module assembly). */
47
+ pendingRec = new Set();
48
+ /** The interning key of a canonical field list — shared by intern and
49
+ * finalizeRecursive so the two registration paths can never disagree. */
50
+ keyOf(fields, tuple, indexValue) {
51
+ return ((tuple ? "tuple!" : "") +
52
+ (indexValue ? `idx<${typeKey(indexValue)}>!` : "") +
53
+ JSON.stringify(fields.map((f) => [f.name, typeKey(f.type)])));
54
+ }
55
+ /** The shape id a back-reference to an in-progress type resolves to:
56
+ * reuses the type's persistent recursive id or mints a PLACEHOLDER
57
+ * entry (empty fields) the outer frame finalizes. */
58
+ recursiveRef(t) {
59
+ let id = this.recIds.get(t);
60
+ if (id === undefined) {
61
+ id = `r${this.shapes.length}`;
62
+ const shape = { id, fields: [] };
63
+ this.byId.set(id, shape);
64
+ this.shapes.push(shape);
65
+ this.recIds.set(t, id);
66
+ this.pendingRec.add(id);
67
+ }
68
+ return id;
69
+ }
70
+ /** The FINALIZED recursive shape for a checker type — undefined while
71
+ * never mapped, mid-construction, or permanently failed. */
72
+ recursiveShapeFor(t) {
73
+ const id = this.recIds.get(t);
74
+ return id !== undefined && !this.pendingRec.has(id) ? id : undefined;
75
+ }
76
+ /** True when a back-reference minted a placeholder for `t` that the
77
+ * outer frame has not (yet) finalized. */
78
+ recursivePending(t) {
79
+ const id = this.recIds.get(t);
80
+ return id !== undefined && this.pendingRec.has(id);
81
+ }
82
+ /** Completes a recursive placeholder with its computed field list and
83
+ * registers the structural key (first writer wins — the key may already
84
+ * belong to a structurally identical shape; the recursive id stays
85
+ * authoritative for its own checker type either way). */
86
+ finalizeRecursive(t, fields, indexValue, declaredOrder) {
87
+ const id = this.recIds.get(t);
88
+ if (id === undefined)
89
+ throw new Error("shape registry bug: finalizeRecursive without a placeholder");
90
+ if (this.pendingRec.has(id)) {
91
+ const shape = this.byId.get(id);
92
+ shape.fields = fields;
93
+ if (indexValue)
94
+ shape.indexValue = indexValue;
95
+ if (declaredOrder)
96
+ shape.declaredOrder = declaredOrder;
97
+ this.pendingRec.delete(id);
98
+ const key = this.keyOf(fields, false, indexValue);
99
+ if (!this.byKey.has(key))
100
+ this.byKey.set(key, id);
101
+ }
102
+ return id;
103
+ }
29
104
  /** Interns a canonical (name-sorted) field list, returning its shapeId.
30
105
  * `tuple` shapes (fields "0".."n-1" from a tuple type) intern SEPARATELY
31
106
  * from structurally identical numeric-keyed object records: the flag is
@@ -35,9 +110,7 @@ export class ShapeRegistry {
35
110
  * declared fields with and without an overflow portion — or with
36
111
  * differently-typed ones — are different structs. */
37
112
  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)]));
113
+ const key = this.keyOf(fields, tuple, indexValue);
41
114
  let id = this.byKey.get(key);
42
115
  if (id === undefined) {
43
116
  id = `r${this.shapes.length}`;
@@ -71,9 +144,60 @@ export class UnionRegistry {
71
144
  byId = new Map();
72
145
  /** All interned unions in first-seen (`u0`, `u1`, ...) order. */
73
146
  unions = [];
74
- /** ts.Types currently being mapped — breaks recursive union types
75
- * (`type T = A | { next: T }`), which mapType rejects by returning null. */
147
+ /** ts.Types currently being mapped — a back-reference to one is the
148
+ * recursive knot passing through a union (`type Tree = Leaf | Branch`
149
+ * whose Branch arm carries `Tree[]`, the optional field `a?: A` of a
150
+ * mutually recursive pair): mapType answers a NAMED RECURSIVE UNION
151
+ * (recursiveRef) whose arms fill in when the outer frame completes. */
76
152
  inProgress = new Set();
153
+ /** Recursive union ids, keyed by checker type identity — the
154
+ * ShapeRegistry.recIds story exactly (per-declaration identity, byKey
155
+ * folding one-level unfoldings in). */
156
+ recIds = new Map();
157
+ pendingRec = new Set();
158
+ /** The union id a back-reference to an in-progress union resolves to:
159
+ * reuses the type's persistent recursive id or mints a PLACEHOLDER
160
+ * entry (empty arms) the outer frame finalizes. */
161
+ recursiveRef(t) {
162
+ let id = this.recIds.get(t);
163
+ if (id === undefined) {
164
+ id = `u${this.unions.length}`;
165
+ const def = { id, arms: [] };
166
+ this.byId.set(id, def);
167
+ this.unions.push(def);
168
+ this.recIds.set(t, id);
169
+ this.pendingRec.add(id);
170
+ }
171
+ return id;
172
+ }
173
+ /** The FINALIZED recursive union for a checker type — undefined while
174
+ * never mapped, mid-construction, or permanently failed. */
175
+ recursiveUnionFor(t) {
176
+ const id = this.recIds.get(t);
177
+ return id !== undefined && !this.pendingRec.has(id) ? id : undefined;
178
+ }
179
+ /** True when a back-reference minted a placeholder for `t` that the
180
+ * outer frame has not (yet) finalized. */
181
+ recursivePending(t) {
182
+ const id = this.recIds.get(t);
183
+ return id !== undefined && this.pendingRec.has(id);
184
+ }
185
+ /** Completes a recursive placeholder with its canonical arm list and
186
+ * registers the structural key (first writer wins, like shapes). */
187
+ finalizeRecursive(t, arms) {
188
+ const id = this.recIds.get(t);
189
+ if (id === undefined)
190
+ throw new Error("union registry bug: finalizeRecursive without a placeholder");
191
+ if (this.pendingRec.has(id)) {
192
+ const def = this.byId.get(id);
193
+ def.arms.push(...arms);
194
+ this.pendingRec.delete(id);
195
+ const key = JSON.stringify(arms.map(typeKey));
196
+ if (!this.byKey.has(key))
197
+ this.byKey.set(key, id);
198
+ }
199
+ return id;
200
+ }
77
201
  /** Interns a canonical (typeKey-sorted, deduplicated) arm list, returning
78
202
  * its unionId. */
79
203
  intern(arms) {
@@ -94,8 +218,9 @@ export class UnionRegistry {
94
218
  }
95
219
  /** Human-readable rendering of an IrType for diagnostics (records expand to
96
220
  * 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) {
221
+ * can't — it never sees IR types). `seen` breaks recursive shapes/unions:
222
+ * a back-reference renders as "..." instead of expanding forever. */
223
+ export function formatIrType(t, shapes, unions, seen = new Set()) {
99
224
  switch (t.kind) {
100
225
  case "f64":
101
226
  return "number";
@@ -117,7 +242,7 @@ export function formatIrType(t, shapes, unions) {
117
242
  case "array": {
118
243
  // Union/func elements need the parens TS syntax would ("(number |
119
244
  // string)[]" — without them the [] reads as binding to the last arm).
120
- const elem = formatIrType(t.elem, shapes, unions);
245
+ const elem = formatIrType(t.elem, shapes, unions, seen);
121
246
  return t.elem.kind === "union" || t.elem.kind === "func" ? `(${elem})[]` : `${elem}[]`;
122
247
  }
123
248
  case "bytes":
@@ -125,11 +250,11 @@ export function formatIrType(t, shapes, unions) {
125
250
  // runtime representation; the message stays honest either way).
126
251
  return t.elem === "u8" ? "Uint8Array" : t.elem === "u32" ? "Uint32Array" : t.elem === "i32" ? "Int32Array" : "Float32Array";
127
252
  case "map":
128
- return `Map<${formatIrType(t.key, shapes, unions)}, ${formatIrType(t.value, shapes, unions)}>`;
253
+ return `Map<${formatIrType(t.key, shapes, unions, seen)}, ${formatIrType(t.value, shapes, unions, seen)}>`;
129
254
  case "set":
130
- return `Set<${formatIrType(t.elem, shapes, unions)}>`;
255
+ return `Set<${formatIrType(t.elem, shapes, unions, seen)}>`;
131
256
  case "func":
132
- return `(${t.params.map((p) => formatIrType(p, shapes, unions)).join(", ")}) => ${formatIrType(t.ret, shapes, unions)}`;
257
+ return `(${t.params.map((p) => formatIrType(p, shapes, unions, seen)).join(", ")}) => ${formatIrType(t.ret, shapes, unions, seen)}`;
133
258
  case "object":
134
259
  // Runtime-provided error classes carry '%'-prefixed IR names
135
260
  // ("%Error") so user classes can never collide; diagnostics show the
@@ -142,33 +267,49 @@ export function formatIrType(t, shapes, unions) {
142
267
  const shape = shapes.get(t.shapeId);
143
268
  if (!shape)
144
269
  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)})`;
270
+ if (seen.has(t.shapeId))
271
+ return "..."; // the recursive knot
272
+ seen.add(t.shapeId);
273
+ try {
274
+ if (shape.tuple) {
275
+ const byIndex = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
276
+ return `[${byIndex.map((f) => formatIrType(f.type, shapes, unions, seen)).join(", ")}]`;
157
277
  }
158
- return `${f.name}: ${formatIrType(f.type, shapes, unions)}`;
159
- });
160
- if (shape.indexValue) {
161
- members.push(`[key: string]: ${formatIrType(shape.indexValue, shapes, unions)}`);
278
+ const members = shape.fields.map((f) => {
279
+ // Accessor slots print in TS's accessor spelling, not the
280
+ // reserved '%'-field encoding.
281
+ const slot = accessorSlotProp(f.name);
282
+ if (slot && f.type.kind === "func") {
283
+ return slot.kind === "get"
284
+ ? `get ${slot.prop}(): ${formatIrType(f.type.ret, shapes, unions, seen)}`
285
+ : `set ${slot.prop}(${formatIrType(f.type.params[0] ?? VOID, shapes, unions, seen)})`;
286
+ }
287
+ return `${f.name}: ${formatIrType(f.type, shapes, unions, seen)}`;
288
+ });
289
+ if (shape.indexValue) {
290
+ members.push(`[key: string]: ${formatIrType(shape.indexValue, shapes, unions, seen)}`);
291
+ }
292
+ if (members.length === 0)
293
+ return "{}";
294
+ return `{ ${members.join("; ")} }`;
295
+ }
296
+ finally {
297
+ seen.delete(t.shapeId); // sibling occurrences still expand
162
298
  }
163
- if (members.length === 0)
164
- return "{}";
165
- return `{ ${members.join("; ")} }`;
166
299
  }
167
300
  case "union": {
168
301
  const def = unions.get(t.unionId);
169
302
  if (!def)
170
303
  return `/* union ${t.unionId} */`;
171
- return def.arms.map((a) => formatIrType(a, shapes, unions)).join(" | ");
304
+ if (seen.has(t.unionId))
305
+ return "..."; // the recursive knot
306
+ seen.add(t.unionId);
307
+ try {
308
+ return def.arms.map((a) => formatIrType(a, shapes, unions, seen)).join(" | ");
309
+ }
310
+ finally {
311
+ seen.delete(t.unionId);
312
+ }
172
313
  }
173
314
  case "jsval":
174
315
  return "any";
@@ -213,9 +354,9 @@ export function formatIrType(t, shapes, unions) {
213
354
  case "procStream":
214
355
  return "WriteStream";
215
356
  case "promise":
216
- return `Promise<${formatIrType(t.inner, shapes, unions)}>`;
357
+ return `Promise<${formatIrType(t.inner, shapes, unions, seen)}>`;
217
358
  case "generator":
218
- return `Generator<${formatIrType(t.yieldT, shapes, unions)}, ${formatIrType(t.retT, shapes, unions)}, ${formatIrType(t.nextT, shapes, unions)}>`;
359
+ return `Generator<${formatIrType(t.yieldT, shapes, unions, seen)}, ${formatIrType(t.retT, shapes, unions, seen)}, ${formatIrType(t.nextT, shapes, unions, seen)}>`;
219
360
  default: {
220
361
  const _exhaustive = t;
221
362
  void _exhaustive;
@@ -378,6 +519,14 @@ function genChannels(yieldTs, retTs, nextTs, ctx) {
378
519
  * fallback for it. */
379
520
  const MAP_TYPE_MAX_DEPTH = 64;
380
521
  let mapTypeDepth = 0;
522
+ /** Bumped whenever mapType resolves a type through CONTEXT-SENSITIVE hooks
523
+ * (a generic body's type parameter, a mixin instantiation) — mappings that
524
+ * make the same ts.Type answer differently across instantiation contexts.
525
+ * The recursive-shape machinery keys shape identity by checker type, which
526
+ * is sound only for context-FREE mappings: a recursive frame that observes
527
+ * a bump between entry and exit stays fenced (recursive generic-open types
528
+ * report SC2001 instead of interning a per-context-wrong shape). */
529
+ let contextResolutions = 0;
381
530
  export function mapType(type, ctx) {
382
531
  if (mapTypeDepth >= MAP_TYPE_MAX_DEPTH)
383
532
  return null;
@@ -408,8 +557,10 @@ function mapTypeInner(type, ctx) {
408
557
  const { checker, unions, classNamer, resolveTypeParam } = ctx;
409
558
  if (resolveTypeParam && type.flags & ts.TypeFlags.TypeParameter) {
410
559
  const bound = resolveTypeParam(type);
411
- if (bound)
560
+ if (bound) {
561
+ contextResolutions++;
412
562
  return bound;
563
+ }
413
564
  }
414
565
  // Narrowed type parameters: `!== undefined` / `!== null` / truthiness on
415
566
  // a `T | undefined`-flavored value inside a generic body leaves the
@@ -420,15 +571,19 @@ function mapTypeInner(type, ctx) {
420
571
  // the mapping is the binding filtered to what the narrowing kept.
421
572
  if (resolveTypeParam) {
422
573
  const viaNarrowedParam = mapNarrowedTypeParam(type, ctx);
423
- if (viaNarrowedParam !== undefined)
574
+ if (viaNarrowedParam !== undefined) {
575
+ contextResolutions++;
424
576
  return viaNarrowedParam;
577
+ }
425
578
  // `Awaited<T>` over a bound type parameter (an async generic body's
426
579
  // `await fn()` result): a CONDITIONAL type the checker keeps symbolic
427
580
  // inside the body — the object-gated utility-alias hook below never
428
581
  // sees it, so it resolves here, before the flag dispatch.
429
582
  const viaAwaited = mapGenericAwaitedAlias(type, ctx);
430
- if (viaAwaited !== null)
583
+ if (viaAwaited !== null) {
584
+ contextResolutions++;
431
585
  return viaAwaited;
586
+ }
432
587
  }
433
588
  const widened = checker.getBaseTypeOfLiteralType(type);
434
589
  const flags = widened.flags;
@@ -770,8 +925,10 @@ function mapTypeInner(type, ctx) {
770
925
  // pinned instantiation; ambiguity stays unmapped (the hook's rules).
771
926
  {
772
927
  const viaMixin = ctx.mixinIntersectionInstance?.(widened);
773
- if (viaMixin)
928
+ if (viaMixin) {
929
+ contextResolutions++;
774
930
  return viaMixin;
931
+ }
775
932
  }
776
933
  }
777
934
  // symbol, but with construct signatures — that is the STATIC side, and
@@ -797,8 +954,10 @@ function mapTypeInner(type, ctx) {
797
954
  // stays unmapped (nothing names a call site).
798
955
  {
799
956
  const viaMixin = ctx.mixinClassInstance?.(classDecl);
800
- if (viaMixin)
957
+ if (viaMixin) {
958
+ contextResolutions++;
801
959
  return viaMixin;
960
+ }
802
961
  }
803
962
  // A class expression inside a function/static block never registers
804
963
  // (a distinct class per evaluation): unmappable for the same reason.
@@ -825,8 +984,10 @@ function mapTypeInner(type, ctx) {
825
984
  // the current instantiation's classval, like the instance type above.
826
985
  {
827
986
  const viaMixin = ctx.mixinClassInstance?.(classDecl);
828
- if (viaMixin?.kind === "object")
987
+ if (viaMixin?.kind === "object") {
988
+ contextResolutions++;
829
989
  return { kind: "classval", className: viaMixin.className };
990
+ }
830
991
  }
831
992
  if (classExprNeverRegisters(classDecl))
832
993
  return null;
@@ -1811,12 +1972,21 @@ function mapTypeInner(type, ctx) {
1811
1972
  // union membership is the only position where the units are
1812
1973
  // representable. Deliberately unmapped: any other unmappable part
1813
1974
  // (poisons the union), void parts, func arms (closure identity vs tag
1814
- // interplay, later), and recursive unions (`type T = A | { next: T }` —
1815
- // an RC cycle anyway).
1975
+ // interplay, later). RECURSIVE unions (`type Tree = Leaf | { children:
1976
+ // Tree[] }`, the optional-field spelling `a?: A` of a mutually recursive
1977
+ // pair) map as NAMED RECURSIVE UNIONS: a back-reference to an in-progress
1978
+ // union answers a placeholder id whose arms fill in when this frame
1979
+ // completes — the ShapeRegistry knot, mirrored (identity per checker
1980
+ // type; context-sensitive frames stay fenced).
1816
1981
  if (widened.isUnionType()) {
1817
- if (unions.inProgress.has(widened))
1818
- return null;
1982
+ const knownRecursive = unions.recursiveUnionFor(widened);
1983
+ if (knownRecursive !== undefined)
1984
+ return { kind: "union", unionId: knownRecursive };
1985
+ if (unions.inProgress.has(widened)) {
1986
+ return { kind: "union", unionId: unions.recursiveRef(widened) };
1987
+ }
1819
1988
  unions.inProgress.add(widened);
1989
+ const sensitivityAtEntry = contextResolutions;
1820
1990
  try {
1821
1991
  const byKey = new Map();
1822
1992
  for (const part of widened.getTypes()) {
@@ -1859,11 +2029,17 @@ function mapTypeInner(type, ctx) {
1859
2029
  const arms = [...byKey.values()];
1860
2030
  // A single surviving UNIT arm cannot stand alone (degenerate — the
1861
2031
  // checker collapsed everything else away); anything else single is
1862
- // just that type.
2032
+ // just that type. A single arm UNDER A MINTED PLACEHOLDER cannot
2033
+ // stand either: back-references already handed out the union id, and
2034
+ // a one-armed union has no representation — fence the degenerate
2035
+ // recursive spelling honestly.
1863
2036
  if (arms.length === 1 && isUnitType(arms[0]))
1864
2037
  return null;
1865
- if (arms.length === 1)
2038
+ if (arms.length === 1) {
2039
+ if (unions.recursivePending(widened))
2040
+ return null;
1866
2041
  return arms[0];
2042
+ }
1867
2043
  if (arms.some((a) => a.kind === "void" || a.kind === "union" ||
1868
2044
  // Map/Set and regex arms stay out (like func against data
1869
2045
  // arms: no narrowing test — no discriminant fields on them).
@@ -1885,6 +2061,16 @@ function mapTypeInner(type, ctx) {
1885
2061
  return null;
1886
2062
  }
1887
2063
  arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
2064
+ if (unions.recursivePending(widened)) {
2065
+ // The knot closed through this union. A frame that resolved
2066
+ // through context-sensitive hooks (generic type parameters, mixin
2067
+ // instantiations) cannot intern by checker-type identity — the
2068
+ // same ts.Type answers differently per instantiation — so
2069
+ // recursive generic-open unions stay fenced.
2070
+ if (contextResolutions !== sensitivityAtEntry)
2071
+ return null;
2072
+ return { kind: "union", unionId: unions.finalizeRecursive(widened, arms) };
2073
+ }
1888
2074
  return { kind: "union", unionId: unions.intern(arms) };
1889
2075
  }
1890
2076
  finally {
@@ -2304,6 +2490,56 @@ export function isGenericCallableMemberType(t, checker) {
2304
2490
  checker.getIndexInfosOfType(t).length === 0);
2305
2491
  }
2306
2492
  function mapRecordType(widened, ctx) {
2493
+ const { shapes } = ctx;
2494
+ // A type whose recursive shape already finalized answers its id straight
2495
+ // off the registry (identity is per checker type; remapping would walk
2496
+ // the same members to the same answer).
2497
+ const knownRecursive = shapes.recursiveShapeFor(widened);
2498
+ if (knownRecursive !== undefined)
2499
+ return { kind: "record", shapeId: knownRecursive };
2500
+ // A BACK-REFERENCE to a shape currently being mapped (`interface
2501
+ // TreeNode { label: string; children: TreeNode[] }` reaching TreeNode
2502
+ // through its own field walk — directly or through arrays, unions,
2503
+ // optionals, Maps): the recursive knot. Answer a NAMED RECURSIVE SHAPE —
2504
+ // a placeholder shape-table entry whose fields fill in when the outer
2505
+ // frame completes (finalizeRecursive below). Backends represent records
2506
+ // as heap references, so the self-reference is an ordinary pointer.
2507
+ if (shapes.inProgress.has(widened)) {
2508
+ return { kind: "record", shapeId: shapes.recursiveRef(widened) };
2509
+ }
2510
+ shapes.inProgress.add(widened);
2511
+ const sensitivityAtEntry = contextResolutions;
2512
+ try {
2513
+ const inner = mapRecordTypeInner(widened, ctx);
2514
+ if (inner === null)
2515
+ return null; // a pending placeholder, if minted, stays unfinalized (prunes as unreachable)
2516
+ if (!("fields" in inner)) {
2517
+ // A whole-type answer (the jsval/dyn absorbs, the header-family
2518
+ // canonical shape). If a back-reference minted a placeholder for
2519
+ // this very type meanwhile, the two answers disagree — fence rather
2520
+ // than leave references to a shape that is not this type's mapping.
2521
+ if (shapes.recursivePending(widened) && inner.kind !== "jsval" && inner.kind !== "dyn")
2522
+ return null;
2523
+ return inner;
2524
+ }
2525
+ if (shapes.recursivePending(widened)) {
2526
+ // The knot closed through this shape. Context-sensitive frames
2527
+ // (generic type parameters, mixin instantiations) cannot intern by
2528
+ // checker-type identity — fence recursive generic-open shapes.
2529
+ if (contextResolutions !== sensitivityAtEntry)
2530
+ return null;
2531
+ return {
2532
+ kind: "record",
2533
+ shapeId: shapes.finalizeRecursive(widened, inner.fields, inner.indexValue, inner.declaredOrder),
2534
+ };
2535
+ }
2536
+ return { kind: "record", shapeId: shapes.intern(inner.fields, false, inner.indexValue, inner.declaredOrder) };
2537
+ }
2538
+ finally {
2539
+ shapes.inProgress.delete(widened);
2540
+ }
2541
+ }
2542
+ function mapRecordTypeInner(widened, ctx) {
2307
2543
  const { checker, shapes } = ctx;
2308
2544
  if (checker.getConstructSignatures(widened).length > 0)
2309
2545
  return null;
@@ -2427,12 +2663,7 @@ function mapRecordType(widened, ctx) {
2427
2663
  // Checker-computed shapes (no user declaration) need two extra fences in
2428
2664
  // the member walk below; see the comments there.
2429
2665
  const computed = widened.isIntersectionType() || isMappedShape(widened);
2430
- // Recursive shapes (`type T = { next: T }`) cannot intern (and would be RC
2431
- // cycles anyway): break the recursion by failing the inner reference.
2432
- if (shapes.inProgress.has(widened))
2433
- return null;
2434
- shapes.inProgress.add(widened);
2435
- try {
2666
+ {
2436
2667
  const props = checker.getPropertiesOfType(widened);
2437
2668
  // A computed shape with NO members is not a real empty record: inside a
2438
2669
  // generic body `Partial<T>` resolves to no members at all (keyof T is
@@ -2590,10 +2821,7 @@ function mapRecordType(widened, ctx) {
2590
2821
  // (OrdinaryOwnPropertyKeys) — Object.keys/JSON/inspect all read it.
2591
2822
  const declaredOrder = esOwnKeyOrder(fields.filter((f) => accessorSlotProp(f.name) === null).map((f) => f.name));
2592
2823
  fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
2593
- return { kind: "record", shapeId: shapes.intern(fields, false, indexValue, declaredOrder) };
2594
- }
2595
- finally {
2596
- shapes.inProgress.delete(widened);
2824
+ return { fields, ...(indexValue ? { indexValue } : {}), declaredOrder };
2597
2825
  }
2598
2826
  }
2599
2827
  //# sourceMappingURL=types.js.map