tywrap 0.2.0 → 0.3.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 (163) hide show
  1. package/README.md +65 -31
  2. package/dist/cli.js +49 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/index.d.ts +11 -7
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/index.js +43 -7
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/core/analyzer.d.ts +1 -0
  9. package/dist/core/analyzer.d.ts.map +1 -1
  10. package/dist/core/analyzer.js +47 -12
  11. package/dist/core/analyzer.js.map +1 -1
  12. package/dist/core/annotation-parser.d.ts +8 -0
  13. package/dist/core/annotation-parser.d.ts.map +1 -0
  14. package/dist/core/annotation-parser.js +409 -0
  15. package/dist/core/annotation-parser.js.map +1 -0
  16. package/dist/core/discovery.d.ts +1 -0
  17. package/dist/core/discovery.d.ts.map +1 -1
  18. package/dist/core/discovery.js +9 -9
  19. package/dist/core/discovery.js.map +1 -1
  20. package/dist/core/generator.d.ts +9 -1
  21. package/dist/core/generator.d.ts.map +1 -1
  22. package/dist/core/generator.js +738 -104
  23. package/dist/core/generator.js.map +1 -1
  24. package/dist/core/mapper.d.ts +9 -7
  25. package/dist/core/mapper.d.ts.map +1 -1
  26. package/dist/core/mapper.js +129 -44
  27. package/dist/core/mapper.js.map +1 -1
  28. package/dist/index.d.ts +23 -7
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +23 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/runtime/base.d.ts +17 -7
  33. package/dist/runtime/base.d.ts.map +1 -1
  34. package/dist/runtime/base.js +18 -1
  35. package/dist/runtime/base.js.map +1 -1
  36. package/dist/runtime/bounded-context.d.ts +252 -0
  37. package/dist/runtime/bounded-context.d.ts.map +1 -0
  38. package/dist/runtime/bounded-context.js +454 -0
  39. package/dist/runtime/bounded-context.js.map +1 -0
  40. package/dist/runtime/bridge-core.d.ts +1 -0
  41. package/dist/runtime/bridge-core.d.ts.map +1 -1
  42. package/dist/runtime/bridge-core.js +52 -17
  43. package/dist/runtime/bridge-core.js.map +1 -1
  44. package/dist/runtime/bridge-protocol.d.ts +184 -0
  45. package/dist/runtime/bridge-protocol.d.ts.map +1 -0
  46. package/dist/runtime/bridge-protocol.js +344 -0
  47. package/dist/runtime/bridge-protocol.js.map +1 -0
  48. package/dist/runtime/disposable.d.ts +40 -0
  49. package/dist/runtime/disposable.d.ts.map +1 -0
  50. package/dist/runtime/disposable.js +49 -0
  51. package/dist/runtime/disposable.js.map +1 -0
  52. package/dist/runtime/errors.d.ts +10 -0
  53. package/dist/runtime/errors.d.ts.map +1 -1
  54. package/dist/runtime/errors.js +9 -0
  55. package/dist/runtime/errors.js.map +1 -1
  56. package/dist/runtime/http-io.d.ts +91 -0
  57. package/dist/runtime/http-io.d.ts.map +1 -0
  58. package/dist/runtime/http-io.js +195 -0
  59. package/dist/runtime/http-io.js.map +1 -0
  60. package/dist/runtime/http.d.ts +48 -13
  61. package/dist/runtime/http.d.ts.map +1 -1
  62. package/dist/runtime/http.js +60 -73
  63. package/dist/runtime/http.js.map +1 -1
  64. package/dist/runtime/node.d.ts +97 -130
  65. package/dist/runtime/node.d.ts.map +1 -1
  66. package/dist/runtime/node.js +392 -521
  67. package/dist/runtime/node.js.map +1 -1
  68. package/dist/runtime/optimized-node.d.ts +1 -1
  69. package/dist/runtime/optimized-node.d.ts.map +1 -1
  70. package/dist/runtime/optimized-node.js +1 -1
  71. package/dist/runtime/optimized-node.js.map +1 -1
  72. package/dist/runtime/pooled-transport.d.ts +135 -0
  73. package/dist/runtime/pooled-transport.d.ts.map +1 -0
  74. package/dist/runtime/pooled-transport.js +177 -0
  75. package/dist/runtime/pooled-transport.js.map +1 -0
  76. package/dist/runtime/process-io.d.ts +209 -0
  77. package/dist/runtime/process-io.d.ts.map +1 -0
  78. package/dist/runtime/process-io.js +729 -0
  79. package/dist/runtime/process-io.js.map +1 -0
  80. package/dist/runtime/pyodide-io.d.ts +154 -0
  81. package/dist/runtime/pyodide-io.d.ts.map +1 -0
  82. package/dist/runtime/pyodide-io.js +459 -0
  83. package/dist/runtime/pyodide-io.js.map +1 -0
  84. package/dist/runtime/pyodide.d.ts +51 -19
  85. package/dist/runtime/pyodide.d.ts.map +1 -1
  86. package/dist/runtime/pyodide.js +57 -186
  87. package/dist/runtime/pyodide.js.map +1 -1
  88. package/dist/runtime/safe-codec.d.ts +103 -0
  89. package/dist/runtime/safe-codec.d.ts.map +1 -0
  90. package/dist/runtime/safe-codec.js +519 -0
  91. package/dist/runtime/safe-codec.js.map +1 -0
  92. package/dist/runtime/transport.d.ts +191 -0
  93. package/dist/runtime/transport.d.ts.map +1 -0
  94. package/dist/runtime/transport.js +86 -0
  95. package/dist/runtime/transport.js.map +1 -0
  96. package/dist/runtime/validators.d.ts +120 -0
  97. package/dist/runtime/validators.d.ts.map +1 -0
  98. package/dist/runtime/validators.js +229 -0
  99. package/dist/runtime/validators.js.map +1 -0
  100. package/dist/runtime/worker-pool.d.ts +208 -0
  101. package/dist/runtime/worker-pool.d.ts.map +1 -0
  102. package/dist/runtime/worker-pool.js +419 -0
  103. package/dist/runtime/worker-pool.js.map +1 -0
  104. package/dist/types/index.d.ts +51 -1
  105. package/dist/types/index.d.ts.map +1 -1
  106. package/dist/tywrap.d.ts.map +1 -1
  107. package/dist/tywrap.js +227 -328
  108. package/dist/tywrap.js.map +1 -1
  109. package/dist/utils/cache.d.ts.map +1 -1
  110. package/dist/utils/cache.js +53 -5
  111. package/dist/utils/cache.js.map +1 -1
  112. package/dist/utils/codec.d.ts.map +1 -1
  113. package/dist/utils/codec.js +114 -1
  114. package/dist/utils/codec.js.map +1 -1
  115. package/dist/utils/ir-cache.d.ts +28 -0
  116. package/dist/utils/ir-cache.d.ts.map +1 -0
  117. package/dist/utils/ir-cache.js +29 -0
  118. package/dist/utils/ir-cache.js.map +1 -0
  119. package/dist/utils/parallel-processor.d.ts +1 -1
  120. package/dist/utils/parallel-processor.d.ts.map +1 -1
  121. package/dist/utils/parallel-processor.js +111 -40
  122. package/dist/utils/parallel-processor.js.map +1 -1
  123. package/dist/utils/runtime.d.ts +5 -1
  124. package/dist/utils/runtime.d.ts.map +1 -1
  125. package/dist/utils/runtime.js +112 -18
  126. package/dist/utils/runtime.js.map +1 -1
  127. package/package.json +40 -18
  128. package/runtime/python_bridge.py +108 -15
  129. package/runtime/safe_codec.py +344 -0
  130. package/src/cli.ts +61 -2
  131. package/src/config/index.ts +63 -17
  132. package/src/core/analyzer.ts +52 -15
  133. package/src/core/annotation-parser.ts +500 -0
  134. package/src/core/discovery.ts +26 -12
  135. package/src/core/generator.ts +956 -116
  136. package/src/core/mapper.ts +158 -51
  137. package/src/index.ts +78 -6
  138. package/src/runtime/base.ts +18 -26
  139. package/src/runtime/bounded-context.ts +608 -0
  140. package/src/runtime/bridge-core.ts +58 -18
  141. package/src/runtime/bridge-protocol.ts +483 -0
  142. package/src/runtime/disposable.ts +65 -0
  143. package/src/runtime/errors.ts +14 -0
  144. package/src/runtime/http-io.ts +245 -0
  145. package/src/runtime/http.ts +76 -115
  146. package/src/runtime/node.ts +563 -676
  147. package/src/runtime/optimized-node.ts +4 -1
  148. package/src/runtime/pooled-transport.ts +263 -0
  149. package/src/runtime/process-io.ts +930 -0
  150. package/src/runtime/pyodide-io.ts +551 -0
  151. package/src/runtime/pyodide.ts +75 -215
  152. package/src/runtime/safe-codec.ts +651 -0
  153. package/src/runtime/transport.ts +278 -0
  154. package/src/runtime/validators.ts +252 -0
  155. package/src/runtime/worker-pool.ts +559 -0
  156. package/src/types/global.d.ts +11 -1
  157. package/src/types/index.ts +63 -0
  158. package/src/tywrap.ts +279 -358
  159. package/src/utils/cache.ts +59 -5
  160. package/src/utils/codec.ts +122 -1
  161. package/src/utils/ir-cache.ts +51 -0
  162. package/src/utils/parallel-processor.ts +119 -43
  163. package/src/utils/runtime.ts +129 -18
