@typra/emitter 0.2.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 (82) hide show
  1. package/dist/src/cleanup/generated-file.d.ts +6 -0
  2. package/dist/src/cleanup/generated-file.js +61 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +110 -0
  5. package/dist/src/decorators.d.ts +56 -0
  6. package/dist/src/decorators.js +177 -0
  7. package/dist/src/emitter.d.ts +13 -0
  8. package/dist/src/emitter.js +137 -0
  9. package/dist/src/generate.d.ts +86 -0
  10. package/dist/src/generate.js +104 -0
  11. package/dist/src/index.d.ts +4 -0
  12. package/dist/src/index.js +5 -0
  13. package/dist/src/ir/ast.d.ts +235 -0
  14. package/dist/src/ir/ast.js +589 -0
  15. package/dist/src/ir/declarations.d.ts +364 -0
  16. package/dist/src/ir/declarations.js +23 -0
  17. package/dist/src/ir/expansion.d.ts +140 -0
  18. package/dist/src/ir/expansion.js +407 -0
  19. package/dist/src/ir/lower.d.ts +53 -0
  20. package/dist/src/ir/lower.js +480 -0
  21. package/dist/src/ir/utilities.d.ts +12 -0
  22. package/dist/src/ir/utilities.js +39 -0
  23. package/dist/src/ir/visitor.d.ts +29 -0
  24. package/dist/src/ir/visitor.js +48 -0
  25. package/dist/src/languages/csharp/driver.d.ts +5 -0
  26. package/dist/src/languages/csharp/driver.js +315 -0
  27. package/dist/src/languages/csharp/emitter.d.ts +33 -0
  28. package/dist/src/languages/csharp/emitter.js +1140 -0
  29. package/dist/src/languages/csharp/scaffolding.d.ts +18 -0
  30. package/dist/src/languages/csharp/scaffolding.js +591 -0
  31. package/dist/src/languages/csharp/test-emitter.d.ts +43 -0
  32. package/dist/src/languages/csharp/test-emitter.js +274 -0
  33. package/dist/src/languages/csharp/visitor.d.ts +14 -0
  34. package/dist/src/languages/csharp/visitor.js +79 -0
  35. package/dist/src/languages/go/driver.d.ts +12 -0
  36. package/dist/src/languages/go/driver.js +128 -0
  37. package/dist/src/languages/go/emitter.d.ts +33 -0
  38. package/dist/src/languages/go/emitter.js +879 -0
  39. package/dist/src/languages/go/scaffolding.d.ts +18 -0
  40. package/dist/src/languages/go/scaffolding.js +53 -0
  41. package/dist/src/languages/go/test-emitter.d.ts +20 -0
  42. package/dist/src/languages/go/test-emitter.js +300 -0
  43. package/dist/src/languages/go/visitor.d.ts +14 -0
  44. package/dist/src/languages/go/visitor.js +78 -0
  45. package/dist/src/languages/markdown/driver.d.ts +19 -0
  46. package/dist/src/languages/markdown/driver.js +408 -0
  47. package/dist/src/languages/python/driver.d.ts +14 -0
  48. package/dist/src/languages/python/driver.js +372 -0
  49. package/dist/src/languages/python/emitter.d.ts +31 -0
  50. package/dist/src/languages/python/emitter.js +856 -0
  51. package/dist/src/languages/python/scaffolding.d.ts +33 -0
  52. package/dist/src/languages/python/scaffolding.js +279 -0
  53. package/dist/src/languages/python/test-emitter.d.ts +29 -0
  54. package/dist/src/languages/python/test-emitter.js +388 -0
  55. package/dist/src/languages/python/visitor.d.ts +14 -0
  56. package/dist/src/languages/python/visitor.js +65 -0
  57. package/dist/src/languages/rust/driver.d.ts +13 -0
  58. package/dist/src/languages/rust/driver.js +624 -0
  59. package/dist/src/languages/rust/emitter.d.ts +45 -0
  60. package/dist/src/languages/rust/emitter.js +1596 -0
  61. package/dist/src/languages/rust/visitor.d.ts +25 -0
  62. package/dist/src/languages/rust/visitor.js +153 -0
  63. package/dist/src/languages/typescript/driver.d.ts +8 -0
  64. package/dist/src/languages/typescript/driver.js +209 -0
  65. package/dist/src/languages/typescript/emitter.d.ts +42 -0
  66. package/dist/src/languages/typescript/emitter.js +904 -0
  67. package/dist/src/languages/typescript/scaffolding.d.ts +32 -0
  68. package/dist/src/languages/typescript/scaffolding.js +303 -0
  69. package/dist/src/languages/typescript/test-emitter.d.ts +23 -0
  70. package/dist/src/languages/typescript/test-emitter.js +204 -0
  71. package/dist/src/languages/typescript/visitor.d.ts +14 -0
  72. package/dist/src/languages/typescript/visitor.js +64 -0
  73. package/dist/src/lib.d.ts +33 -0
  74. package/dist/src/lib.js +101 -0
  75. package/dist/src/testing/index.d.ts +2 -0
  76. package/dist/src/testing/index.js +8 -0
  77. package/dist/src/testing/test-context.d.ts +63 -0
  78. package/dist/src/testing/test-context.js +355 -0
  79. package/fixtures/shapes/main.tsp +43 -0
  80. package/fixtures/tspconfig.yaml +13 -0
  81. package/package.json +76 -0
  82. package/src/lib/main.tsp +110 -0
