tywrap 0.8.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 (157) hide show
  1. package/README.md +22 -7
  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/generator.d.ts +23 -4
  11. package/dist/core/generator.d.ts.map +1 -1
  12. package/dist/core/generator.js +243 -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 +1 -1
  31. package/dist/runtime/bridge-codec.d.ts.map +1 -1
  32. package/dist/runtime/bridge-codec.js +78 -55
  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 +49 -0
  76. package/dist/runtime/validators.d.ts.map +1 -1
  77. package/dist/runtime/validators.js +152 -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 +1 -0
  88. package/dist/utils/codec.d.ts.map +1 -1
  89. package/dist/utils/codec.js +13 -13
  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.js +1 -1
  98. package/package.json +7 -6
  99. package/runtime/frame_codec.py +7 -1
  100. package/runtime/python_bridge.py +85 -125
  101. package/runtime/tywrap_bridge_core.py +133 -150
  102. package/src/config/index.ts +11 -0
  103. package/src/core/annotation-parser.ts +7 -4
  104. package/src/core/generator.ts +315 -205
  105. package/src/core/mapper.ts +8 -8
  106. package/src/index.ts +7 -4
  107. package/src/runtime/base-bridge.ts +5 -30
  108. package/src/runtime/bounded-context.ts +12 -67
  109. package/src/runtime/bridge-codec.ts +94 -65
  110. package/src/runtime/errors.ts +21 -0
  111. package/src/runtime/http-transport.ts +6 -1
  112. package/src/runtime/index.ts +6 -0
  113. package/src/runtime/node.ts +9 -120
  114. package/src/runtime/pooled-transport.ts +424 -90
  115. package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
  116. package/src/runtime/pyodide-transport.ts +7 -3
  117. package/src/runtime/rpc-client.ts +37 -139
  118. package/src/runtime/subprocess-transport.ts +210 -285
  119. package/src/runtime/timed-out-request-tracker.ts +1 -1
  120. package/src/runtime/transport.ts +15 -23
  121. package/src/runtime/validators.ts +204 -0
  122. package/src/types/index.ts +21 -60
  123. package/src/tywrap.ts +157 -22
  124. package/src/utils/cache.ts +3 -3
  125. package/src/utils/codec.ts +21 -13
  126. package/src/utils/ir-cache.ts +1 -1
  127. package/src/utils/runtime.ts +17 -128
  128. package/src/version.ts +1 -1
  129. package/dist/core/discovery.d.ts +0 -103
  130. package/dist/core/discovery.d.ts.map +0 -1
  131. package/dist/core/discovery.js +0 -380
  132. package/dist/core/discovery.js.map +0 -1
  133. package/dist/core/validation.d.ts +0 -102
  134. package/dist/core/validation.d.ts.map +0 -1
  135. package/dist/core/validation.js +0 -490
  136. package/dist/core/validation.js.map +0 -1
  137. package/dist/runtime/base.d.ts +0 -22
  138. package/dist/runtime/base.d.ts.map +0 -1
  139. package/dist/runtime/base.js +0 -23
  140. package/dist/runtime/base.js.map +0 -1
  141. package/dist/runtime/transport-pool.d.ts +0 -196
  142. package/dist/runtime/transport-pool.d.ts.map +0 -1
  143. package/dist/runtime/transport-pool.js +0 -418
  144. package/dist/runtime/transport-pool.js.map +0 -1
  145. package/runtime/__pycache__/_tywrap_conformance_chunking_fixtures.cpython-311.pyc +0 -0
  146. package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
  147. package/runtime/__pycache__/_tywrap_w4_chunking_fixture.cpython-311.pyc +0 -0
  148. package/runtime/__pycache__/_tywrap_w5_request_chunking_fixture.cpython-311.pyc +0 -0
  149. package/runtime/__pycache__/_tywrap_w6_pool_chunking_fixture.cpython-311.pyc +0 -0
  150. package/runtime/__pycache__/frame_codec.cpython-311.pyc +0 -0
  151. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  152. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  153. package/runtime/safe_codec.py +0 -352
  154. package/src/core/discovery.ts +0 -477
  155. package/src/core/validation.ts +0 -729
  156. package/src/runtime/base.ts +0 -24
  157. 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,143 @@ 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
