tywrap 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +42 -22
  2. package/SECURITY.md +38 -0
  3. package/dist/config/index.d.ts.map +1 -1
  4. package/dist/config/index.js +8 -0
  5. package/dist/config/index.js.map +1 -1
  6. package/dist/core/annotation-parser.d.ts +2 -1
  7. package/dist/core/annotation-parser.d.ts.map +1 -1
  8. package/dist/core/annotation-parser.js +6 -3
  9. package/dist/core/annotation-parser.js.map +1 -1
  10. package/dist/core/generator.d.ts +23 -4
  11. package/dist/core/generator.d.ts.map +1 -1
  12. package/dist/core/generator.js +254 -170
  13. package/dist/core/generator.js.map +1 -1
  14. package/dist/core/mapper.d.ts +3 -2
  15. package/dist/core/mapper.d.ts.map +1 -1
  16. package/dist/core/mapper.js +5 -5
  17. package/dist/core/mapper.js.map +1 -1
  18. package/dist/index.d.ts +7 -5
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +6 -4
  21. package/dist/index.js.map +1 -1
  22. package/dist/runtime/base-bridge.d.ts +3 -7
  23. package/dist/runtime/base-bridge.d.ts.map +1 -1
  24. package/dist/runtime/base-bridge.js +4 -17
  25. package/dist/runtime/base-bridge.js.map +1 -1
  26. package/dist/runtime/bounded-context.d.ts +3 -22
  27. package/dist/runtime/bounded-context.d.ts.map +1 -1
  28. package/dist/runtime/bounded-context.js +13 -53
  29. package/dist/runtime/bounded-context.js.map +1 -1
  30. package/dist/runtime/bridge-codec.d.ts +3 -3
  31. package/dist/runtime/bridge-codec.d.ts.map +1 -1
  32. package/dist/runtime/bridge-codec.js +129 -62
  33. package/dist/runtime/bridge-codec.js.map +1 -1
  34. package/dist/runtime/errors.d.ts +16 -0
  35. package/dist/runtime/errors.d.ts.map +1 -1
  36. package/dist/runtime/errors.js +17 -0
  37. package/dist/runtime/errors.js.map +1 -1
  38. package/dist/runtime/http-transport.d.ts +1 -1
  39. package/dist/runtime/http-transport.d.ts.map +1 -1
  40. package/dist/runtime/http-transport.js +1 -1
  41. package/dist/runtime/http-transport.js.map +1 -1
  42. package/dist/runtime/index.d.ts +1 -0
  43. package/dist/runtime/index.d.ts.map +1 -1
  44. package/dist/runtime/index.js +1 -0
  45. package/dist/runtime/index.js.map +1 -1
  46. package/dist/runtime/node.d.ts +7 -36
  47. package/dist/runtime/node.d.ts.map +1 -1
  48. package/dist/runtime/node.js +2 -80
  49. package/dist/runtime/node.js.map +1 -1
  50. package/dist/runtime/pooled-transport.d.ts +120 -59
  51. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  52. package/dist/runtime/pooled-transport.js +345 -78
  53. package/dist/runtime/pooled-transport.js.map +1 -1
  54. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
  55. package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
  56. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
  57. package/dist/runtime/pyodide-transport.d.ts +1 -1
  58. package/dist/runtime/pyodide-transport.d.ts.map +1 -1
  59. package/dist/runtime/pyodide-transport.js +2 -3
  60. package/dist/runtime/pyodide-transport.js.map +1 -1
  61. package/dist/runtime/rpc-client.d.ts +7 -36
  62. package/dist/runtime/rpc-client.d.ts.map +1 -1
  63. package/dist/runtime/rpc-client.js +20 -102
  64. package/dist/runtime/rpc-client.js.map +1 -1
  65. package/dist/runtime/subprocess-transport.d.ts +30 -69
  66. package/dist/runtime/subprocess-transport.d.ts.map +1 -1
  67. package/dist/runtime/subprocess-transport.js +179 -236
  68. package/dist/runtime/subprocess-transport.js.map +1 -1
  69. package/dist/runtime/timed-out-request-tracker.d.ts +2 -1
  70. package/dist/runtime/timed-out-request-tracker.d.ts.map +1 -1
  71. package/dist/runtime/transport.d.ts +9 -19
  72. package/dist/runtime/transport.d.ts.map +1 -1
  73. package/dist/runtime/transport.js +1 -1
  74. package/dist/runtime/transport.js.map +1 -1
  75. package/dist/runtime/validators.d.ts +50 -0
  76. package/dist/runtime/validators.d.ts.map +1 -1
  77. package/dist/runtime/validators.js +154 -0
  78. package/dist/runtime/validators.js.map +1 -1
  79. package/dist/types/index.d.ts +14 -39
  80. package/dist/types/index.d.ts.map +1 -1
  81. package/dist/tywrap.d.ts +3 -2
  82. package/dist/tywrap.d.ts.map +1 -1
  83. package/dist/tywrap.js +140 -13
  84. package/dist/tywrap.js.map +1 -1
  85. package/dist/utils/cache.d.ts +3 -16
  86. package/dist/utils/cache.d.ts.map +1 -1
  87. package/dist/utils/codec.d.ts +17 -0
  88. package/dist/utils/codec.d.ts.map +1 -1
  89. package/dist/utils/codec.js +500 -85
  90. package/dist/utils/codec.js.map +1 -1
  91. package/dist/utils/ir-cache.d.ts +2 -1
  92. package/dist/utils/ir-cache.d.ts.map +1 -1
  93. package/dist/utils/runtime.d.ts +0 -29
  94. package/dist/utils/runtime.d.ts.map +1 -1
  95. package/dist/utils/runtime.js +16 -107
  96. package/dist/utils/runtime.js.map +1 -1
  97. package/dist/version.d.ts.map +1 -1
  98. package/dist/version.js +1 -1
  99. package/dist/version.js.map +1 -1
  100. package/package.json +7 -6
  101. package/runtime/frame_codec.py +7 -1
  102. package/runtime/python_bridge.py +85 -125
  103. package/runtime/tywrap_bridge_core.py +515 -187
  104. package/src/config/index.ts +11 -0
  105. package/src/core/annotation-parser.ts +7 -4
  106. package/src/core/generator.ts +330 -205
  107. package/src/core/mapper.ts +8 -8
  108. package/src/index.ts +7 -4
  109. package/src/runtime/base-bridge.ts +5 -30
  110. package/src/runtime/bounded-context.ts +12 -67
  111. package/src/runtime/bridge-codec.ts +158 -74
  112. package/src/runtime/errors.ts +21 -0
  113. package/src/runtime/http-transport.ts +6 -1
  114. package/src/runtime/index.ts +6 -0
  115. package/src/runtime/node.ts +9 -120
  116. package/src/runtime/pooled-transport.ts +424 -90
  117. package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
  118. package/src/runtime/pyodide-transport.ts +7 -3
  119. package/src/runtime/rpc-client.ts +37 -139
  120. package/src/runtime/subprocess-transport.ts +210 -285
  121. package/src/runtime/timed-out-request-tracker.ts +1 -1
  122. package/src/runtime/transport.ts +15 -23
  123. package/src/runtime/validators.ts +212 -0
  124. package/src/types/index.ts +21 -60
  125. package/src/tywrap.ts +157 -22
  126. package/src/utils/cache.ts +3 -3
  127. package/src/utils/codec.ts +749 -112
  128. package/src/utils/ir-cache.ts +1 -1
  129. package/src/utils/runtime.ts +17 -128
  130. package/src/version.ts +1 -1
  131. package/dist/core/discovery.d.ts +0 -103
  132. package/dist/core/discovery.d.ts.map +0 -1
  133. package/dist/core/discovery.js +0 -380
  134. package/dist/core/discovery.js.map +0 -1
  135. package/dist/core/validation.d.ts +0 -102
  136. package/dist/core/validation.d.ts.map +0 -1
  137. package/dist/core/validation.js +0 -490
  138. package/dist/core/validation.js.map +0 -1
  139. package/dist/runtime/base.d.ts +0 -22
  140. package/dist/runtime/base.d.ts.map +0 -1
  141. package/dist/runtime/base.js +0 -23
  142. package/dist/runtime/base.js.map +0 -1
  143. package/dist/runtime/transport-pool.d.ts +0 -196
  144. package/dist/runtime/transport-pool.d.ts.map +0 -1
  145. package/dist/runtime/transport-pool.js +0 -418
  146. package/dist/runtime/transport-pool.js.map +0 -1
  147. package/runtime/__pycache__/_tywrap_conformance_chunking_fixtures.cpython-311.pyc +0 -0
  148. package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
  149. package/runtime/__pycache__/_tywrap_w4_chunking_fixture.cpython-311.pyc +0 -0
  150. package/runtime/__pycache__/_tywrap_w5_request_chunking_fixture.cpython-311.pyc +0 -0
  151. package/runtime/__pycache__/_tywrap_w6_pool_chunking_fixture.cpython-311.pyc +0 -0
  152. package/runtime/__pycache__/frame_codec.cpython-311.pyc +0 -0
  153. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  154. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  155. package/runtime/safe_codec.py +0 -352
  156. package/src/core/discovery.ts +0 -477
  157. package/src/core/validation.ts +0 -729
  158. package/src/runtime/base.ts +0 -24
  159. package/src/runtime/transport-pool.ts +0 -538
