tywrap 0.5.1 → 0.6.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 (155) hide show
  1. package/dist/cli.js +8 -6
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config/index.d.ts.map +1 -1
  4. package/dist/config/index.js +236 -147
  5. package/dist/config/index.js.map +1 -1
  6. package/dist/core/annotation-parser.d.ts.map +1 -1
  7. package/dist/core/annotation-parser.js +93 -83
  8. package/dist/core/annotation-parser.js.map +1 -1
  9. package/dist/core/discovery.d.ts.map +1 -1
  10. package/dist/core/discovery.js +26 -0
  11. package/dist/core/discovery.js.map +1 -1
  12. package/dist/core/emit-call.d.ts +58 -0
  13. package/dist/core/emit-call.d.ts.map +1 -0
  14. package/dist/core/emit-call.js +103 -0
  15. package/dist/core/emit-call.js.map +1 -0
  16. package/dist/core/generator.d.ts +2 -0
  17. package/dist/core/generator.d.ts.map +1 -1
  18. package/dist/core/generator.js +54 -211
  19. package/dist/core/generator.js.map +1 -1
  20. package/dist/core/mapper.d.ts.map +1 -1
  21. package/dist/core/mapper.js +118 -104
  22. package/dist/core/mapper.js.map +1 -1
  23. package/dist/index.d.ts +6 -25
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +9 -31
  26. package/dist/index.js.map +1 -1
  27. package/dist/runtime/bounded-context.d.ts +2 -2
  28. package/dist/runtime/bounded-context.js +2 -2
  29. package/dist/runtime/{safe-codec.d.ts → bridge-codec.d.ts} +11 -11
  30. package/dist/runtime/bridge-codec.d.ts.map +1 -0
  31. package/dist/runtime/{safe-codec.js → bridge-codec.js} +26 -26
  32. package/dist/runtime/bridge-codec.js.map +1 -0
  33. package/dist/runtime/{http-io.d.ts → http-transport.d.ts} +6 -6
  34. package/dist/runtime/http-transport.d.ts.map +1 -0
  35. package/dist/runtime/{http-io.js → http-transport.js} +3 -3
  36. package/dist/runtime/http-transport.js.map +1 -0
  37. package/dist/runtime/http.d.ts +3 -3
  38. package/dist/runtime/http.d.ts.map +1 -1
  39. package/dist/runtime/http.js +4 -4
  40. package/dist/runtime/http.js.map +1 -1
  41. package/dist/runtime/index.d.ts +9 -1
  42. package/dist/runtime/index.d.ts.map +1 -1
  43. package/dist/runtime/index.js +9 -1
  44. package/dist/runtime/index.js.map +1 -1
  45. package/dist/runtime/node.d.ts +6 -6
  46. package/dist/runtime/node.d.ts.map +1 -1
  47. package/dist/runtime/node.js +7 -7
  48. package/dist/runtime/node.js.map +1 -1
  49. package/dist/runtime/pooled-transport.d.ts +11 -11
  50. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  51. package/dist/runtime/pooled-transport.js +10 -10
  52. package/dist/runtime/pooled-transport.js.map +1 -1
  53. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
  54. package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
  55. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
  56. package/dist/runtime/{pyodide-io.d.ts → pyodide-transport.d.ts} +8 -8
  57. package/dist/runtime/pyodide-transport.d.ts.map +1 -0
  58. package/dist/runtime/{pyodide-io.js → pyodide-transport.js} +5 -5
  59. package/dist/runtime/pyodide-transport.js.map +1 -0
  60. package/dist/runtime/pyodide.d.ts +3 -3
  61. package/dist/runtime/pyodide.d.ts.map +1 -1
  62. package/dist/runtime/pyodide.js +5 -5
  63. package/dist/runtime/pyodide.js.map +1 -1
  64. package/dist/runtime/rpc-client.d.ts +5 -5
  65. package/dist/runtime/rpc-client.d.ts.map +1 -1
  66. package/dist/runtime/rpc-client.js +6 -7
  67. package/dist/runtime/rpc-client.js.map +1 -1
  68. package/dist/runtime/{process-io.d.ts → subprocess-transport.d.ts} +9 -9
  69. package/dist/runtime/subprocess-transport.d.ts.map +1 -0
  70. package/dist/runtime/{process-io.js → subprocess-transport.js} +6 -6
  71. package/dist/runtime/subprocess-transport.js.map +1 -0
  72. package/dist/runtime/{worker-pool.d.ts → transport-pool.d.ts} +15 -15
  73. package/dist/runtime/transport-pool.d.ts.map +1 -0
  74. package/dist/runtime/{worker-pool.js → transport-pool.js} +6 -6
  75. package/dist/runtime/transport-pool.js.map +1 -0
  76. package/dist/runtime/transport.d.ts +14 -8
  77. package/dist/runtime/transport.d.ts.map +1 -1
  78. package/dist/runtime/transport.js +10 -4
  79. package/dist/runtime/transport.js.map +1 -1
  80. package/dist/types/index.d.ts +10 -1
  81. package/dist/types/index.d.ts.map +1 -1
  82. package/dist/utils/cache.d.ts +2 -2
  83. package/dist/utils/cache.d.ts.map +1 -1
  84. package/dist/utils/cache.js +2 -2
  85. package/dist/utils/cache.js.map +1 -1
  86. package/dist/utils/codec.d.ts +1 -1
  87. package/dist/utils/codec.d.ts.map +1 -1
  88. package/dist/utils/codec.js +181 -182
  89. package/dist/utils/codec.js.map +1 -1
  90. package/dist/version.d.ts +12 -0
  91. package/dist/version.d.ts.map +1 -0
  92. package/dist/version.js +12 -0
  93. package/dist/version.js.map +1 -0
  94. package/package.json +3 -2
  95. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  96. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  97. package/runtime/python_bridge.py +54 -6
  98. package/runtime/safe_codec.py +8 -8
  99. package/runtime/tywrap_bridge_core.py +159 -23
  100. package/src/cli.ts +8 -12
  101. package/src/config/index.ts +271 -165
  102. package/src/core/annotation-parser.ts +112 -105
  103. package/src/core/discovery.ts +32 -0
  104. package/src/core/emit-call.ts +171 -0
  105. package/src/core/generator.ts +67 -263
  106. package/src/core/mapper.ts +125 -110
  107. package/src/index.ts +11 -68
  108. package/src/runtime/bounded-context.ts +2 -2
  109. package/src/runtime/{safe-codec.ts → bridge-codec.ts} +27 -27
  110. package/src/runtime/{http-io.ts → http-transport.ts} +5 -5
  111. package/src/runtime/http.ts +5 -5
  112. package/src/runtime/index.ts +23 -1
  113. package/src/runtime/node.ts +15 -15
  114. package/src/runtime/pooled-transport.ts +15 -15
  115. package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
  116. package/src/runtime/{pyodide-io.ts → pyodide-transport.ts} +7 -7
  117. package/src/runtime/pyodide.ts +6 -6
  118. package/src/runtime/rpc-client.ts +12 -8
  119. package/src/runtime/{process-io.ts → subprocess-transport.ts} +8 -8
  120. package/src/runtime/{worker-pool.ts → transport-pool.ts} +26 -26
  121. package/src/runtime/transport.ts +15 -8
  122. package/src/types/index.ts +10 -1
  123. package/src/utils/cache.ts +2 -2
  124. package/src/utils/codec.ts +217 -196
  125. package/src/version.ts +12 -0
  126. package/dist/runtime/http-io.d.ts.map +0 -1
  127. package/dist/runtime/http-io.js.map +0 -1
  128. package/dist/runtime/optimized-node.d.ts +0 -23
  129. package/dist/runtime/optimized-node.d.ts.map +0 -1
  130. package/dist/runtime/optimized-node.js +0 -23
  131. package/dist/runtime/optimized-node.js.map +0 -1
  132. package/dist/runtime/process-io.d.ts.map +0 -1
  133. package/dist/runtime/process-io.js.map +0 -1
  134. package/dist/runtime/protocol.d.ts +0 -3
  135. package/dist/runtime/protocol.d.ts.map +0 -1
  136. package/dist/runtime/protocol.js +0 -3
  137. package/dist/runtime/protocol.js.map +0 -1
  138. package/dist/runtime/pyodide-io.d.ts.map +0 -1
  139. package/dist/runtime/pyodide-io.js.map +0 -1
  140. package/dist/runtime/safe-codec.d.ts.map +0 -1
  141. package/dist/runtime/safe-codec.js.map +0 -1
  142. package/dist/runtime/worker-pool.d.ts.map +0 -1
  143. package/dist/runtime/worker-pool.js.map +0 -1
  144. package/dist/utils/bundle-optimizer.d.ts +0 -182
  145. package/dist/utils/bundle-optimizer.d.ts.map +0 -1
  146. package/dist/utils/bundle-optimizer.js +0 -680
  147. package/dist/utils/bundle-optimizer.js.map +0 -1
  148. package/dist/utils/memory-profiler.d.ts +0 -123
  149. package/dist/utils/memory-profiler.d.ts.map +0 -1
  150. package/dist/utils/memory-profiler.js +0 -426
  151. package/dist/utils/memory-profiler.js.map +0 -1
  152. package/src/runtime/optimized-node.ts +0 -25
  153. package/src/runtime/protocol.ts +0 -2
  154. package/src/utils/bundle-optimizer.ts +0 -876
  155. package/src/utils/memory-profiler.ts +0 -582
