tywrap 0.7.0 → 0.9.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 (165) hide show
  1. package/README.md +36 -11
  2. package/SECURITY.md +39 -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/emit-call.d.ts.map +1 -1
  11. package/dist/core/emit-call.js +1 -1
  12. package/dist/core/emit-call.js.map +1 -1
  13. package/dist/core/generator.d.ts +23 -4
  14. package/dist/core/generator.d.ts.map +1 -1
  15. package/dist/core/generator.js +243 -170
  16. package/dist/core/generator.js.map +1 -1
  17. package/dist/core/mapper.d.ts +3 -2
  18. package/dist/core/mapper.d.ts.map +1 -1
  19. package/dist/core/mapper.js +5 -5
  20. package/dist/core/mapper.js.map +1 -1
  21. package/dist/dev.d.ts.map +1 -1
  22. package/dist/dev.js +1 -3
  23. package/dist/dev.js.map +1 -1
  24. package/dist/index.d.ts +7 -5
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +6 -4
  27. package/dist/index.js.map +1 -1
  28. package/dist/runtime/base-bridge.d.ts +3 -7
  29. package/dist/runtime/base-bridge.d.ts.map +1 -1
  30. package/dist/runtime/base-bridge.js +4 -17
  31. package/dist/runtime/base-bridge.js.map +1 -1
  32. package/dist/runtime/bounded-context.d.ts +3 -22
  33. package/dist/runtime/bounded-context.d.ts.map +1 -1
  34. package/dist/runtime/bounded-context.js +13 -53
  35. package/dist/runtime/bounded-context.js.map +1 -1
  36. package/dist/runtime/bridge-codec.d.ts +1 -1
  37. package/dist/runtime/bridge-codec.d.ts.map +1 -1
  38. package/dist/runtime/bridge-codec.js +78 -55
  39. package/dist/runtime/bridge-codec.js.map +1 -1
  40. package/dist/runtime/errors.d.ts +16 -0
  41. package/dist/runtime/errors.d.ts.map +1 -1
  42. package/dist/runtime/errors.js +17 -0
  43. package/dist/runtime/errors.js.map +1 -1
  44. package/dist/runtime/frame-codec.d.ts +111 -0
  45. package/dist/runtime/frame-codec.d.ts.map +1 -0
  46. package/dist/runtime/frame-codec.js +352 -0
  47. package/dist/runtime/frame-codec.js.map +1 -0
  48. package/dist/runtime/http-transport.d.ts +1 -1
  49. package/dist/runtime/http-transport.d.ts.map +1 -1
  50. package/dist/runtime/http-transport.js +1 -1
  51. package/dist/runtime/http-transport.js.map +1 -1
  52. package/dist/runtime/index.d.ts +2 -1
  53. package/dist/runtime/index.d.ts.map +1 -1
  54. package/dist/runtime/index.js +2 -1
  55. package/dist/runtime/index.js.map +1 -1
  56. package/dist/runtime/node.d.ts +7 -23
  57. package/dist/runtime/node.d.ts.map +1 -1
  58. package/dist/runtime/node.js +5 -78
  59. package/dist/runtime/node.js.map +1 -1
  60. package/dist/runtime/pooled-transport.d.ts +120 -59
  61. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  62. package/dist/runtime/pooled-transport.js +345 -78
  63. package/dist/runtime/pooled-transport.js.map +1 -1
  64. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
  65. package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
  66. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
  67. package/dist/runtime/pyodide-transport.d.ts +1 -1
  68. package/dist/runtime/pyodide-transport.d.ts.map +1 -1
  69. package/dist/runtime/pyodide-transport.js +2 -3
  70. package/dist/runtime/pyodide-transport.js.map +1 -1
  71. package/dist/runtime/rpc-client.d.ts +7 -36
  72. package/dist/runtime/rpc-client.d.ts.map +1 -1
  73. package/dist/runtime/rpc-client.js +37 -72
  74. package/dist/runtime/rpc-client.js.map +1 -1
  75. package/dist/runtime/subprocess-transport.d.ts +134 -8
  76. package/dist/runtime/subprocess-transport.d.ts.map +1 -1
  77. package/dist/runtime/subprocess-transport.js +490 -65
  78. package/dist/runtime/subprocess-transport.js.map +1 -1
  79. package/dist/runtime/timed-out-request-tracker.d.ts +2 -1
  80. package/dist/runtime/timed-out-request-tracker.d.ts.map +1 -1
  81. package/dist/runtime/transport.d.ts +90 -18
  82. package/dist/runtime/transport.d.ts.map +1 -1
  83. package/dist/runtime/transport.js +21 -1
  84. package/dist/runtime/transport.js.map +1 -1
  85. package/dist/runtime/validators.d.ts +49 -0
  86. package/dist/runtime/validators.d.ts.map +1 -1
  87. package/dist/runtime/validators.js +152 -0
  88. package/dist/runtime/validators.js.map +1 -1
  89. package/dist/types/index.d.ts +14 -15
  90. package/dist/types/index.d.ts.map +1 -1
  91. package/dist/tywrap.d.ts +3 -2
  92. package/dist/tywrap.d.ts.map +1 -1
  93. package/dist/tywrap.js +140 -21
  94. package/dist/tywrap.js.map +1 -1
  95. package/dist/utils/cache.d.ts +3 -16
  96. package/dist/utils/cache.d.ts.map +1 -1
  97. package/dist/utils/codec.d.ts +1 -0
  98. package/dist/utils/codec.d.ts.map +1 -1
  99. package/dist/utils/codec.js +165 -17
  100. package/dist/utils/codec.js.map +1 -1
  101. package/dist/utils/ir-cache.d.ts +2 -1
  102. package/dist/utils/ir-cache.d.ts.map +1 -1
  103. package/dist/utils/runtime.d.ts +0 -29
  104. package/dist/utils/runtime.d.ts.map +1 -1
  105. package/dist/utils/runtime.js +16 -107
  106. package/dist/utils/runtime.js.map +1 -1
  107. package/dist/version.js +1 -1
  108. package/package.json +7 -6
  109. package/runtime/frame_codec.py +430 -0
  110. package/runtime/python_bridge.py +213 -54
  111. package/runtime/tywrap_bridge_core.py +217 -147
  112. package/src/config/index.ts +11 -0
  113. package/src/core/annotation-parser.ts +9 -5
  114. package/src/core/emit-call.ts +1 -7
  115. package/src/core/generator.ts +315 -205
  116. package/src/core/mapper.ts +8 -8
  117. package/src/dev.ts +1 -3
  118. package/src/index.ts +7 -3
  119. package/src/runtime/base-bridge.ts +5 -30
  120. package/src/runtime/bounded-context.ts +12 -67
  121. package/src/runtime/bridge-codec.ts +94 -65
  122. package/src/runtime/errors.ts +21 -0
  123. package/src/runtime/frame-codec.ts +469 -0
  124. package/src/runtime/http-transport.ts +6 -1
  125. package/src/runtime/index.ts +7 -6
  126. package/src/runtime/node.ts +17 -104
  127. package/src/runtime/pooled-transport.ts +424 -90
  128. package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
  129. package/src/runtime/pyodide-transport.ts +7 -3
  130. package/src/runtime/rpc-client.ts +58 -93
  131. package/src/runtime/subprocess-transport.ts +585 -80
  132. package/src/runtime/timed-out-request-tracker.ts +1 -1
  133. package/src/runtime/transport.ts +112 -22
  134. package/src/runtime/validators.ts +204 -0
  135. package/src/types/index.ts +21 -35
  136. package/src/tywrap.ts +157 -30
  137. package/src/utils/cache.ts +3 -3
  138. package/src/utils/codec.ts +205 -16
  139. package/src/utils/ir-cache.ts +1 -1
  140. package/src/utils/runtime.ts +17 -128
  141. package/src/version.ts +1 -1
  142. package/dist/core/discovery.d.ts +0 -103
  143. package/dist/core/discovery.d.ts.map +0 -1
  144. package/dist/core/discovery.js +0 -380
  145. package/dist/core/discovery.js.map +0 -1
  146. package/dist/core/validation.d.ts +0 -102
  147. package/dist/core/validation.d.ts.map +0 -1
  148. package/dist/core/validation.js +0 -490
  149. package/dist/core/validation.js.map +0 -1
  150. package/dist/runtime/base.d.ts +0 -22
  151. package/dist/runtime/base.d.ts.map +0 -1
  152. package/dist/runtime/base.js +0 -23
  153. package/dist/runtime/base.js.map +0 -1
  154. package/dist/runtime/transport-pool.d.ts +0 -196
  155. package/dist/runtime/transport-pool.d.ts.map +0 -1
  156. package/dist/runtime/transport-pool.js +0 -418
  157. package/dist/runtime/transport-pool.js.map +0 -1
  158. package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
  159. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  160. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  161. package/runtime/safe_codec.py +0 -352
  162. package/src/core/discovery.ts +0 -477
  163. package/src/core/validation.ts +0 -729
  164. package/src/runtime/base.ts +0 -24
  165. 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,155 @@ 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