@@ -3,9 +3,12 @@
3
3
  */
4
4
 
5
5
  import type {
6
+ PythonGenericParameter,
6
7
  PythonFunction,
7
8
  PythonClass,
8
9
  PythonModule,
10
+ PythonType,
11
+ PythonTypeAlias,
9
12
  GeneratedCode,
10
13
  TypescriptType,
11
14
  } from '../types/index.js';
@@ -13,6 +16,19 @@ import { globalCache } from '../utils/cache.js';
13
16
 
14
17
  import { TypeMapper } from './mapper.js';
15
18
 
19
+ interface GenericRenderParam {
20
+ name: string;
21
+ declaration: string;
22
+ }
23
+
24
+ interface GenericRenderContext {
25
+ currentModule?: string;
26
+ declaration: string;
27
+ typeArguments: string;
28
+ emittedNames: Set<string>;
29
+ emittedParamSpecs: Set<string>;
30
+ }
31
+
16
32
  export class CodeGenerator {
17
33
  private readonly mapper: TypeMapper;
18
34
  private readonly reservedTsIdentifiers = new Set([
@@ -109,6 +125,217 @@ export class CodeGenerator {
109
125
  });
110
126
  }
111
127
 
128
+ private getTypeParameters(
129
+ typeParameters?: readonly PythonGenericParameter[]
130
+ ): readonly PythonGenericParameter[] {
131
+ return typeParameters ?? [];
132
+ }
133
+
134
+ private buildGenericRenderContext(
135
+ typeParameters: readonly PythonGenericParameter[],
136
+ types: readonly PythonType[],
137
+ currentModule?: string
138
+ ): GenericRenderContext {
139
+ const callableParamSpecs = new Set<string>();
140
+ types.forEach(type => this.collectCallableParamSpecs(type, callableParamSpecs));
141
+
142
+ const emitted: GenericRenderParam[] = [];
143
+ const emittedNames = new Set<string>();
144
+ const emittedParamSpecs = new Set<string>();
145
+
146
+ typeParameters.forEach(param => {
147
+ if (
148
+ param.kind === 'typevar' &&
149
+ !param.bound &&
150
+ !(param.constraints && param.constraints.length > 0) &&
151
+ (!param.variance || param.variance === 'invariant')
152
+ ) {
153
+ emitted.push({ name: param.name, declaration: param.name });
154
+ emittedNames.add(param.name);
155
+ return;
156
+ }
157
+
158
+ if (param.kind === 'paramspec' && callableParamSpecs.has(param.name)) {
159
+ emitted.push({ name: param.name, declaration: `${param.name} extends unknown[]` });
160
+ emittedNames.add(param.name);
161
+ emittedParamSpecs.add(param.name);
162
+ }
163
+ });
164
+
165
+ return {
166
+ currentModule,
167
+ declaration:
168
+ emitted.length > 0 ? `<${emitted.map(param => param.declaration).join(', ')}>` : '',
169
+ typeArguments: emitted.length > 0 ? `<${emitted.map(param => param.name).join(', ')}>` : '',
170
+ emittedNames,
171
+ emittedParamSpecs,
172
+ };
173
+ }
174
+
175
+ private mergeGenericRenderContexts(
176
+ outer: GenericRenderContext,
177
+ inner: GenericRenderContext
178
+ ): GenericRenderContext {
179
+ return {
180
+ currentModule: inner.currentModule ?? outer.currentModule,
181
+ declaration: inner.declaration,
182
+ typeArguments: inner.typeArguments,
183
+ emittedNames: new Set([...outer.emittedNames, ...inner.emittedNames]),
184
+ emittedParamSpecs: new Set([...outer.emittedParamSpecs, ...inner.emittedParamSpecs]),
185
+ };
186
+ }
187
+
188
+ private collectCallableParamSpecs(type: PythonType, out: Set<string>): void {
189
+ switch (type.kind) {
190
+ case 'collection':
191
+ type.itemTypes.forEach(item => this.collectCallableParamSpecs(item, out));
192
+ break;
193
+ case 'paramspec':
194
+ out.add(type.name);
195
+ break;
196
+ case 'union':
197
+ type.types.forEach(item => this.collectCallableParamSpecs(item, out));
198
+ break;
199
+ case 'optional':
200
+ this.collectCallableParamSpecs(type.type, out);
201
+ break;
202
+ case 'generic':
203
+ type.typeArgs.forEach(item => this.collectCallableParamSpecs(item, out));
204
+ break;
205
+ case 'callable':
206
+ if (type.parameterSpec?.kind === 'paramspec') {
207
+ out.add(type.parameterSpec.name);
208
+ }
209
+ type.parameters.forEach(item => this.collectCallableParamSpecs(item, out));
210
+ this.collectCallableParamSpecs(type.returnType, out);
211
+ break;
212
+ case 'annotated':
213
+ this.collectCallableParamSpecs(type.base, out);
214
+ break;
215
+ case 'final':
216
+ case 'classvar':
217
+ this.collectCallableParamSpecs(type.type, out);
218
+ break;
219
+ case 'unpack':
220
+ this.collectCallableParamSpecs(type.type, out);
221
+ break;
222
+ default:
223
+ break;
224
+ }
225
+ }
226
+
227
+ private sanitizeType(type: PythonType, ctx: GenericRenderContext): PythonType {
228
+ const unknownType = (): PythonType => ({ kind: 'custom', name: 'Any', module: 'typing' });
229
+
230
+ switch (type.kind) {
231
+ case 'primitive':
232
+ case 'literal':
233
+ return type;
234
+ case 'custom':
235
+ return type;
236
+ case 'collection':
237
+ return {
238
+ ...type,
239
+ itemTypes: type.itemTypes.map(item => this.sanitizeType(item, ctx)),
240
+ };
241
+ case 'union':
242
+ return {
243
+ ...type,
244
+ types: type.types.map(item => this.sanitizeType(item, ctx)),
245
+ };
246
+ case 'optional':
247
+ return { ...type, type: this.sanitizeType(type.type, ctx) };
248
+ case 'generic':
249
+ return {
250
+ ...type,
251
+ module: type.module === ctx.currentModule ? undefined : type.module,
252
+ typeArgs: type.typeArgs.map(item => this.sanitizeType(item, ctx)),
253
+ };
254
+ case 'callable':
255
+ if (type.parameterSpec && !ctx.emittedParamSpecs.has(type.parameterSpec.name)) {
256
+ return {
257
+ ...type,
258
+ parameters: [{ kind: 'custom', name: '...' }],
259
+ parameterSpec: undefined,
260
+ returnType: this.sanitizeType(type.returnType, ctx),
261
+ };
262
+ }
263
+ return {
264
+ ...type,
265
+ parameters: type.parameters.map(item => this.sanitizeType(item, ctx)),
266
+ parameterSpec:
267
+ type.parameterSpec && ctx.emittedParamSpecs.has(type.parameterSpec.name)
268
+ ? type.parameterSpec
269
+ : undefined,
270
+ returnType: this.sanitizeType(type.returnType, ctx),
271
+ };
272
+ case 'annotated':
273
+ return { ...type, base: this.sanitizeType(type.base, ctx) };
274
+ case 'typevar':
275
+ case 'paramspec':
276
+ return ctx.emittedNames.has(type.name) ? type : unknownType();
277
+ case 'paramspec_args':
278
+ return {
279
+ kind: 'collection',
280
+ name: 'list',
281
+ itemTypes: [unknownType()],
282
+ };
283
+ case 'paramspec_kwargs':
284
+ return {
285
+ kind: 'collection',
286
+ name: 'dict',
287
+ itemTypes: [{ kind: 'primitive', name: 'str' }, unknownType()],
288
+ };
289
+ case 'typevartuple':
290
+ return unknownType();
291
+ case 'unpack':
292
+ return unknownType();
293
+ case 'final':
294
+ return { ...type, type: this.sanitizeType(type.type, ctx) };
295
+ case 'classvar':
296
+ return { ...type, type: this.sanitizeType(type.type, ctx) };
297
+ }
298
+ }
299
+
300
+ private typeToTsFromPython(
301
+ type: PythonType,
302
+ ctx: GenericRenderContext,
303
+ mappingContext: 'value' | 'return'
304
+ ): string {
305
+ return this.typeToTs(this.mapper.mapPythonType(this.sanitizeType(type, ctx), mappingContext));
306
+ }
307
+
308
+ private renderLooksLikeKwargsExpr(
309
+ valueExpr: string,
310
+ options: {
311
+ keywordOnlyNames: string[];
312
+ requiredKwOnlyNames: string[];
313
+ hasVarKwArgs: boolean;
314
+ }
315
+ ): string {
316
+ const base = `typeof ${valueExpr} === 'object' && ${valueExpr} !== null && !globalThis.Array.isArray(${valueExpr}) && (Object.getPrototypeOf(${valueExpr}) === Object.prototype || Object.getPrototypeOf(${valueExpr}) === null)`;
317
+
318
+ const keyCheck = (() => {
319
+ if (options.requiredKwOnlyNames.length > 0) {
320
+ return options.requiredKwOnlyNames
321
+ .map(k => `Object.prototype.hasOwnProperty.call(${valueExpr}, ${JSON.stringify(k)})`)
322
+ .join(' && ');
323
+ }
324
+ if (options.hasVarKwArgs) {
325
+ // With **kwargs, any plain object could be kwargs.
326
+ return 'true';
327
+ }
328
+ if (options.keywordOnlyNames.length > 0) {
329
+ return options.keywordOnlyNames
330
+ .map(k => `Object.prototype.hasOwnProperty.call(${valueExpr}, ${JSON.stringify(k)})`)
331
+ .join(' || ');
332
+ }
333
+ return 'false';
334
+ })();
335
+
336
+ return `(${base} && (${keyCheck}))`;
337
+ }
338
+
112
339
  generateFunctionWrapper(
113
340
  func: PythonFunction,
114
341
  moduleName?: string,
@@ -119,25 +346,84 @@ export class CodeGenerator {
119
346
  annotatedJSDoc ? func.parameters.map(p => String(p.type)) : undefined
120
347
  );
121
348
  const filteredParams = func.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
122
- const renderParam = (p: (typeof filteredParams)[number]): string => {
349
+ const keywordOnlyParams = filteredParams.filter(p => p.keywordOnly);
350
+ const positionalOnlyNames = filteredParams.filter(p => p.positionalOnly).map(p => p.name);
351
+ const hasVarKwArgs = filteredParams.some(p => p.kwArgs);
352
+ const needsKwargsParam = keywordOnlyParams.length > 0 || hasVarKwArgs;
353
+
354
+ const varArgsParam = filteredParams.find(p => p.varArgs);
355
+ const needsVarArgsArray = Boolean(varArgsParam) && needsKwargsParam;
356
+
357
+ const positionalParams = filteredParams.filter(p => !p.keywordOnly && !p.varArgs && !p.kwArgs);
358
+ const genericContext = this.buildGenericRenderContext(
359
+ this.getTypeParameters(func.typeParameters),
360
+ [func.returnType, ...filteredParams.map(param => param.type)],
361
+ moduleName
362
+ );
363
+ const typeParamDecl = genericContext.declaration;
364
+
365
+ const tsTypeForValue = (p: (typeof filteredParams)[number]): string =>
366
+ this.typeToTsFromPython(p.type, genericContext, 'value');
367
+
368
+ const kwargsType = (() => {
369
+ if (!needsKwargsParam) {
370
+ return '';
371
+ }
372
+ if (keywordOnlyParams.length === 0 && hasVarKwArgs) {
373
+ return 'Record<string, unknown>';
374
+ }
375
+ const props = keywordOnlyParams
376
+ .map(p => `${JSON.stringify(p.name)}${p.optional ? '?' : ''}: ${tsTypeForValue(p)};`)
377
+ .join(' ');
378
+ const obj = `{ ${props} }`;
379
+ return hasVarKwArgs ? `(${obj} & Record<string, unknown>)` : obj;
380
+ })();
381
+
382
+ const renderPositionalParam = (
383
+ p: (typeof positionalParams)[number],
384
+ forceRequired = false
385
+ ): string => {
123
386
  const pname = this.escapeIdentifier(p.name);
124
- if (p.varArgs) {
387
+ const opt = !forceRequired && p.optional ? '?' : '';
388
+ return `${pname}${opt}: ${tsTypeForValue(p)}`;
389
+ };
390
+
391
+ const renderVarArgsParam = (forceRequired = false): string | null => {
392
+ if (!varArgsParam) {
393
+ return null;
394
+ }
395
+ const pname = this.escapeIdentifier(varArgsParam.name);
396
+ if (!needsVarArgsArray) {
125
397
  return `...${pname}: unknown[]`;
126
398
  }
127
- if (p.kwArgs) {
128
- return `kwargs?: Record<string, unknown>`;
399
+ const opt = forceRequired ? '' : '?';
400
+ return `${pname}${opt}: unknown[]`;
401
+ };
402
+
403
+ const renderKwargsParam = (forceRequired = false): string | null => {
404
+ if (!needsKwargsParam) {
405
+ return null;
129
406
  }
130
- return `${pname}${p.optional ? '?' : ''}: ${this.typeToTs(
131
- this.mapper.mapPythonType(p.type, 'value')
132
- )}`;
407
+ const opt = forceRequired ? '' : '?';
408
+ return `kwargs${opt}: ${kwargsType}`;
133
409
  };
134
- const paramDecl = filteredParams.map(renderParam).join(', ');
135
- const positionalParams = filteredParams.filter(p => !p.kwArgs);
136
- const callArgs = positionalParams
137
- .map(p => (p.varArgs ? `...${this.escapeIdentifier(p.name)}` : this.escapeIdentifier(p.name)))
138
- .join(', ');
139
- const hasKwArgs = filteredParams.some(p => p.kwArgs);
140
- const returnType = this.typeToTs(this.mapper.mapPythonType(func.returnType, 'return'));
410
+
411
+ const implParams: string[] = [];
412
+ for (const p of positionalParams) {
413
+ implParams.push(renderPositionalParam(p));
414
+ }
415
+ const varArgsDecl = renderVarArgsParam(false);
416
+ if (varArgsDecl) {
417
+ implParams.push(varArgsDecl);
418
+ }
419
+ const kwargsDecl = needsKwargsParam ? `kwargs?: ${kwargsType}` : null;
420
+ if (kwargsDecl) {
421
+ implParams.push(kwargsDecl);
422
+ }
423
+ const paramDecl = implParams.join(', ');
424
+
425
+ const hasKwArgs = needsKwargsParam;
426
+ const returnType = this.typeToTsFromPython(func.returnType, genericContext, 'return');
141
427
  const fname = this.escapeIdentifier(func.name);
142
428
  const moduleId = moduleName ?? '__main__';
143
429
 
@@ -146,25 +432,171 @@ export class CodeGenerator {
146
432
  // `fn(a)`, `fn(a, b)`, ... all to typecheck. We emit a family of overloads that progressively
147
433
  // "drop" optional tail args, but also include the full positional signature (<= length) so a
148
434
  // call that supplies all args still matches an overload.
149
- const positional = filteredParams.filter(p => !p.varArgs && !p.kwArgs);
150
- const firstOptionalIndex = positional.findIndex(p => p.optional);
435
+ const firstOptionalIndex = positionalParams.findIndex(p => p.optional);
436
+ const requiredKwOnlyNames = keywordOnlyParams.filter(p => !p.optional).map(p => p.name);
437
+ const keywordOnlyNames = keywordOnlyParams.map(p => p.name);
151
438
  const overloads: string[] = [];
152
- if (firstOptionalIndex >= 0) {
153
- for (let i = firstOptionalIndex; i <= positional.length; i++) {
154
- const head = positional.slice(0, i);
155
- const rest = filteredParams.filter(p => p.varArgs || p.kwArgs);
156
- const sigParams = [...head, ...rest].map(renderParam).join(', ');
157
- overloads.push(`export function ${fname}(${sigParams}): Promise<${returnType}>;`);
439
+ if (requiredKwOnlyNames.length > 0) {
440
+ // Required keyword-only params must be represented with a required `kwargs` parameter.
441
+ // Avoid "required after optional" by emitting overloads where all preceding parameters are required.
442
+ const requiredPosCount =
443
+ firstOptionalIndex >= 0 ? firstOptionalIndex : positionalParams.length;
444
+ for (let i = requiredPosCount; i <= positionalParams.length; i++) {
445
+ const head = positionalParams.slice(0, i).map(p => renderPositionalParam(p, true));
446
+ const rest: string[] = [];
447
+ const v = (() => {
448
+ if (!varArgsParam) {
449
+ return null;
450
+ }
451
+ const pname = this.escapeIdentifier(varArgsParam.name);
452
+ if (!needsVarArgsArray) {
453
+ return `...${pname}: unknown[]`;
454
+ }
455
+ // In overloads where `kwargs` is required, keep the `args` surrogate parameter required,
456
+ // but allow `undefined` as a placeholder so callers can omit varargs while still passing kwargs.
457
+ return `${pname}: unknown[] | undefined`;
458
+ })();
459
+ if (v) {
460
+ rest.push(v);
461
+ }
462
+ const k = renderKwargsParam(true);
463
+ if (k) {
464
+ rest.push(k);
465
+ }
466
+ overloads.push(
467
+ `export function ${fname}${typeParamDecl}(${[...head, ...rest].join(', ')}): Promise<${returnType}>;`
468
+ );
469
+ if (varArgsParam && needsVarArgsArray) {
470
+ // Also allow callers to omit the varargs surrogate parameter entirely (i.e. `fn(kwargs)`).
471
+ overloads.push(
472
+ `export function ${fname}${typeParamDecl}(${[...head, renderKwargsParam(true)].join(', ')}): Promise<${returnType}>;`
473
+ );
474
+ }
475
+ }
476
+ } else if (firstOptionalIndex >= 0 && !varArgsParam && !needsKwargsParam) {
477
+ for (let i = firstOptionalIndex; i <= positionalParams.length; i++) {
478
+ const head = positionalParams.slice(0, i).map(p => renderPositionalParam(p));
479
+ const rest: string[] = [];
480
+ const v = renderVarArgsParam(false);
481
+ if (v) {
482
+ rest.push(v);
483
+ }
484
+ const k = renderKwargsParam(false);
485
+ if (k) {
486
+ rest.push(k);
487
+ }
488
+ overloads.push(
489
+ `export function ${fname}${typeParamDecl}(${[...head, ...rest].join(', ')}): Promise<${returnType}>;`
490
+ );
158
491
  }
159
492
  }
160
493
 
161
494
  const overloadDecl = overloads.length > 0 ? `${overloads.join('\n')}\n` : '';
162
- const ts = `${jsdoc}${overloadDecl}export async function ${fname}(${paramDecl}): Promise<${returnType}> {
163
- return getRuntimeBridge().call('${moduleId}', '${func.name}', [${callArgs}]${hasKwArgs ? ', kwargs' : ''});
495
+ const guardLines: string[] = [];
496
+ if (hasKwArgs && positionalOnlyNames.length > 0) {
497
+ guardLines.push(
498
+ ` const __positionalOnly = ${JSON.stringify(positionalOnlyNames)} as const;`
499
+ );
500
+ guardLines.push(` for (const key of __positionalOnly) {`);
501
+ guardLines.push(` if (__kwargs && Object.prototype.hasOwnProperty.call(__kwargs, key)) {`);
502
+ guardLines.push(
503
+ ` throw new Error(\`${func.name} does not accept positional-only argument "\${key}" as a keyword argument\`);`
504
+ );
505
+ guardLines.push(` }`);
506
+ guardLines.push(` }`);
507
+ }
508
+ if (hasKwArgs && requiredKwOnlyNames.length > 0) {
509
+ guardLines.push(
510
+ ` const __requiredKwOnly = ${JSON.stringify(requiredKwOnlyNames)} as const;`
511
+ );
512
+ guardLines.push(` const __missing: string[] = [];`);
513
+ guardLines.push(` for (const key of __requiredKwOnly) {`);
514
+ guardLines.push(
515
+ ` if (!__kwargs || !Object.prototype.hasOwnProperty.call(__kwargs, key)) {`
516
+ );
517
+ guardLines.push(` __missing.push(key);`);
518
+ guardLines.push(` }`);
519
+ guardLines.push(` }`);
520
+ guardLines.push(` if (__missing.length > 0) {`);
521
+ guardLines.push(
522
+ ` throw new Error(\`Missing required keyword-only arguments for ${func.name}: \${__missing.join(', ')}\`);`
523
+ );
524
+ guardLines.push(` }`);
525
+ }
526
+ const guards = guardLines.length > 0 ? `${guardLines.join('\n')}\n` : '';
527
+
528
+ const firstOptionalPosIndex = positionalParams.findIndex(p => p.optional);
529
+ const requiredPosCount =
530
+ firstOptionalPosIndex >= 0 ? firstOptionalPosIndex : positionalParams.length;
531
+ const callPreludeLines: string[] = [];
532
+ if (hasKwArgs) {
533
+ callPreludeLines.push(` let __kwargs = kwargs;`);
534
+ }
535
+ const positionalArgExprs = positionalParams.map(p => this.escapeIdentifier(p.name));
536
+ callPreludeLines.push(` const __args: unknown[] = [${positionalArgExprs.join(', ')}];`);
537
+ if (requiredPosCount < positionalParams.length) {
538
+ callPreludeLines.push(
539
+ ` while (__args.length > ${requiredPosCount} && __args[__args.length - 1] === undefined) {`
540
+ );
541
+ callPreludeLines.push(` __args.pop();`);
542
+ callPreludeLines.push(` }`);
543
+ }
544
+ if (hasKwArgs && requiredPosCount < positionalParams.length) {
545
+ const looksLikeKwargs = this.renderLooksLikeKwargsExpr('__candidate', {
546
+ keywordOnlyNames,
547
+ requiredKwOnlyNames,
548
+ hasVarKwArgs,
549
+ });
550
+ callPreludeLines.push(
551
+ ` if (__kwargs === undefined && __args.length > ${requiredPosCount}) {`
552
+ );
553
+ callPreludeLines.push(` const __candidate = __args[__args.length - 1];`);
554
+ callPreludeLines.push(` if (${looksLikeKwargs}) {`);
555
+ callPreludeLines.push(` __kwargs = __candidate as any;`);
556
+ callPreludeLines.push(` __args.pop();`);
557
+ callPreludeLines.push(` }`);
558
+ callPreludeLines.push(` }`);
559
+ }
560
+ if (varArgsParam) {
561
+ const vname = this.escapeIdentifier(varArgsParam.name);
562
+ if (needsVarArgsArray) {
563
+ const looksLikeKwargs = this.renderLooksLikeKwargsExpr(vname, {
564
+ keywordOnlyNames,
565
+ requiredKwOnlyNames,
566
+ hasVarKwArgs,
567
+ });
568
+ callPreludeLines.push(` let __varargs: unknown[] = [];`);
569
+ callPreludeLines.push(` if (${vname} !== undefined) {`);
570
+ callPreludeLines.push(` if (globalThis.Array.isArray(${vname})) {`);
571
+ callPreludeLines.push(` __varargs = ${vname};`);
572
+ callPreludeLines.push(` } else if (__kwargs === undefined && ${looksLikeKwargs}) {`);
573
+ callPreludeLines.push(` __kwargs = ${vname} as any;`);
574
+ callPreludeLines.push(` } else {`);
575
+ callPreludeLines.push(
576
+ ` throw new Error(\`${func.name} expected ${varArgsParam.name} to be an array\`);`
577
+ );
578
+ callPreludeLines.push(` }`);
579
+ callPreludeLines.push(` }`);
580
+ callPreludeLines.push(` __args.push(...__varargs);`);
581
+ } else {
582
+ callPreludeLines.push(` __args.push(...${vname});`);
583
+ }
584
+ }
585
+ const callPrelude = callPreludeLines.length > 0 ? `${callPreludeLines.join('\n')}\n` : '';
586
+
587
+ const ts = `${jsdoc}${overloadDecl}export async function ${fname}${typeParamDecl}(${paramDecl}): Promise<${returnType}> {
588
+ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.name}', __args${
589
+ hasKwArgs ? ', __kwargs' : ''
590
+ });
164
591
  }
165
592
  `;
166
593
 
167
- return this.wrap(ts, [func.name]);
594
+ const declarationBody =
595
+ overloads.length > 0
596
+ ? overloadDecl
597
+ : `export function ${fname}${typeParamDecl}(${paramDecl}): Promise<${returnType}>;\n`;
598
+
599
+ return this.wrap(ts, `${jsdoc}${declarationBody}`, [func.name]);
168
600
  }
169
601
 
170
602
  generateClassWrapper(
@@ -173,140 +605,534 @@ export class CodeGenerator {
173
605
  _annotatedJSDoc = false
174
606
  ): GeneratedCode {
175
607
  const jsdoc = this.generateJsDoc(cls.docstring);
176
- // Structural type aliases for special kinds
608
+ const classGenericContext = this.buildGenericRenderContext(
609
+ this.getTypeParameters(cls.typeParameters),
610
+ [
611
+ ...cls.properties.map(property => property.type),
612
+ ...cls.methods.flatMap(method => [
613
+ method.returnType,
614
+ ...method.parameters.map(p => p.type),
615
+ ]),
616
+ ],
617
+ moduleName
618
+ );
619
+ const classTypeParamDecl = classGenericContext.declaration;
620
+ const cname = this.escapeIdentifier(cls.name);
621
+ const classSelfType = `${cname}${classGenericContext.typeArguments}`;
622
+ const tsValueType = (p: (typeof cls.methods)[number]['parameters'][number]): string =>
623
+ this.typeToTsFromPython(p.type, classGenericContext, 'value');
624
+
625
+ const wrapAlias = (body: string): GeneratedCode => {
626
+ const ts = `${jsdoc}export type ${cname}${classTypeParamDecl} = ${body}\n`;
627
+ return this.wrap(ts, ts, [cls.name]);
628
+ };
629
+
177
630
  if (cls.decorators.includes('__typed_dict__') || cls.kind === 'typed_dict') {
178
631
  const props = cls.properties
179
632
  .map(p => {
180
633
  const pname = this.escapeIdentifier(p.name);
181
634
  const opt = (p as unknown as { optional?: boolean }).optional === true ? '?' : '';
182
- const t = this.typeToTs(this.mapper.mapPythonType(p.type, 'value'));
635
+ const t = this.typeToTsFromPython(p.type, classGenericContext, 'value');
183
636
  return `${pname}${opt}: ${t};`;
184
637
  })
185
638
  .join(' ');
186
- const cname = this.escapeIdentifier(cls.name);
187
- const ts = `${jsdoc}export type ${cname} = { ${props} }\n`;
188
- return this.wrap(ts, [cls.name]);
639
+ return wrapAlias(`{ ${props} }`);
189
640
  }
190
641
 
191
642
  if (cls.kind === 'namedtuple') {
192
- // NamedTuple -> readonly tuple type alias `[T1, T2, ...]`
193
643
  const elements = cls.properties.map(p =>
194
- this.typeToTs(this.mapper.mapPythonType(p.type, 'value'))
644
+ this.typeToTsFromPython(p.type, classGenericContext, 'value')
195
645
  );
196
- const cname = this.escapeIdentifier(cls.name);
197
- const ts = `${jsdoc}export type ${cname} = readonly [${elements.join(', ')}]\n`;
198
- return this.wrap(ts, [cls.name]);
646
+ return wrapAlias(`readonly [${elements.join(', ')}]`);
199
647
  }
200
648
 
201
649
  if (cls.kind === 'protocol') {
202
- // Protocol -> structural interface-like type alias for attributes and callables (subset)
203
650
  const props = cls.properties
204
651
  .map(
205
652
  p =>
206
- `${this.escapeIdentifier(p.name)}: ${this.typeToTs(this.mapper.mapPythonType(p.type, 'value'))};`
653
+ `${this.escapeIdentifier(p.name)}: ${this.typeToTsFromPython(p.type, classGenericContext, 'value')};`
207
654
  )
208
655
  .join(' ');
209
656
  const methods = cls.methods
657
+ .filter(m => m.name !== '__init__')
210
658
  .map(m => {
211
659
  const fparams = m.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
660
+ const methodOwnGenericContext = this.buildGenericRenderContext(
661
+ this.getTypeParameters(m.typeParameters),
662
+ [m.returnType, ...fparams.map(param => param.type)],
663
+ moduleName
664
+ );
665
+ const methodGenericContext = this.mergeGenericRenderContexts(
666
+ classGenericContext,
667
+ methodOwnGenericContext
668
+ );
669
+ const methodTypeParamDecl = methodOwnGenericContext.declaration;
212
670
  const paramsDecl = fparams
213
671
  .map(
214
672
  p =>
215
- `${this.escapeIdentifier(p.name)}${p.optional ? '?' : ''}: ${this.typeToTs(this.mapper.mapPythonType(p.type, 'value'))}`
673
+ `${this.escapeIdentifier(p.name)}${p.optional ? '?' : ''}: ${this.typeToTsFromPython(p.type, methodGenericContext, 'value')}`
216
674
  )
217
675
  .join(', ');
218
- const returnType = this.typeToTs(this.mapper.mapPythonType(m.returnType, 'return'));
219
- return `${this.escapeIdentifier(m.name)}: (${paramsDecl}) => ${returnType};`;
676
+ const returnType = this.typeToTsFromPython(m.returnType, methodGenericContext, 'return');
677
+ return `${this.escapeIdentifier(m.name)}: ${methodTypeParamDecl}(${paramsDecl}) => ${returnType};`;
220
678
  })
221
679
  .join(' ');
222
- const cname = this.escapeIdentifier(cls.name);
223
- const ts = `${jsdoc}export type ${cname} = { ${props} ${methods} }\n`;
224
- return this.wrap(ts, [cls.name]);
680
+ return wrapAlias(`{ ${props} ${methods} }`);
225
681
  }
226
682
 
227
683
  if (cls.kind === 'dataclass' || cls.kind === 'pydantic') {
228
- // Data containers -> object type alias
229
684
  const props = cls.properties
230
685
  .map(p => {
231
686
  const pname = this.escapeIdentifier(p.name);
232
687
  const opt = (p as unknown as { optional?: boolean }).optional === true ? '?' : '';
233
- const t = this.typeToTs(this.mapper.mapPythonType(p.type, 'value'));
688
+ const t = this.typeToTsFromPython(p.type, classGenericContext, 'value');
234
689
  return `${pname}${opt}: ${t};`;
235
690
  })
236
691
  .join(' ');
237
- const cname = this.escapeIdentifier(cls.name);
238
- const ts = `${jsdoc}export type ${cname} = { ${props} }\n`;
239
- return this.wrap(ts, [cls.name]);
692
+ return wrapAlias(`{ ${props} }`);
240
693
  }
694
+
241
695
  const sortedMethods = [...cls.methods].sort((a, b) => a.name.localeCompare(b.name));
242
- const methodBodies = sortedMethods
243
- .filter(m => m.name !== '__init__')
244
- .map(m => {
245
- const fparams = m.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
246
- const paramsDecl = fparams
247
- .map(p =>
248
- p.varArgs
249
- ? `...${this.escapeIdentifier(p.name)}: unknown[]`
250
- : p.kwArgs
251
- ? `kwargs?: Record<string, unknown>`
252
- : `${this.escapeIdentifier(p.name)}${p.optional ? '?' : ''}: ${this.typeToTs(this.mapper.mapPythonType(p.type, 'value'))}`
253
- )
254
- .join(', ');
255
- const positionalParams = fparams.filter(p => !p.kwArgs);
256
- const callPassthrough = positionalParams
257
- .map(p =>
258
- p.varArgs ? `...${this.escapeIdentifier(p.name)}` : this.escapeIdentifier(p.name)
259
- )
260
- .join(', ');
261
- const hasKwArgs = fparams.some(p => p.kwArgs);
262
- const returnType = this.typeToTs(this.mapper.mapPythonType(m.returnType, 'return'));
263
- return ` async ${this.escapeIdentifier(m.name)}(${paramsDecl}): Promise<${returnType}> { return getRuntimeBridge().callMethod(this.__handle, '${m.name}', [${callPassthrough}]${hasKwArgs ? ', kwargs' : ''}); }`;
264
- })
265
- .join('\n');
696
+ const methodBodies: string[] = [];
697
+ const methodDeclarations: string[] = [];
698
+
699
+ sortedMethods
700
+ .filter(method => method.name !== '__init__')
701
+ .forEach(method => {
702
+ const fparams = method.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
703
+ const methodOwnGenericContext = this.buildGenericRenderContext(
704
+ this.getTypeParameters(method.typeParameters),
705
+ [method.returnType, ...fparams.map(param => param.type)],
706
+ moduleName
707
+ );
708
+ const methodGenericContext = this.mergeGenericRenderContexts(
709
+ classGenericContext,
710
+ methodOwnGenericContext
711
+ );
712
+ const methodTypeParamDecl = methodOwnGenericContext.declaration;
713
+ const methodTsValueType = (p: (typeof fparams)[number]): string =>
714
+ this.typeToTsFromPython(p.type, methodGenericContext, 'value');
715
+ const keywordOnlyParams = fparams.filter(p => p.keywordOnly);
716
+ const positionalOnlyNames = fparams.filter(p => p.positionalOnly).map(p => p.name);
717
+ const hasVarKwArgs = fparams.some(p => p.kwArgs);
718
+ const needsKwargsParam = keywordOnlyParams.length > 0 || hasVarKwArgs;
719
+ const varArgsParam = fparams.find(p => p.varArgs);
720
+ const needsVarArgsArray = Boolean(varArgsParam) && needsKwargsParam;
721
+ const positionalParams = fparams.filter(p => !p.keywordOnly && !p.varArgs && !p.kwArgs);
722
+ const firstOptionalPosIndex = positionalParams.findIndex(p => p.optional);
723
+ const requiredPosCount =
724
+ firstOptionalPosIndex >= 0 ? firstOptionalPosIndex : positionalParams.length;
725
+ const keywordOnlyNames = keywordOnlyParams.map(p => p.name);
726
+
727
+ const renderPositionalParam = (
728
+ p: (typeof positionalParams)[number],
729
+ forceRequired = false
730
+ ): string => {
731
+ const pname = this.escapeIdentifier(p.name);
732
+ const opt = !forceRequired && p.optional ? '?' : '';
733
+ return `${pname}${opt}: ${methodTsValueType(p)}`;
734
+ };
735
+
736
+ const kwargsType = (() => {
737
+ if (!needsKwargsParam) {
738
+ return '';
739
+ }
740
+ if (keywordOnlyParams.length === 0 && hasVarKwArgs) {
741
+ return 'Record<string, unknown>';
742
+ }
743
+ const props = keywordOnlyParams
744
+ .map(p => `${JSON.stringify(p.name)}${p.optional ? '?' : ''}: ${methodTsValueType(p)};`)
745
+ .join(' ');
746
+ const obj = `{ ${props} }`;
747
+ return hasVarKwArgs ? `(${obj} & Record<string, unknown>)` : obj;
748
+ })();
749
+
750
+ const paramsDeclParts: string[] = [];
751
+ positionalParams.forEach(p => {
752
+ paramsDeclParts.push(renderPositionalParam(p));
753
+ });
754
+ if (varArgsParam) {
755
+ const vname = this.escapeIdentifier(varArgsParam.name);
756
+ paramsDeclParts.push(
757
+ needsVarArgsArray ? `${vname}?: unknown[]` : `...${vname}: unknown[]`
758
+ );
759
+ }
760
+ if (needsKwargsParam) {
761
+ paramsDeclParts.push(`kwargs?: ${kwargsType}`);
762
+ }
763
+ const paramsDecl = paramsDeclParts.join(', ');
764
+
765
+ const requiredKwOnlyNames = keywordOnlyParams.filter(p => !p.optional).map(p => p.name);
766
+ const returnType = this.typeToTsFromPython(
767
+ method.returnType,
768
+ methodGenericContext,
769
+ 'return'
770
+ );
771
+ const mname = this.escapeIdentifier(method.name);
772
+
773
+ const overloads: string[] = [];
774
+ if (needsKwargsParam && requiredKwOnlyNames.length > 0) {
775
+ const firstOptionalIndex = positionalParams.findIndex(p => p.optional);
776
+ const requiredPosCount =
777
+ firstOptionalIndex >= 0 ? firstOptionalIndex : positionalParams.length;
778
+ for (let i = requiredPosCount; i <= positionalParams.length; i++) {
779
+ const head = positionalParams.slice(0, i).map(p => renderPositionalParam(p, true));
780
+ const rest: string[] = [];
781
+ if (varArgsParam) {
782
+ const vname = this.escapeIdentifier(varArgsParam.name);
783
+ rest.push(
784
+ needsVarArgsArray ? `${vname}: unknown[] | undefined` : `...${vname}: unknown[]`
785
+ );
786
+ }
787
+ rest.push(`kwargs: ${kwargsType}`);
788
+ overloads.push(
789
+ ` ${mname}${methodTypeParamDecl}(${[...head, ...rest].join(', ')}): Promise<${returnType}>;`
790
+ );
791
+ if (varArgsParam && needsVarArgsArray) {
792
+ overloads.push(
793
+ ` ${mname}${methodTypeParamDecl}(${[...head, `kwargs: ${kwargsType}`].join(', ')}): Promise<${returnType}>;`
794
+ );
795
+ }
796
+ }
797
+ }
798
+ const overloadDecl = overloads.length > 0 ? `${overloads.join('\n')}\n` : '';
799
+
800
+ const callPreludeLines: string[] = [];
801
+ if (needsKwargsParam) {
802
+ callPreludeLines.push(` let __kwargs = kwargs;`);
803
+ }
804
+ const positionalArgExprs = positionalParams.map(p => this.escapeIdentifier(p.name));
805
+ callPreludeLines.push(` const __args: unknown[] = [${positionalArgExprs.join(', ')}];`);
806
+ if (requiredPosCount < positionalParams.length) {
807
+ callPreludeLines.push(
808
+ ` while (__args.length > ${requiredPosCount} && __args[__args.length - 1] === undefined) {`
809
+ );
810
+ callPreludeLines.push(` __args.pop();`);
811
+ callPreludeLines.push(` }`);
812
+ }
813
+ if (needsKwargsParam && requiredPosCount < positionalParams.length) {
814
+ const looksLikeKwargs = this.renderLooksLikeKwargsExpr('__candidate', {
815
+ keywordOnlyNames,
816
+ requiredKwOnlyNames,
817
+ hasVarKwArgs,
818
+ });
819
+ callPreludeLines.push(
820
+ ` if (__kwargs === undefined && __args.length > ${requiredPosCount}) {`
821
+ );
822
+ callPreludeLines.push(` const __candidate = __args[__args.length - 1];`);
823
+ callPreludeLines.push(` if (${looksLikeKwargs}) {`);
824
+ callPreludeLines.push(` __kwargs = __candidate as any;`);
825
+ callPreludeLines.push(` __args.pop();`);
826
+ callPreludeLines.push(` }`);
827
+ callPreludeLines.push(` }`);
828
+ }
829
+ if (varArgsParam) {
830
+ const vname = this.escapeIdentifier(varArgsParam.name);
831
+ if (needsVarArgsArray) {
832
+ const looksLikeKwargs = this.renderLooksLikeKwargsExpr(vname, {
833
+ keywordOnlyNames,
834
+ requiredKwOnlyNames,
835
+ hasVarKwArgs,
836
+ });
837
+ callPreludeLines.push(` let __varargs: unknown[] = [];`);
838
+ callPreludeLines.push(` if (${vname} !== undefined) {`);
839
+ callPreludeLines.push(` if (globalThis.Array.isArray(${vname})) {`);
840
+ callPreludeLines.push(` __varargs = ${vname};`);
841
+ callPreludeLines.push(
842
+ ` } else if (__kwargs === undefined && ${looksLikeKwargs}) {`
843
+ );
844
+ callPreludeLines.push(` __kwargs = ${vname} as any;`);
845
+ callPreludeLines.push(` } else {`);
846
+ callPreludeLines.push(
847
+ ` throw new Error(\`${method.name} expected ${varArgsParam.name} to be an array\`);`
848
+ );
849
+ callPreludeLines.push(` }`);
850
+ callPreludeLines.push(` }`);
851
+ callPreludeLines.push(` __args.push(...__varargs);`);
852
+ } else {
853
+ callPreludeLines.push(` __args.push(...${vname});`);
854
+ }
855
+ }
856
+ const callPrelude = callPreludeLines.length > 0 ? `${callPreludeLines.join('\n')}\n` : '';
857
+
858
+ const guardLines: string[] = [];
859
+ if (needsKwargsParam && positionalOnlyNames.length > 0) {
860
+ guardLines.push(
861
+ ` const __positionalOnly = ${JSON.stringify(positionalOnlyNames)} as const;`
862
+ );
863
+ guardLines.push(` for (const key of __positionalOnly) {`);
864
+ guardLines.push(
865
+ ` if (__kwargs && Object.prototype.hasOwnProperty.call(__kwargs, key)) {`
866
+ );
867
+ guardLines.push(
868
+ ` throw new Error(\`${method.name} does not accept positional-only argument "\${key}" as a keyword argument\`);`
869
+ );
870
+ guardLines.push(` }`);
871
+ guardLines.push(` }`);
872
+ }
873
+ if (needsKwargsParam && requiredKwOnlyNames.length > 0) {
874
+ guardLines.push(
875
+ ` const __requiredKwOnly = ${JSON.stringify(requiredKwOnlyNames)} as const;`
876
+ );
877
+ guardLines.push(` const __missing: string[] = [];`);
878
+ guardLines.push(` for (const key of __requiredKwOnly) {`);
879
+ guardLines.push(
880
+ ` if (!__kwargs || !Object.prototype.hasOwnProperty.call(__kwargs, key)) {`
881
+ );
882
+ guardLines.push(` __missing.push(key);`);
883
+ guardLines.push(` }`);
884
+ guardLines.push(` }`);
885
+ guardLines.push(` if (__missing.length > 0) {`);
886
+ guardLines.push(
887
+ ` throw new Error(\`Missing required keyword-only arguments for ${method.name}: \${__missing.join(', ')}\`);`
888
+ );
889
+ guardLines.push(` }`);
890
+ }
891
+ const guards = guardLines.length > 0 ? `${guardLines.join('\n')}\n` : '';
892
+
893
+ methodBodies.push(`${overloadDecl} async ${mname}${methodTypeParamDecl}(${paramsDecl}): Promise<${returnType}> {
894
+ ${callPrelude}${guards} return getRuntimeBridge().callMethod(this.__handle, '${method.name}', __args${
895
+ needsKwargsParam ? ', __kwargs' : ''
896
+ });
897
+ }`);
898
+ methodDeclarations.push(
899
+ `${overloadDecl}${overloads.length > 0 ? '' : ` ${mname}${methodTypeParamDecl}(${paramsDecl}): Promise<${returnType}>;\n`}`
900
+ );
901
+ });
266
902
 
267
- // Constructor typing from __init__
268
903
  const init = cls.methods.find(m => m.name === '__init__');
269
- const ctorParams = init
270
- ? init.parameters
271
- .filter(p => p.name !== 'self' && p.name !== 'cls')
272
- .map(p =>
273
- p.varArgs
274
- ? `...${this.escapeIdentifier(p.name)}: unknown[]`
275
- : p.kwArgs
276
- ? `kwargs?: Record<string, unknown>`
277
- : `${this.escapeIdentifier(p.name)}${p.optional ? '?' : ''}: ${this.typeToTs(this.mapper.mapPythonType(p.type, 'value'))}`
278
- )
279
- .join(', ')
280
- : `...args: unknown[]`;
281
-
282
- const ctorHasKwArgs = init
283
- ? init.parameters.some(p => p.name !== 'self' && p.name !== 'cls' && p.kwArgs)
284
- : false;
285
-
286
- const ctorArgsPassthrough = init
287
- ? init.parameters
288
- .filter(p => p.name !== 'self' && p.name !== 'cls' && !p.kwArgs)
289
- .map(p =>
290
- p.varArgs ? `...${this.escapeIdentifier(p.name)}` : this.escapeIdentifier(p.name)
291
- )
292
- .join(', ')
293
- : '...args';
904
+ const ctorSpec = (() => {
905
+ if (!init) {
906
+ return {
907
+ overloadDecl: '',
908
+ declaration: ` static create${classTypeParamDecl}(...args: unknown[]): Promise<${classSelfType}>;\n`,
909
+ paramsDecl: `...args: unknown[]`,
910
+ callPrelude: ` const __args: unknown[] = [...args];\n`,
911
+ hasKwargs: false,
912
+ guardLines: [] as string[],
913
+ };
914
+ }
915
+
916
+ const fparams = init.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
917
+ const keywordOnlyParams = fparams.filter(p => p.keywordOnly);
918
+ const positionalOnlyNames = fparams.filter(p => p.positionalOnly).map(p => p.name);
919
+ const hasVarKwArgs = fparams.some(p => p.kwArgs);
920
+ const needsKwargsParam = keywordOnlyParams.length > 0 || hasVarKwArgs;
921
+ const varArgsParam = fparams.find(p => p.varArgs);
922
+ const needsVarArgsArray = Boolean(varArgsParam) && needsKwargsParam;
923
+ const positionalParams = fparams.filter(p => !p.keywordOnly && !p.varArgs && !p.kwArgs);
924
+ const firstOptionalPosIndex = positionalParams.findIndex(p => p.optional);
925
+ const requiredPosCount =
926
+ firstOptionalPosIndex >= 0 ? firstOptionalPosIndex : positionalParams.length;
927
+ const keywordOnlyNames = keywordOnlyParams.map(p => p.name);
928
+
929
+ const renderPositionalParam = (
930
+ p: (typeof positionalParams)[number],
931
+ forceRequired = false
932
+ ): string => {
933
+ const pname = this.escapeIdentifier(p.name);
934
+ const opt = !forceRequired && p.optional ? '?' : '';
935
+ return `${pname}${opt}: ${tsValueType(p)}`;
936
+ };
937
+
938
+ const kwargsType = (() => {
939
+ if (!needsKwargsParam) {
940
+ return '';
941
+ }
942
+ if (keywordOnlyParams.length === 0 && hasVarKwArgs) {
943
+ return 'Record<string, unknown>';
944
+ }
945
+ const props = keywordOnlyParams
946
+ .map(p => `${JSON.stringify(p.name)}${p.optional ? '?' : ''}: ${tsValueType(p)};`)
947
+ .join(' ');
948
+ const obj = `{ ${props} }`;
949
+ return hasVarKwArgs ? `(${obj} & Record<string, unknown>)` : obj;
950
+ })();
951
+
952
+ const paramsDeclParts: string[] = [];
953
+ positionalParams.forEach(p => {
954
+ paramsDeclParts.push(renderPositionalParam(p));
955
+ });
956
+ if (varArgsParam) {
957
+ const vname = this.escapeIdentifier(varArgsParam.name);
958
+ paramsDeclParts.push(needsVarArgsArray ? `${vname}?: unknown[]` : `...${vname}: unknown[]`);
959
+ }
960
+ if (needsKwargsParam) {
961
+ paramsDeclParts.push(`kwargs?: ${kwargsType}`);
962
+ }
963
+ const paramsDecl = paramsDeclParts.join(', ');
964
+
965
+ const requiredKwOnlyNames = keywordOnlyParams.filter(p => !p.optional).map(p => p.name);
966
+ const overloads: string[] = [];
967
+ if (needsKwargsParam && requiredKwOnlyNames.length > 0) {
968
+ const firstOptionalIndex = positionalParams.findIndex(p => p.optional);
969
+ const requiredPosCount =
970
+ firstOptionalIndex >= 0 ? firstOptionalIndex : positionalParams.length;
971
+ for (let i = requiredPosCount; i <= positionalParams.length; i++) {
972
+ const head = positionalParams.slice(0, i).map(p => renderPositionalParam(p, true));
973
+ const rest: string[] = [];
974
+ if (varArgsParam) {
975
+ const vname = this.escapeIdentifier(varArgsParam.name);
976
+ rest.push(
977
+ needsVarArgsArray ? `${vname}: unknown[] | undefined` : `...${vname}: unknown[]`
978
+ );
979
+ }
980
+ rest.push(`kwargs: ${kwargsType}`);
981
+ overloads.push(
982
+ ` static create${classTypeParamDecl}(${[...head, ...rest].join(', ')}): Promise<${classSelfType}>;`
983
+ );
984
+ if (varArgsParam && needsVarArgsArray) {
985
+ overloads.push(
986
+ ` static create${classTypeParamDecl}(${[...head, `kwargs: ${kwargsType}`].join(', ')}): Promise<${classSelfType}>;`
987
+ );
988
+ }
989
+ }
990
+ }
991
+ const overloadDecl = overloads.length > 0 ? `${overloads.join('\n')}\n` : '';
992
+ const declaration =
993
+ overloads.length > 0
994
+ ? overloadDecl
995
+ : ` static create${classTypeParamDecl}(${paramsDecl}): Promise<${classSelfType}>;\n`;
996
+
997
+ const guardLines: string[] = [];
998
+ const callPreludeLines: string[] = [];
999
+ if (needsKwargsParam) {
1000
+ callPreludeLines.push(` let __kwargs = kwargs;`);
1001
+ }
1002
+ const positionalArgExprs = positionalParams.map(p => this.escapeIdentifier(p.name));
1003
+ callPreludeLines.push(` const __args: unknown[] = [${positionalArgExprs.join(', ')}];`);
1004
+ if (requiredPosCount < positionalParams.length) {
1005
+ callPreludeLines.push(
1006
+ ` while (__args.length > ${requiredPosCount} && __args[__args.length - 1] === undefined) {`
1007
+ );
1008
+ callPreludeLines.push(` __args.pop();`);
1009
+ callPreludeLines.push(` }`);
1010
+ }
1011
+ if (needsKwargsParam && requiredPosCount < positionalParams.length) {
1012
+ const looksLikeKwargs = this.renderLooksLikeKwargsExpr('__candidate', {
1013
+ keywordOnlyNames,
1014
+ requiredKwOnlyNames,
1015
+ hasVarKwArgs,
1016
+ });
1017
+ callPreludeLines.push(
1018
+ ` if (__kwargs === undefined && __args.length > ${requiredPosCount}) {`
1019
+ );
1020
+ callPreludeLines.push(` const __candidate = __args[__args.length - 1];`);
1021
+ callPreludeLines.push(` if (${looksLikeKwargs}) {`);
1022
+ callPreludeLines.push(` __kwargs = __candidate as any;`);
1023
+ callPreludeLines.push(` __args.pop();`);
1024
+ callPreludeLines.push(` }`);
1025
+ callPreludeLines.push(` }`);
1026
+ }
1027
+ if (varArgsParam) {
1028
+ const vname = this.escapeIdentifier(varArgsParam.name);
1029
+ if (needsVarArgsArray) {
1030
+ const looksLikeKwargs = this.renderLooksLikeKwargsExpr(vname, {
1031
+ keywordOnlyNames,
1032
+ requiredKwOnlyNames,
1033
+ hasVarKwArgs,
1034
+ });
1035
+ callPreludeLines.push(` let __varargs: unknown[] = [];`);
1036
+ callPreludeLines.push(` if (${vname} !== undefined) {`);
1037
+ callPreludeLines.push(` if (globalThis.Array.isArray(${vname})) {`);
1038
+ callPreludeLines.push(` __varargs = ${vname};`);
1039
+ callPreludeLines.push(` } else if (__kwargs === undefined && ${looksLikeKwargs}) {`);
1040
+ callPreludeLines.push(` __kwargs = ${vname} as any;`);
1041
+ callPreludeLines.push(` } else {`);
1042
+ callPreludeLines.push(
1043
+ ` throw new Error(\`__init__ expected ${varArgsParam.name} to be an array\`);`
1044
+ );
1045
+ callPreludeLines.push(` }`);
1046
+ callPreludeLines.push(` }`);
1047
+ callPreludeLines.push(` __args.push(...__varargs);`);
1048
+ } else {
1049
+ callPreludeLines.push(` __args.push(...${vname});`);
1050
+ }
1051
+ }
1052
+ const callPrelude = callPreludeLines.length > 0 ? `${callPreludeLines.join('\n')}\n` : '';
1053
+
1054
+ if (needsKwargsParam && positionalOnlyNames.length > 0) {
1055
+ guardLines.push(
1056
+ ` const __positionalOnly = ${JSON.stringify(positionalOnlyNames)} as const;`
1057
+ );
1058
+ guardLines.push(` for (const key of __positionalOnly) {`);
1059
+ guardLines.push(
1060
+ ` if (__kwargs && Object.prototype.hasOwnProperty.call(__kwargs, key)) {`
1061
+ );
1062
+ guardLines.push(
1063
+ ` throw new Error(\`__init__ does not accept positional-only argument "\${key}" as a keyword argument\`);`
1064
+ );
1065
+ guardLines.push(` }`);
1066
+ guardLines.push(` }`);
1067
+ }
1068
+ if (needsKwargsParam && requiredKwOnlyNames.length > 0) {
1069
+ guardLines.push(
1070
+ ` const __requiredKwOnly = ${JSON.stringify(requiredKwOnlyNames)} as const;`
1071
+ );
1072
+ guardLines.push(` const __missing: string[] = [];`);
1073
+ guardLines.push(` for (const key of __requiredKwOnly) {`);
1074
+ guardLines.push(
1075
+ ` if (!__kwargs || !Object.prototype.hasOwnProperty.call(__kwargs, key)) {`
1076
+ );
1077
+ guardLines.push(` __missing.push(key);`);
1078
+ guardLines.push(` }`);
1079
+ guardLines.push(` }`);
1080
+ guardLines.push(` if (__missing.length > 0) {`);
1081
+ guardLines.push(
1082
+ ` throw new Error(\`Missing required keyword-only arguments for __init__: \${__missing.join(', ')}\`);`
1083
+ );
1084
+ guardLines.push(` }`);
1085
+ }
1086
+
1087
+ return {
1088
+ overloadDecl,
1089
+ declaration,
1090
+ paramsDecl,
1091
+ callPrelude,
1092
+ hasKwargs: needsKwargsParam,
1093
+ guardLines,
1094
+ };
1095
+ })();
294
1096
 
295
- const cname = this.escapeIdentifier(cls.name);
296
1097
  const moduleId = moduleName ?? '__main__';
297
- const methodsSection = methodBodies ? `\n${methodBodies}\n` : '\n';
298
- const ts = `${jsdoc}export class ${cname} {
1098
+ const methodsSection = methodBodies.length > 0 ? `\n${methodBodies.join('\n')}\n` : '\n';
1099
+ const declarationMethodsSection =
1100
+ methodDeclarations.length > 0 ? `\n${methodDeclarations.join('')}\n` : '\n';
1101
+ const ctorGuards = ctorSpec.guardLines.length > 0 ? `${ctorSpec.guardLines.join('\n')}\n` : '';
1102
+ const newClassExpr = `new ${cname}${classGenericContext.typeArguments}(handle)`;
1103
+ const ts = `${jsdoc}export class ${cname}${classTypeParamDecl} {
299
1104
  private readonly __handle: string;
300
1105
  private constructor(handle: string) { this.__handle = handle; }
301
- static async create(${ctorParams}): Promise<${cname}> {
302
- const handle = await getRuntimeBridge().instantiate<string>('${moduleId}', '${cls.name}', [${ctorArgsPassthrough}]${ctorHasKwArgs ? ', kwargs' : ''});
303
- return new ${cname}(handle);
1106
+ ${ctorSpec.overloadDecl} static async create${classTypeParamDecl}(${ctorSpec.paramsDecl}): Promise<${classSelfType}> {
1107
+ ${ctorSpec.callPrelude}${ctorGuards} const handle = await getRuntimeBridge().instantiate<string>('${moduleId}', '${cls.name}', __args${
1108
+ ctorSpec.hasKwargs ? ', __kwargs' : ''
1109
+ });
1110
+ return ${newClassExpr};
304
1111
  }
305
- static fromHandle(handle: string): ${cname} { return new ${cname}(handle); }${methodsSection} async disposeHandle(): Promise<void> { await getRuntimeBridge().disposeInstance(this.__handle); }
1112
+ static fromHandle${classTypeParamDecl}(handle: string): ${classSelfType} { return ${newClassExpr}; }${methodsSection} async disposeHandle(): Promise<void> { await getRuntimeBridge().disposeInstance(this.__handle); }
306
1113
  }
307
1114
  `;
308
1115
 
309
- return this.wrap(ts, [cls.name]);
1116
+ const declaration = `${jsdoc}export class ${cname}${classTypeParamDecl} {
1117
+ private readonly __handle: string;
1118
+ private constructor(handle: string);
1119
+ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string): ${classSelfType};${declarationMethodsSection} disposeHandle(): Promise<void>;
1120
+ }
1121
+ `;
1122
+
1123
+ return this.wrap(ts, declaration, [cls.name]);
1124
+ }
1125
+
1126
+ generateTypeAlias(alias: PythonTypeAlias, moduleName?: string): GeneratedCode {
1127
+ const genericContext = this.buildGenericRenderContext(
1128
+ this.getTypeParameters(alias.typeParameters),
1129
+ [alias.type],
1130
+ moduleName
1131
+ );
1132
+ const aliasName = this.escapeIdentifier(alias.name, { preserveCase: true });
1133
+ const body = this.typeToTsFromPython(alias.type, genericContext, 'value');
1134
+ const ts = `export type ${aliasName}${genericContext.declaration} = ${body}\n`;
1135
+ return this.wrap(ts, ts, [alias.name]);
310
1136
  }
311
1137
 
312
1138
  /**
@@ -335,16 +1161,22 @@ export class CodeGenerator {
335
1161
  }
336
1162
 
337
1163
  generateModuleDefinition(module: PythonModule, annotatedJSDoc = false): GeneratedCode {
338
- const functionCodes = [...module.functions]
1164
+ const functionResults = [...module.functions]
1165
+ .sort((a, b) => a.name.localeCompare(b.name))
1166
+ .map(f => this.generateFunctionWrapper(f, module.name, annotatedJSDoc));
1167
+ const classResults = [...module.classes]
339
1168
  .sort((a, b) => a.name.localeCompare(b.name))
340
- .map(f => this.generateFunctionWrapper(f, module.name, annotatedJSDoc).typescript)
341
- .join('\n');
342
- const classCodes = [...module.classes]
1169
+ .map(c => this.generateClassWrapper(c, module.name, annotatedJSDoc));
1170
+ const typeAliasResults = [...(module.typeAliases ?? [])]
343
1171
  .sort((a, b) => a.name.localeCompare(b.name))
344
- .map(c => this.generateClassWrapper(c, module.name, annotatedJSDoc).typescript)
345
- .join('\n');
1172
+ .map(alias => this.generateTypeAlias(alias, module.name));
1173
+
1174
+ const functionCodes = functionResults.map(result => result.typescript).join('\n');
1175
+ const classCodes = classResults.map(result => result.typescript).join('\n');
1176
+ const typeAliasCodes = typeAliasResults.map(result => result.typescript).join('\n');
346
1177
 
347
1178
  const header = `// Generated by tywrap\n// Module: ${module.name}\n// DO NOT EDIT MANUALLY\n\n`;
1179
+ const declarationHeader = `// Generated by tywrap\n// Type Declarations\n// DO NOT EDIT MANUALLY\n\n`;
348
1180
  const hasRuntimeClasses = module.classes.some(c => {
349
1181
  const kind = c.kind ?? 'class';
350
1182
  return kind === 'class' && !c.decorators.includes('__typed_dict__');
@@ -352,8 +1184,13 @@ export class CodeGenerator {
352
1184
  const needsRuntime = module.functions.length > 0 || hasRuntimeClasses;
353
1185
  const bridgeDecl = needsRuntime ? `import { getRuntimeBridge } from 'tywrap/runtime';\n\n` : '';
354
1186
 
355
- const ts = `${header + bridgeDecl + functionCodes}\n${classCodes}`;
356
- return this.wrap(ts, [module.name]);
1187
+ const ts = `${`${header}${bridgeDecl}${functionCodes}\n${classCodes}\n${typeAliasCodes}`.trimEnd()}\n`;
1188
+ const declaration = `${`${declarationHeader}${functionResults
1189
+ .map(result => result.declaration)
1190
+ .join('\n')}\n${classResults
1191
+ .map(result => result.declaration)
1192
+ .join('\n')}\n${typeAliasResults.map(result => result.declaration).join('\n')}`.trimEnd()}\n`;
1193
+ return this.wrap(ts, declaration, [module.name]);
357
1194
  }
358
1195
 
359
1196
  private generateJsDoc(doc?: string, paramAnnotations?: readonly string[]): string {
@@ -373,10 +1210,10 @@ export class CodeGenerator {
373
1210
  return `/**\n${lines.join('\n')}\n */\n`;
374
1211
  }
375
1212
 
376
- private wrap(typescript: string, _sources: string[]): GeneratedCode {
1213
+ private wrap(typescript: string, declaration: string, _sources: string[]): GeneratedCode {
377
1214
  return {
378
1215
  typescript,
379
- declaration: '',
1216
+ declaration,
380
1217
  sourceMap: undefined,
381
1218
  metadata: {
382
1219
  generatedAt: new Date(),
@@ -438,6 +1275,9 @@ export class CodeGenerator {
438
1275
  }
439
1276
  case 'custom': {
440
1277
  const c = type as { kind: 'custom'; name: string };
1278
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(c.name)) {
1279
+ return 'unknown';
1280
+ }
441
1281
  return c.name;
442
1282
  }
443
1283
  case 'literal': {