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
@@ -6,6 +6,28 @@ import { emitArgGuards, emitCallPrelude, } from './emit-call.js';
6
6
  import { TypeMapper } from './mapper.js';
7
7
  export class CodeGenerator {
8
8
  mapper;
9
+ onTypeDegrade;
10
+ builtinGenericNames = new Set([
11
+ 'Array',
12
+ 'AsyncIterator',
13
+ 'Generator',
14
+ 'Iterable',
15
+ 'Iterator',
16
+ 'Promise',
17
+ 'Record',
18
+ ]);
19
+ // Iterator-protocol objects can never cross the bridge: the Python side
20
+ // rejects them loudly at serialization, so a return typed as one of these
21
+ // could never carry a value. Returns degrade to `unknown` (and count as a
22
+ // degrade for --fail-on-warn). Iterable/Sequence are NOT here — a list
23
+ // satisfies those annotations and decodes to an array, which structurally
24
+ // satisfies the emitted TS type.
25
+ nonDecodableReturnGenerics = new Set([
26
+ 'Generator',
27
+ 'AsyncGenerator',
28
+ 'Iterator',
29
+ 'AsyncIterator',
30
+ ]);
9
31
  reservedTsIdentifiers = new Set([
10
32
  'default',
11
33
  'delete',
@@ -34,8 +56,9 @@ export class CodeGenerator {
34
56
  'true',
35
57
  'false',
36
58
  ]);
37
- constructor(mapper = new TypeMapper()) {
59
+ constructor(mapper = new TypeMapper(), options = {}) {
38
60
  this.mapper = mapper;
61
+ this.onTypeDegrade = options.onTypeDegrade;
39
62
  }
40
63
  /**
41
64
  * Convert Python snake_case to TypeScript camelCase and escape reserved words
@@ -93,7 +116,7 @@ export class CodeGenerator {
93
116
  getTypeParameters(typeParameters) {
94
117
  return typeParameters ?? [];
95
118
  }
96
- buildGenericRenderContext(typeParameters, types, currentModule) {
119
+ buildGenericRenderContext(typeParameters, types, currentModule, localDeclaredNames = new Set()) {
97
120
  const callableParamSpecs = new Set();
98
121
  types.forEach(type => this.collectCallableParamSpecs(type, callableParamSpecs));
99
122
  const emitted = [];
@@ -116,6 +139,7 @@ export class CodeGenerator {
116
139
  });
117
140
  return {
118
141
  currentModule,
142
+ localDeclaredNames,
119
143
  declaration: emitted.length > 0 ? `<${emitted.map(param => param.declaration).join(', ')}>` : '',
120
144
  typeArguments: emitted.length > 0 ? `<${emitted.map(param => param.name).join(', ')}>` : '',
121
145
  emittedNames,
@@ -125,6 +149,7 @@ export class CodeGenerator {
125
149
  mergeGenericRenderContexts(outer, inner) {
126
150
  return {
127
151
  currentModule: inner.currentModule ?? outer.currentModule,
152
+ localDeclaredNames: inner.localDeclaredNames,
128
153
  declaration: inner.declaration,
129
154
  typeArguments: inner.typeArguments,
130
155
  emittedNames: new Set([...outer.emittedNames, ...inner.emittedNames]),
@@ -240,7 +265,18 @@ export class CodeGenerator {
240
265
  }
241
266
  }
242
267
  typeToTsFromPython(type, ctx, mappingContext) {
243
- return this.typeToTs(this.mapper.mapPythonType(this.sanitizeType(type, ctx), mappingContext));
268
+ return this.typeToTs(this.mapper.mapPythonType(this.sanitizeType(type, ctx), mappingContext), ctx, mappingContext);
269
+ }
270
+ isLocalTypeIdentity(type, ctx) {
271
+ if (!ctx.localDeclaredNames.has(type.name)) {
272
+ return false;
273
+ }
274
+ return type.module === undefined || type.module === ctx.currentModule;
275
+ }
276
+ degradeType(type) {
277
+ const identity = type.module ? `${type.module}.${type.name}` : type.name;
278
+ this.onTypeDegrade?.(identity);
279
+ return 'unknown';
244
280
  }
245
281
  renderLooksLikeKwargsExpr(valueExpr, options) {
246
282
  const base = `typeof ${valueExpr} === 'object' && ${valueExpr} !== null && !globalThis.Array.isArray(${valueExpr}) && (Object.getPrototypeOf(${valueExpr}) === Object.prototype || Object.getPrototypeOf(${valueExpr}) === null)`;
@@ -270,7 +306,154 @@ export class CodeGenerator {
270
306
  renderLooksLikeKwargsExpr: (valueExpr, options) => this.renderLooksLikeKwargsExpr(valueExpr, options),
271
307
  };
272
308
  }
273
- generateFunctionWrapper(func, moduleName, annotatedJSDoc = false) {
309
+ /**
310
+ * Preserve Python provenance in generated return validators instead of
311
+ * reconstructing a checker from the final TypeScript spelling. In particular
312
+ * pandas/NumPy values retain their marker contract after Arrow decoding.
313
+ */
314
+ returnSchema(type, definitions = new Set()) {
315
+ // A bare `-> None` return maps to TS void and validates nothing, but a
316
+ // NESTED None (union member, tuple element) is a real wire value — it
317
+ // decodes to null and must be checked as null, or a union containing it
318
+ // would carry an any-member and accept everything.
319
+ if (type.kind === 'primitive' && type.name === 'None') {
320
+ return { kind: 'any' };
321
+ }
322
+ const schema = (current) => {
323
+ switch (current.kind) {
324
+ case 'primitive':
325
+ return current.name === 'int' || current.name === 'float'
326
+ ? { kind: 'primitive', type: 'number' }
327
+ : current.name === 'str'
328
+ ? { kind: 'primitive', type: 'string' }
329
+ : current.name === 'bool'
330
+ ? { kind: 'primitive', type: 'boolean' }
331
+ : current.name === 'bytes'
332
+ ? { kind: 'primitive', type: 'Uint8Array' }
333
+ : current.name === 'None'
334
+ ? { kind: 'primitive', type: 'null' }
335
+ : { kind: 'any' };
336
+ case 'literal':
337
+ return { kind: 'literal', value: current.value };
338
+ case 'annotated':
339
+ return schema(current.base);
340
+ case 'final':
341
+ case 'classvar':
342
+ return schema(current.type);
343
+ case 'optional':
344
+ return {
345
+ kind: 'union',
346
+ options: [schema(current.type), { kind: 'primitive', type: 'null' }],
347
+ };
348
+ case 'union':
349
+ return { kind: 'union', options: current.types.map(schema) };
350
+ case 'collection': {
351
+ if (current.name === 'tuple') {
352
+ if (current.itemTypes[1]?.kind === 'custom' && current.itemTypes[1].name === '...') {
353
+ return {
354
+ kind: 'array',
355
+ element: schema(current.itemTypes[0] ?? { kind: 'custom', name: 'Any' }),
356
+ };
357
+ }
358
+ return { kind: 'tuple', elements: current.itemTypes.map(schema) };
359
+ }
360
+ if (current.name === 'dict') {
361
+ return {
362
+ kind: 'record',
363
+ values: schema(current.itemTypes[1] ?? { kind: 'custom', name: 'Any' }),
364
+ };
365
+ }
366
+ return {
367
+ kind: 'array',
368
+ element: schema(current.itemTypes[0] ?? { kind: 'custom', name: 'Any' }),
369
+ };
370
+ }
371
+ case 'generic': {
372
+ const leaf = current.name.split('.').at(-1) ?? current.name;
373
+ if (leaf === 'NDArray' || leaf === 'ndarray') {
374
+ return { kind: 'marker', marker: 'ndarray' };
375
+ }
376
+ // Iterator/Generator are deliberately absent: those returns emit
377
+ // `unknown` (see nonDecodableReturnGenerics) and validate nothing.
378
+ if (['list', 'List', 'Sequence', 'Iterable', 'set', 'frozenset'].includes(leaf)) {
379
+ return {
380
+ kind: 'array',
381
+ element: schema(current.typeArgs[0] ?? { kind: 'custom', name: 'Any' }),
382
+ };
383
+ }
384
+ if (['dict', 'Dict', 'Mapping', 'MutableMapping'].includes(leaf)) {
385
+ return {
386
+ kind: 'record',
387
+ values: schema(current.typeArgs[1] ?? { kind: 'custom', name: 'Any' }),
388
+ };
389
+ }
390
+ return definitions.has(leaf) ? { kind: 'ref', name: leaf } : { kind: 'any' };
391
+ }
392
+ case 'custom': {
393
+ const leaf = current.name.split('.').at(-1) ?? current.name;
394
+ const full = `${current.module ?? ''}.${leaf}`;
395
+ if (leaf === 'None') {
396
+ return { kind: 'primitive', type: 'null' };
397
+ }
398
+ if (leaf === 'Any' || leaf === 'object' || leaf === 'NoReturn' || leaf === 'Never') {
399
+ return { kind: 'any' };
400
+ }
401
+ if (leaf === 'DataFrame' && (!current.module || current.module.startsWith('pandas'))) {
402
+ return { kind: 'marker', marker: 'dataframe' };
403
+ }
404
+ if (leaf === 'Series' && (!current.module || current.module.startsWith('pandas'))) {
405
+ return { kind: 'marker', marker: 'series' };
406
+ }
407
+ if (leaf === 'ndarray' || leaf === 'NDArray' || full.includes('numpy')) {
408
+ return { kind: 'marker', marker: 'ndarray' };
409
+ }
410
+ if (['csr_matrix', 'csc_matrix', 'coo_matrix', 'spmatrix'].includes(leaf) &&
411
+ (!current.module || current.module.startsWith('scipy'))) {
412
+ return { kind: 'marker', marker: 'scipy.sparse' };
413
+ }
414
+ if (leaf === 'Tensor' && (!current.module || current.module.startsWith('torch'))) {
415
+ return { kind: 'marker', marker: 'torch.tensor' };
416
+ }
417
+ if (leaf === 'BaseEstimator' &&
418
+ (!current.module || current.module.startsWith('sklearn'))) {
419
+ return { kind: 'marker', marker: 'sklearn.estimator' };
420
+ }
421
+ return definitions.has(leaf) ? { kind: 'ref', name: leaf } : { kind: 'any' };
422
+ }
423
+ case 'typevar':
424
+ case 'paramspec':
425
+ case 'paramspec_args':
426
+ case 'paramspec_kwargs':
427
+ case 'typevartuple':
428
+ case 'unpack':
429
+ case 'callable':
430
+ return { kind: 'any' };
431
+ }
432
+ };
433
+ return schema(type);
434
+ }
435
+ returnDefinitions(module) {
436
+ return new Set(module.classes
437
+ .filter(cls => cls.kind === 'typed_dict' || cls.decorators.includes('__typed_dict__'))
438
+ .map(cls => cls.name));
439
+ }
440
+ emitReturnDefinitions(module) {
441
+ const definitions = this.returnDefinitions(module);
442
+ const entries = module.classes
443
+ .filter(cls => cls.kind === 'typed_dict' || cls.decorators.includes('__typed_dict__'))
444
+ .map(cls => {
445
+ const fields = Object.fromEntries(cls.properties.map(property => [
446
+ property.name,
447
+ {
448
+ schema: this.returnSchema(property.type, definitions),
449
+ optional: property.optional === true,
450
+ },
451
+ ]));
452
+ return [cls.name, { kind: 'record', fields }];
453
+ });
454
+ return `const __tywrapReturnDefinitions: Record<string, ReturnSchema> = ${JSON.stringify(Object.fromEntries(entries))};\n\n`;
455
+ }
456
+ generateFunctionWrapper(func, moduleName, annotatedJSDoc = false, localDeclaredNames = new Set(), returnDefinitions = new Set()) {
274
457
  const jsdoc = this.generateJsDoc(func.docstring, annotatedJSDoc ? func.parameters.map(p => String(p.type)) : undefined);
275
458
  const filteredParams = func.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
276
459
  const keywordOnlyParams = filteredParams.filter(p => p.keywordOnly);
@@ -280,7 +463,7 @@ export class CodeGenerator {
280
463
  const varArgsParam = filteredParams.find(p => p.varArgs);
281
464
  const needsVarArgsArray = Boolean(varArgsParam) && needsKwargsParam;
282
465
  const positionalParams = filteredParams.filter(p => !p.keywordOnly && !p.varArgs && !p.kwArgs);
283
- const genericContext = this.buildGenericRenderContext(this.getTypeParameters(func.typeParameters), [func.returnType, ...filteredParams.map(param => param.type)], moduleName);
466
+ const genericContext = this.buildGenericRenderContext(this.getTypeParameters(func.typeParameters), [func.returnType, ...filteredParams.map(param => param.type)], moduleName, localDeclaredNames);
284
467
  const typeParamDecl = genericContext.declaration;
285
468
  const tsTypeForValue = (p) => this.typeToTsFromPython(p.type, genericContext, 'value');
286
469
  const kwargsType = (() => {
@@ -336,6 +519,8 @@ export class CodeGenerator {
336
519
  const returnType = this.typeToTsFromPython(func.returnType, genericContext, 'return');
337
520
  const fname = this.escapeIdentifier(func.name);
338
521
  const moduleId = moduleName ?? '__main__';
522
+ const validatorName = `__validate${this.escapeIdentifier(func.name, { preserveCase: true })}Result`;
523
+ const returnValidator = `const ${validatorName} = createReturnValidator(${JSON.stringify(this.returnSchema(func.returnType, returnDefinitions))}, ${JSON.stringify(`${moduleId}.${func.name}`)}, __tywrapReturnDefinitions);\n\n`;
339
524
  // Overloads: generate trailing optional parameter drop variants (exclude *args/**kwargs).
340
525
  // Why: Python APIs frequently have many optional tail params. TypeScript callers expect
341
526
  // `fn(a)`, `fn(a, b)`, ... all to typecheck. We emit a family of overloads that progressively
@@ -413,8 +598,8 @@ export class CodeGenerator {
413
598
  const guards = guardLines.length > 0 ? `${guardLines.join('\n')}\n` : '';
414
599
  const callPreludeLines = emitCallPrelude(callDescriptor, this.callEmitHelpers());
415
600
  const callPrelude = callPreludeLines.length > 0 ? `${callPreludeLines.join('\n')}\n` : '';
416
- const ts = `${jsdoc}${overloadDecl}export async function ${fname}${typeParamDecl}(${paramDecl}): Promise<${returnType}> {
417
- ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.name}', __args${hasKwArgs ? ', __kwargs' : ''});
601
+ const ts = `${jsdoc}${returnValidator}${overloadDecl}export async function ${fname}${typeParamDecl}(${paramDecl}): Promise<${returnType}> {
602
+ ${callPrelude}${guards} return getRuntimeBridge().call<${returnType}>('${moduleId}', '${func.name}', __args, ${hasKwArgs ? '__kwargs' : 'undefined'}, ${validatorName});
418
603
  }
419
604
  `;
420
605
  const declarationBody = overloads.length > 0
@@ -422,7 +607,9 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
422
607
  : `export function ${fname}${typeParamDecl}(${paramDecl}): Promise<${returnType}>;\n`;
423
608
  return this.wrap(ts, `${jsdoc}${declarationBody}`, [func.name]);
424
609
  }
425
- generateClassWrapper(cls, moduleName, _annotatedJSDoc = false) {
610
+ generateClassWrapper(cls, moduleName, _annotatedJSDoc = false, localDeclaredNames = new Set(), returnDefinitions = new Set()) {
611
+ const moduleDeclaredNames = new Set(localDeclaredNames);
612
+ moduleDeclaredNames.add(cls.name);
426
613
  const jsdoc = this.generateJsDoc(cls.docstring);
427
614
  const classGenericContext = this.buildGenericRenderContext(this.getTypeParameters(cls.typeParameters), [
428
615
  ...cls.properties.map(property => property.type),
@@ -431,11 +618,9 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
431
618
  method.returnType,
432
619
  ...method.parameters.map(p => p.type),
433
620
  ]),
434
- ], moduleName);
621
+ ], moduleName, moduleDeclaredNames);
435
622
  const classTypeParamDecl = classGenericContext.declaration;
436
623
  const cname = this.escapeIdentifier(cls.name);
437
- const classSelfType = `${cname}${classGenericContext.typeArguments}`;
438
- const tsValueType = (p) => this.typeToTsFromPython(p.type, classGenericContext, 'value');
439
624
  const wrapAlias = (body) => {
440
625
  const ts = `${jsdoc}export type ${cname}${classTypeParamDecl} = ${body}\n`;
441
626
  return this.wrap(ts, ts, [cls.name]);
@@ -469,7 +654,7 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
469
654
  .filter(m => m.name !== '__init__')
470
655
  .map(m => {
471
656
  const fparams = m.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
472
- const methodOwnGenericContext = this.buildGenericRenderContext(this.getTypeParameters(m.typeParameters), [m.returnType, ...fparams.map(param => param.type)], moduleName);
657
+ const methodOwnGenericContext = this.buildGenericRenderContext(this.getTypeParameters(m.typeParameters), [m.returnType, ...fparams.map(param => param.type)], moduleName, moduleDeclaredNames);
473
658
  const methodGenericContext = this.mergeGenericRenderContexts(classGenericContext, methodOwnGenericContext);
474
659
  const methodTypeParamDecl = methodOwnGenericContext.declaration;
475
660
  const paramsDecl = fparams
@@ -497,16 +682,14 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
497
682
  const methodBodies = [];
498
683
  const methodDeclarations = [];
499
684
  sortedMethods
500
- .filter(method => method.name !== '__init__')
685
+ .filter(method => method.name !== '__init__' &&
686
+ (method.methodKind === 'class' || method.methodKind === 'static'))
501
687
  .forEach(method => {
502
- // @classmethod and @staticmethod become `static` members invoked through
503
- // the class (`Class.method`), not the instance handle. Instance methods
504
- // (the default, or an absent methodKind) keep the existing emission so
505
- // their output stays byte-identical.
506
- const isStatic = method.methodKind === 'class' || method.methodKind === 'static';
507
- const staticPrefix = isStatic ? 'static ' : '';
688
+ // v0.9 wrappers expose only class/static methods. They route through
689
+ // the ordinary module call path and never retain process-local state.
690
+ const staticPrefix = 'static ';
508
691
  const fparams = method.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
509
- const methodOwnGenericContext = this.buildGenericRenderContext(this.getTypeParameters(method.typeParameters), [method.returnType, ...fparams.map(param => param.type)], moduleName);
692
+ const methodOwnGenericContext = this.buildGenericRenderContext(this.getTypeParameters(method.typeParameters), [method.returnType, ...fparams.map(param => param.type)], moduleName, moduleDeclaredNames);
510
693
  const methodGenericContext = this.mergeGenericRenderContexts(classGenericContext, methodOwnGenericContext);
511
694
  const methodTypeParamDecl = methodOwnGenericContext.declaration;
512
695
  const methodTsValueType = (p) => this.typeToTsFromPython(p.type, methodGenericContext, 'value');
@@ -553,6 +736,8 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
553
736
  const requiredKwOnlyNames = keywordOnlyParams.filter(p => !p.optional).map(p => p.name);
554
737
  const returnType = this.typeToTsFromPython(method.returnType, methodGenericContext, 'return');
555
738
  const mname = this.escapeIdentifier(method.name);
739
+ const validatorName = `__validate${this.escapeIdentifier(cls.name, { preserveCase: true })}${this.escapeIdentifier(method.name, { preserveCase: true })}Result`;
740
+ const returnValidator = `const ${validatorName} = createReturnValidator(${JSON.stringify(this.returnSchema(method.returnType, returnDefinitions))}, ${JSON.stringify(`${moduleId}.${cls.name}.${method.name}`)}, __tywrapReturnDefinitions);\n\n`;
556
741
  const overloads = [];
557
742
  if (needsKwargsParam && requiredKwOnlyNames.length > 0) {
558
743
  const firstOptionalIndex = positionalParams.findIndex(p => p.optional);
@@ -589,154 +774,33 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
589
774
  const callPrelude = callPreludeLines.length > 0 ? `${callPreludeLines.join('\n')}\n` : '';
590
775
  const guardLines = emitArgGuards(callDescriptor);
591
776
  const guards = guardLines.length > 0 ? `${guardLines.join('\n')}\n` : '';
592
- const callExpr = isStatic
593
- ? `getRuntimeBridge().call('${moduleId}', '${cls.name}.${method.name}', __args${needsKwargsParam ? ', __kwargs' : ''})`
594
- : `getRuntimeBridge().callMethod(this.__handle, '${method.name}', __args${needsKwargsParam ? ', __kwargs' : ''})`;
777
+ const callExpr = `getRuntimeBridge().call<${returnType}>('${moduleId}', '${cls.name}.${method.name}', __args, ${needsKwargsParam ? '__kwargs' : 'undefined'}, ${validatorName})`;
595
778
  methodBodies.push(`${overloadDecl} ${staticPrefix}async ${mname}${methodTypeParamDecl}(${paramsDecl}): Promise<${returnType}> {
596
- ${callPrelude}${guards} return ${callExpr};
779
+ ${returnValidator}${callPrelude}${guards} return ${callExpr};
597
780
  }`);
598
781
  methodDeclarations.push(`${overloadDecl}${overloads.length > 0 ? '' : ` ${staticPrefix}${mname}${methodTypeParamDecl}(${paramsDecl}): Promise<${returnType}>;\n`}`);
599
782
  });
600
- const init = cls.methods.find(m => m.name === '__init__');
601
- const ctorSpec = (() => {
602
- if (!init) {
603
- return {
604
- overloadDecl: '',
605
- declaration: ` static create${classTypeParamDecl}(...args: unknown[]): Promise<${classSelfType}>;\n`,
606
- paramsDecl: `...args: unknown[]`,
607
- callPrelude: ` const __args: unknown[] = [...args];\n`,
608
- hasKwargs: false,
609
- guardLines: [],
610
- };
611
- }
612
- const fparams = init.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
613
- const keywordOnlyParams = fparams.filter(p => p.keywordOnly);
614
- const positionalOnlyNames = fparams.filter(p => p.positionalOnly).map(p => p.name);
615
- const hasVarKwArgs = fparams.some(p => p.kwArgs);
616
- const needsKwargsParam = keywordOnlyParams.length > 0 || hasVarKwArgs;
617
- const varArgsParam = fparams.find(p => p.varArgs);
618
- const needsVarArgsArray = Boolean(varArgsParam) && needsKwargsParam;
619
- const positionalParams = fparams.filter(p => !p.keywordOnly && !p.varArgs && !p.kwArgs);
620
- const firstOptionalPosIndex = positionalParams.findIndex(p => p.optional);
621
- const requiredPosCount = firstOptionalPosIndex >= 0 ? firstOptionalPosIndex : positionalParams.length;
622
- const keywordOnlyNames = keywordOnlyParams.map(p => p.name);
623
- const renderPositionalParam = (p, forceRequired = false) => {
624
- const pname = this.escapeIdentifier(p.name);
625
- const opt = !forceRequired && p.optional ? '?' : '';
626
- return `${pname}${opt}: ${tsValueType(p)}`;
627
- };
628
- const kwargsType = (() => {
629
- if (!needsKwargsParam) {
630
- return '';
631
- }
632
- if (keywordOnlyParams.length === 0 && hasVarKwArgs) {
633
- return 'Record<string, unknown>';
634
- }
635
- const props = keywordOnlyParams
636
- .map(p => `${JSON.stringify(p.name)}${p.optional ? '?' : ''}: ${tsValueType(p)};`)
637
- .join(' ');
638
- const obj = `{ ${props} }`;
639
- return hasVarKwArgs ? `(${obj} & Record<string, unknown>)` : obj;
640
- })();
641
- const paramsDeclParts = [];
642
- positionalParams.forEach(p => {
643
- paramsDeclParts.push(renderPositionalParam(p));
644
- });
645
- if (varArgsParam) {
646
- const vname = this.escapeIdentifier(varArgsParam.name);
647
- paramsDeclParts.push(needsVarArgsArray ? `${vname}?: unknown[]` : `...${vname}: unknown[]`);
648
- }
649
- if (needsKwargsParam) {
650
- paramsDeclParts.push(`kwargs?: ${kwargsType}`);
651
- }
652
- const paramsDecl = paramsDeclParts.join(', ');
653
- const requiredKwOnlyNames = keywordOnlyParams.filter(p => !p.optional).map(p => p.name);
654
- const overloads = [];
655
- if (needsKwargsParam && requiredKwOnlyNames.length > 0) {
656
- const firstOptionalIndex = positionalParams.findIndex(p => p.optional);
657
- const requiredPosCount = firstOptionalIndex >= 0 ? firstOptionalIndex : positionalParams.length;
658
- for (let i = requiredPosCount; i <= positionalParams.length; i++) {
659
- const head = positionalParams.slice(0, i).map(p => renderPositionalParam(p, true));
660
- const rest = [];
661
- if (varArgsParam) {
662
- const vname = this.escapeIdentifier(varArgsParam.name);
663
- rest.push(needsVarArgsArray ? `${vname}: unknown[] | undefined` : `...${vname}: unknown[]`);
664
- }
665
- rest.push(`kwargs: ${kwargsType}`);
666
- overloads.push(` static create${classTypeParamDecl}(${[...head, ...rest].join(', ')}): Promise<${classSelfType}>;`);
667
- if (varArgsParam && needsVarArgsArray) {
668
- overloads.push(` static create${classTypeParamDecl}(${[...head, `kwargs: ${kwargsType}`].join(', ')}): Promise<${classSelfType}>;`);
669
- }
670
- }
671
- }
672
- const overloadDecl = overloads.length > 0 ? `${overloads.join('\n')}\n` : '';
673
- const declaration = overloads.length > 0
674
- ? overloadDecl
675
- : ` static create${classTypeParamDecl}(${paramsDecl}): Promise<${classSelfType}>;\n`;
676
- const callDescriptor = {
677
- positionalParams,
678
- varArgsParam,
679
- needsVarArgsArray,
680
- hasKwArgs: needsKwargsParam,
681
- hasVarKwArgs,
682
- keywordOnlyNames,
683
- requiredKwOnlyNames,
684
- positionalOnlyNames,
685
- requiredPosCount,
686
- indent: ' ',
687
- errorLabel: '__init__',
688
- };
689
- const callPreludeLines = emitCallPrelude(callDescriptor, this.callEmitHelpers());
690
- const callPrelude = callPreludeLines.length > 0 ? `${callPreludeLines.join('\n')}\n` : '';
691
- const guardLines = emitArgGuards(callDescriptor);
692
- return {
693
- overloadDecl,
694
- declaration,
695
- paramsDecl,
696
- callPrelude,
697
- hasKwargs: needsKwargsParam,
698
- guardLines,
699
- };
700
- })();
701
- // @property / functools.cached_property → TS getters returning Promise<T>.
702
- // Reading the attribute fires the getter on the Python side; the bridge
703
- // resolves it via callMethod with no args. Sorted for stable output.
704
- const accessorBodies = [];
705
- const accessorDeclarations = [];
706
- const sortedAccessors = [...(cls.accessors ?? [])].sort((a, b) => a.name.localeCompare(b.name));
707
- sortedAccessors.forEach(accessor => {
708
- const aname = this.escapeIdentifier(accessor.name);
709
- const accessorType = this.typeToTsFromPython(accessor.type, classGenericContext, 'return');
710
- const jsdocAcc = this.generateJsDoc(accessor.docstring);
711
- accessorBodies.push(`${jsdocAcc} get ${aname}(): Promise<${accessorType}> { return getRuntimeBridge().callMethod(this.__handle, '${accessor.name}', []); }`);
712
- accessorDeclarations.push(`${jsdocAcc} get ${aname}(): Promise<${accessorType}>;\n`);
713
- });
714
- const accessorsImpl = accessorBodies.length > 0 ? `${accessorBodies.join('\n')}\n` : '';
715
- const accessorsDecl = accessorDeclarations.length > 0 ? `${accessorDeclarations.join('')}` : '';
716
783
  const methodsSection = methodBodies.length > 0 ? `\n${methodBodies.join('\n')}\n` : '\n';
717
784
  const declarationMethodsSection = methodDeclarations.length > 0 ? `\n${methodDeclarations.join('')}\n` : '\n';
718
- const ctorGuards = ctorSpec.guardLines.length > 0 ? `${ctorSpec.guardLines.join('\n')}\n` : '';
719
- const newClassExpr = `new ${cname}${classGenericContext.typeArguments}(handle)`;
785
+ // The constructor counts: a class whose only member was __init__ loses
786
+ // create(), so it needs the migration note as much as one with methods.
787
+ const omittedInstanceMembers = cls.methods.some(method => method.methodKind !== 'class' && method.methodKind !== 'static') ||
788
+ (cls.accessors?.length ?? 0) > 0;
789
+ const migrationNote = omittedInstanceMembers
790
+ ? ' // NOTE: Instance members are not generated in v0.9; migrate this API to value-returning module functions.\n'
791
+ : '';
720
792
  const ts = `${jsdoc}export class ${cname}${classTypeParamDecl} {
721
- private readonly __handle: string;
722
- private constructor(handle: string) { this.__handle = handle; }
723
- ${ctorSpec.overloadDecl} static async create${classTypeParamDecl}(${ctorSpec.paramsDecl}): Promise<${classSelfType}> {
724
- ${ctorSpec.callPrelude}${ctorGuards} const handle = await getRuntimeBridge().instantiate<string>('${moduleId}', '${cls.name}', __args${ctorSpec.hasKwargs ? ', __kwargs' : ''});
725
- return ${newClassExpr};
726
- }
727
- static fromHandle${classTypeParamDecl}(handle: string): ${classSelfType} { return ${newClassExpr}; }${methodsSection}${accessorsImpl} async disposeHandle(): Promise<void> { await getRuntimeBridge().disposeInstance(this.__handle); }
793
+ ${migrationNote}${methodsSection}
728
794
  }
729
795
  `;
730
796
  const declaration = `${jsdoc}export class ${cname}${classTypeParamDecl} {
731
- private readonly __handle: string;
732
- private constructor(handle: string);
733
- ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string): ${classSelfType};${declarationMethodsSection}${accessorsDecl} disposeHandle(): Promise<void>;
797
+ ${migrationNote}${declarationMethodsSection}
734
798
  }
735
799
  `;
736
800
  return this.wrap(ts, declaration, [cls.name]);
737
801
  }
738
- generateTypeAlias(alias, moduleName) {
739
- const genericContext = this.buildGenericRenderContext(this.getTypeParameters(alias.typeParameters), [alias.type], moduleName);
802
+ generateTypeAlias(alias, moduleName, localDeclaredNames = new Set()) {
803
+ const genericContext = this.buildGenericRenderContext(this.getTypeParameters(alias.typeParameters), [alias.type], moduleName, localDeclaredNames);
740
804
  const aliasName = this.escapeIdentifier(alias.name, { preserveCase: true });
741
805
  const body = this.typeToTsFromPython(alias.type, genericContext, 'value');
742
806
  const ts = `export type ${aliasName}${genericContext.declaration} = ${body}\n`;
@@ -761,15 +825,19 @@ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string):
761
825
  return result;
762
826
  }
763
827
  generateModuleDefinition(module, annotatedJSDoc = false) {
828
+ const localDeclaredNames = new Set([
829
+ ...module.classes.map(cls => cls.name),
830
+ ...(module.typeAliases ?? []).map(alias => alias.name),
831
+ ]);
764
832
  const functionResults = [...module.functions]
765
833
  .sort((a, b) => a.name.localeCompare(b.name))
766
- .map(f => this.generateFunctionWrapper(f, module.name, annotatedJSDoc));
834
+ .map(f => this.generateFunctionWrapper(f, module.name, annotatedJSDoc, localDeclaredNames, this.returnDefinitions(module)));
767
835
  const classResults = [...module.classes]
768
836
  .sort((a, b) => a.name.localeCompare(b.name))
769
- .map(c => this.generateClassWrapper(c, module.name, annotatedJSDoc));
837
+ .map(c => this.generateClassWrapper(c, module.name, annotatedJSDoc, localDeclaredNames, this.returnDefinitions(module)));
770
838
  const typeAliasResults = [...(module.typeAliases ?? [])]
771
839
  .sort((a, b) => a.name.localeCompare(b.name))
772
- .map(alias => this.generateTypeAlias(alias, module.name));
840
+ .map(alias => this.generateTypeAlias(alias, module.name, localDeclaredNames));
773
841
  const functionCodes = functionResults.map(result => result.typescript).join('\n');
774
842
  const classCodes = classResults.map(result => result.typescript).join('\n');
775
843
  const typeAliasCodes = typeAliasResults.map(result => result.typescript).join('\n');
@@ -780,7 +848,9 @@ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string):
780
848
  return kind === 'class' && !c.decorators.includes('__typed_dict__');
781
849
  });
782
850
  const needsRuntime = module.functions.length > 0 || hasRuntimeClasses;
783
- const bridgeDecl = needsRuntime ? `import { getRuntimeBridge } from 'tywrap/runtime';\n\n` : '';
851
+ const bridgeDecl = needsRuntime
852
+ ? `import { createReturnValidator, getRuntimeBridge, type ReturnSchema } from 'tywrap/runtime';\n\n${this.emitReturnDefinitions(module)}`
853
+ : '';
784
854
  const ts = `${`${header}${bridgeDecl}${functionCodes}\n${classCodes}\n${typeAliasCodes}`.trimEnd()}\n`;
785
855
  const declaration = `${`${declarationHeader}${functionResults
786
856
  .map(result => result.declaration)
@@ -818,53 +888,67 @@ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string):
818
888
  },
819
889
  };
820
890
  }
821
- typeToTs(type) {
891
+ typeToTs(type, ctx, mappingContext = 'value') {
822
892
  switch (type.kind) {
823
893
  case 'primitive':
824
894
  return type.name;
825
895
  case 'array':
826
- return `${this.typeToTs(type.elementType)}[]`;
896
+ return `${this.typeToTs(type.elementType, ctx, mappingContext)}[]`;
827
897
  case 'tuple': {
828
898
  const t = type;
829
- const parts = t.elementTypes.map(e => this.typeToTs(e)).join(', ');
899
+ const parts = t.elementTypes.map(e => this.typeToTs(e, ctx, mappingContext)).join(', ');
830
900
  return `[${parts}]`;
831
901
  }
832
902
  case 'object': {
833
903
  const t = type;
834
904
  // If it's a simple Record<string, T> pattern, use that syntax
835
905
  if (t.properties.length === 0 && t.indexSignature) {
836
- const keyType = this.typeToTs(t.indexSignature.keyType);
837
- const valueType = this.typeToTs(t.indexSignature.valueType);
906
+ const keyType = this.typeToTs(t.indexSignature.keyType, ctx, mappingContext);
907
+ const valueType = this.typeToTs(t.indexSignature.valueType, ctx, mappingContext);
838
908
  if (keyType === 'string') {
839
909
  return `Record<string, ${valueType}>`;
840
910
  }
841
911
  }
842
912
  const props = t.properties
843
- .map(p => `${p.readonly ? 'readonly ' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type)};`)
913
+ .map(p => `${p.readonly ? 'readonly ' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type, ctx, mappingContext)};`)
844
914
  .join(' ');
845
915
  const indexSig = t.indexSignature
846
- ? `[key: ${this.typeToTs(t.indexSignature.keyType)}]: ${this.typeToTs(t.indexSignature.valueType)};`
916
+ ? `[key: ${this.typeToTs(t.indexSignature.keyType, ctx, mappingContext)}]: ${this.typeToTs(t.indexSignature.valueType, ctx, mappingContext)};`
847
917
  : '';
848
918
  return `{ ${props} ${indexSig} }`;
849
919
  }
850
920
  case 'union':
851
- return type.types.map(t => this.typeToTs(t)).join(' | ');
921
+ return type.types.map(t => this.typeToTs(t, ctx, mappingContext)).join(' | ');
852
922
  case 'function': {
853
923
  const ft = type;
854
924
  const params = ft.parameters
855
- .map(p => `${p.rest ? '...' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type)}`)
925
+ .map(p => `${p.rest ? '...' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type, ctx, mappingContext)}`)
856
926
  .join(', ');
857
- return `(${params}) => ${this.typeToTs(ft.returnType)}`;
927
+ return `(${params}) => ${this.typeToTs(ft.returnType, ctx, mappingContext)}`;
858
928
  }
859
929
  case 'generic': {
860
930
  const g = type;
861
- const args = g.typeArgs.map(a => this.typeToTs(a)).join(', ');
931
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(g.name)) {
932
+ return this.degradeType(g);
933
+ }
934
+ if (mappingContext === 'return' && this.nonDecodableReturnGenerics.has(g.name)) {
935
+ return this.degradeType(g);
936
+ }
937
+ if (ctx && !this.builtinGenericNames.has(g.name) && !this.isLocalTypeIdentity(g, ctx)) {
938
+ return this.degradeType(g);
939
+ }
940
+ const args = g.typeArgs.map(a => this.typeToTs(a, ctx, mappingContext)).join(', ');
862
941
  return `${g.name}<${args}>`;
863
942
  }
864
943
  case 'custom': {
865
944
  const c = type;
866
945
  if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(c.name)) {
867
- return 'unknown';
946
+ return this.degradeType(c);
947
+ }
948
+ if (ctx &&
949
+ !this.isLocalTypeIdentity(c, ctx) &&
950
+ !(c.module === 'typing' && ctx.emittedNames.has(c.name))) {
951
+ return this.degradeType(c);
868
952
  }
869
953
  return c.name;
870
954
  }