+ return definitions.has(leaf) ? { kind: 'ref', name: leaf } : { kind: 'any' };
516
+ }
517
+ case 'typevar':
518
+ case 'paramspec':
519
+ case 'paramspec_args':
520
+ case 'paramspec_kwargs':
521
+ case 'typevartuple':
522
+ case 'unpack':
523
+ case 'callable':
524
+ return { kind: 'any' };
525
+ }
526
+ };
527
+ return schema(type);
528
+ }
529
+
530
+ private returnDefinitions(module: PythonModule): ReturnDefinitionNames {
531
+ return new Set(
532
+ module.classes
533
+ .filter(cls => cls.kind === 'typed_dict' || cls.decorators.includes('__typed_dict__'))
534
+ .map(cls => cls.name)
535
+ );
536
+ }
537
+
538
+ private emitReturnDefinitions(module: PythonModule): string {
539
+ const definitions = this.returnDefinitions(module);
540
+ const entries = module.classes
541
+ .filter(cls => cls.kind === 'typed_dict' || cls.decorators.includes('__typed_dict__'))
542
+ .map(cls => {
543
+ const fields = Object.fromEntries(
544
+ cls.properties.map(property => [
545
+ property.name,
546
+ {
547
+ schema: this.returnSchema(property.type, definitions),
548
+ optional: property.optional === true,
549
+ },
550
+ ])
551
+ );
552
+ return [cls.name, { kind: 'record', fields }];
553
+ });
554
+ return `const __tywrapReturnDefinitions: Record<string, ReturnSchema> = ${JSON.stringify(Object.fromEntries(entries))};\n\n`;
555
+ }
556
+
360
557
  generateFunctionWrapper(
361
558
  func: PythonFunction,
362
559
  moduleName?: string,
363
- annotatedJSDoc = false
560
+ annotatedJSDoc = false,
561
+ localDeclaredNames: Set<string> = new Set(),
562
+ returnDefinitions: ReturnDefinitionNames = new Set()
364
563
  ): GeneratedCode {
365
564
  const jsdoc = this.generateJsDoc(
366
565
  func.docstring,
@@ -379,7 +578,8 @@ export class CodeGenerator {
379
578
  const genericContext = this.buildGenericRenderContext(
380
579
  this.getTypeParameters(func.typeParameters),
381
580
  [func.returnType, ...filteredParams.map(param => param.type)],
382
- moduleName
581
+ moduleName,
582
+ localDeclaredNames
383
583
  );
384
584
  const typeParamDecl = genericContext.declaration;
385
585
 
@@ -447,6 +647,8 @@ export class CodeGenerator {
447
647
  const returnType = this.typeToTsFromPython(func.returnType, genericContext, 'return');
448
648
  const fname = this.escapeIdentifier(func.name);
449
649
  const moduleId = moduleName ?? '__main__';
650
+ const validatorName = `__validate${this.escapeIdentifier(func.name, { preserveCase: true })}Result`;
651
+ const returnValidator = `const ${validatorName} = createReturnValidator(${JSON.stringify(this.returnSchema(func.returnType, returnDefinitions))}, ${JSON.stringify(`${moduleId}.${func.name}`)}, __tywrapReturnDefinitions);\n\n`;
450
652
 
451
653
  // Overloads: generate trailing optional parameter drop variants (exclude *args/**kwargs).
452
654
  // Why: Python APIs frequently have many optional tail params. TypeScript callers expect
@@ -535,10 +737,8 @@ export class CodeGenerator {
535
737
  const callPreludeLines = emitCallPrelude(callDescriptor, this.callEmitHelpers());
536
738
  const callPrelude = callPreludeLines.length > 0 ? `${callPreludeLines.join('\n')}\n` : '';
537
739
 
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
- });
740
+ const ts = `${jsdoc}${returnValidator}${overloadDecl}export async function ${fname}${typeParamDecl}(${paramDecl}): Promise<${returnType}> {
741
+ ${callPrelude}${guards} return getRuntimeBridge().call<${returnType}>('${moduleId}', '${func.name}', __args, ${hasKwArgs ? '__kwargs' : 'undefined'}, ${validatorName});
542
742
  }
543
743
  `;
544
744
 
@@ -553,8 +753,12 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
553
753
  generateClassWrapper(
554
754
  cls: PythonClass,
555
755
  moduleName?: string,
556
- _annotatedJSDoc = false
756
+ _annotatedJSDoc = false,
757
+ localDeclaredNames: Set<string> = new Set(),
758
+ returnDefinitions: ReturnDefinitionNames = new Set()
557
759
  ): GeneratedCode {
760
+ const moduleDeclaredNames = new Set(localDeclaredNames);
761
+ moduleDeclaredNames.add(cls.name);
558
762
  const jsdoc = this.generateJsDoc(cls.docstring);
559
763
  const classGenericContext = this.buildGenericRenderContext(
560
764
  this.getTypeParameters(cls.typeParameters),
@@ -566,14 +770,11 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
566
770
  ...method.parameters.map(p => p.type),
567
771
  ]),
568
772
  ],
569
- moduleName
773
+ moduleName,
774
+ moduleDeclaredNames
570
775
  );
571
776
  const classTypeParamDecl = classGenericContext.declaration;
572
777
  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
778
  const wrapAlias = (body: string): GeneratedCode => {
578
779
  const ts = `${jsdoc}export type ${cname}${classTypeParamDecl} = ${body}\n`;
579
780
  return this.wrap(ts, ts, [cls.name]);
@@ -621,7 +822,8 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
621
822
  const methodOwnGenericContext = this.buildGenericRenderContext(
622
823
  this.getTypeParameters(m.typeParameters),
623
824
  [m.returnType, ...fparams.map(param => param.type)],
624
- moduleName
825
+ moduleName,
826
+ moduleDeclaredNames
625
827
  );
626
828
  const methodGenericContext = this.mergeGenericRenderContexts(
627
829
  classGenericContext,
@@ -659,19 +861,21 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
659
861
  const methodDeclarations: string[] = [];
660
862
 
661
863
  sortedMethods
662
- .filter(method => method.name !== '__init__')
864
+ .filter(
865
+ method =>
866
+ method.name !== '__init__' &&
867
+ (method.methodKind === 'class' || method.methodKind === 'static')
868
+ )
663
869
  .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 ' : '';
870
+ // v0.9 wrappers expose only class/static methods. They route through
871
+ // the ordinary module call path and never retain process-local state.
872
+ const staticPrefix = 'static ';
670
873
  const fparams = method.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
671
874
  const methodOwnGenericContext = this.buildGenericRenderContext(
672
875
  this.getTypeParameters(method.typeParameters),
673
876
  [method.returnType, ...fparams.map(param => param.type)],
674
- moduleName
877
+ moduleName,
878
+ moduleDeclaredNames
675
879
  );
676
880
  const methodGenericContext = this.mergeGenericRenderContexts(
677
881
  classGenericContext,
@@ -737,6 +941,8 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
737
941
  'return'
738
942
  );
739
943
  const mname = this.escapeIdentifier(method.name);
944
+ const validatorName = `__validate${this.escapeIdentifier(cls.name, { preserveCase: true })}${this.escapeIdentifier(method.name, { preserveCase: true })}Result`;
945
+ const returnValidator = `const ${validatorName} = createReturnValidator(${JSON.stringify(this.returnSchema(method.returnType, returnDefinitions))}, ${JSON.stringify(`${moduleId}.${cls.name}.${method.name}`)}, __tywrapReturnDefinitions);\n\n`;
740
946
 
741
947
  const overloads: string[] = [];
742
948
  if (needsKwargsParam && requiredKwOnlyNames.length > 0) {
@@ -784,193 +990,49 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
784
990
  const guardLines = emitArgGuards(callDescriptor);
785
991
  const guards = guardLines.length > 0 ? `${guardLines.join('\n')}\n` : '';
786
992
 
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
- })`;
993
+ const callExpr = `getRuntimeBridge().call<${returnType}>('${moduleId}', '${cls.name}.${method.name}', __args, ${needsKwargsParam ? '__kwargs' : 'undefined'}, ${validatorName})`;
794
994
  methodBodies.push(`${overloadDecl} ${staticPrefix}async ${mname}${methodTypeParamDecl}(${paramsDecl}): Promise<${returnType}> {
795
- ${callPrelude}${guards} return ${callExpr};
995
+ ${returnValidator}${callPrelude}${guards} return ${callExpr};
796
996
  }`);
797
997
  methodDeclarations.push(
798
998
  `${overloadDecl}${overloads.length > 0 ? '' : ` ${staticPrefix}${mname}${methodTypeParamDecl}(${paramsDecl}): Promise<${returnType}>;\n`}`
799
999
  );
800
1000
  });
801
1001
 
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
1002
  const methodsSection = methodBodies.length > 0 ? `\n${methodBodies.join('\n')}\n` : '\n';
942
1003
  const declarationMethodsSection =
943
1004
  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)`;
1005
+ // The constructor counts: a class whose only member was __init__ loses
1006
+ // create(), so it needs the migration note as much as one with methods.
1007
+ const omittedInstanceMembers =
1008
+ cls.methods.some(method => method.methodKind !== 'class' && method.methodKind !== 'static') ||
1009
+ (cls.accessors?.length ?? 0) > 0;
1010
+ const migrationNote = omittedInstanceMembers
1011
+ ? ' // NOTE: Instance members are not generated in v0.9; migrate this API to value-returning module functions.\n'
1012
+ : '';
946
1013
  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); }