+ return definitions.has(leaf) ? { kind: 'ref', name: leaf } : { kind: 'any' };
411
+ }
412
+ case 'typevar':
413
+ case 'paramspec':
414
+ case 'paramspec_args':
415
+ case 'paramspec_kwargs':
416
+ case 'typevartuple':
417
+ case 'unpack':
418
+ case 'callable':
419
+ return { kind: 'any' };
420
+ }
421
+ };
422
+ return schema(type);
423
+ }
424
+ returnDefinitions(module) {
425
+ return new Set(module.classes
426
+ .filter(cls => cls.kind === 'typed_dict' || cls.decorators.includes('__typed_dict__'))
427
+ .map(cls => cls.name));
428
+ }
429
+ emitReturnDefinitions(module) {
430
+ const definitions = this.returnDefinitions(module);
431
+ const entries = module.classes
432
+ .filter(cls => cls.kind === 'typed_dict' || cls.decorators.includes('__typed_dict__'))
433
+ .map(cls => {
434
+ const fields = Object.fromEntries(cls.properties.map(property => [
435
+ property.name,
436
+ {
437
+ schema: this.returnSchema(property.type, definitions),
438
+ optional: property.optional === true,
439
+ },
440
+ ]));
441
+ return [cls.name, { kind: 'record', fields }];
442
+ });
443
+ return `const __tywrapReturnDefinitions: Record<string, ReturnSchema> = ${JSON.stringify(Object.fromEntries(entries))};\n\n`;
444
+ }
445
+ generateFunctionWrapper(func, moduleName, annotatedJSDoc = false, localDeclaredNames = new Set(), returnDefinitions = new Set()) {
274
446
  const jsdoc = this.generateJsDoc(func.docstring, annotatedJSDoc ? func.parameters.map(p => String(p.type)) : undefined);
275
447
  const filteredParams = func.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
276
448
  const keywordOnlyParams = filteredParams.filter(p => p.keywordOnly);
@@ -280,7 +452,7 @@ export class CodeGenerator {
280
452
  const varArgsParam = filteredParams.find(p => p.varArgs);
281
453
  const needsVarArgsArray = Boolean(varArgsParam) && needsKwargsParam;
282
454
  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);
455
+ const genericContext = this.buildGenericRenderContext(this.getTypeParameters(func.typeParameters), [func.returnType, ...filteredParams.map(param => param.type)], moduleName, localDeclaredNames);
284
456
  const typeParamDecl = genericContext.declaration;
285
457
  const tsTypeForValue = (p) => this.typeToTsFromPython(p.type, genericContext, 'value');
286
458
  const kwargsType = (() => {
@@ -336,6 +508,8 @@ export class CodeGenerator {
336
508
  const returnType = this.typeToTsFromPython(func.returnType, genericContext, 'return');
337
509
  const fname = this.escapeIdentifier(func.name);
338
510
  const moduleId = moduleName ?? '__main__';
511
+ const validatorName = `__validate${this.escapeIdentifier(func.name, { preserveCase: true })}Result`;
512
+ const returnValidator = `const ${validatorName} = createReturnValidator(${JSON.stringify(this.returnSchema(func.returnType, returnDefinitions))}, ${JSON.stringify(`${moduleId}.${func.name}`)}, __tywrapReturnDefinitions);\n\n`;
339
513
  // Overloads: generate trailing optional parameter drop variants (exclude *args/**kwargs).
340
514
  // Why: Python APIs frequently have many optional tail params. TypeScript callers expect
341
515
  // `fn(a)`, `fn(a, b)`, ... all to typecheck. We emit a family of overloads that progressively
@@ -413,8 +587,8 @@ export class CodeGenerator {
413
587
  const guards = guardLines.length > 0 ? `${guardLines.join('\n')}\n` : '';
414
588
  const callPreludeLines = emitCallPrelude(callDescriptor, this.callEmitHelpers());
415
589
  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' : ''});
590
+ const ts = `${jsdoc}${returnValidator}${overloadDecl}export async function ${fname}${typeParamDecl}(${paramDecl}): Promise<${returnType}> {
591
+ ${callPrelude}${guards} return getRuntimeBridge().call<${returnType}>('${moduleId}', '${func.name}', __args, ${hasKwArgs ? '__kwargs' : 'undefined'}, ${validatorName});
418
592
  }
419
593
  `;
420
594
  const declarationBody = overloads.length > 0
@@ -422,7 +596,9 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
422
596
  : `export function ${fname}${typeParamDecl}(${paramDecl}): Promise<${returnType}>;\n`;
423
597
  return this.wrap(ts, `${jsdoc}${declarationBody}`, [func.name]);
424
598
  }
425
- generateClassWrapper(cls, moduleName, _annotatedJSDoc = false) {
599
+ generateClassWrapper(cls, moduleName, _annotatedJSDoc = false, localDeclaredNames = new Set(), returnDefinitions = new Set()) {
600
+ const moduleDeclaredNames = new Set(localDeclaredNames);
601
+ moduleDeclaredNames.add(cls.name);
426
602
  const jsdoc = this.generateJsDoc(cls.docstring);
427
603
  const classGenericContext = this.buildGenericRenderContext(this.getTypeParameters(cls.typeParameters), [
428
604
  ...cls.properties.map(property => property.type),
@@ -431,11 +607,9 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
431
607
  method.returnType,
432
608
  ...method.parameters.map(p => p.type),
433
609
  ]),
434
- ], moduleName);
610
+ ], moduleName, moduleDeclaredNames);
435
611
  const classTypeParamDecl = classGenericContext.declaration;
436
612
  const cname = this.escapeIdentifier(cls.name);
437
- const classSelfType = `${cname}${classGenericContext.typeArguments}`;
438
- const tsValueType = (p) => this.typeToTsFromPython(p.type, classGenericContext, 'value');
439
613
  const wrapAlias = (body) => {
440
614
  const ts = `${jsdoc}export type ${cname}${classTypeParamDecl} = ${body}\n`;
441
615
  return this.wrap(ts, ts, [cls.name]);
@@ -469,7 +643,7 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
469
643
  .filter(m => m.name !== '__init__')
470
644
  .map(m => {
471
645
  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);
646
+ const methodOwnGenericContext = this.buildGenericRenderContext(this.getTypeParameters(m.typeParameters), [m.returnType, ...fparams.map(param => param.type)], moduleName, moduleDeclaredNames);
473
647
  const methodGenericContext = this.mergeGenericRenderContexts(classGenericContext, methodOwnGenericContext);
474
648
  const methodTypeParamDecl = methodOwnGenericContext.declaration;
475
649
  const paramsDecl = fparams
@@ -497,16 +671,14 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
497
671
  const methodBodies = [];
498
672
  const methodDeclarations = [];
499
673
  sortedMethods
500
- .filter(method => method.name !== '__init__')
674
+ .filter(method => method.name !== '__init__' &&
675
+ (method.methodKind === 'class' || method.methodKind === 'static'))
501
676
  .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 ' : '';
677
+ // v0.9 wrappers expose only class/static methods. They route through
678
+ // the ordinary module call path and never retain process-local state.
679
+ const staticPrefix = 'static ';
508
680
  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);
681
+ const methodOwnGenericContext = this.buildGenericRenderContext(this.getTypeParameters(method.typeParameters), [method.returnType, ...fparams.map(param => param.type)], moduleName, moduleDeclaredNames);
510
682
  const methodGenericContext = this.mergeGenericRenderContexts(classGenericContext, methodOwnGenericContext);
511
683
  const methodTypeParamDecl = methodOwnGenericContext.declaration;
512
684
  const methodTsValueType = (p) => this.typeToTsFromPython(p.type, methodGenericContext, 'value');
@@ -553,6 +725,8 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
553
725
  const requiredKwOnlyNames = keywordOnlyParams.filter(p => !p.optional).map(p => p.name);
554
726
  const returnType = this.typeToTsFromPython(method.returnType, methodGenericContext, 'return');
555
727
  const mname = this.escapeIdentifier(method.name);
728
+ const validatorName = `__validate${this.escapeIdentifier(cls.name, { preserveCase: true })}${this.escapeIdentifier(method.name, { preserveCase: true })}Result`;
729
+ const returnValidator = `const ${validatorName} = createReturnValidator(${JSON.stringify(this.returnSchema(method.returnType, returnDefinitions))}, ${JSON.stringify(`${moduleId}.${cls.name}.${method.name}`)}, __tywrapReturnDefinitions);\n\n`;
556
730
  const overloads = [];
557
731
  if (needsKwargsParam && requiredKwOnlyNames.length > 0) {
558
732
  const firstOptionalIndex = positionalParams.findIndex(p => p.optional);
@@ -589,154 +763,33 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
589
763
  const callPrelude = callPreludeLines.length > 0 ? `${callPreludeLines.join('\n')}\n` : '';
590
764
  const guardLines = emitArgGuards(callDescriptor);
591
765
  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' : ''})`;
766
+ const callExpr = `getRuntimeBridge().call<${returnType}>('${moduleId}', '${cls.name}.${method.name}', __args, ${needsKwargsParam ? '__kwargs' : 'undefined'}, ${validatorName})`;
595
767
  methodBodies.push(`${overloadDecl} ${staticPrefix}async ${mname}${methodTypeParamDecl}(${paramsDecl}): Promise<${returnType}> {
596
- ${callPrelude}${guards} return ${callExpr};
768
+ ${returnValidator}${callPrelude}${guards} return ${callExpr};
597
769
  }`);
598
770
  methodDeclarations.push(`${overloadDecl}${overloads.length > 0 ? '' : ` ${staticPrefix}${mname}${methodTypeParamDecl}(${paramsDecl}): Promise<${returnType}>;\n`}`);
599
771
  });
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
772
  const methodsSection = methodBodies.length > 0 ? `\n${methodBodies.join('\n')}\n` : '\n';
717
773
  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)`;
774
+ // The constructor counts: a class whose only member was __init__ loses
775
+ // create(), so it needs the migration note as much as one with methods.
776
+ const omittedInstanceMembers = cls.methods.some(method => method.methodKind !== 'class' && method.methodKind !== 'static') ||
777
+ (cls.accessors?.length ?? 0) > 0;
778
+ const migrationNote = omittedInstanceMembers
779
+ ? ' // NOTE: Instance members are not generated in v0.9; migrate this API to value-returning module functions.\n'
780
+ : '';
720
781
  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); }
782
+ ${migrationNote}${methodsSection}
728
783
  }
729
784
  `;
730
785
  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>;
786
+ ${migrationNote}${declarationMethodsSection}
734
787
  }