@@ -245,6 +245,19 @@ export function parseAnnotationToPythonType(
245
245
  return results;
246
246
  };
247
247
 
248
+ // Returns the content between the first `[` and the last `]` of a special-form
249
+ // annotation (e.g. the `int, str` of `Union[int, str]`). Callers gate this on a
250
+ // prefix check, so the brackets are known to be present.
251
+ const bracketInner = (raw: string): string => raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
252
+
253
+ // True when `raw` opens with one of the supported module prefixes followed by
254
+ // `name[`. Mirrors the inlined `raw.startsWith('typing.Name[') || ...` checks so
255
+ // the branch ordering in `parse` is unchanged.
256
+ const startsWithSpecialForm = (raw: string, name: string): boolean =>
257
+ raw.startsWith(`typing.${name}[`) ||
258
+ raw.startsWith(`typing_extensions.${name}[`) ||
259
+ raw.startsWith(`${name}[`);
260
+
248
261
  const splitGenericInvocation = (raw: string): { name: string; inner: string } | null => {
249
262
  if (!raw.endsWith(']')) {
250
263
  return null;
@@ -277,6 +290,86 @@ export function parseAnnotationToPythonType(
277
290
  };
278
291
  };
279
292
 
293
+ // Parses the body of a `Callable[...]` form (the `inner` already stripped of the
294
+ // outer brackets). `recurse` is the top-level `parse`, threaded in so this stays a
295
+ // standalone helper. Returns null only when the form has fewer than the required
296
+ // two comma-separated sections, matching the original guarded branch exactly.
297
+ const parseCallableInner = (
298
+ inner: string,
299
+ depth: number,
300
+ recurse: (ann: unknown, depth?: number) => PythonType
301
+ ): PythonType | null => {
302
+ const parts = splitTopLevel(inner, ',');
303
+ if (parts.length < 2) {
304
+ return null;
305
+ }
306
+
307
+ const paramsPart = (parts[0] ?? '').trim();
308
+ const returnPart = parts.slice(1).join(',').trim();
309
+ const returnType = recurse(returnPart, depth + 1);
310
+
311
+ if (paramsPart === '...' || paramsPart === 'Ellipsis') {
312
+ return {
313
+ kind: 'callable',
314
+ parameters: [{ kind: 'custom', name: '...' }],
315
+ returnType,
316
+ };
317
+ }
318
+
319
+ const directParamSpec = recurse(paramsPart, depth + 1);
320
+ if (directParamSpec.kind === 'paramspec') {
321
+ return {
322
+ kind: 'callable',
323
+ parameters: [],
324
+ parameterSpec: directParamSpec,
325
+ returnType,
326
+ };
327
+ }
328
+
329
+ const paramInner =
330
+ paramsPart.startsWith('[') && paramsPart.endsWith(']') ? paramsPart.slice(1, -1) : '';
331
+ const trimmed = paramInner.trim();
332
+ if (trimmed === '...' || trimmed === 'Ellipsis') {
333
+ return {
334
+ kind: 'callable',
335
+ parameters: [{ kind: 'custom', name: '...' }],
336
+ returnType,
337
+ };
338
+ }
339
+
340
+ const parameters = trimmed
341
+ ? splitTopLevel(trimmed, ',').map(p => recurse(p.trim(), depth + 1))
342
+ : [];
343
+ return { kind: 'callable', parameters, returnType };
344
+ };
345
+
346
+ // Handles the `typing.TypeVar('T')` / `ParamSpec('P')` / `TypeVarTuple('Ts')`
347
+ // factory-call forms. Returns null when `rawText` is none of them, preserving
348
+ // the original sequential fall-through (TypeVar, then ParamSpec, then TypeVarTuple).
349
+ const parseTypingFactory = (rawText: string): PythonType | null => {
350
+ const typeVarName = parseTypingFactoryName(rawText, 'TypeVar');
351
+ if (typeVarName) {
352
+ return mapKnownTypeParameter(typeVarName) ?? { kind: 'typevar', name: typeVarName };
353
+ }
354
+
355
+ const paramSpecName = parseTypingFactoryName(rawText, 'ParamSpec');
356
+ if (paramSpecName) {
357
+ return mapKnownTypeParameter(paramSpecName) ?? { kind: 'paramspec', name: paramSpecName };
358
+ }
359
+
360
+ const typeVarTupleName = parseTypingFactoryName(rawText, 'TypeVarTuple');
361
+ if (typeVarTupleName) {
362
+ return (
363
+ mapKnownTypeParameter(typeVarTupleName) ?? {
364
+ kind: 'typevartuple',
365
+ name: typeVarTupleName,
366
+ }
367
+ );
368
+ }
369
+
370
+ return null;
371
+ };
372
+
280
373
  const parse = (ann: unknown, depth = 0): PythonType => {
281
374
  if (ann === null || ann === undefined) {
282
375
  return unknownType();
@@ -320,33 +413,18 @@ export function parseAnnotationToPythonType(
320
413
  }
321
414
  }
322
415
 
323
- if (
324
- raw.startsWith('typing.Union[') ||
325
- raw.startsWith('typing_extensions.Union[') ||
326
- raw.startsWith('Union[')
327
- ) {
328
- const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
329
- const parts = splitTopLevel(inner, ',');
416
+ if (startsWithSpecialForm(raw, 'Union')) {
417
+ const parts = splitTopLevel(bracketInner(raw), ',');
330
418
  const types = parts.map(p => parse(p.trim(), depth + 1));
331
419
  return { kind: 'union', types };
332
420
  }
333
421
 
334
- if (
335
- raw.startsWith('typing.Optional[') ||
336
- raw.startsWith('typing_extensions.Optional[') ||
337
- raw.startsWith('Optional[')
338
- ) {
339
- const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
340
- return { kind: 'optional', type: parse(inner, depth + 1) };
422
+ if (startsWithSpecialForm(raw, 'Optional')) {
423
+ return { kind: 'optional', type: parse(bracketInner(raw), depth + 1) };
341
424
  }
342
425
 
343
- if (
344
- raw.startsWith('typing.Literal[') ||
345
- raw.startsWith('typing_extensions.Literal[') ||
346
- raw.startsWith('Literal[')
347
- ) {
348
- const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
349
- const parts = splitTopLevel(inner, ',');
426
+ if (startsWithSpecialForm(raw, 'Literal')) {
427
+ const parts = splitTopLevel(bracketInner(raw), ',');
350
428
  if (parts.length === 1) {
351
429
  return mapLiteral(String(parts[0] ?? '').trim());
352
430
  }
@@ -360,24 +438,9 @@ export function parseAnnotationToPythonType(
360
438
  return parse(extMatch[3] ?? '', depth + 1);
361
439
  }
362
440
 
363
- const typeVarName = parseTypingFactoryName(rawText, 'TypeVar');
364
- if (typeVarName) {
365
- return mapKnownTypeParameter(typeVarName) ?? { kind: 'typevar', name: typeVarName };
366
- }
367
-
368
- const paramSpecName = parseTypingFactoryName(rawText, 'ParamSpec');
369
- if (paramSpecName) {
370
- return mapKnownTypeParameter(paramSpecName) ?? { kind: 'paramspec', name: paramSpecName };
371
- }
372
-
373
- const typeVarTupleName = parseTypingFactoryName(rawText, 'TypeVarTuple');
374
- if (typeVarTupleName) {
375
- return (
376
- mapKnownTypeParameter(typeVarTupleName) ?? {
377
- kind: 'typevartuple',
378
- name: typeVarTupleName,
379
- }
380
- );
441
+ const typingFactory = parseTypingFactory(rawText);
442
+ if (typingFactory) {
443
+ return typingFactory;
381
444
  }
382
445
  if (
383
446
  raw === 'typing.LiteralString' ||
@@ -387,61 +450,15 @@ export function parseAnnotationToPythonType(
387
450
  return { kind: 'primitive', name: 'str' };
388
451
  }
389
452
 
390
- if (
391
- raw.startsWith('typing.Callable[') ||
392
- raw.startsWith('typing_extensions.Callable[') ||
393
- raw.startsWith('Callable[')
394
- ) {
395
- const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
396
- const parts = splitTopLevel(inner, ',');
397
- if (parts.length >= 2) {
398
- const paramsPart = (parts[0] ?? '').trim();
399
- const returnPart = parts.slice(1).join(',').trim();
400
- const returnType = parse(returnPart, depth + 1);
401
-
402
- if (paramsPart === '...' || paramsPart === 'Ellipsis') {
403
- return {
404
- kind: 'callable',
405
- parameters: [{ kind: 'custom', name: '...' }],
406
- returnType,
407
- };
408
- }
409
-
410
- const directParamSpec = parse(paramsPart, depth + 1);
411
- if (directParamSpec.kind === 'paramspec') {
412
- return {
413
- kind: 'callable',
414
- parameters: [],
415
- parameterSpec: directParamSpec,
416
- returnType,
417
- };
418
- }
419
-
420
- const paramInner =
421
- paramsPart.startsWith('[') && paramsPart.endsWith(']') ? paramsPart.slice(1, -1) : '';
422
- const trimmed = paramInner.trim();
423
- if (trimmed === '...' || trimmed === 'Ellipsis') {
424
- return {
425
- kind: 'callable',
426
- parameters: [{ kind: 'custom', name: '...' }],
427
- returnType,
428
- };
429
- }
430
-
431
- const parameters = trimmed
432
- ? splitTopLevel(trimmed, ',').map(p => parse(p.trim(), depth + 1))
433
- : [];
434
- return { kind: 'callable', parameters, returnType };
453
+ if (startsWithSpecialForm(raw, 'Callable')) {
454
+ const callable = parseCallableInner(bracketInner(raw), depth, parse);
455
+ if (callable) {
456
+ return callable;
435
457
  }
436
458
  }
437
459
 
438
- if (
439
- raw.startsWith('typing.Mapping[') ||
440
- raw.startsWith('typing_extensions.Mapping[') ||
441
- raw.startsWith('Mapping[')
442
- ) {
443
- const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
444
- const parts = splitTopLevel(inner, ',');
460
+ if (startsWithSpecialForm(raw, 'Mapping')) {
461
+ const parts = splitTopLevel(bracketInner(raw), ',');
445
462
  return {
446
463
  kind: 'collection',
447
464
  name: 'dict',
@@ -449,13 +466,8 @@ export function parseAnnotationToPythonType(
449
466
  };
450
467
  }
451
468
 
452
- if (
453
- raw.startsWith('typing.Annotated[') ||
454
- raw.startsWith('typing_extensions.Annotated[') ||
455
- raw.startsWith('Annotated[')
456
- ) {
457
- const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
458
- const parts = splitTopLevel(inner, ',');
469
+ if (startsWithSpecialForm(raw, 'Annotated')) {
470
+ const parts = splitTopLevel(bracketInner(raw), ',');
459
471
  if (parts.length > 0) {
460
472
  return {
461
473
  kind: 'annotated',
@@ -465,13 +477,8 @@ export function parseAnnotationToPythonType(
465
477
  }
466
478
  }
467
479
 
468
- if (
469
- raw.startsWith('typing.Unpack[') ||
470
- raw.startsWith('typing_extensions.Unpack[') ||
471
- raw.startsWith('Unpack[')
472
- ) {
473
- const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
474
- return { kind: 'unpack', type: parse(inner, depth + 1) };
480
+ if (startsWithSpecialForm(raw, 'Unpack')) {
481
+ return { kind: 'unpack', type: parse(bracketInner(raw), depth + 1) };
475
482
  }
476
483
 
477
484
  const coll = normalizeCollectionName(raw);
@@ -12,6 +12,32 @@ import { getComponentLogger } from '../utils/logger.js';
12
12
 
13
13
  const log = getComponentLogger('Discovery');
14
14
 
15
+ /**
16
+ * Strict allow-list for Python module names.
17
+ *
18
+ * Module names are interpolated into a `python -c` source string, so anything
19
+ * outside a conservative identifier-plus-dots shape (e.g. quotes, semicolons,
20
+ * newlines, parentheses, whitespace) could break out of the intended
21
+ * expression and inject arbitrary code into the interpreter. Reject those
22
+ * before they ever reach the subprocess argv.
23
+ */
24
+ const SAFE_MODULE_NAME = /^[A-Za-z_][A-Za-z0-9_.]*$/;
25
+
26
+ /**
27
+ * Validate a Python module name against {@link SAFE_MODULE_NAME}.
28
+ *
29
+ * @throws {Error} if the name is empty or contains characters that could
30
+ * escape the `python -c` expression it is interpolated into.
31
+ */
32
+ function assertSafeModuleName(moduleName: string): void {
33
+ if (typeof moduleName !== 'string' || !SAFE_MODULE_NAME.test(moduleName)) {
34
+ throw new Error(
35
+ `Unsafe Python module name rejected: ${JSON.stringify(moduleName)}. ` +
36
+ 'Module names must match /^[A-Za-z_][A-Za-z0-9_.]*$/.'
37
+ );
38
+ }
39
+ }
40
+
15
41
  export interface ModuleInfo {
16
42
  name: string;
17
43
  path: string;
@@ -87,6 +113,9 @@ export class ModuleDiscovery {
87
113
  * Resolve Python module path from module name
88
114
  */
89
115
  async resolvePythonPath(moduleName: string): Promise<string | null> {
116
+ // Reject names that could break out of the `python -c` expression below.
117
+ assertSafeModuleName(moduleName);
118
+
90
119
  // Check cache first
91
120
  const cached = this.moduleCache.get(moduleName);
92
121
  if (cached) {
@@ -383,6 +412,9 @@ export class ModuleDiscovery {
383
412
  * Get module version information
384
413
  */
385
414
  async getModuleVersion(moduleName: string): Promise<string | undefined> {
415
+ // Reject names that could break out of the `python -c` expression below.
416
+ assertSafeModuleName(moduleName);
417
+
386
418
  if (!processUtils.isAvailable()) {
387
419
  return undefined;
388
420
  }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Shared call-emission helpers for the TypeScript wrapper generator.
3
+ *
4
+ * The function, method, and constructor wrappers all emit the same runtime
5
+ * prelude (assemble `__args`, fold a trailing object into `__kwargs`, unpack
6
+ * `*args`) and the same argument guards (reject positional-only-as-keyword,
7
+ * require keyword-only arguments). They differ only by indentation, the error
8
+ * label used in thrown messages, and the terminal RPC verb (which the call
9
+ * sites still emit themselves).
10
+ *
11
+ * The emitted strings MUST stay byte-identical to the previous inline copies.
12
+ */
13
+
14
+ import type { Parameter } from '../types/index.js';
15
+
16
+ /** Describes one call site for prelude/guard emission. */
17
+ export interface CallDescriptor {
18
+ /** Positional parameters (no *args/**kwargs/keyword-only). */
19
+ positionalParams: readonly Parameter[];
20
+ /** The `*args` parameter, if any. */
21
+ varArgsParam?: Parameter;
22
+ /** Whether `*args` is modeled as an array param (true when kwargs coexist). */
23
+ needsVarArgsArray: boolean;
24
+ /** Whether the wrapper takes a `kwargs` parameter. */
25
+ hasKwArgs: boolean;
26
+ /** Whether the callee declares `**kwargs`. */
27
+ hasVarKwArgs: boolean;
28
+ /** Names of keyword-only parameters. */
29
+ keywordOnlyNames: readonly string[];
30
+ /** Names of required keyword-only parameters. */
31
+ requiredKwOnlyNames: readonly string[];
32
+ /** Names of positional-only parameters. */
33
+ positionalOnlyNames: readonly string[];
34
+ /** Number of leading required positional parameters. */
35
+ requiredPosCount: number;
36
+ /** Base indentation (e.g. ' ' for functions, ' ' for methods/ctors). */
37
+ indent: string;
38
+ /** Error label used in thrown messages (function/method name or '__init__'). */
39
+ errorLabel: string;
40
+ }
41
+
42
+ /** Helpers wired from the generator instance (they depend on `this`). */
43
+ export interface CallEmitHelpers {
44
+ escapeIdentifier(name: string): string;
45
+ renderLooksLikeKwargsExpr(
46
+ valueExpr: string,
47
+ options: {
48
+ keywordOnlyNames: string[];
49
+ requiredKwOnlyNames: string[];
50
+ hasVarKwArgs: boolean;
51
+ }
52
+ ): string;
53
+ }
54
+
55
+ /**
56
+ * Emit the runtime prelude lines that build the `__args` array, fold a trailing
57
+ * plain object into `__kwargs`, and unpack `*args`.
58
+ */
59
+ export function emitCallPrelude(desc: CallDescriptor, helpers: CallEmitHelpers): string[] {
60
+ const {
61
+ positionalParams,
62
+ varArgsParam,
63
+ needsVarArgsArray,
64
+ hasKwArgs,
65
+ hasVarKwArgs,
66
+ keywordOnlyNames,
67
+ requiredKwOnlyNames,
68
+ requiredPosCount,
69
+ indent: i,
70
+ errorLabel,
71
+ } = desc;
72
+ const i2 = `${i} `;
73
+ const i3 = `${i2} `;
74
+
75
+ const lines: string[] = [];
76
+ if (hasKwArgs) {
77
+ lines.push(`${i}let __kwargs = kwargs;`);
78
+ }
79
+ const positionalArgExprs = positionalParams.map(p => helpers.escapeIdentifier(p.name));
80
+ lines.push(`${i}const __args: unknown[] = [${positionalArgExprs.join(', ')}];`);
81
+ if (requiredPosCount < positionalParams.length) {
82
+ lines.push(
83
+ `${i}while (__args.length > ${requiredPosCount} && __args[__args.length - 1] === undefined) {`
84
+ );
85
+ lines.push(`${i2}__args.pop();`);
86
+ lines.push(`${i}}`);
87
+ }
88
+ if (hasKwArgs && requiredPosCount < positionalParams.length) {
89
+ const looksLikeKwargs = helpers.renderLooksLikeKwargsExpr('__candidate', {
90
+ keywordOnlyNames: [...keywordOnlyNames],
91
+ requiredKwOnlyNames: [...requiredKwOnlyNames],
92
+ hasVarKwArgs,
93
+ });
94
+ lines.push(`${i}if (__kwargs === undefined && __args.length > ${requiredPosCount}) {`);
95
+ lines.push(`${i2}const __candidate = __args[__args.length - 1];`);
96
+ lines.push(`${i2}if (${looksLikeKwargs}) {`);
97
+ lines.push(`${i3}__kwargs = __candidate as any;`);
98
+ lines.push(`${i3}__args.pop();`);
99
+ lines.push(`${i2}}`);
100
+ lines.push(`${i}}`);
101
+ }
102
+ if (varArgsParam) {
103
+ const vname = helpers.escapeIdentifier(varArgsParam.name);
104
+ if (needsVarArgsArray) {
105
+ const looksLikeKwargs = helpers.renderLooksLikeKwargsExpr(vname, {
106
+ keywordOnlyNames: [...keywordOnlyNames],
107
+ requiredKwOnlyNames: [...requiredKwOnlyNames],
108
+ hasVarKwArgs,
109
+ });
110
+ lines.push(`${i}let __varargs: unknown[] = [];`);
111
+ lines.push(`${i}if (${vname} !== undefined) {`);
112
+ lines.push(`${i2}if (globalThis.Array.isArray(${vname})) {`);
113
+ lines.push(`${i3}__varargs = ${vname};`);
114
+ lines.push(`${i2}} else if (__kwargs === undefined && ${looksLikeKwargs}) {`);
115
+ lines.push(`${i3}__kwargs = ${vname} as any;`);
116
+ lines.push(`${i2}} else {`);
117
+ lines.push(
118
+ `${i3}throw new Error(\`${errorLabel} expected ${varArgsParam.name} to be an array\`);`
119
+ );
120
+ lines.push(`${i2}}`);
121
+ lines.push(`${i}}`);
122
+ lines.push(`${i}__args.push(...__varargs);`);
123
+ } else {
124
+ lines.push(`${i}__args.push(...${vname});`);
125
+ }
126
+ }
127
+ return lines;
128
+ }
129
+
130
+ /**
131
+ * Emit the argument guard lines that reject positional-only arguments passed as
132
+ * keywords and enforce required keyword-only arguments.
133
+ */
134
+ export function emitArgGuards(desc: CallDescriptor): string[] {
135
+ const {
136
+ hasKwArgs,
137
+ positionalOnlyNames,
138
+ requiredKwOnlyNames,
139
+ indent: i,
140
+ errorLabel,
141
+ } = desc;
142
+ const i2 = `${i} `;
143
+ const i3 = `${i2} `;
144
+
145
+ const lines: string[] = [];
146
+ if (hasKwArgs && positionalOnlyNames.length > 0) {
147
+ lines.push(`${i}const __positionalOnly = ${JSON.stringify(positionalOnlyNames)} as const;`);
148
+ lines.push(`${i}for (const key of __positionalOnly) {`);
149
+ lines.push(`${i2}if (__kwargs && Object.prototype.hasOwnProperty.call(__kwargs, key)) {`);
150
+ lines.push(
151
+ `${i3}throw new Error(\`${errorLabel} does not accept positional-only argument "\${key}" as a keyword argument\`);`
152
+ );
153
+ lines.push(`${i2}}`);
154
+ lines.push(`${i}}`);
155
+ }
156
+ if (hasKwArgs && requiredKwOnlyNames.length > 0) {
157
+ lines.push(`${i}const __requiredKwOnly = ${JSON.stringify(requiredKwOnlyNames)} as const;`);
158
+ lines.push(`${i}const __missing: string[] = [];`);
159
+ lines.push(`${i}for (const key of __requiredKwOnly) {`);
160
+ lines.push(`${i2}if (!__kwargs || !Object.prototype.hasOwnProperty.call(__kwargs, key)) {`);
161
+ lines.push(`${i3}__missing.push(key);`);
162
+ lines.push(`${i2}}`);
163
+ lines.push(`${i}}`);
164
+ lines.push(`${i}if (__missing.length > 0) {`);
165
+ lines.push(
166
+ `${i2}throw new Error(\`Missing required keyword-only arguments for ${errorLabel}: \${__missing.join(', ')}\`);`
167
+ );
168
+ lines.push(`${i}}`);
169
+ }
170
+ return lines;
171
+ }