@@ -29,14 +29,44 @@ interface GenericRenderParam {
29
29
 
30
30
  interface GenericRenderContext {
31
31
  currentModule?: string;
32
+ localDeclaredNames: Set<string>;
32
33
  declaration: string;
33
34
  typeArguments: string;
34
35
  emittedNames: Set<string>;
35
36
  emittedParamSpecs: Set<string>;
36
37
  }
37
38
 
39
+ type ReturnDefinitionNames = ReadonlySet<string>;
40
+
41
+ export interface CodeGeneratorOptions {
42
+ /** Reports a generated annotation that cannot be represented by emitted declarations. */
43
+ onTypeDegrade?: (typeName: string) => void;
44
+ }
45
+
38
46
  export class CodeGenerator {
39
47
  private readonly mapper: TypeMapper;
48
+ private readonly onTypeDegrade?: (typeName: string) => void;
49
+ private readonly builtinGenericNames = new Set([
50
+ 'Array',
51
+ 'AsyncIterator',
52
+ 'Generator',
53
+ 'Iterable',
54
+ 'Iterator',
55
+ 'Promise',
56
+ 'Record',
57
+ ]);
58
+ // Iterator-protocol objects can never cross the bridge: the Python side
59
+ // rejects them loudly at serialization, so a return typed as one of these
60
+ // could never carry a value. Returns degrade to `unknown` (and count as a
61
+ // degrade for --fail-on-warn). Iterable/Sequence are NOT here — a list
62
+ // satisfies those annotations and decodes to an array, which structurally
63
+ // satisfies the emitted TS type.
64
+ private readonly nonDecodableReturnGenerics = new Set([
65
+ 'Generator',
66
+ 'AsyncGenerator',
67
+ 'Iterator',
68
+ 'AsyncIterator',
69
+ ]);
40
70
  private readonly reservedTsIdentifiers = new Set([
41
71
  'default',
42
72
  'delete',
@@ -66,8 +96,9 @@ export class CodeGenerator {
66
96
  'false',
67
97
  ]);
68
98
 
69
- constructor(mapper: TypeMapper = new TypeMapper()) {
99
+ constructor(mapper: TypeMapper = new TypeMapper(), options: CodeGeneratorOptions = {}) {
70
100
  this.mapper = mapper;
101
+ this.onTypeDegrade = options.onTypeDegrade;
71
102
  }
72
103
 
73
104
  /**
@@ -140,7 +171,8 @@ export class CodeGenerator {
140
171
  private buildGenericRenderContext(
141
172
  typeParameters: readonly PythonGenericParameter[],
142
173
  types: readonly PythonType[],
143
- currentModule?: string
174
+ currentModule?: string,
175
+ localDeclaredNames: Set<string> = new Set()
144
176
  ): GenericRenderContext {
145
177
  const callableParamSpecs = new Set<string>();
146
178
  types.forEach(type => this.collectCallableParamSpecs(type, callableParamSpecs));
@@ -170,6 +202,7 @@ export class CodeGenerator {
170
202
 
171
203
  return {
172
204
  currentModule,
205
+ localDeclaredNames,
173
206
  declaration:
174
207
  emitted.length > 0 ? `<${emitted.map(param => param.declaration).join(', ')}>` : '',
175
208
  typeArguments: emitted.length > 0 ? `<${emitted.map(param => param.name).join(', ')}>` : '',
@@ -184,6 +217,7 @@ export class CodeGenerator {
184
217
  ): GenericRenderContext {
185
218
  return {
186
219
  currentModule: inner.currentModule ?? outer.currentModule,
220
+ localDeclaredNames: inner.localDeclaredNames,
187
221
  declaration: inner.declaration,
188
222
  typeArguments: inner.typeArguments,
189
223
  emittedNames: new Set([...outer.emittedNames, ...inner.emittedNames]),
@@ -308,7 +342,27 @@ export class CodeGenerator {
308
342
  ctx: GenericRenderContext,
309
343
  mappingContext: 'value' | 'return'
310
344
  ): string {
311
- return this.typeToTs(this.mapper.mapPythonType(this.sanitizeType(type, ctx), mappingContext));
345
+ return this.typeToTs(
346
+ this.mapper.mapPythonType(this.sanitizeType(type, ctx), mappingContext),
347
+ ctx,
348
+ mappingContext
349
+ );
350
+ }
351
+
352
+ private isLocalTypeIdentity(
353
+ type: { name: string; module?: string },
354
+ ctx: GenericRenderContext
355
+ ): boolean {
356
+ if (!ctx.localDeclaredNames.has(type.name)) {
357
+ return false;
358
+ }
359
+ return type.module === undefined || type.module === ctx.currentModule;
360
+ }
361
+
362
+ private degradeType(type: { name: string; module?: string }): string {
363
+ const identity = type.module ? `${type.module}.${type.name}` : type.name;
364
+ this.onTypeDegrade?.(identity);
365
+ return 'unknown';
312
366
  }
313
367
 
314
368
  private renderLooksLikeKwargsExpr(
@@ -357,10 +411,170 @@ export class CodeGenerator {
357
411
  };
358
412
  }
359
413
 
414
+ /**
415
+ * Preserve Python provenance in generated return validators instead of
416
+ * reconstructing a checker from the final TypeScript spelling. In particular
417
+ * pandas/NumPy values retain their marker contract after Arrow decoding.
418
+ */
419
+ private returnSchema(type: PythonType, definitions: ReturnDefinitionNames = new Set()): unknown {
420
+ // A bare `-> None` return maps to TS void and validates nothing, but a
421
+ // NESTED None (union member, tuple element) is a real wire value — it
422
+ // decodes to null and must be checked as null, or a union containing it
423
+ // would carry an any-member and accept everything.
424
+ if (type.kind === 'primitive' && type.name === 'None') {
425
+ return { kind: 'any' };
426
+ }
427
+ const schema = (current: PythonType): unknown => {
428
+ switch (current.kind) {
429
+ case 'primitive':
430
+ return current.name === 'int' || current.name === 'float'
431
+ ? { kind: 'primitive', type: 'number' }
432
+ : current.name === 'str'
433
+ ? { kind: 'primitive', type: 'string' }
434
+ : current.name === 'bool'
435
+ ? { kind: 'primitive', type: 'boolean' }
436
+ : current.name === 'bytes'
437
+ ? { kind: 'primitive', type: 'Uint8Array' }
438
+ : current.name === 'None'
439
+ ? { kind: 'primitive', type: 'null' }
440
+ : { kind: 'any' };
441
+ case 'literal':
442
+ return { kind: 'literal', value: current.value };
443
+ case 'annotated':
444
+ return schema(current.base);
445
+ case 'final':
446
+ case 'classvar':
447
+ return schema(current.type);
448
+ case 'optional':
449
+ return {
450
+ kind: 'union',
451
+ options: [schema(current.type), { kind: 'primitive', type: 'null' }],
452
+ };
453
+ case 'union':
454
+ return { kind: 'union', options: current.types.map(schema) };
455
+ case 'collection': {
456
+ if (current.name === 'tuple') {
457
+ if (current.itemTypes[1]?.kind === 'custom' && current.itemTypes[1].name === '...') {
458
+ return {
459
+ kind: 'array',
460
+ element: schema(current.itemTypes[0] ?? { kind: 'custom', name: 'Any' }),
461
+ };
462
+ }
463
+ return { kind: 'tuple', elements: current.itemTypes.map(schema) };
464
+ }
465
+ if (current.name === 'dict') {
466
+ return {
467
+ kind: 'record',
468
+ values: schema(current.itemTypes[1] ?? { kind: 'custom', name: 'Any' }),
469
+ };
470
+ }
471
+ return {
472
+ kind: 'array',
473
+ element: schema(current.itemTypes[0] ?? { kind: 'custom', name: 'Any' }),
474
+ };
475
+ }
476
+ case 'generic': {
477
+ const leaf = current.name.split('.').at(-1) ?? current.name;
478
+ if (leaf === 'NDArray' || leaf === 'ndarray') {
479
+ return { kind: 'marker', marker: 'ndarray' };
480
+ }
481
+ // Iterator/Generator are deliberately absent: those returns emit
482
+ // `unknown` (see nonDecodableReturnGenerics) and validate nothing.
483
+ if (['list', 'List', 'Sequence', 'Iterable', 'set', 'frozenset'].includes(leaf)) {
484
+ return {
485
+ kind: 'array',
486
+ element: schema(current.typeArgs[0] ?? { kind: 'custom', name: 'Any' }),
487
+ };
488
+ }
489
+ if (['dict', 'Dict', 'Mapping', 'MutableMapping'].includes(leaf)) {
490
+ return {
491
+ kind: 'record',
492
+ values: schema(current.typeArgs[1] ?? { kind: 'custom', name: 'Any' }),
493
+ };
494
+ }
495
+ return definitions.has(leaf) ? { kind: 'ref', name: leaf } : { kind: 'any' };
496
+ }
497
+ case 'custom': {
498
+ const leaf = current.name.split('.').at(-1) ?? current.name;
499
+ const full = `${current.module ?? ''}.${leaf}`;
500
+ if (leaf === 'None') {
501
+ return { kind: 'primitive', type: 'null' };
502
+ }
503
+ if (leaf === 'Any' || leaf === 'object' || leaf === 'NoReturn' || leaf === 'Never') {
504
+ return { kind: 'any' };
505
+ }
506
+ if (leaf === 'DataFrame' && (!current.module || current.module.startsWith('pandas'))) {
507
+ return { kind: 'marker', marker: 'dataframe' };
508
+ }
509
+ if (leaf === 'Series' && (!current.module || current.module.startsWith('pandas'))) {
510
+ return { kind: 'marker', marker: 'series' };
511
+ }
512
+ if (leaf === 'ndarray' || leaf === 'NDArray' || full.includes('numpy')) {
513
+ return { kind: 'marker', marker: 'ndarray' };
514
+ }
515
+ if (
516
+ ['csr_matrix', 'csc_matrix', 'coo_matrix', 'spmatrix'].includes(leaf) &&
517
+ (!current.module || current.module.startsWith('scipy'))
518
+ ) {
519
+ return { kind: 'marker', marker: 'scipy.sparse' };
520
+ }
521
+ if (leaf === 'Tensor' && (!current.module || current.module.startsWith('torch'))) {
522
+ return { kind: 'marker', marker: 'torch.tensor' };
523
+ }
524
+ if (
525
+ leaf === 'BaseEstimator' &&
526
+ (!current.module || current.module.startsWith('sklearn'))
527
+ ) {
528
+ return { kind: 'marker', marker: 'sklearn.estimator' };
529
+ }
530
+ return definitions.has(leaf) ? { kind: 'ref', name: leaf } : { kind: 'any' };
531
+ }
532
+ case 'typevar':
533
+ case 'paramspec':
534
+ case 'paramspec_args':
535
+ case 'paramspec_kwargs':
536
+ case 'typevartuple':
537
+ case 'unpack':
538
+ case 'callable':
539
+ return { kind: 'any' };
540
+ }
541
+ };
542
+ return schema(type);
543
+ }
544
+
545
+ private returnDefinitions(module: PythonModule): ReturnDefinitionNames {
546
+ return new Set(
547
+ module.classes
548
+ .filter(cls => cls.kind === 'typed_dict' || cls.decorators.includes('__typed_dict__'))
549
+ .map(cls => cls.name)
550
+ );
551
+ }
552
+
553
+ private emitReturnDefinitions(module: PythonModule): string {
554
+ const definitions = this.returnDefinitions(module);
555
+ const entries = module.classes
556
+ .filter(cls => cls.kind === 'typed_dict' || cls.decorators.includes('__typed_dict__'))
557
+ .map(cls => {
558
+ const fields = Object.fromEntries(
559
+ cls.properties.map(property => [
560
+ property.name,
561
+ {
562
+ schema: this.returnSchema(property.type, definitions),
563
+ optional: property.optional === true,
564
+ },
565
+ ])
566
+ );
567
+ return [cls.name, { kind: 'record', fields }];
568
+ });
569
+ return `const __tywrapReturnDefinitions: Record<string, ReturnSchema> = ${JSON.stringify(Object.fromEntries(entries))};\n\n`;
570
+ }
571
+
360
572
  generateFunctionWrapper(
361
573
  func: PythonFunction,
362
574
  moduleName?: string,
363
- annotatedJSDoc = false
575
+ annotatedJSDoc = false,
576
+ localDeclaredNames: Set<string> = new Set(),
577
+ returnDefinitions: ReturnDefinitionNames = new Set()
364
578
  ): GeneratedCode {
365
579
  const jsdoc = this.generateJsDoc(
366
580
  func.docstring,
@@ -379,7 +593,8 @@ export class CodeGenerator {
379
593
  const genericContext = this.buildGenericRenderContext(
380
594
  this.getTypeParameters(func.typeParameters),
381
595
  [func.returnType, ...filteredParams.map(param => param.type)],
382
- moduleName
596
+ moduleName,
597
+ localDeclaredNames
383
598
  );
384
599
  const typeParamDecl = genericContext.declaration;
385
600
 
@@ -447,6 +662,8 @@ export class CodeGenerator {
447
662
  const returnType = this.typeToTsFromPython(func.returnType, genericContext, 'return');
448
663
  const fname = this.escapeIdentifier(func.name);
449
664
  const moduleId = moduleName ?? '__main__';
665
+ const validatorName = `__validate${this.escapeIdentifier(func.name, { preserveCase: true })}Result`;
666
+ const returnValidator = `const ${validatorName} = createReturnValidator(${JSON.stringify(this.returnSchema(func.returnType, returnDefinitions))}, ${JSON.stringify(`${moduleId}.${func.name}`)}, __tywrapReturnDefinitions);\n\n`;
450
667
 
451
668
  // Overloads: generate trailing optional parameter drop variants (exclude *args/**kwargs).
452
669
  // Why: Python APIs frequently have many optional tail params. TypeScript callers expect
@@ -535,10 +752,8 @@ export class CodeGenerator {
535
752
  const callPreludeLines = emitCallPrelude(callDescriptor, this.callEmitHelpers());
536
753
  const callPrelude = callPreludeLines.length > 0 ? `${callPreludeLines.join('\n')}\n` : '';
537
754
 
538
- const ts = `${jsdoc}${overloadDecl}export async function ${fname}${typeParamDecl}(${paramDecl}): Promise<${returnType}> {
539
- ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.name}', __args${
540
- hasKwArgs ? ', __kwargs' : ''
541
- });
755
+ const ts = `${jsdoc}${returnValidator}${overloadDecl}export async function ${fname}${typeParamDecl}(${paramDecl}): Promise<${returnType}> {
756
+ ${callPrelude}${guards} return getRuntimeBridge().call<${returnType}>('${moduleId}', '${func.name}', __args, ${hasKwArgs ? '__kwargs' : 'undefined'}, ${validatorName});
542
757
  }
543
758
  `;
544
759
 
@@ -553,8 +768,12 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
553
768
  generateClassWrapper(
554
769
  cls: PythonClass,
555
770
  moduleName?: string,
556
- _annotatedJSDoc = false
771
+ _annotatedJSDoc = false,
772
+ localDeclaredNames: Set<string> = new Set(),
773
+ returnDefinitions: ReturnDefinitionNames = new Set()
557
774
  ): GeneratedCode {
775
+ const moduleDeclaredNames = new Set(localDeclaredNames);
776
+ moduleDeclaredNames.add(cls.name);
558
777
  const jsdoc = this.generateJsDoc(cls.docstring);
559
778
  const classGenericContext = this.buildGenericRenderContext(
560
779
  this.getTypeParameters(cls.typeParameters),
@@ -566,14 +785,11 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
566
785
  ...method.parameters.map(p => p.type),
567
786
  ]),
568
787
  ],
569
- moduleName
788
+ moduleName,
789
+ moduleDeclaredNames
570
790
  );
571
791
  const classTypeParamDecl = classGenericContext.declaration;
572
792
  const cname = this.escapeIdentifier(cls.name);
573
- const classSelfType = `${cname}${classGenericContext.typeArguments}`;
574
- const tsValueType = (p: (typeof cls.methods)[number]['parameters'][number]): string =>
575
- this.typeToTsFromPython(p.type, classGenericContext, 'value');
576
-
577
793
  const wrapAlias = (body: string): GeneratedCode => {
578
794
  const ts = `${jsdoc}export type ${cname}${classTypeParamDecl} = ${body}\n`;
579
795
  return this.wrap(ts, ts, [cls.name]);
@@ -621,7 +837,8 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
621
837
  const methodOwnGenericContext = this.buildGenericRenderContext(
622
838
  this.getTypeParameters(m.typeParameters),
623
839
  [m.returnType, ...fparams.map(param => param.type)],
624
- moduleName
840
+ moduleName,
841
+ moduleDeclaredNames
625
842
  );
626
843
  const methodGenericContext = this.mergeGenericRenderContexts(
627
844
  classGenericContext,
@@ -659,19 +876,21 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
659
876
  const methodDeclarations: string[] = [];
660
877
 
661
878
  sortedMethods
662
- .filter(method => method.name !== '__init__')
879
+ .filter(
880
+ method =>
881
+ method.name !== '__init__' &&
882
+ (method.methodKind === 'class' || method.methodKind === 'static')
883
+ )
663
884
  .forEach(method => {
664
- // @classmethod and @staticmethod become `static` members invoked through
665
- // the class (`Class.method`), not the instance handle. Instance methods
666
- // (the default, or an absent methodKind) keep the existing emission so
667
- // their output stays byte-identical.
668
- const isStatic = method.methodKind === 'class' || method.methodKind === 'static';
669
- const staticPrefix = isStatic ? 'static ' : '';
885
+ // v0.9 wrappers expose only class/static methods. They route through
886
+ // the ordinary module call path and never retain process-local state.
887
+ const staticPrefix = 'static ';
670
888
  const fparams = method.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
671
889
  const methodOwnGenericContext = this.buildGenericRenderContext(
672
890
  this.getTypeParameters(method.typeParameters),
673
891
  [method.returnType, ...fparams.map(param => param.type)],
674
- moduleName
892
+ moduleName,
893
+ moduleDeclaredNames
675
894
  );
676
895
  const methodGenericContext = this.mergeGenericRenderContexts(
677
896
  classGenericContext,
@@ -737,6 +956,8 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
737
956
  'return'
738
957
  );
739
958
  const mname = this.escapeIdentifier(method.name);
959
+ const validatorName = `__validate${this.escapeIdentifier(cls.name, { preserveCase: true })}${this.escapeIdentifier(method.name, { preserveCase: true })}Result`;
960
+ const returnValidator = `const ${validatorName} = createReturnValidator(${JSON.stringify(this.returnSchema(method.returnType, returnDefinitions))}, ${JSON.stringify(`${moduleId}.${cls.name}.${method.name}`)}, __tywrapReturnDefinitions);\n\n`;
740
961
 
741
962
  const overloads: string[] = [];
742
963
  if (needsKwargsParam && requiredKwOnlyNames.length > 0) {
@@ -784,193 +1005,49 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
784
1005
  const guardLines = emitArgGuards(callDescriptor);
785
1006
  const guards = guardLines.length > 0 ? `${guardLines.join('\n')}\n` : '';
786
1007
 
787
- const callExpr = isStatic
788
- ? `getRuntimeBridge().call('${moduleId}', '${cls.name}.${method.name}', __args${
789
- needsKwargsParam ? ', __kwargs' : ''
790
- })`
791
- : `getRuntimeBridge().callMethod(this.__handle, '${method.name}', __args${
792
- needsKwargsParam ? ', __kwargs' : ''
793
- })`;
1008
+ const callExpr = `getRuntimeBridge().call<${returnType}>('${moduleId}', '${cls.name}.${method.name}', __args, ${needsKwargsParam ? '__kwargs' : 'undefined'}, ${validatorName})`;
794
1009
  methodBodies.push(`${overloadDecl} ${staticPrefix}async ${mname}${methodTypeParamDecl}(${paramsDecl}): Promise<${returnType}> {
795
- ${callPrelude}${guards} return ${callExpr};
1010
+ ${returnValidator}${callPrelude}${guards} return ${callExpr};
796
1011
  }`);
797
1012
  methodDeclarations.push(
798
1013
  `${overloadDecl}${overloads.length > 0 ? '' : ` ${staticPrefix}${mname}${methodTypeParamDecl}(${paramsDecl}): Promise<${returnType}>;\n`}`
799
1014
  );
800
1015
  });
801
1016
 
802
- const init = cls.methods.find(m => m.name === '__init__');
803
- const ctorSpec = (() => {
804
- if (!init) {
805
- return {
806
- overloadDecl: '',
807
- declaration: ` static create${classTypeParamDecl}(...args: unknown[]): Promise<${classSelfType}>;\n`,
808
- paramsDecl: `...args: unknown[]`,
809
- callPrelude: ` const __args: unknown[] = [...args];\n`,
810
- hasKwargs: false,
811
- guardLines: [] as string[],
812
- };
813
- }
814
-
815
- const fparams = init.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
816
- const keywordOnlyParams = fparams.filter(p => p.keywordOnly);
817
- const positionalOnlyNames = fparams.filter(p => p.positionalOnly).map(p => p.name);
818
- const hasVarKwArgs = fparams.some(p => p.kwArgs);
819
- const needsKwargsParam = keywordOnlyParams.length > 0 || hasVarKwArgs;
820
- const varArgsParam = fparams.find(p => p.varArgs);
821
- const needsVarArgsArray = Boolean(varArgsParam) && needsKwargsParam;
822
- const positionalParams = fparams.filter(p => !p.keywordOnly && !p.varArgs && !p.kwArgs);
823
- const firstOptionalPosIndex = positionalParams.findIndex(p => p.optional);
824
- const requiredPosCount =
825
- firstOptionalPosIndex >= 0 ? firstOptionalPosIndex : positionalParams.length;
826
- const keywordOnlyNames = keywordOnlyParams.map(p => p.name);
827
-
828
- const renderPositionalParam = (
829
- p: (typeof positionalParams)[number],
830
- forceRequired = false
831
- ): string => {
832
- const pname = this.escapeIdentifier(p.name);
833
- const opt = !forceRequired && p.optional ? '?' : '';
834
- return `${pname}${opt}: ${tsValueType(p)}`;
835
- };
836
-
837
- const kwargsType = (() => {
838
- if (!needsKwargsParam) {
839
- return '';
840
- }
841
- if (keywordOnlyParams.length === 0 && hasVarKwArgs) {
842
- return 'Record<string, unknown>';
843
- }
844
- const props = keywordOnlyParams
845
- .map(p => `${JSON.stringify(p.name)}${p.optional ? '?' : ''}: ${tsValueType(p)};`)
846
- .join(' ');
847
- const obj = `{ ${props} }`;
848
- return hasVarKwArgs ? `(${obj} & Record<string, unknown>)` : obj;
849
- })();
850
-
851
- const paramsDeclParts: string[] = [];
852
- positionalParams.forEach(p => {
853
- paramsDeclParts.push(renderPositionalParam(p));
854
- });
855
- if (varArgsParam) {
856
- const vname = this.escapeIdentifier(varArgsParam.name);
857
- paramsDeclParts.push(needsVarArgsArray ? `${vname}?: unknown[]` : `...${vname}: unknown[]`);
858
- }
859
- if (needsKwargsParam) {
860
- paramsDeclParts.push(`kwargs?: ${kwargsType}`);
861
- }
862
- const paramsDecl = paramsDeclParts.join(', ');
863
-
864
- const requiredKwOnlyNames = keywordOnlyParams.filter(p => !p.optional).map(p => p.name);
865
- const overloads: string[] = [];
866
- if (needsKwargsParam && requiredKwOnlyNames.length > 0) {
867
- const firstOptionalIndex = positionalParams.findIndex(p => p.optional);
868
- const requiredPosCount =
869
- firstOptionalIndex >= 0 ? firstOptionalIndex : positionalParams.length;
870
- for (let i = requiredPosCount; i <= positionalParams.length; i++) {
871
- const head = positionalParams.slice(0, i).map(p => renderPositionalParam(p, true));
872
- const rest: string[] = [];
873
- if (varArgsParam) {
874
- const vname = this.escapeIdentifier(varArgsParam.name);
875
- rest.push(
876
- needsVarArgsArray ? `${vname}: unknown[] | undefined` : `...${vname}: unknown[]`
877
- );
878
- }
879
- rest.push(`kwargs: ${kwargsType}`);
880
- overloads.push(
881
- ` static create${classTypeParamDecl}(${[...head, ...rest].join(', ')}): Promise<${classSelfType}>;`
882
- );
883
- if (varArgsParam && needsVarArgsArray) {
884
- overloads.push(
885
- ` static create${classTypeParamDecl}(${[...head, `kwargs: ${kwargsType}`].join(', ')}): Promise<${classSelfType}>;`
886
- );
887
- }
888
- }
889
- }
890
- const overloadDecl = overloads.length > 0 ? `${overloads.join('\n')}\n` : '';
891
- const declaration =
892
- overloads.length > 0
893
- ? overloadDecl
894
- : ` static create${classTypeParamDecl}(${paramsDecl}): Promise<${classSelfType}>;\n`;
895
-
896
- const callDescriptor: CallDescriptor = {
897
- positionalParams,
898
- varArgsParam,
899
- needsVarArgsArray,
900
- hasKwArgs: needsKwargsParam,
901
- hasVarKwArgs,
902
- keywordOnlyNames,
903
- requiredKwOnlyNames,
904
- positionalOnlyNames,
905
- requiredPosCount,
906
- indent: ' ',
907
- errorLabel: '__init__',
908
- };
909
- const callPreludeLines = emitCallPrelude(callDescriptor, this.callEmitHelpers());
910
- const callPrelude = callPreludeLines.length > 0 ? `${callPreludeLines.join('\n')}\n` : '';
911
- const guardLines = emitArgGuards(callDescriptor);
912
-
913
- return {
914
- overloadDecl,
915
- declaration,
916
- paramsDecl,
917
- callPrelude,
918
- hasKwargs: needsKwargsParam,
919
- guardLines,
920
- };
921
- })();
922
-
923
- // @property / functools.cached_property → TS getters returning Promise<T>.
924
- // Reading the attribute fires the getter on the Python side; the bridge
925
- // resolves it via callMethod with no args. Sorted for stable output.
926
- const accessorBodies: string[] = [];
927
- const accessorDeclarations: string[] = [];
928
- const sortedAccessors = [...(cls.accessors ?? [])].sort((a, b) => a.name.localeCompare(b.name));
929
- sortedAccessors.forEach(accessor => {
930
- const aname = this.escapeIdentifier(accessor.name);
931
- const accessorType = this.typeToTsFromPython(accessor.type, classGenericContext, 'return');
932
- const jsdocAcc = this.generateJsDoc(accessor.docstring);
933
- accessorBodies.push(
934
- `${jsdocAcc} get ${aname}(): Promise<${accessorType}> { return getRuntimeBridge().callMethod(this.__handle, '${accessor.name}', []); }`
935
- );
936
- accessorDeclarations.push(`${jsdocAcc} get ${aname}(): Promise<${accessorType}>;\n`);
937
- });
938
- const accessorsImpl = accessorBodies.length > 0 ? `${accessorBodies.join('\n')}\n` : '';
939
- const accessorsDecl = accessorDeclarations.length > 0 ? `${accessorDeclarations.join('')}` : '';
940
-
941
1017
  const methodsSection = methodBodies.length > 0 ? `\n${methodBodies.join('\n')}\n` : '\n';
942
1018
  const declarationMethodsSection =
943
1019
  methodDeclarations.length > 0 ? `\n${methodDeclarations.join('')}\n` : '\n';
944
- const ctorGuards = ctorSpec.guardLines.length > 0 ? `${ctorSpec.guardLines.join('\n')}\n` : '';
945
- const newClassExpr = `new ${cname}${classGenericContext.typeArguments}(handle)`;
1020
+ // The constructor counts: a class whose only member was __init__ loses
1021
+ // create(), so it needs the migration note as much as one with methods.
1022
+ const omittedInstanceMembers =
1023
+ cls.methods.some(method => method.methodKind !== 'class' && method.methodKind !== 'static') ||
1024
+ (cls.accessors?.length ?? 0) > 0;
1025
+ const migrationNote = omittedInstanceMembers
1026
+ ? ' // NOTE: Instance members are not generated in v0.9; migrate this API to value-returning module functions.\n'
1027
+ : '';
946
1028
  const ts = `${jsdoc}export class ${cname}${classTypeParamDecl} {
947
- private readonly __handle: string;
948
- private constructor(handle: string) { this.__handle = handle; }
949
- ${ctorSpec.overloadDecl} static async create${classTypeParamDecl}(${ctorSpec.paramsDecl}): Promise<${classSelfType}> {
950
- ${ctorSpec.callPrelude}${ctorGuards} const handle = await getRuntimeBridge().instantiate<string>('${moduleId}', '${cls.name}', __args${
951
- ctorSpec.hasKwargs ? ', __kwargs' : ''
952
- });
953
- return ${newClassExpr};
954
- }
955
- static fromHandle${classTypeParamDecl}(handle: string): ${classSelfType} { return ${newClassExpr}; }${methodsSection}${accessorsImpl} async disposeHandle(): Promise<void> { await getRuntimeBridge().disposeInstance(this.__handle); }
1029
+ ${migrationNote}${methodsSection}
956
1030
  }
957
1031
  `;
958
1032
 
959
1033
  const declaration = `${jsdoc}export class ${cname}${classTypeParamDecl} {
960
- private readonly __handle: string;
961
- private constructor(handle: string);
962
- ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string): ${classSelfType};${declarationMethodsSection}${accessorsDecl} disposeHandle(): Promise<void>;
1034
+ ${migrationNote}${declarationMethodsSection}
963
1035
  }
964
1036
  `;
965
1037
 
966
1038
  return this.wrap(ts, declaration, [cls.name]);
967
1039
  }
968
1040
 
969
- generateTypeAlias(alias: PythonTypeAlias, moduleName?: string): GeneratedCode {
1041
+ generateTypeAlias(
1042
+ alias: PythonTypeAlias,
1043
+ moduleName?: string,
1044
+ localDeclaredNames: Set<string> = new Set()
1045
+ ): GeneratedCode {
970
1046
  const genericContext = this.buildGenericRenderContext(
971
1047
  this.getTypeParameters(alias.typeParameters),
972
1048
  [alias.type],
973
- moduleName
1049
+ moduleName,
1050
+ localDeclaredNames
974
1051
  );
975
1052
  const aliasName = this.escapeIdentifier(alias.name, { preserveCase: true });
976
1053
  const body = this.typeToTsFromPython(alias.type, genericContext, 'value');
@@ -1004,15 +1081,35 @@ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string):
1004
1081
  }
1005
1082
 
1006
1083
  generateModuleDefinition(module: PythonModule, annotatedJSDoc = false): GeneratedCode {
1084
+ const localDeclaredNames = new Set([
1085
+ ...module.classes.map(cls => cls.name),
1086
+ ...(module.typeAliases ?? []).map(alias => alias.name),
1087
+ ]);
1007
1088
  const functionResults = [...module.functions]
1008
1089
  .sort((a, b) => a.name.localeCompare(b.name))
1009
- .map(f => this.generateFunctionWrapper(f, module.name, annotatedJSDoc));
1090
+ .map(f =>
1091
+ this.generateFunctionWrapper(
1092
+ f,
1093
+ module.name,
1094
+ annotatedJSDoc,
1095
+ localDeclaredNames,
1096
+ this.returnDefinitions(module)
1097
+ )
1098
+ );
1010
1099
  const classResults = [...module.classes]
1011
1100
  .sort((a, b) => a.name.localeCompare(b.name))
1012
- .map(c => this.generateClassWrapper(c, module.name, annotatedJSDoc));
1101
+ .map(c =>
1102
+ this.generateClassWrapper(
1103
+ c,
1104
+ module.name,
1105
+ annotatedJSDoc,
1106
+ localDeclaredNames,
1107
+ this.returnDefinitions(module)
1108
+ )
1109
+ );
1013
1110
  const typeAliasResults = [...(module.typeAliases ?? [])]
1014
1111
  .sort((a, b) => a.name.localeCompare(b.name))
1015
- .map(alias => this.generateTypeAlias(alias, module.name));
1112
+ .map(alias => this.generateTypeAlias(alias, module.name, localDeclaredNames));
1016
1113
 
1017
1114
  const functionCodes = functionResults.map(result => result.typescript).join('\n');
1018
1115
  const classCodes = classResults.map(result => result.typescript).join('\n');
@@ -1025,7 +1122,9 @@ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string):
1025
1122
  return kind === 'class' && !c.decorators.includes('__typed_dict__');
1026
1123
  });
1027
1124
  const needsRuntime = module.functions.length > 0 || hasRuntimeClasses;
1028
- const bridgeDecl = needsRuntime ? `import { getRuntimeBridge } from 'tywrap/runtime';\n\n` : '';
1125
+ const bridgeDecl = needsRuntime
1126
+ ? `import { createReturnValidator, getRuntimeBridge, type ReturnSchema } from 'tywrap/runtime';\n\n${this.emitReturnDefinitions(module)}`
1127
+ : '';
1029
1128
 
1030
1129
  const ts = `${`${header}${bridgeDecl}${functionCodes}\n${classCodes}\n${typeAliasCodes}`.trimEnd()}\n`;
1031
1130
  const declaration = `${`${declarationHeader}${functionResults
@@ -1067,23 +1166,27 @@ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string):
1067
1166
  };
1068
1167
  }
1069
1168
 
1070
- private typeToTs(type: TypescriptType): string {
1169
+ private typeToTs(
1170
+ type: TypescriptType,
1171
+ ctx?: GenericRenderContext,
1172
+ mappingContext: 'value' | 'return' = 'value'
1173
+ ): string {
1071
1174
  switch (type.kind) {
1072
1175
  case 'primitive':
1073
1176
  return type.name;
1074
1177
  case 'array':
1075
- return `${this.typeToTs(type.elementType)}[]`;
1178
+ return `${this.typeToTs(type.elementType, ctx, mappingContext)}[]`;
1076
1179
  case 'tuple': {
1077
1180
  const t = type as { kind: 'tuple'; elementTypes: TypescriptType[] };
1078
- const parts = t.elementTypes.map(e => this.typeToTs(e)).join(', ');
1181
+ const parts = t.elementTypes.map(e => this.typeToTs(e, ctx, mappingContext)).join(', ');
1079
1182
  return `[${parts}]`;
1080
1183
  }
1081
1184
  case 'object': {
1082
1185
  const t = type;
1083
1186
  // If it's a simple Record<string, T> pattern, use that syntax
1084
1187
  if (t.properties.length === 0 && t.indexSignature) {
1085
- const keyType = this.typeToTs(t.indexSignature.keyType);
1086
- const valueType = this.typeToTs(t.indexSignature.valueType);
1188
+ const keyType = this.typeToTs(t.indexSignature.keyType, ctx, mappingContext);
1189
+ const valueType = this.typeToTs(t.indexSignature.valueType, ctx, mappingContext);
1087
1190
  if (keyType === 'string') {
1088
1191
  return `Record<string, ${valueType}>`;
1089
1192
  }
@@ -1092,34 +1195,56 @@ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string):
1092
1195
  const props = t.properties
1093
1196
  .map(
1094
1197
  p =>
1095
- `${p.readonly ? 'readonly ' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type)};`
1198
+ `${p.readonly ? 'readonly ' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type, ctx, mappingContext)};`
1096
1199
  )
1097
1200
  .join(' ');
1098
1201
  const indexSig = t.indexSignature
1099
- ? `[key: ${this.typeToTs(t.indexSignature.keyType)}]: ${this.typeToTs(t.indexSignature.valueType)};`
1202
+ ? `[key: ${this.typeToTs(t.indexSignature.keyType, ctx, mappingContext)}]: ${this.typeToTs(t.indexSignature.valueType, ctx, mappingContext)};`
1100
1203
  : '';
1101
1204
  return `{ ${props} ${indexSig} }`;
1102
1205
  }
1103
1206
  case 'union':
1104
- return type.types.map(t => this.typeToTs(t)).join(' | ');
1207
+ return type.types.map(t => this.typeToTs(t, ctx, mappingContext)).join(' | ');
1105
1208
  case 'function': {
1106
1209
  const ft = type;
1107
1210
  const params = ft.parameters
1108
1211
  .map(
1109
- p => `${p.rest ? '...' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type)}`
1212
+ p =>
1213
+ `${p.rest ? '...' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type, ctx, mappingContext)}`
1110
1214
  )
1111
1215
  .join(', ');
1112
- return `(${params}) => ${this.typeToTs(ft.returnType)}`;
1216
+ return `(${params}) => ${this.typeToTs(ft.returnType, ctx, mappingContext)}`;
1113
1217
  }
1114
1218
  case 'generic': {
1115
- const g = type as { kind: 'generic'; name: string; typeArgs: TypescriptType[] };
1116
- const args = g.typeArgs.map(a => this.typeToTs(a)).join(', ');
1219
+ const g = type as {
1220
+ kind: 'generic';
1221
+ name: string;
1222
+ module?: string;
1223
+ typeArgs: TypescriptType[];
1224
+ };
1225
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(g.name)) {
1226
+ return this.degradeType(g);
1227
+ }
1228
+ if (mappingContext === 'return' && this.nonDecodableReturnGenerics.has(g.name)) {
1229
+ return this.degradeType(g);
1230
+ }
1231
+ if (ctx && !this.builtinGenericNames.has(g.name) && !this.isLocalTypeIdentity(g, ctx)) {
1232
+ return this.degradeType(g);
1233
+ }
1234
+ const args = g.typeArgs.map(a => this.typeToTs(a, ctx, mappingContext)).join(', ');
1117
1235
  return `${g.name}<${args}>`;
1118
1236
  }
1119
1237
  case 'custom': {
1120
- const c = type as { kind: 'custom'; name: string };
1238
+ const c = type as { kind: 'custom'; name: string; module?: string };
1121
1239
  if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(c.name)) {
1122
- return 'unknown';
1240
+ return this.degradeType(c);
1241
+ }
1242
+ if (
1243
+ ctx &&
1244
+ !this.isLocalTypeIdentity(c, ctx) &&
1245
+ !(c.module === 'typing' && ctx.emittedNames.has(c.name))
1246
+ ) {
1247
+ return this.degradeType(c);
1123
1248
  }
1124
1249
  return c.name;
1125
1250
  }