1014
+ ${migrationNote}${methodsSection}
956
1015
  }
957
1016
  `;
958
1017
 
959
1018
  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>;
1019
+ ${migrationNote}${declarationMethodsSection}
963
1020
  }
964
1021
  `;
965
1022
 
966
1023
  return this.wrap(ts, declaration, [cls.name]);
967
1024
  }
968
1025
 
969
- generateTypeAlias(alias: PythonTypeAlias, moduleName?: string): GeneratedCode {
1026
+ generateTypeAlias(
1027
+ alias: PythonTypeAlias,
1028
+ moduleName?: string,
1029
+ localDeclaredNames: Set<string> = new Set()
1030
+ ): GeneratedCode {
970
1031
  const genericContext = this.buildGenericRenderContext(
971
1032
  this.getTypeParameters(alias.typeParameters),
972
1033
  [alias.type],
973
- moduleName
1034
+ moduleName,
1035
+ localDeclaredNames
974
1036
  );
975
1037
  const aliasName = this.escapeIdentifier(alias.name, { preserveCase: true });
976
1038
  const body = this.typeToTsFromPython(alias.type, genericContext, 'value');
@@ -1004,15 +1066,35 @@ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string):
1004
1066
  }
1005
1067
 
1006
1068
  generateModuleDefinition(module: PythonModule, annotatedJSDoc = false): GeneratedCode {
1069
+ const localDeclaredNames = new Set([
1070
+ ...module.classes.map(cls => cls.name),
1071
+ ...(module.typeAliases ?? []).map(alias => alias.name),
1072
+ ]);
1007
1073
  const functionResults = [...module.functions]
1008
1074
  .sort((a, b) => a.name.localeCompare(b.name))
1009
- .map(f => this.generateFunctionWrapper(f, module.name, annotatedJSDoc));
1075
+ .map(f =>
1076
+ this.generateFunctionWrapper(
1077
+ f,
1078
+ module.name,
1079
+ annotatedJSDoc,
1080
+ localDeclaredNames,
1081
+ this.returnDefinitions(module)
1082
+ )
1083
+ );
1010
1084
  const classResults = [...module.classes]
1011
1085
  .sort((a, b) => a.name.localeCompare(b.name))
1012
- .map(c => this.generateClassWrapper(c, module.name, annotatedJSDoc));
1086
+ .map(c =>
1087
+ this.generateClassWrapper(
1088
+ c,
1089
+ module.name,
1090
+ annotatedJSDoc,
1091
+ localDeclaredNames,
1092
+ this.returnDefinitions(module)
1093
+ )
1094
+ );
1013
1095
  const typeAliasResults = [...(module.typeAliases ?? [])]
1014
1096
  .sort((a, b) => a.name.localeCompare(b.name))
1015
- .map(alias => this.generateTypeAlias(alias, module.name));
1097
+ .map(alias => this.generateTypeAlias(alias, module.name, localDeclaredNames));
1016
1098
 
1017
1099
  const functionCodes = functionResults.map(result => result.typescript).join('\n');
1018
1100
  const classCodes = classResults.map(result => result.typescript).join('\n');
@@ -1025,7 +1107,9 @@ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string):
1025
1107
  return kind === 'class' && !c.decorators.includes('__typed_dict__');
