@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,407 @@
1
+ /**
2
+ * Expression IR — Type-directed lowering for the emitter.
3
+ *
4
+ * This module implements the "lowering" pass of the transpiler:
5
+ * given an untyped data literal (from @factory sets or @coerce expansion)
6
+ * and a target type (TypeNode/PropertyNode), produce a typed Expr tree.
7
+ *
8
+ * The Expr tree is language-agnostic. Per-language visitors in render-expr.ts
9
+ * walk it to produce target language code.
10
+ *
11
+ * Architecture (following TypeScript/Roslyn/Babel pattern):
12
+ * Data literal + Type graph → resolve() → Expr tree → visit() → code string
13
+ */
14
+ // ============================================================================
15
+ // Type reference collection — for factory import resolution
16
+ // ============================================================================
17
+ /**
18
+ * Recursively collect all TypeName references from an Expr tree.
19
+ * Used by emitters to determine which additional types need importing
20
+ * when factory/coercion expressions reference types from other modules.
21
+ */
22
+ export function collectExprTypeRefs(expr) {
23
+ const refs = [];
24
+ function walk(e) {
25
+ switch (e.kind) {
26
+ case "construct":
27
+ refs.push(e.typeName);
28
+ e.fields.forEach(f => walk(f.value));
29
+ break;
30
+ case "variant":
31
+ refs.push(e.baseTypeName);
32
+ refs.push(e.variantTypeName);
33
+ e.fields.forEach(f => walk(f.value));
34
+ break;
35
+ case "array":
36
+ refs.push(e.elementTypeName);
37
+ e.items.forEach(walk);
38
+ break;
39
+ case "dict":
40
+ e.entries.forEach(ent => walk(ent.value));
41
+ break;
42
+ case "string":
43
+ case "number":
44
+ case "boolean":
45
+ case "null":
46
+ case "param":
47
+ case "field_read":
48
+ break;
49
+ }
50
+ }
51
+ walk(expr);
52
+ return refs;
53
+ }
54
+ // ============================================================================
55
+ // Type Registry — flat lookup for type-directed resolution
56
+ // ============================================================================
57
+ /**
58
+ * Registry of TypeNodes by name, enabling the resolver to look up types
59
+ * when processing nested objects and discriminated unions.
60
+ *
61
+ * Built from the emitter's type graph (TypeNode tree + enumerateTypes).
62
+ */
63
+ export class TypeRegistry {
64
+ types = new Map();
65
+ /** Register a type by its simple name. */
66
+ register(node) {
67
+ this.types.set(node.typeName.name, node);
68
+ }
69
+ /** Look up a type by simple name. Returns undefined if not found. */
70
+ get(name) {
71
+ return this.types.get(name);
72
+ }
73
+ /** Build a registry from a root TypeNode by walking all reachable types. */
74
+ static fromTypeGraph(roots) {
75
+ const registry = new TypeRegistry();
76
+ const visited = new Set();
77
+ function walk(node) {
78
+ const key = `${node.typeName.namespace}.${node.typeName.name}`;
79
+ if (visited.has(key))
80
+ return;
81
+ visited.add(key);
82
+ registry.register(node);
83
+ for (const child of node.childTypes) {
84
+ walk(child);
85
+ }
86
+ for (const prop of node.properties) {
87
+ if (prop.type) {
88
+ walk(prop.type);
89
+ }
90
+ }
91
+ }
92
+ for (const root of roots) {
93
+ walk(root);
94
+ }
95
+ return registry;
96
+ }
97
+ }
98
+ // ============================================================================
99
+ // Resolver — type-directed lowering (the "frontend")
100
+ // ============================================================================
101
+ /** Regex matching a `{paramName}` placeholder (must be the entire string). */
102
+ const PARAM_PLACEHOLDER = /^\{(\w+)\}$/;
103
+ /**
104
+ * Resolve a @factory decorator into a typed Expr tree.
105
+ *
106
+ * @param sets - Field assignments from the decorator (e.g., { allowed: true })
107
+ * @param params - Parameter declarations (e.g., { reason: "string" })
108
+ * @param targetType - The TypeNode this factory constructs
109
+ * @param registry - Type registry for resolving nested types
110
+ * @returns A Construct expression representing the factory body
111
+ */
112
+ export function resolveFactoryExpr(sets, params, targetType, registry) {
113
+ const fields = [];
114
+ // 1. Resolve each explicitly-set field
115
+ for (const [fieldName, value] of Object.entries(sets)) {
116
+ const prop = targetType.properties.find(p => p.name === fieldName);
117
+ if (!prop) {
118
+ throw new Error(`Property '${fieldName}' not found on type '${targetType.typeName.name}'. ` +
119
+ `Available: [${targetType.properties.map(p => p.name).join(", ")}]`);
120
+ }
121
+ fields.push({
122
+ propertyName: fieldName,
123
+ value: resolveValue(value, prop, params, registry),
124
+ isOptional: prop.isOptional,
125
+ });
126
+ }
127
+ // 2. Add flat params — params that match a top-level property not already in sets
128
+ for (const [paramName, paramType] of Object.entries(params)) {
129
+ if (paramName in sets)
130
+ continue; // already handled as a nested placeholder
131
+ // Check if this param was consumed as a placeholder inside sets
132
+ if (isParamConsumedInSets(paramName, sets))
133
+ continue;
134
+ const prop = targetType.properties.find(p => p.name === paramName);
135
+ if (!prop) {
136
+ throw new Error(`Parameter '${paramName}' does not match any property on type '${targetType.typeName.name}'. ` +
137
+ `Available: [${targetType.properties.map(p => p.name).join(", ")}]`);
138
+ }
139
+ fields.push({
140
+ propertyName: paramName,
141
+ value: { kind: "param", name: paramName, paramType },
142
+ isOptional: prop.isOptional,
143
+ });
144
+ }
145
+ return {
146
+ kind: "construct",
147
+ typeName: targetType.typeName,
148
+ fields,
149
+ };
150
+ }
151
+ /**
152
+ * Resolve a @coerce decorator into a typed Expr tree.
153
+ *
154
+ * A coercion is essentially a factory with a single implicit parameter named "value".
155
+ * The expansion dict maps property names to values, where "{value}" is the parameter ref.
156
+ *
157
+ * @param expansion - The expansion dict (e.g., { id: "{value}" })
158
+ * @param scalarType - The scalar type string (e.g., "string")
159
+ * @param targetType - The TypeNode this coercion constructs
160
+ * @param registry - Type registry for resolving nested types
161
+ * @returns A Construct expression representing the coercion expansion
162
+ */
163
+ export function resolveCoerceExpr(expansion, scalarType, targetType, registry, paramName = "value") {
164
+ // The expansion uses {value} as the fixed placeholder. Resolve with "value" as param,
165
+ // then rename the ParamRef to the caller's desired paramName.
166
+ const expr = resolveFactoryExpr(expansion, { value: scalarType }, targetType, registry);
167
+ if (paramName !== "value") {
168
+ renameParam(expr, "value", paramName);
169
+ }
170
+ return expr;
171
+ }
172
+ /** Recursively rename a ParamRef in an Expr tree. */
173
+ function renameParam(expr, from, to) {
174
+ switch (expr.kind) {
175
+ case "param":
176
+ if (expr.name === from)
177
+ expr.name = to;
178
+ break;
179
+ case "construct":
180
+ for (const f of expr.fields)
181
+ renameParam(f.value, from, to);
182
+ break;
183
+ case "variant":
184
+ for (const f of expr.fields)
185
+ renameParam(f.value, from, to);
186
+ break;
187
+ case "array":
188
+ for (const item of expr.items)
189
+ renameParam(item, from, to);
190
+ break;
191
+ case "dict":
192
+ for (const entry of expr.entries)
193
+ renameParam(entry.value, from, to);
194
+ break;
195
+ // Literals and field reads don't contain params
196
+ case "string":
197
+ case "number":
198
+ case "boolean":
199
+ case "null":
200
+ case "field_read":
201
+ break;
202
+ default: {
203
+ const _exhaustive = expr;
204
+ throw new Error(`Unknown expr kind: ${_exhaustive.kind}`);
205
+ }
206
+ }
207
+ }
208
+ // ============================================================================
209
+ // Internal resolution — structural recursion, type-directed
210
+ // ============================================================================
211
+ /**
212
+ * Resolve a single value against a property's type.
213
+ * This is the core recursive function — it dispatches on the value's shape
214
+ * and the target property's type information.
215
+ */
216
+ function resolveValue(value, prop, params, registry) {
217
+ // String value — could be a literal, a param ref, or a nested field
218
+ if (typeof value === "string") {
219
+ return resolveStringValue(value, prop, params);
220
+ }
221
+ // Boolean literal
222
+ if (typeof value === "boolean") {
223
+ return { kind: "boolean", value };
224
+ }
225
+ // Number literal
226
+ if (typeof value === "number") {
227
+ return { kind: "number", value };
228
+ }
229
+ // Null
230
+ if (value === null || value === undefined) {
231
+ return { kind: "null" };
232
+ }
233
+ // Array — resolve each element against the collection's element type
234
+ if (Array.isArray(value)) {
235
+ return resolveArrayValue(value, prop, params, registry);
236
+ }
237
+ // Object — resolve as a typed construction (possibly polymorphic)
238
+ if (typeof value === "object") {
239
+ return resolveObjectValue(value, prop, params, registry);
240
+ }
241
+ throw new Error(`Cannot resolve value of type '${typeof value}' for property '${prop.name}'`);
242
+ }
243
+ /**
244
+ * Resolve a string value — either a param placeholder or a string literal.
245
+ */
246
+ function resolveStringValue(value, _prop, params) {
247
+ const match = PARAM_PLACEHOLDER.exec(value);
248
+ if (match) {
249
+ const paramName = match[1];
250
+ if (paramName in params) {
251
+ return { kind: "param", name: paramName, paramType: params[paramName] };
252
+ }
253
+ throw new Error(`Placeholder '{${paramName}}' does not match any declared parameter. ` +
254
+ `Available: [${Object.keys(params).join(", ")}]`);
255
+ }
256
+ return { kind: "string", value };
257
+ }
258
+ /**
259
+ * Resolve an array value against a collection property.
260
+ */
261
+ function resolveArrayValue(items, prop, params, registry) {
262
+ if (!prop.isCollection && !prop.type) {
263
+ // If property isn't marked as collection, use its type name for the array
264
+ }
265
+ // Get the element type — from prop.type (which is the element TypeNode for collections)
266
+ const elementType = prop.type;
267
+ const elementTypeName = prop.typeName;
268
+ const resolvedItems = items.map((item, index) => {
269
+ if (elementType && typeof item === "object" && item !== null && !Array.isArray(item)) {
270
+ return resolveObjectAgainstType(item, elementType, params, registry);
271
+ }
272
+ // For scalar array elements, create a synthetic property to resolve against
273
+ if (typeof item === "string") {
274
+ return resolveStringValue(item, prop, params);
275
+ }
276
+ if (typeof item === "boolean") {
277
+ return { kind: "boolean", value: item };
278
+ }
279
+ if (typeof item === "number") {
280
+ return { kind: "number", value: item };
281
+ }
282
+ throw new Error(`Cannot resolve array element at index ${index} for property '${prop.name}'`);
283
+ });
284
+ return {
285
+ kind: "array",
286
+ elementTypeName,
287
+ items: resolvedItems,
288
+ };
289
+ }
290
+ /**
291
+ * Resolve an object value against a property's type.
292
+ * Delegates to resolveObjectAgainstType after finding the target type.
293
+ */
294
+ function resolveObjectValue(obj, prop, params, registry) {
295
+ // Find the target type — either from prop.type or registry lookup
296
+ let targetType = prop.type;
297
+ if (!targetType) {
298
+ targetType = registry.get(prop.typeName.name);
299
+ }
300
+ if (!targetType) {
301
+ throw new Error(`Cannot resolve object for property '${prop.name}': type '${prop.typeName.name}' not found in registry`);
302
+ }
303
+ return resolveObjectAgainstType(obj, targetType, params, registry);
304
+ }
305
+ /**
306
+ * Resolve an object against a known TypeNode.
307
+ * Handles discriminated unions (→ VariantConstruct) and plain types (→ Construct).
308
+ */
309
+ function resolveObjectAgainstType(obj, targetType, params, registry) {
310
+ // Check for discriminated union dispatch
311
+ if (targetType.discriminator && targetType.childTypes.length > 0) {
312
+ const discriminatorValue = obj[targetType.discriminator];
313
+ if (typeof discriminatorValue === "string") {
314
+ return resolveVariantConstruct(obj, targetType, discriminatorValue, params, registry);
315
+ }
316
+ }
317
+ // Plain type — resolve all fields
318
+ return resolveConstruct(obj, targetType, params, registry);
319
+ }
320
+ /**
321
+ * Resolve a discriminated union variant.
322
+ * Looks up the child type by discriminator value, then resolves fields
323
+ * against the child type's properties.
324
+ */
325
+ function resolveVariantConstruct(obj, baseType, discriminatorValue, params, registry) {
326
+ // Find the child type matching this discriminator value
327
+ const childType = baseType.childTypes.find(child => {
328
+ const discProp = child.properties.find(p => p.name === baseType.discriminator);
329
+ return discProp?.defaultValue === discriminatorValue;
330
+ });
331
+ if (!childType) {
332
+ throw new Error(`No child type of '${baseType.typeName.name}' has ${baseType.discriminator}='${discriminatorValue}'. ` +
333
+ `Available: [${baseType.childTypes.map(c => {
334
+ const dp = c.properties.find(p => p.name === baseType.discriminator);
335
+ return dp?.defaultValue ?? "*";
336
+ }).join(", ")}]`);
337
+ }
338
+ // Resolve fields against the child type (excluding the discriminator itself)
339
+ const fields = [];
340
+ for (const [fieldName, value] of Object.entries(obj)) {
341
+ if (fieldName === baseType.discriminator)
342
+ continue; // discriminator is implicit
343
+ // Look for the property on the child type first, then base type
344
+ const prop = childType.properties.find(p => p.name === fieldName)
345
+ ?? baseType.properties.find(p => p.name === fieldName);
346
+ if (!prop) {
347
+ throw new Error(`Property '${fieldName}' not found on variant '${childType.typeName.name}' ` +
348
+ `or base '${baseType.typeName.name}'`);
349
+ }
350
+ fields.push({
351
+ propertyName: fieldName,
352
+ value: resolveValue(value, prop, params, registry),
353
+ isOptional: prop.isOptional,
354
+ });
355
+ }
356
+ return {
357
+ kind: "variant",
358
+ baseTypeName: baseType.typeName,
359
+ discriminator: baseType.discriminator,
360
+ discriminatorValue,
361
+ variantTypeName: childType.typeName,
362
+ fields,
363
+ };
364
+ }
365
+ /**
366
+ * Resolve a plain (non-polymorphic) object construction.
367
+ */
368
+ function resolveConstruct(obj, targetType, params, registry) {
369
+ const fields = [];
370
+ for (const [fieldName, value] of Object.entries(obj)) {
371
+ const prop = targetType.properties.find(p => p.name === fieldName);
372
+ if (!prop) {
373
+ throw new Error(`Property '${fieldName}' not found on type '${targetType.typeName.name}'. ` +
374
+ `Available: [${targetType.properties.map(p => p.name).join(", ")}]`);
375
+ }
376
+ fields.push({
377
+ propertyName: fieldName,
378
+ value: resolveValue(value, prop, params, registry),
379
+ isOptional: prop.isOptional,
380
+ });
381
+ }
382
+ return {
383
+ kind: "construct",
384
+ typeName: targetType.typeName,
385
+ fields,
386
+ };
387
+ }
388
+ /**
389
+ * Check if a param name is used as a {placeholder} anywhere in the sets tree.
390
+ * Used to avoid double-emitting params that appear both as top-level property
391
+ * matches and as nested placeholders.
392
+ */
393
+ function isParamConsumedInSets(paramName, sets) {
394
+ const placeholder = `{${paramName}}`;
395
+ function search(value) {
396
+ if (value === placeholder)
397
+ return true;
398
+ if (Array.isArray(value))
399
+ return value.some(search);
400
+ if (value !== null && typeof value === "object") {
401
+ return Object.values(value).some(search);
402
+ }
403
+ return false;
404
+ }
405
+ return Object.values(sets).some(search);
406
+ }
407
+ //# sourceMappingURL=expansion.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Lowering pass — TypeNode graph → Declaration IR.
3
+ *
4
+ * This module converts the emitter's type graph (TypeNode/PropertyNode)
5
+ * into the language-agnostic Declaration IR (FileDecl/TypeDecl).
6
+ *
7
+ * The lowering is shared across all 5 target languages. Per-language
8
+ * emitter functions consume the FileDecl tree and emit code.
9
+ *
10
+ * Key responsibilities:
11
+ * - Classify every property into a PropertyCategory
12
+ * - Build load/save method specifications
13
+ * - Resolve polymorphic dispatch
14
+ * - Resolve collection helpers
15
+ * - Resolve factory methods via the Expression IR
16
+ * - Compute file-level imports
17
+ */
18
+ import { TypeNode, PropertyNode } from "./ast.js";
19
+ import { TypeRegistry } from "./expansion.js";
20
+ import { PropertyCategory, FileDecl, TypeDecl } from "./declarations.js";
21
+ /**
22
+ * Lower a base TypeNode (and all its children) into a FileDecl.
23
+ *
24
+ * This is the main entry point for the lowering pass. It produces a complete
25
+ * FileDecl containing one or more TypeDecls (parent + children for polymorphic types).
26
+ *
27
+ * The result is fully language-agnostic — per-language emitters handle rendering.
28
+ *
29
+ * @param node - The base TypeNode (must not have a parent — i.e., `node.base === null`)
30
+ * @param registry - TypeRegistry for resolving type references
31
+ * @param polymorphicTypeNames - Set of type names that are polymorphic bases
32
+ */
33
+ export declare function lowerFile(node: TypeNode, registry: TypeRegistry, polymorphicTypeNames?: Set<string>): FileDecl;
34
+ /**
35
+ * Collect all polymorphic type names from a set of nodes.
36
+ */
37
+ export declare function collectPolymorphicTypeNames(rootNode: TypeNode, registry: TypeRegistry): Set<string>;
38
+ /**
39
+ * Lower a single TypeNode into a TypeDecl.
40
+ */
41
+ export declare function lowerType(node: TypeNode, registry: TypeRegistry, polymorphicTypeNames: Set<string>): TypeDecl;
42
+ /**
43
+ * Classify a property into one of 5 categories.
44
+ * This is the fundamental decision that drives ALL code generation.
45
+ *
46
+ * Decision tree:
47
+ * isDict → "dict"
48
+ * isCollection && isScalar → "collection_scalar"
49
+ * isCollection && !isScalar → "collection_complex"
50
+ * isScalar → "scalar"
51
+ * !isScalar → "complex"
52
+ */
53
+ export declare function classifyProperty(prop: PropertyNode, polymorphicTypeNames: Set<string>): PropertyCategory;