@@ -0,0 +1,856 @@
1
+ /**
2
+ * Python code emitter — Declaration IR → Python source code.
3
+ *
4
+ * Replaces `file.py.njk` + `_macros.njk` (~619 lines of Nunjucks templates)
5
+ * with a typed TypeScript function that walks the FileDecl tree.
6
+ *
7
+ * The emitter produces correctly-formatted Python 3.11+ code using modern
8
+ * `X | None` union syntax. Output should be byte-identical to the template
9
+ * output after ruff+black formatting.
10
+ *
11
+ * Structural blocks emitted (in order):
12
+ * 1. Header comment (auto-generated warning)
13
+ * 2. Imports (abc, dataclasses, typing, context, local types)
14
+ * 3. For each type in the file:
15
+ * a. @dataclass class definition with docstring
16
+ * b. _shorthand_property ClassVar
17
+ * c. Field declarations with type annotations + defaults
18
+ * d. load() static method
19
+ * e. Collection helpers (load_X / save_X)
20
+ * f. Polymorphic dispatch (load_<discriminator>)
21
+ * g. save() instance method
22
+ * h. to_yaml() and to_json() methods
23
+ * i. Factory classmethods
24
+ * j. Method stubs (as comments)
25
+ */
26
+ import { toSnakeCase } from "../../ir/utilities.js";
27
+ /**
28
+ * Type mapping from TypeSpec scalar types to Python types.
29
+ */
30
+ const TYPE_MAP = {
31
+ "string": "str",
32
+ "number": "float",
33
+ "array": "list",
34
+ "object": "dict",
35
+ "boolean": "bool",
36
+ "int64": "int",
37
+ "int32": "int",
38
+ "float64": "float",
39
+ "float32": "float",
40
+ "integer": "int",
41
+ "float": "float",
42
+ "numeric": "float",
43
+ "any": "Any",
44
+ "dictionary": "dict[str, Any]",
45
+ };
46
+ function paramType(typeStr) {
47
+ if (typeStr === "string")
48
+ return "str";
49
+ if (typeStr === "boolean")
50
+ return "bool";
51
+ if (typeStr === "integer" || typeStr === "int32" || typeStr === "int64")
52
+ return "int";
53
+ if (typeStr === "float" || typeStr === "float64" || typeStr === "float32")
54
+ return "float";
55
+ return "Any";
56
+ }
57
+ function returnType(typeStr) {
58
+ if (typeStr === "string")
59
+ return "str";
60
+ if (typeStr === "boolean")
61
+ return "bool";
62
+ if (typeStr === "integer" || typeStr === "int32" || typeStr === "int64")
63
+ return "int";
64
+ if (typeStr === "float" || typeStr === "float64" || typeStr === "float32")
65
+ return "float";
66
+ return typeStr;
67
+ }
68
+ // ============================================================================
69
+ // Main entry point
70
+ // ============================================================================
71
+ /**
72
+ * Emit a complete Python file from a FileDecl.
73
+ */
74
+ export function emitPythonFile(decl, visitor, group = "") {
75
+ const lines = [];
76
+ // 1. Header
77
+ lines.push("##########################################");
78
+ lines.push("# WARNING: This is an auto-generated file.");
79
+ lines.push("# DO NOT EDIT THIS FILE DIRECTLY");
80
+ lines.push("# ANY EDITS WILL BE LOST");
81
+ lines.push("##########################################");
82
+ lines.push("");
83
+ // 2. Imports — sorted alphabetically as ruff/isort would
84
+ // Collect all import lines, then sort them
85
+ const stdlibImports = [];
86
+ const localImports = [];
87
+ const hasProtocol = decl.types.some(t => t.isProtocol);
88
+ const hasNonProtocol = decl.types.some(t => !t.isProtocol);
89
+ const hasMethodHelpers = decl.types.some(t => !t.isProtocol && t.methods.length > 0);
90
+ if (decl.containsAbstract) {
91
+ stdlibImports.push("from abc import ABC");
92
+ }
93
+ if (hasNonProtocol) {
94
+ stdlibImports.push("from dataclasses import dataclass, field");
95
+ }
96
+ const typingImports = ["Any"];
97
+ if (hasNonProtocol)
98
+ typingImports.push("ClassVar");
99
+ if (decl.enums.length > 0)
100
+ typingImports.push("Literal");
101
+ if (hasProtocol || hasMethodHelpers)
102
+ typingImports.push("Protocol", "runtime_checkable");
103
+ typingImports.sort();
104
+ stdlibImports.push(`from typing import ${typingImports.join(", ")}`);
105
+ // Context import — go up one level when inside a group subfolder
106
+ if (hasNonProtocol) {
107
+ const ctxPrefix = group ? ".." : ".";
108
+ localImports.push(`from ${ctxPrefix}_context import LoadContext, SaveContext`);
109
+ }
110
+ // Cross-group and same-group type imports
111
+ for (const imp of decl.imports) {
112
+ if (imp.group === group) {
113
+ // Same group (or both root): relative sibling import
114
+ localImports.push(`from ._${imp.module} import ${imp.names.join(", ")}`);
115
+ }
116
+ else if (imp.group) {
117
+ // Different non-empty group: go up to model root, then into the group subfolder
118
+ localImports.push(`from ..${imp.group}._${imp.module} import ${imp.names.join(", ")}`);
119
+ }
120
+ else {
121
+ // Imported module is at model root (no group): go up one level
122
+ localImports.push(`from .._${imp.module} import ${imp.names.join(", ")}`);
123
+ }
124
+ }
125
+ localImports.sort();
126
+ for (const line of stdlibImports) {
127
+ lines.push(line);
128
+ }
129
+ lines.push("");
130
+ for (const line of localImports) {
131
+ lines.push(line);
132
+ }
133
+ // 2.5. Enum type aliases
134
+ if (decl.enums.length > 0) {
135
+ lines.push("");
136
+ for (const enumDef of decl.enums) {
137
+ const values = enumDef.values.map(v => `"${v}"`).join(", ");
138
+ if (enumDef.isOpen) {
139
+ // Open enum — Literal values or any str
140
+ lines.push(`${enumDef.name} = Literal[${values}] | str`);
141
+ }
142
+ else {
143
+ lines.push(`${enumDef.name} = Literal[${values}]`);
144
+ }
145
+ }
146
+ }
147
+ // 3. Emit each type
148
+ for (const type of decl.types) {
149
+ lines.push("");
150
+ lines.push("");
151
+ emitType(type, lines, visitor);
152
+ }
153
+ return lines.join("\n") + "\n";
154
+ }
155
+ // ============================================================================
156
+ // Type emission
157
+ // ============================================================================
158
+ function emitType(type, lines, visitor) {
159
+ const name = type.typeName.name;
160
+ // Protocol types → emit as Protocol class with method signatures
161
+ if (type.isProtocol) {
162
+ emitProtocolClass(type, lines);
163
+ return;
164
+ }
165
+ // Class definition
166
+ lines.push("@dataclass");
167
+ const bases = [];
168
+ if (type.base)
169
+ bases.push(type.base.name);
170
+ if (type.isAbstract)
171
+ bases.push("ABC");
172
+ const baseSuffix = bases.length > 0 ? `(${bases.join(", ")})` : "";
173
+ lines.push(`class ${name}${baseSuffix}:`);
174
+ // Docstring
175
+ emitDocstring(type, lines);
176
+ // _shorthand_property ClassVar
177
+ lines.push("");
178
+ const shorthand = type.coercionProperty ? `"${type.coercionProperty}"` : "None";
179
+ lines.push(` _shorthand_property: ClassVar[str | None] = ${shorthand}`);
180
+ // Field declarations (blank line between _shorthand and first field)
181
+ lines.push("");
182
+ for (const field of type.fields) {
183
+ lines.push(` ${toSnakeCase(field.name)}: ${pythonTypeAnnotation(field)}${pythonDefaultValue(field)}`);
184
+ }
185
+ // load() method
186
+ lines.push("");
187
+ emitLoadMethod(type, lines);
188
+ // Collection helpers (between load and polymorphic dispatch)
189
+ for (const helper of type.collectionHelpers) {
190
+ emitCollectionLoadHelper(name, helper, lines);
191
+ lines.push("");
192
+ emitCollectionSaveHelper(name, helper, lines);
193
+ }
194
+ // Polymorphic dispatch
195
+ if (type.polymorphicDispatch) {
196
+ lines.push("");
197
+ lines.push("");
198
+ emitPolymorphicDispatch(name, type.polymorphicDispatch, type.isAbstract, lines);
199
+ }
200
+ // save() method
201
+ lines.push("");
202
+ emitSaveMethod(type, lines);
203
+ // to_wire() method (only when wire mappings exist)
204
+ if (type.wire) {
205
+ emitToWireMethod(type, lines);
206
+ }
207
+ // to_yaml() method
208
+ emitToYaml(name, lines);
209
+ // to_json() method
210
+ emitToJson(name, lines);
211
+ // Factory methods
212
+ if (type.factories.length > 0) {
213
+ const fieldNames = new Set(type.fields.map(f => f.name));
214
+ lines.push("");
215
+ for (const factory of type.factories) {
216
+ emitFactory(name, factory, visitor, fieldNames, lines);
217
+ lines.push("");
218
+ }
219
+ }
220
+ lines.push("");
221
+ // Method stubs — emit as a sibling Protocol class outside the dataclass
222
+ if (type.methods.length > 0) {
223
+ emitMethodHelpersProtocol(type, lines);
224
+ }
225
+ }
226
+ /**
227
+ * Emit a typing.Protocol class named `<TypeName>Helpers` that declares the
228
+ * `@method` contract for a concrete type. The generated dataclass does NOT
229
+ * satisfy this Protocol on its own — runtime code must provide an
230
+ * implementation (either by attaching methods to the class or by providing
231
+ * a wrapper type). Using `@runtime_checkable` allows isinstance() checks.
232
+ */
233
+ function emitMethodHelpersProtocol(type, lines) {
234
+ const name = type.typeName.name;
235
+ lines.push("");
236
+ lines.push("");
237
+ lines.push("@runtime_checkable");
238
+ lines.push(`class ${name}Helpers(Protocol):`);
239
+ lines.push(` """Helper contract for ${name}.`);
240
+ lines.push("");
241
+ lines.push(` Runtime implementations must provide these methods on every ${name}`);
242
+ lines.push(" instance (either by attaching them to the generated class or by wrapping it).");
243
+ lines.push(" The type checker can verify conformance by annotating against this Protocol");
244
+ lines.push(` or by calling isinstance(instance, ${name}Helpers) at runtime.`);
245
+ lines.push(` """`);
246
+ for (const m of type.methods) {
247
+ const params = Object.entries(m.params)
248
+ .map(([pName, pType]) => `${toSnakeCase(pName)}: ${protocolType(pType)}`)
249
+ .join(", ");
250
+ const paramList = params ? `, ${params}` : "";
251
+ const ret = protocolType(m.returns);
252
+ const snakeName = toSnakeCase(m.name);
253
+ // Zero-param, non-verb methods are emitted as @property — idiomatic Python
254
+ // for accessor-style helpers (matches the C# emitter's heuristic).
255
+ const isProperty = Object.keys(m.params).length === 0 && !isMethodStyle(m.name);
256
+ lines.push("");
257
+ if (isProperty) {
258
+ lines.push(" @property");
259
+ }
260
+ lines.push(` def ${snakeName}(self${paramList}) -> ${ret}:`);
261
+ if (m.description) {
262
+ lines.push(` """${m.description}"""`);
263
+ }
264
+ lines.push(" ...");
265
+ }
266
+ }
267
+ /**
268
+ * Verbs that indicate an action — zero-param @methods whose name starts with
269
+ * one of these are emitted as regular Python methods. Everything else becomes
270
+ * a ``@property``. Mirrors the C# emitter's heuristic so both runtimes expose
271
+ * ``message.text`` as a property and ``message.to_text_content()`` as a method.
272
+ */
273
+ const PY_METHOD_VERB_PREFIXES = [
274
+ "to", "get", "set", "fetch", "compute", "make", "build", "create",
275
+ "load", "save", "convert", "parse", "format", "render", "serialize",
276
+ "deserialize", "find", "calculate", "invoke", "execute", "run",
277
+ ];
278
+ function isMethodStyle(name) {
279
+ const lower = name.toLowerCase();
280
+ for (const prefix of PY_METHOD_VERB_PREFIXES) {
281
+ if (lower === prefix)
282
+ return true;
283
+ if (lower.startsWith(prefix) && name.length > prefix.length) {
284
+ const next = name[prefix.length];
285
+ if (next === next.toUpperCase() && next !== next.toLowerCase()) {
286
+ return true;
287
+ }
288
+ }
289
+ }
290
+ return false;
291
+ }
292
+ // ============================================================================
293
+ // Protocol class emission
294
+ // ============================================================================
295
+ /** Map a protocol type string to a Python type annotation. */
296
+ function protocolType(typeStr) {
297
+ // Handle nullable types
298
+ if (typeStr.endsWith("?")) {
299
+ const inner = typeStr.slice(0, -1);
300
+ return `${protocolType(inner)} | None`;
301
+ }
302
+ // Handle array types
303
+ if (typeStr.endsWith("[]")) {
304
+ const inner = typeStr.slice(0, -2);
305
+ return `list[${protocolType(inner)}]`;
306
+ }
307
+ // Handle Record/dict types
308
+ if (typeStr === "Record<unknown>" || typeStr === "dictionary")
309
+ return "dict[str, Any]";
310
+ if (typeStr === "unknown" || typeStr === "any")
311
+ return "Any";
312
+ // Scalar types
313
+ const mapped = TYPE_MAP[typeStr];
314
+ if (mapped)
315
+ return mapped;
316
+ return typeStr;
317
+ }
318
+ /**
319
+ * Emit a Python Protocol class for a protocol type.
320
+ * Uses typing.Protocol for structural subtyping.
321
+ */
322
+ function emitProtocolClass(type, lines) {
323
+ const name = type.typeName.name;
324
+ lines.push("@runtime_checkable");
325
+ lines.push(`class ${name}(Protocol):`);
326
+ // Docstring
327
+ if (type.description) {
328
+ const descLines = type.description.split("\n");
329
+ lines.push(` """${descLines[0]}`);
330
+ for (let i = 1; i < descLines.length; i++) {
331
+ lines.push(` ${descLines[i]}`);
332
+ }
333
+ lines.push(` """`);
334
+ }
335
+ if (type.methods.length === 0) {
336
+ lines.push(" ...");
337
+ return;
338
+ }
339
+ for (const method of type.methods) {
340
+ const params = Object.entries(method.params)
341
+ .map(([pName, pType]) => `${toSnakeCase(pName)}: ${protocolType(pType)}`)
342
+ .join(", ");
343
+ const ret = protocolType(method.returns);
344
+ // Sync method
345
+ lines.push("");
346
+ if (method.description) {
347
+ lines.push(` def ${toSnakeCase(method.name)}(self, ${params}) -> ${ret}:`);
348
+ lines.push(` """${method.description}"""`);
349
+ if (method.optional) {
350
+ lines.push(" return None");
351
+ }
352
+ else {
353
+ lines.push(" ...");
354
+ }
355
+ }
356
+ else {
357
+ if (method.optional) {
358
+ lines.push(` def ${toSnakeCase(method.name)}(self, ${params}) -> ${ret}:`);
359
+ lines.push(" return None");
360
+ }
361
+ else {
362
+ lines.push(` def ${toSnakeCase(method.name)}(self, ${params}) -> ${ret}: ...`);
363
+ }
364
+ }
365
+ // Async variant (skip for sync-only methods)
366
+ if (!method.sync) {
367
+ lines.push("");
368
+ if (method.description) {
369
+ lines.push(` async def ${toSnakeCase(method.name)}_async(self, ${params}) -> ${ret}:`);
370
+ lines.push(` """${method.description} (async variant)"""`);
371
+ if (method.optional) {
372
+ lines.push(" return None");
373
+ }
374
+ else {
375
+ lines.push(" ...");
376
+ }
377
+ }
378
+ else {
379
+ if (method.optional) {
380
+ lines.push(` async def ${toSnakeCase(method.name)}_async(self, ${params}) -> ${ret}:`);
381
+ lines.push(" return None");
382
+ }
383
+ else {
384
+ lines.push(` async def ${toSnakeCase(method.name)}_async(self, ${params}) -> ${ret}: ...`);
385
+ }
386
+ }
387
+ }
388
+ }
389
+ lines.push("");
390
+ }
391
+ // ============================================================================
392
+ // Docstring
393
+ // ============================================================================
394
+ function emitDocstring(type, lines) {
395
+ const descLines = type.description.split("\n");
396
+ lines.push(` """${descLines[0]}`);
397
+ for (let i = 1; i < descLines.length; i++) {
398
+ lines.push(` ${descLines[i]}`);
399
+ }
400
+ if (type.fields.length > 0) {
401
+ lines.push(" ");
402
+ lines.push(" Attributes");
403
+ lines.push(" ----------");
404
+ for (const f of type.fields) {
405
+ lines.push(` ${toSnakeCase(f.name)} : ${pythonDocstringType(f)}`);
406
+ lines.push(` ${f.description}`);
407
+ }
408
+ }
409
+ lines.push(` """`);
410
+ }
411
+ // ============================================================================
412
+ // Type annotations — uses modern X | None syntax
413
+ // ============================================================================
414
+ function pythonTypeAnnotation(f) {
415
+ // Named enum field — use the enum type alias
416
+ if (f.enumName && f.allowedValues.length > 0) {
417
+ return f.isOptional ? `${f.enumName} | None` : f.enumName;
418
+ }
419
+ const cat = f.category;
420
+ switch (cat.kind) {
421
+ case "dict":
422
+ return f.isOptional ? "dict[str, Any] | None" : "dict[str, Any]";
423
+ case "collection_scalar":
424
+ return `list[${TYPE_MAP[cat.scalarType] || "Any"}]`;
425
+ case "collection_complex":
426
+ return `list[${cat.typeName}]`;
427
+ case "scalar": {
428
+ const pyType = TYPE_MAP[cat.scalarType] || "Any";
429
+ return f.isOptional ? `${pyType} | None` : pyType;
430
+ }
431
+ case "complex":
432
+ return f.isOptional ? `${cat.typeName} | None` : cat.typeName;
433
+ }
434
+ }
435
+ function pythonDocstringType(f) {
436
+ const cat = f.category;
437
+ let baseType;
438
+ switch (cat.kind) {
439
+ case "dict":
440
+ baseType = "dict[str, Any]";
441
+ break;
442
+ case "collection_scalar":
443
+ baseType = `list[${TYPE_MAP[cat.scalarType] || "Any"}]`;
444
+ break;
445
+ case "collection_complex":
446
+ baseType = `list[${cat.typeName}]`;
447
+ break;
448
+ case "scalar":
449
+ baseType = TYPE_MAP[cat.scalarType] || "Any";
450
+ break;
451
+ case "complex":
452
+ baseType = cat.typeName;
453
+ break;
454
+ }
455
+ if (f.isOptional)
456
+ return `Optional[${baseType}]`;
457
+ return baseType;
458
+ }
459
+ // ============================================================================
460
+ // Default values
461
+ // ============================================================================
462
+ function pythonDefaultValue(f) {
463
+ const cat = f.category;
464
+ if (cat.kind === "collection_scalar" || cat.kind === "collection_complex") {
465
+ return " = field(default_factory=list)";
466
+ }
467
+ if (f.isOptional) {
468
+ return " = None";
469
+ }
470
+ // Enum fields — use the field's default value or the first allowed value
471
+ if (f.enumName && f.allowedValues.length > 0) {
472
+ const dv = typeof f.defaultValue === "string" && f.allowedValues.includes(f.defaultValue)
473
+ ? f.defaultValue
474
+ : f.allowedValues[0];
475
+ return ` = field(default="${dv}")`;
476
+ }
477
+ if (cat.kind === "scalar") {
478
+ const t = cat.scalarType;
479
+ if (t === "boolean") {
480
+ return ` = field(default=${f.defaultValue ? "True" : "False"})`;
481
+ }
482
+ if (t === "string") {
483
+ return ` = field(default="${f.defaultValue ?? ""}")`;
484
+ }
485
+ if (t === "number" || t === "numeric" || t === "float64" || t === "float32" || t === "float") {
486
+ return ` = field(default=${f.defaultValue ?? "0.0"})`;
487
+ }
488
+ if (t === "int64" || t === "int32" || t === "integer") {
489
+ return ` = field(default=${f.defaultValue ?? "0"})`;
490
+ }
491
+ if (t === "dictionary") {
492
+ return " = field(default_factory=dict)";
493
+ }
494
+ return ` = field(default=${f.defaultValue ?? "None"})`;
495
+ }
496
+ if (cat.kind === "dict") {
497
+ return " = field(default_factory=dict)";
498
+ }
499
+ if (cat.kind === "complex") {
500
+ return ` = field(default_factory=${f.typeName.name})`;
501
+ }
502
+ return " = None";
503
+ }
504
+ // ============================================================================
505
+ // load() method
506
+ // ============================================================================
507
+ function emitLoadMethod(type, lines) {
508
+ const name = type.typeName.name;
509
+ lines.push(" @staticmethod");
510
+ lines.push(` def load(data: Any, context: LoadContext | None = None) -> "${name}":`);
511
+ lines.push(` """Load a ${name} instance.`);
512
+ lines.push(" Args:");
513
+ lines.push(" data (Any): The data to load the instance from.");
514
+ lines.push(" context (Optional[LoadContext]): Optional context with pre/post processing callbacks.");
515
+ lines.push(" Returns:");
516
+ lines.push(` ${name}: The loaded ${name} instance.`);
517
+ lines.push("");
518
+ lines.push(` """`);
519
+ lines.push("");
520
+ lines.push(" if context is not None:");
521
+ lines.push(" data = context.process_input(data)");
522
+ // Coercion checks — direct property setting instead of dict construction
523
+ if (type.load.coercions.length > 0) {
524
+ lines.push(" ");
525
+ lines.push(" # handle alternate representations");
526
+ for (const c of type.load.coercions) {
527
+ emitCoercionBranch(name, c, type, lines);
528
+ }
529
+ }
530
+ lines.push(" ");
531
+ lines.push(` if not isinstance(data, dict):`);
532
+ lines.push(` raise ValueError(f"Invalid data for ${name}: {data}")`);
533
+ // Create instance (polymorphic dispatch or direct)
534
+ if (type.load.hasPolymorphicDispatch && type.polymorphicDispatch) {
535
+ const discSnake = toSnakeCase(type.polymorphicDispatch.discriminatorField);
536
+ lines.push("");
537
+ lines.push(` # load polymorphic ${name} instance`);
538
+ lines.push(` instance = ${name}.load_${discSnake}(data, context)`);
539
+ }
540
+ else {
541
+ lines.push("");
542
+ lines.push(` # create new instance`);
543
+ lines.push(` instance = ${name}()`);
544
+ }
545
+ // Blank line(s) after instance creation
546
+ lines.push("");
547
+ if (type.load.hasPolymorphicDispatch) {
548
+ lines.push("");
549
+ }
550
+ // Per-property assignments
551
+ for (const a of type.load.assignments) {
552
+ lines.push(` if data is not None and "${a.sourceName}" in data:`);
553
+ lines.push(` ${emitLoadAssignment(a)}`);
554
+ }
555
+ // Context post-processing
556
+ lines.push(" if context is not None:");
557
+ lines.push(" instance = context.process_output(instance)");
558
+ lines.push(" return instance");
559
+ lines.push("");
560
+ lines.push("");
561
+ }
562
+ function emitLoadAssignment(a) {
563
+ const snake = toSnakeCase(a.fieldName);
564
+ const cat = a.category;
565
+ switch (cat.kind) {
566
+ case "scalar":
567
+ case "dict":
568
+ case "collection_scalar":
569
+ return `instance.${snake} = data["${a.sourceName}"]`;
570
+ case "collection_complex":
571
+ return `instance.${snake} = ${a.parentTypeName}.load_${snake}(data["${a.sourceName}"], context)`;
572
+ case "complex":
573
+ return `instance.${snake} = ${cat.typeName}.load(data["${a.sourceName}"], context)`;
574
+ }
575
+ }
576
+ /**
577
+ * Emit a single coercion branch — direct property setting with early return.
578
+ *
579
+ * For types where the coercion involves a dynamic discriminator with child
580
+ * variants, falls back to calling the dispatch method.
581
+ */
582
+ function emitCoercionBranch(typeName, c, type, lines) {
583
+ const pyScalar = TYPE_MAP[c.scalarType] || c.scalarType;
584
+ lines.push(` if isinstance(data, ${pyScalar}):`);
585
+ if (c.needsDispatch && type.polymorphicDispatch) {
586
+ // Dynamic discriminator — must go through dispatch
587
+ const discSnake = toSnakeCase(type.polymorphicDispatch.discriminatorField);
588
+ // Build a minimal dict for the dispatch method
589
+ const dictFields = c.assignments.map(a => {
590
+ const val = a.isInput ? "data" : `"${a.literalValue}"`;
591
+ return `"${a.fieldName}": ${val}`;
592
+ });
593
+ lines.push(` return ${typeName}.load_${discSnake}({${dictFields.join(", ")}}, context)`);
594
+ }
595
+ else {
596
+ // Direct property setting — no intermediate dict
597
+ lines.push(` instance = ${typeName}()`);
598
+ for (const a of c.assignments) {
599
+ const snake = toSnakeCase(a.fieldName);
600
+ if (a.isInput) {
601
+ lines.push(` instance.${snake} = data`);
602
+ }
603
+ else {
604
+ lines.push(` instance.${snake} = "${a.literalValue}"`);
605
+ }
606
+ }
607
+ lines.push(" if context is not None:");
608
+ lines.push(" instance = context.process_output(instance)");
609
+ lines.push(" return instance");
610
+ }
611
+ }
612
+ // ============================================================================
613
+ // Collection helpers (load_X / save_X)
614
+ // ============================================================================
615
+ function emitCollectionLoadHelper(parentName, helper, lines) {
616
+ const snake = toSnakeCase(helper.propertyName);
617
+ const elemName = helper.elementTypeName.name;
618
+ const firstInnerField = helper.innerFields[0] || "kind";
619
+ lines.push("");
620
+ lines.push(" @staticmethod");
621
+ lines.push(` def load_${snake}(data: dict | list, context: LoadContext | None) -> list[${elemName}]:`);
622
+ lines.push(" if isinstance(data, dict):");
623
+ lines.push(` # convert simple named ${helper.propertyName} to list of ${elemName}`);
624
+ lines.push(" result = []");
625
+ lines.push(" for k, v in data.items():");
626
+ lines.push(" if isinstance(v, dict):");
627
+ lines.push(" # value is an object, spread its properties");
628
+ lines.push(` result.append({"name": k, **v})`);
629
+ lines.push(" else:");
630
+ lines.push(" # value is a scalar, use it as the primary property");
631
+ lines.push(` result.append({"name": k, "${firstInnerField}": v})`);
632
+ lines.push(" data = result");
633
+ lines.push(` return [${elemName}.load(item, context) for item in data]`);
634
+ }
635
+ function emitCollectionSaveHelper(parentName, helper, lines) {
636
+ const snake = toSnakeCase(helper.propertyName);
637
+ const elemName = helper.elementTypeName.name;
638
+ lines.push(" @staticmethod");
639
+ lines.push(` def save_${snake}(items: list[${elemName}], context: SaveContext | None) -> dict[str, Any] | list[dict[str, Any]]:`);
640
+ lines.push(" if context is None:");
641
+ lines.push(" context = SaveContext()");
642
+ if (helper.hasNameProperty) {
643
+ lines.push("");
644
+ lines.push(' if context.collection_format == "array":');
645
+ lines.push(" return [item.save(context) for item in items]");
646
+ lines.push("");
647
+ lines.push(" # Object format: use name as key");
648
+ lines.push(" result: dict[str, Any] = {}");
649
+ lines.push(" for item in items:");
650
+ lines.push(" item_data = item.save(context)");
651
+ lines.push(' name = item_data.pop("name", None)');
652
+ lines.push(" if name:");
653
+ lines.push(" # Check if we can use shorthand (only primary property set)");
654
+ lines.push(" if context.use_shorthand and hasattr(item, '_shorthand_property'):");
655
+ lines.push(" shorthand_prop = item._shorthand_property");
656
+ lines.push(" if shorthand_prop and len(item_data) == 1 and shorthand_prop in item_data:");
657
+ lines.push(" result[name] = item_data[shorthand_prop]");
658
+ lines.push(" continue");
659
+ lines.push(" result[name] = item_data");
660
+ lines.push(" else:");
661
+ lines.push(' # No name, fall back to array format for this item');
662
+ lines.push(' if "_unnamed" not in result:');
663
+ lines.push(' result["_unnamed"] = []');
664
+ lines.push(' result["_unnamed"].append(item_data)');
665
+ lines.push(" return result");
666
+ }
667
+ else {
668
+ lines.push("");
669
+ lines.push(" # This type doesn't have a 'name' property, so always use array format");
670
+ lines.push(" return [item.save(context) for item in items]");
671
+ }
672
+ }
673
+ // ============================================================================
674
+ // Polymorphic dispatch
675
+ // ============================================================================
676
+ function emitPolymorphicDispatch(parentName, dispatch, isAbstract, lines) {
677
+ const discSnake = toSnakeCase(dispatch.discriminatorField);
678
+ lines.push(" @staticmethod");
679
+ lines.push(` def load_${discSnake}(data: dict, context: LoadContext | None) -> "${parentName}":`);
680
+ lines.push(` # load polymorphic ${parentName} instance`);
681
+ lines.push(` if data is not None and "${dispatch.discriminatorField}" in data:`);
682
+ lines.push(` discriminator_value = str(data["${dispatch.discriminatorField}"]).lower()`);
683
+ for (let i = 0; i < dispatch.variants.length; i++) {
684
+ const v = dispatch.variants[i];
685
+ const keyword = i === 0 ? "if" : "elif";
686
+ lines.push(` ${keyword} discriminator_value == "${v.value}":`);
687
+ lines.push(` return ${v.typeName.name}.load(data, context)`);
688
+ }
689
+ // Default handling — matches template whitespace exactly
690
+ if (dispatch.defaultVariant) {
691
+ lines.push("");
692
+ lines.push(" else:");
693
+ if (dispatch.defaultVariant.isSelfReference) {
694
+ lines.push(` # create new instance (stop recursion)`);
695
+ lines.push(` return ${parentName}()`);
696
+ }
697
+ else {
698
+ lines.push("");
699
+ lines.push(` # load default instance`);
700
+ lines.push(` return ${dispatch.defaultVariant.typeName.name}.load(data, context)`);
701
+ }
702
+ }
703
+ else {
704
+ lines.push("");
705
+ lines.push(" else:");
706
+ lines.push(` raise ValueError(f"Unknown ${parentName} discriminator value: {discriminator_value}")`);
707
+ }
708
+ lines.push(" else:");
709
+ if (isAbstract) {
710
+ lines.push("");
711
+ lines.push(` raise ValueError("Missing ${parentName} discriminator property: '${dispatch.discriminatorField}'")`);
712
+ }
713
+ else {
714
+ lines.push(` # create new instance`);
715
+ lines.push(` return ${parentName}()`);
716
+ }
717
+ lines.push("");
718
+ }
719
+ // ============================================================================
720
+ // save() method
721
+ // ============================================================================
722
+ function emitSaveMethod(type, lines) {
723
+ const name = type.typeName.name;
724
+ lines.push(` def save(self, context: SaveContext | None = None) -> dict[str, Any]:`);
725
+ lines.push(` """Save the ${name} instance to a dictionary.`);
726
+ lines.push(" Args:");
727
+ lines.push(" context (Optional[SaveContext]): Optional context with pre/post processing callbacks.");
728
+ lines.push(" Returns:");
729
+ lines.push(" dict[str, Any]: The dictionary representation of this instance.");
730
+ lines.push("");
731
+ lines.push(` """`);
732
+ lines.push(" obj = self");
733
+ lines.push(" if context is not None:");
734
+ lines.push(" obj = context.process_object(obj)");
735
+ lines.push("");
736
+ if (type.save.hasBase) {
737
+ lines.push("");
738
+ lines.push(" # Start with parent class properties");
739
+ lines.push(" result = super().save(context)");
740
+ lines.push("");
741
+ }
742
+ else {
743
+ lines.push("");
744
+ lines.push(" result: dict[str, Any] = {}");
745
+ }
746
+ // Per-property save assignments
747
+ lines.push("");
748
+ for (const a of type.save.assignments) {
749
+ const snake = toSnakeCase(a.fieldName);
750
+ lines.push(` if obj.${snake} is not None:`);
751
+ lines.push(` ${emitSaveAssignment(a)}`);
752
+ }
753
+ // Context post-processing (only for root types without base)
754
+ if (!type.save.hasBase) {
755
+ lines.push("");
756
+ lines.push(" if context is not None:");
757
+ lines.push(" result = context.process_dict(result)");
758
+ }
759
+ lines.push(" return result");
760
+ lines.push("");
761
+ }
762
+ function emitSaveAssignment(a) {
763
+ const snake = toSnakeCase(a.fieldName);
764
+ const cat = a.category;
765
+ switch (cat.kind) {
766
+ case "scalar":
767
+ case "dict":
768
+ case "collection_scalar":
769
+ return `result["${a.targetName}"] = obj.${snake}`;
770
+ case "collection_complex":
771
+ return `result["${a.targetName}"] = ${a.parentTypeName}.save_${snake}(obj.${snake}, context)`;
772
+ case "complex":
773
+ return `result["${a.targetName}"] = obj.${snake}.save(context)`;
774
+ }
775
+ }
776
+ // ============================================================================
777
+ // to_wire() method
778
+ // ============================================================================
779
+ function emitToWireMethod(type, lines) {
780
+ const name = type.typeName.name;
781
+ const wire = type.wire;
782
+ lines.push(` def to_wire(self, provider: str) -> dict[str, Any]:`);
783
+ lines.push(` """Convert to provider-specific wire format.`);
784
+ lines.push(" Args:");
785
+ lines.push(" provider (str): The provider to convert to (e.g., \"openai\", \"anthropic\").");
786
+ lines.push(" Returns:");
787
+ lines.push(" dict[str, Any]: The wire-format dictionary with provider-specific field names.");
788
+ lines.push("");
789
+ lines.push(` """`);
790
+ lines.push(" data = self.save()");
791
+ lines.push(" result: dict[str, Any] = {}");
792
+ // Build the wire_map literal from the IR mappings
793
+ lines.push(" wire_map: dict[str, dict[str, str]] = {");
794
+ for (const mapping of wire.mappings) {
795
+ const entries = Object.entries(mapping.wireNames)
796
+ .map(([provider, wireName]) => `"${provider}": "${wireName}"`)
797
+ .join(", ");
798
+ lines.push(` "${mapping.fieldName}": {${entries}},`);
799
+ }
800
+ lines.push(" }");
801
+ lines.push(" for key, value in data.items():");
802
+ lines.push(" mapping = wire_map.get(key)");
803
+ lines.push(" if mapping is not None and provider in mapping:");
804
+ lines.push(" result[mapping[provider]] = value");
805
+ lines.push(" return result");
806
+ lines.push("");
807
+ }
808
+ // ============================================================================
809
+ // to_yaml() / to_json() methods
810
+ // ============================================================================
811
+ function emitToYaml(name, lines) {
812
+ lines.push(` def to_yaml(self, context: SaveContext | None = None) -> str:`);
813
+ lines.push(` """Convert the ${name} instance to a YAML string.`);
814
+ lines.push(" Args:");
815
+ lines.push(" context (Optional[SaveContext]): Optional context with pre/post processing callbacks.");
816
+ lines.push(" Returns:");
817
+ lines.push(" str: The YAML string representation of this instance.");
818
+ lines.push("");
819
+ lines.push(` """`);
820
+ lines.push(" if context is None:");
821
+ lines.push(" context = SaveContext()");
822
+ lines.push(" return context.to_yaml(self.save(context))");
823
+ lines.push("");
824
+ }
825
+ function emitToJson(name, lines) {
826
+ lines.push(` def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str:`);
827
+ lines.push(` """Convert the ${name} instance to a JSON string.`);
828
+ lines.push(" Args:");
829
+ lines.push(" context (Optional[SaveContext]): Optional context with pre/post processing callbacks.");
830
+ lines.push(" indent (int): Number of spaces for indentation. Defaults to 2.");
831
+ lines.push(" Returns:");
832
+ lines.push(" str: The JSON string representation of this instance.");
833
+ lines.push("");
834
+ lines.push(` """`);
835
+ lines.push(" if context is None:");
836
+ lines.push(" context = SaveContext()");
837
+ lines.push(" return context.to_json(self.save(context), indent)");
838
+ lines.push("");
839
+ }
840
+ // ============================================================================
841
+ // Factory methods
842
+ // ============================================================================
843
+ function emitFactory(parentName, factory, visitor, fieldNames, lines) {
844
+ const params = Object.entries(factory.params)
845
+ .map(([pName, pType]) => `${toSnakeCase(pName)}: ${paramType(pType)}`)
846
+ .join(", ");
847
+ const paramStr = params ? `, ${params}` : "";
848
+ // In Python, a @classmethod with the same name as a dataclass field shadows
849
+ // the field's default. Prefix with create_ to avoid the collision.
850
+ const methodName = fieldNames.has(factory.name) ? `create_${factory.name}` : factory.name;
851
+ lines.push(" @classmethod");
852
+ lines.push(` def ${methodName}(cls${paramStr}) -> "${parentName}":`);
853
+ lines.push(` """Create a ${parentName} with preset field values."""`);
854
+ lines.push(` return ${visitor.visitExpr(factory.body)}`);
855
+ }
856
+ //# sourceMappingURL=emitter.js.map