1026
1108
  });
1027
1109
  const needsRuntime = module.functions.length > 0 || hasRuntimeClasses;
1028
- const bridgeDecl = needsRuntime ? `import { getRuntimeBridge } from 'tywrap/runtime';\n\n` : '';
1110
+ const bridgeDecl = needsRuntime
1111
+ ? `import { createReturnValidator, getRuntimeBridge, type ReturnSchema } from 'tywrap/runtime';\n\n${this.emitReturnDefinitions(module)}`
1112
+ : '';
1029
1113
 
1030
1114
  const ts = `${`${header}${bridgeDecl}${functionCodes}\n${classCodes}\n${typeAliasCodes}`.trimEnd()}\n`;
1031
1115
  const declaration = `${`${declarationHeader}${functionResults
@@ -1067,23 +1151,27 @@ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string):
1067
1151
  };
1068
1152
  }
1069
1153
 
1070
- private typeToTs(type: TypescriptType): string {
1154
+ private typeToTs(
1155
+ type: TypescriptType,
1156
+ ctx?: GenericRenderContext,
1157
+ mappingContext: 'value' | 'return' = 'value'
1158
+ ): string {
1071
1159
  switch (type.kind) {
1072
1160
  case 'primitive':
1073
1161
  return type.name;
1074
1162
  case 'array':
1075
- return `${this.typeToTs(type.elementType)}[]`;
1163
+ return `${this.typeToTs(type.elementType, ctx, mappingContext)}[]`;
1076
1164
  case 'tuple': {
1077
1165
  const t = type as { kind: 'tuple'; elementTypes: TypescriptType[] };
1078
- const parts = t.elementTypes.map(e => this.typeToTs(e)).join(', ');
1166
+ const parts = t.elementTypes.map(e => this.typeToTs(e, ctx, mappingContext)).join(', ');
1079
1167
  return `[${parts}]`;
1080
1168
  }
1081
1169
  case 'object': {
1082
1170
  const t = type;
1083
1171
  // If it's a simple Record<string, T> pattern, use that syntax
1084
1172
  if (t.properties.length === 0 && t.indexSignature) {
1085
- const keyType = this.typeToTs(t.indexSignature.keyType);
1086
- const valueType = this.typeToTs(t.indexSignature.valueType);
1173
+ const keyType = this.typeToTs(t.indexSignature.keyType, ctx, mappingContext);
1174
+ const valueType = this.typeToTs(t.indexSignature.valueType, ctx, mappingContext);
1087
1175
  if (keyType === 'string') {
1088
1176
  return `Record<string, ${valueType}>`;
1089
1177
  }
@@ -1092,34 +1180,56 @@ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string):
1092
1180
  const props = t.properties
1093
1181
  .map(
1094
1182
  p =>
1095
- `${p.readonly ? 'readonly ' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type)};`
1183
+ `${p.readonly ? 'readonly ' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type, ctx, mappingContext)};`
1096
1184
  )
1097
1185
  .join(' ');
1098
1186
  const indexSig = t.indexSignature
1099
- ? `[key: ${this.typeToTs(t.indexSignature.keyType)}]: ${this.typeToTs(t.indexSignature.valueType)};`
1187
+ ? `[key: ${this.typeToTs(t.indexSignature.keyType, ctx, mappingContext)}]: ${this.typeToTs(t.indexSignature.valueType, ctx, mappingContext)};`
1100
1188
  : '';
1101
1189
  return `{ ${props} ${indexSig} }`;
1102
1190
  }
1103
1191
  case 'union':
1104
- return type.types.map(t => this.typeToTs(t)).join(' | ');
1192
+ return type.types.map(t => this.typeToTs(t, ctx, mappingContext)).join(' | ');
1105
1193
  case 'function': {
1106
1194
  const ft = type;
1107
1195
  const params = ft.parameters
1108
1196
  .map(
1109
- p => `${p.rest ? '...' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type)}`
1197
+ p =>
1198
+ `${p.rest ? '...' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type, ctx, mappingContext)}`
1110
1199
  )
1111
1200
  .join(', ');
1112
- return `(${params}) => ${this.typeToTs(ft.returnType)}`;
1201
+ return `(${params}) => ${this.typeToTs(ft.returnType, ctx, mappingContext)}`;
1113
1202
  }
1114
1203
  case 'generic': {
1115
- const g = type as { kind: 'generic'; name: string; typeArgs: TypescriptType[] };
1116
- const args = g.typeArgs.map(a => this.typeToTs(a)).join(', ');
1204
+ const g = type as {
1205
+ kind: 'generic';
1206
+ name: string;
1207
+ module?: string;
1208
+ typeArgs: TypescriptType[];
1209
+ };
1210
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(g.name)) {
1211
+ return this.degradeType(g);
1212
+ }
1213
+ if (mappingContext === 'return' && this.nonDecodableReturnGenerics.has(g.name)) {
1214
+ return this.degradeType(g);
1215
+ }
1216
+ if (ctx && !this.builtinGenericNames.has(g.name) && !this.isLocalTypeIdentity(g, ctx)) {
1217
+ return this.degradeType(g);
1218
+ }
1219
+ const args = g.typeArgs.map(a => this.typeToTs(a, ctx, mappingContext)).join(', ');
1117
1220
  return `${g.name}<${args}>`;
1118
1221
  }
1119
1222
  case 'custom': {
1120
- const c = type as { kind: 'custom'; name: string };
1223
+ const c = type as { kind: 'custom'; name: string; module?: string };
1121
1224
  if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(c.name)) {
1122
- return 'unknown';
1225
+ return this.degradeType(c);
1226
+ }
1227
+ if (
1228
+ ctx &&
1229
+ !this.isLocalTypeIdentity(c, ctx) &&
1230
+ !(c.module === 'typing' && ctx.emittedNames.has(c.name))
1231
+ ) {
1232
+ return this.degradeType(c);
1123
1233
  }
1124
1234
  return c.name;
1125
1235
  }