735
788
  `;
736
789
  return this.wrap(ts, declaration, [cls.name]);
737
790
  }
738
- generateTypeAlias(alias, moduleName) {
739
- const genericContext = this.buildGenericRenderContext(this.getTypeParameters(alias.typeParameters), [alias.type], moduleName);
791
+ generateTypeAlias(alias, moduleName, localDeclaredNames = new Set()) {
792
+ const genericContext = this.buildGenericRenderContext(this.getTypeParameters(alias.typeParameters), [alias.type], moduleName, localDeclaredNames);
740
793
  const aliasName = this.escapeIdentifier(alias.name, { preserveCase: true });
741
794
  const body = this.typeToTsFromPython(alias.type, genericContext, 'value');
742
795
  const ts = `export type ${aliasName}${genericContext.declaration} = ${body}\n`;
@@ -761,15 +814,19 @@ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string):
761
814
  return result;
762
815
  }
763
816
  generateModuleDefinition(module, annotatedJSDoc = false) {
817
+ const localDeclaredNames = new Set([
818
+ ...module.classes.map(cls => cls.name),
819
+ ...(module.typeAliases ?? []).map(alias => alias.name),
820
+ ]);
764
821
  const functionResults = [...module.functions]
765
822
  .sort((a, b) => a.name.localeCompare(b.name))
766
- .map(f => this.generateFunctionWrapper(f, module.name, annotatedJSDoc));
823
+ .map(f => this.generateFunctionWrapper(f, module.name, annotatedJSDoc, localDeclaredNames, this.returnDefinitions(module)));
767
824
  const classResults = [...module.classes]
768
825
  .sort((a, b) => a.name.localeCompare(b.name))
769
- .map(c => this.generateClassWrapper(c, module.name, annotatedJSDoc));
826
+ .map(c => this.generateClassWrapper(c, module.name, annotatedJSDoc, localDeclaredNames, this.returnDefinitions(module)));
770
827
  const typeAliasResults = [...(module.typeAliases ?? [])]
771
828
  .sort((a, b) => a.name.localeCompare(b.name))
772
- .map(alias => this.generateTypeAlias(alias, module.name));
829
+ .map(alias => this.generateTypeAlias(alias, module.name, localDeclaredNames));
773
830
  const functionCodes = functionResults.map(result => result.typescript).join('\n');
774
831
  const classCodes = classResults.map(result => result.typescript).join('\n');
775
832
  const typeAliasCodes = typeAliasResults.map(result => result.typescript).join('\n');
@@ -780,7 +837,9 @@ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string):
780
837
  return kind === 'class' && !c.decorators.includes('__typed_dict__');
781
838
  });
782
839
  const needsRuntime = module.functions.length > 0 || hasRuntimeClasses;
783
- const bridgeDecl = needsRuntime ? `import { getRuntimeBridge } from 'tywrap/runtime';\n\n` : '';
840
+ const bridgeDecl = needsRuntime
841
+ ? `import { createReturnValidator, getRuntimeBridge, type ReturnSchema } from 'tywrap/runtime';\n\n${this.emitReturnDefinitions(module)}`
842
+ : '';
784
843
  const ts = `${`${header}${bridgeDecl}${functionCodes}\n${classCodes}\n${typeAliasCodes}`.trimEnd()}\n`;
785
844
  const declaration = `${`${declarationHeader}${functionResults
786
845
  .map(result => result.declaration)
@@ -818,53 +877,67 @@ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string):
818
877
  },
819
878
  };
820
879
  }
821
- typeToTs(type) {
880
+ typeToTs(type, ctx, mappingContext = 'value') {
822
881
  switch (type.kind) {
823
882
  case 'primitive':
824
883
  return type.name;
825
884
  case 'array':
826
- return `${this.typeToTs(type.elementType)}[]`;
885
+ return `${this.typeToTs(type.elementType, ctx, mappingContext)}[]`;
827
886
  case 'tuple': {
828
887
  const t = type;
829
- const parts = t.elementTypes.map(e => this.typeToTs(e)).join(', ');
888
+ const parts = t.elementTypes.map(e => this.typeToTs(e, ctx, mappingContext)).join(', ');
830
889
  return `[${parts}]`;
831
890
  }
832
891
  case 'object': {
833
892
  const t = type;
834
893
  // If it's a simple Record<string, T> pattern, use that syntax
835
894
  if (t.properties.length === 0 && t.indexSignature) {
836
- const keyType = this.typeToTs(t.indexSignature.keyType);
837
- const valueType = this.typeToTs(t.indexSignature.valueType);
895
+ const keyType = this.typeToTs(t.indexSignature.keyType, ctx, mappingContext);
896
+ const valueType = this.typeToTs(t.indexSignature.valueType, ctx, mappingContext);
838
897
  if (keyType === 'string') {
839
898
  return `Record<string, ${valueType}>`;
840
899
  }
841
900
  }
842
901
  const props = t.properties
843
- .map(p => `${p.readonly ? 'readonly ' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type)};`)
902
+ .map(p => `${p.readonly ? 'readonly ' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type, ctx, mappingContext)};`)
844
903
  .join(' ');
845
904
  const indexSig = t.indexSignature
846
- ? `[key: ${this.typeToTs(t.indexSignature.keyType)}]: ${this.typeToTs(t.indexSignature.valueType)};`
905
+ ? `[key: ${this.typeToTs(t.indexSignature.keyType, ctx, mappingContext)}]: ${this.typeToTs(t.indexSignature.valueType, ctx, mappingContext)};`
847
906
  : '';
848
907
  return `{ ${props} ${indexSig} }`;
849
908
  }
850
909
  case 'union':
851
- return type.types.map(t => this.typeToTs(t)).join(' | ');
910
+ return type.types.map(t => this.typeToTs(t, ctx, mappingContext)).join(' | ');
852
911
  case 'function': {
853
912
  const ft = type;
854
913
  const params = ft.parameters
855
- .map(p => `${p.rest ? '...' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type)}`)
914
+ .map(p => `${p.rest ? '...' : ''}${p.name}${p.optional ? '?' : ''}: ${this.typeToTs(p.type, ctx, mappingContext)}`)
856
915
  .join(', ');
857
- return `(${params}) => ${this.typeToTs(ft.returnType)}`;
916
+ return `(${params}) => ${this.typeToTs(ft.returnType, ctx, mappingContext)}`;
858
917
  }
859
918
  case 'generic': {
860
919
  const g = type;
861
- const args = g.typeArgs.map(a => this.typeToTs(a)).join(', ');
920
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(g.name)) {
921
+ return this.degradeType(g);
922
+ }
923
+ if (mappingContext === 'return' && this.nonDecodableReturnGenerics.has(g.name)) {
924
+ return this.degradeType(g);
925
+ }
926
+ if (ctx && !this.builtinGenericNames.has(g.name) && !this.isLocalTypeIdentity(g, ctx)) {
927
+ return this.degradeType(g);
928
+ }
929
+ const args = g.typeArgs.map(a => this.typeToTs(a, ctx, mappingContext)).join(', ');
862
930
  return `${g.name}<${args}>`;
863
931
  }
864
932
  case 'custom': {
865
933
  const c = type;
866
934
  if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(c.name)) {
867
- return 'unknown';
935
+ return this.degradeType(c);
936
+ }
937
+ if (ctx &&
938
+ !this.isLocalTypeIdentity(c, ctx) &&
939
+ !(c.module === 'typing' && ctx.emittedNames.has(c.name))) {
940
+ return this.degradeType(c);
868
941
  }
869
942
  return c.name;
870
943
  }