@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.
- package/dist/src/cleanup/generated-file.d.ts +6 -0
- package/dist/src/cleanup/generated-file.js +61 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +110 -0
- package/dist/src/decorators.d.ts +56 -0
- package/dist/src/decorators.js +177 -0
- package/dist/src/emitter.d.ts +13 -0
- package/dist/src/emitter.js +137 -0
- package/dist/src/generate.d.ts +86 -0
- package/dist/src/generate.js +104 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +5 -0
- package/dist/src/ir/ast.d.ts +235 -0
- package/dist/src/ir/ast.js +589 -0
- package/dist/src/ir/declarations.d.ts +364 -0
- package/dist/src/ir/declarations.js +23 -0
- package/dist/src/ir/expansion.d.ts +140 -0
- package/dist/src/ir/expansion.js +407 -0
- package/dist/src/ir/lower.d.ts +53 -0
- package/dist/src/ir/lower.js +480 -0
- package/dist/src/ir/utilities.d.ts +12 -0
- package/dist/src/ir/utilities.js +39 -0
- package/dist/src/ir/visitor.d.ts +29 -0
- package/dist/src/ir/visitor.js +48 -0
- package/dist/src/languages/csharp/driver.d.ts +5 -0
- package/dist/src/languages/csharp/driver.js +315 -0
- package/dist/src/languages/csharp/emitter.d.ts +33 -0
- package/dist/src/languages/csharp/emitter.js +1140 -0
- package/dist/src/languages/csharp/scaffolding.d.ts +18 -0
- package/dist/src/languages/csharp/scaffolding.js +591 -0
- package/dist/src/languages/csharp/test-emitter.d.ts +43 -0
- package/dist/src/languages/csharp/test-emitter.js +274 -0
- package/dist/src/languages/csharp/visitor.d.ts +14 -0
- package/dist/src/languages/csharp/visitor.js +79 -0
- package/dist/src/languages/go/driver.d.ts +12 -0
- package/dist/src/languages/go/driver.js +128 -0
- package/dist/src/languages/go/emitter.d.ts +33 -0
- package/dist/src/languages/go/emitter.js +879 -0
- package/dist/src/languages/go/scaffolding.d.ts +18 -0
- package/dist/src/languages/go/scaffolding.js +53 -0
- package/dist/src/languages/go/test-emitter.d.ts +20 -0
- package/dist/src/languages/go/test-emitter.js +300 -0
- package/dist/src/languages/go/visitor.d.ts +14 -0
- package/dist/src/languages/go/visitor.js +78 -0
- package/dist/src/languages/markdown/driver.d.ts +19 -0
- package/dist/src/languages/markdown/driver.js +408 -0
- package/dist/src/languages/python/driver.d.ts +14 -0
- package/dist/src/languages/python/driver.js +372 -0
- package/dist/src/languages/python/emitter.d.ts +31 -0
- package/dist/src/languages/python/emitter.js +856 -0
- package/dist/src/languages/python/scaffolding.d.ts +33 -0
- package/dist/src/languages/python/scaffolding.js +279 -0
- package/dist/src/languages/python/test-emitter.d.ts +29 -0
- package/dist/src/languages/python/test-emitter.js +388 -0
- package/dist/src/languages/python/visitor.d.ts +14 -0
- package/dist/src/languages/python/visitor.js +65 -0
- package/dist/src/languages/rust/driver.d.ts +13 -0
- package/dist/src/languages/rust/driver.js +624 -0
- package/dist/src/languages/rust/emitter.d.ts +45 -0
- package/dist/src/languages/rust/emitter.js +1596 -0
- package/dist/src/languages/rust/visitor.d.ts +25 -0
- package/dist/src/languages/rust/visitor.js +153 -0
- package/dist/src/languages/typescript/driver.d.ts +8 -0
- package/dist/src/languages/typescript/driver.js +209 -0
- package/dist/src/languages/typescript/emitter.d.ts +42 -0
- package/dist/src/languages/typescript/emitter.js +904 -0
- package/dist/src/languages/typescript/scaffolding.d.ts +32 -0
- package/dist/src/languages/typescript/scaffolding.js +303 -0
- package/dist/src/languages/typescript/test-emitter.d.ts +23 -0
- package/dist/src/languages/typescript/test-emitter.js +204 -0
- package/dist/src/languages/typescript/visitor.d.ts +14 -0
- package/dist/src/languages/typescript/visitor.js +64 -0
- package/dist/src/lib.d.ts +33 -0
- package/dist/src/lib.js +101 -0
- package/dist/src/testing/index.d.ts +2 -0
- package/dist/src/testing/index.js +8 -0
- package/dist/src/testing/test-context.d.ts +63 -0
- package/dist/src/testing/test-context.js +355 -0
- package/fixtures/shapes/main.tsp +43 -0
- package/fixtures/tspconfig.yaml +13 -0
- package/package.json +76 -0
- package/src/lib/main.tsp +110 -0
|
@@ -0,0 +1,480 @@
|
|
|
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 { resolveFactoryExpr, collectExprTypeRefs, } from "./expansion.js";
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Public API
|
|
21
|
+
// ============================================================================
|
|
22
|
+
/**
|
|
23
|
+
* Lower a base TypeNode (and all its children) into a FileDecl.
|
|
24
|
+
*
|
|
25
|
+
* This is the main entry point for the lowering pass. It produces a complete
|
|
26
|
+
* FileDecl containing one or more TypeDecls (parent + children for polymorphic types).
|
|
27
|
+
*
|
|
28
|
+
* The result is fully language-agnostic — per-language emitters handle rendering.
|
|
29
|
+
*
|
|
30
|
+
* @param node - The base TypeNode (must not have a parent — i.e., `node.base === null`)
|
|
31
|
+
* @param registry - TypeRegistry for resolving type references
|
|
32
|
+
* @param polymorphicTypeNames - Set of type names that are polymorphic bases
|
|
33
|
+
*/
|
|
34
|
+
export function lowerFile(node, registry, polymorphicTypeNames) {
|
|
35
|
+
const polyNames = polymorphicTypeNames ?? collectPolymorphicTypeNames(node, registry);
|
|
36
|
+
// Lower all types in this file (parent + children)
|
|
37
|
+
const types = [
|
|
38
|
+
lowerType(node, registry, polyNames),
|
|
39
|
+
...node.childTypes.map(ct => lowerType(ct, registry, polyNames)),
|
|
40
|
+
];
|
|
41
|
+
// Resolve file-level imports
|
|
42
|
+
const imports = resolveImports(node, types, registry);
|
|
43
|
+
// Collect unique enum definitions from all fields across all types
|
|
44
|
+
const enums = collectEnums(types);
|
|
45
|
+
return {
|
|
46
|
+
typeName: node.typeName,
|
|
47
|
+
types,
|
|
48
|
+
imports,
|
|
49
|
+
containsAbstract: node.isAbstract || node.childTypes.some(c => c.isAbstract),
|
|
50
|
+
enums,
|
|
51
|
+
group: node.group,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Collect all polymorphic type names from a set of nodes.
|
|
56
|
+
*/
|
|
57
|
+
export function collectPolymorphicTypeNames(rootNode, registry) {
|
|
58
|
+
const names = new Set();
|
|
59
|
+
function walk(node) {
|
|
60
|
+
if (node.discriminator && node.childTypes.length > 0) {
|
|
61
|
+
names.add(node.typeName.name);
|
|
62
|
+
}
|
|
63
|
+
for (const prop of node.properties) {
|
|
64
|
+
if (prop.type)
|
|
65
|
+
walk(prop.type);
|
|
66
|
+
}
|
|
67
|
+
for (const child of node.childTypes) {
|
|
68
|
+
walk(child);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
walk(rootNode);
|
|
72
|
+
return names;
|
|
73
|
+
}
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Type lowering
|
|
76
|
+
// ============================================================================
|
|
77
|
+
/**
|
|
78
|
+
* Lower a single TypeNode into a TypeDecl.
|
|
79
|
+
*/
|
|
80
|
+
export function lowerType(node, registry, polymorphicTypeNames) {
|
|
81
|
+
const fields = node.properties.map(p => lowerField(p, polymorphicTypeNames));
|
|
82
|
+
const collectionHelpers = lowerCollectionHelpers(node);
|
|
83
|
+
const polymorphicDispatch = lowerPolymorphicDispatch(node);
|
|
84
|
+
const factories = lowerFactories(node, registry);
|
|
85
|
+
const coercionProperty = findCoercionProperty(node);
|
|
86
|
+
// Clear enum metadata from discriminator fields — they're handled by polymorphic dispatch
|
|
87
|
+
if (polymorphicDispatch) {
|
|
88
|
+
for (const field of fields) {
|
|
89
|
+
if (field.name === polymorphicDispatch.discriminatorField) {
|
|
90
|
+
field.enumName = null;
|
|
91
|
+
field.isOpenEnum = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Build load/save method specs
|
|
96
|
+
const load = lowerLoad(node, fields, polymorphicDispatch);
|
|
97
|
+
const save = lowerSave(node, fields);
|
|
98
|
+
return {
|
|
99
|
+
typeName: node.typeName,
|
|
100
|
+
base: node.base,
|
|
101
|
+
isAbstract: node.isAbstract,
|
|
102
|
+
isProtocol: node.isProtocol,
|
|
103
|
+
description: node.description,
|
|
104
|
+
fields,
|
|
105
|
+
coercionProperty,
|
|
106
|
+
load,
|
|
107
|
+
save,
|
|
108
|
+
factories,
|
|
109
|
+
collectionHelpers,
|
|
110
|
+
polymorphicDispatch,
|
|
111
|
+
methods: lowerMethods(node),
|
|
112
|
+
wire: lowerWire(node, fields),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Property classification — the core insight
|
|
117
|
+
// ============================================================================
|
|
118
|
+
/**
|
|
119
|
+
* Classify a property into one of 5 categories.
|
|
120
|
+
* This is the fundamental decision that drives ALL code generation.
|
|
121
|
+
*
|
|
122
|
+
* Decision tree:
|
|
123
|
+
* isDict → "dict"
|
|
124
|
+
* isCollection && isScalar → "collection_scalar"
|
|
125
|
+
* isCollection && !isScalar → "collection_complex"
|
|
126
|
+
* isScalar → "scalar"
|
|
127
|
+
* !isScalar → "complex"
|
|
128
|
+
*/
|
|
129
|
+
export function classifyProperty(prop, polymorphicTypeNames) {
|
|
130
|
+
if (prop.isDict) {
|
|
131
|
+
return { kind: "dict" };
|
|
132
|
+
}
|
|
133
|
+
if (prop.isCollection) {
|
|
134
|
+
if (prop.isScalar) {
|
|
135
|
+
return { kind: "collection_scalar", scalarType: prop.typeName.name };
|
|
136
|
+
}
|
|
137
|
+
return { kind: "collection_complex", typeName: prop.typeName.name };
|
|
138
|
+
}
|
|
139
|
+
if (prop.isScalar) {
|
|
140
|
+
return { kind: "scalar", scalarType: prop.typeName.name };
|
|
141
|
+
}
|
|
142
|
+
return { kind: "complex", typeName: prop.typeName.name };
|
|
143
|
+
}
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// Field lowering
|
|
146
|
+
// ============================================================================
|
|
147
|
+
/**
|
|
148
|
+
* Lower a PropertyNode into a FieldDecl.
|
|
149
|
+
*/
|
|
150
|
+
function lowerField(prop, polymorphicTypeNames) {
|
|
151
|
+
const knownAs = {};
|
|
152
|
+
for (const entry of prop.knownAs) {
|
|
153
|
+
knownAs[entry.provider] = entry.name;
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
name: prop.name,
|
|
157
|
+
typeName: prop.typeName,
|
|
158
|
+
category: classifyProperty(prop, polymorphicTypeNames),
|
|
159
|
+
isOptional: prop.isOptional,
|
|
160
|
+
defaultValue: prop.defaultValue,
|
|
161
|
+
allowedValues: prop.allowedValues,
|
|
162
|
+
enumName: prop.enumName,
|
|
163
|
+
isOpenEnum: prop.isOpenEnum,
|
|
164
|
+
description: prop.description,
|
|
165
|
+
knownAs,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// ============================================================================
|
|
169
|
+
// Load method lowering
|
|
170
|
+
// ============================================================================
|
|
171
|
+
/**
|
|
172
|
+
* Lower the load/deserialization method specification.
|
|
173
|
+
*
|
|
174
|
+
* Produces language-agnostic coercion and assignment data. Each emitter
|
|
175
|
+
* decides variable names, rendering, and expression formatting.
|
|
176
|
+
*/
|
|
177
|
+
function lowerLoad(node, fields, polymorphicDispatch) {
|
|
178
|
+
// Determine if this type has a discriminator with child variants
|
|
179
|
+
const hasDiscriminatorWithChildren = node.discriminator != null &&
|
|
180
|
+
(node.childTypes?.length ?? 0) > 0;
|
|
181
|
+
const coercions = (node.coercions || []).map(c => {
|
|
182
|
+
// Build structured assignments from the expansion dict
|
|
183
|
+
const assignments = Object.entries(c.expansion).map(([key, value]) => ({
|
|
184
|
+
fieldName: key,
|
|
185
|
+
isInput: value === "{value}",
|
|
186
|
+
literalValue: value === "{value}" ? undefined : String(value),
|
|
187
|
+
}));
|
|
188
|
+
// Determine if this coercion needs runtime dispatch:
|
|
189
|
+
// only when the discriminator field is set dynamically AND child types exist
|
|
190
|
+
const setsDiscriminator = node.discriminator != null &&
|
|
191
|
+
assignments.some(a => a.fieldName === node.discriminator && a.isInput);
|
|
192
|
+
const needsDispatch = setsDiscriminator && hasDiscriminatorWithChildren;
|
|
193
|
+
return {
|
|
194
|
+
scalarType: c.scalar,
|
|
195
|
+
assignments,
|
|
196
|
+
needsDispatch,
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
// Per-property load assignments
|
|
200
|
+
const assignments = fields.map(f => ({
|
|
201
|
+
sourceName: f.name,
|
|
202
|
+
fieldName: f.name,
|
|
203
|
+
category: f.category,
|
|
204
|
+
isOptional: f.isOptional,
|
|
205
|
+
parentTypeName: node.typeName.name,
|
|
206
|
+
enumName: f.enumName,
|
|
207
|
+
allowedValues: f.allowedValues,
|
|
208
|
+
defaultValue: f.defaultValue,
|
|
209
|
+
isOpenEnum: f.isOpenEnum,
|
|
210
|
+
}));
|
|
211
|
+
return {
|
|
212
|
+
coercions,
|
|
213
|
+
assignments,
|
|
214
|
+
hasPolymorphicDispatch: polymorphicDispatch !== null,
|
|
215
|
+
hasContextHooks: true, // All types support context hooks
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// ============================================================================
|
|
219
|
+
// Save method lowering
|
|
220
|
+
// ============================================================================
|
|
221
|
+
/**
|
|
222
|
+
* Lower the save/serialization method specification.
|
|
223
|
+
*/
|
|
224
|
+
function lowerSave(node, fields) {
|
|
225
|
+
const assignments = fields.map(f => ({
|
|
226
|
+
targetName: f.name,
|
|
227
|
+
fieldName: f.name,
|
|
228
|
+
category: f.category,
|
|
229
|
+
isOptional: f.isOptional,
|
|
230
|
+
parentTypeName: node.typeName.name,
|
|
231
|
+
enumName: f.enumName,
|
|
232
|
+
isOpenEnum: f.isOpenEnum,
|
|
233
|
+
}));
|
|
234
|
+
return {
|
|
235
|
+
assignments,
|
|
236
|
+
hasBase: node.base !== null,
|
|
237
|
+
hasContextHooks: true,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// Polymorphic dispatch lowering
|
|
242
|
+
// ============================================================================
|
|
243
|
+
/**
|
|
244
|
+
* Lower polymorphic dispatch specification from TypeNode.
|
|
245
|
+
* Returns null if the type is not polymorphic (no discriminator or no children).
|
|
246
|
+
*/
|
|
247
|
+
function lowerPolymorphicDispatch(node) {
|
|
248
|
+
const polyTypes = node.retrievePolymorphicTypes();
|
|
249
|
+
if (!polyTypes)
|
|
250
|
+
return null;
|
|
251
|
+
const variants = polyTypes.types.map((t) => ({
|
|
252
|
+
value: t.value,
|
|
253
|
+
typeName: t.instance.typeName,
|
|
254
|
+
}));
|
|
255
|
+
let defaultVariant = null;
|
|
256
|
+
if (polyTypes.default) {
|
|
257
|
+
const defaultNode = polyTypes.default.instance;
|
|
258
|
+
defaultVariant = {
|
|
259
|
+
typeName: defaultNode.typeName,
|
|
260
|
+
isSelfReference: defaultNode.typeName.name === node.typeName.name,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const baseDispatch = {
|
|
264
|
+
discriminatorField: node.discriminator,
|
|
265
|
+
variants,
|
|
266
|
+
defaultVariant,
|
|
267
|
+
isAbstract: node.isAbstract,
|
|
268
|
+
};
|
|
269
|
+
return baseDispatch;
|
|
270
|
+
}
|
|
271
|
+
// ============================================================================
|
|
272
|
+
// Collection helper lowering
|
|
273
|
+
// ============================================================================
|
|
274
|
+
/**
|
|
275
|
+
* Lower collection helpers for complex collection properties.
|
|
276
|
+
* These are properties like `tools: Tool[]` or `parts: ContentPart[]`
|
|
277
|
+
* that need dedicated load/save helper methods for dict↔array conversion.
|
|
278
|
+
*/
|
|
279
|
+
function lowerCollectionHelpers(node) {
|
|
280
|
+
return node.properties
|
|
281
|
+
.filter(p => p.isCollection && !p.isScalar && !p.isDict)
|
|
282
|
+
.map(p => ({
|
|
283
|
+
propertyName: p.name,
|
|
284
|
+
elementTypeName: p.typeName,
|
|
285
|
+
innerFields: p.type?.properties.filter(t => t.name !== "name").map(t => t.name) || [],
|
|
286
|
+
hasNameProperty: p.type?.properties.some(t => t.name === "name") || false,
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
// ============================================================================
|
|
290
|
+
// Factory method lowering
|
|
291
|
+
// ============================================================================
|
|
292
|
+
/**
|
|
293
|
+
* Lower factory methods. Resolves the Expr tree via the Expression IR
|
|
294
|
+
* but stores it as a typed Expr (not pre-rendered string).
|
|
295
|
+
* Emitters will visit the Expr with their own visitor for language-specific output.
|
|
296
|
+
*/
|
|
297
|
+
function lowerFactories(node, registry) {
|
|
298
|
+
if (!node.factories || node.factories.length === 0)
|
|
299
|
+
return [];
|
|
300
|
+
return node.factories.map(f => {
|
|
301
|
+
const expr = resolveFactoryExpr(f.sets, f.params, node, registry);
|
|
302
|
+
return {
|
|
303
|
+
name: f.name,
|
|
304
|
+
params: f.params,
|
|
305
|
+
body: expr,
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
// ============================================================================
|
|
310
|
+
// Method stub lowering
|
|
311
|
+
// ============================================================================
|
|
312
|
+
function lowerMethods(node) {
|
|
313
|
+
return (node.methods || []).map(m => ({
|
|
314
|
+
name: m.name,
|
|
315
|
+
returns: m.returns,
|
|
316
|
+
description: m.description,
|
|
317
|
+
params: m.params || {},
|
|
318
|
+
optional: m.optional ?? false,
|
|
319
|
+
sync: m.sync ?? false,
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
// ============================================================================
|
|
323
|
+
// Wire conversion lowering
|
|
324
|
+
// ============================================================================
|
|
325
|
+
/**
|
|
326
|
+
* Lower wire conversion data from knownAs mappings on fields.
|
|
327
|
+
* Returns null if no field has wire mappings.
|
|
328
|
+
*/
|
|
329
|
+
function lowerWire(node, fields) {
|
|
330
|
+
const providerSet = new Set();
|
|
331
|
+
const mappings = [];
|
|
332
|
+
for (const field of fields) {
|
|
333
|
+
if (Object.keys(field.knownAs).length > 0) {
|
|
334
|
+
for (const provider of Object.keys(field.knownAs)) {
|
|
335
|
+
providerSet.add(provider);
|
|
336
|
+
}
|
|
337
|
+
mappings.push({
|
|
338
|
+
fieldName: field.name,
|
|
339
|
+
category: field.category,
|
|
340
|
+
isOptional: field.isOptional,
|
|
341
|
+
parentTypeName: node.typeName.name,
|
|
342
|
+
wireNames: field.knownAs,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (mappings.length === 0)
|
|
347
|
+
return null;
|
|
348
|
+
return {
|
|
349
|
+
providers: Array.from(providerSet).sort(),
|
|
350
|
+
mappings,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
// ============================================================================
|
|
354
|
+
// Coercion property detection
|
|
355
|
+
// ============================================================================
|
|
356
|
+
/**
|
|
357
|
+
* Find the property that receives "{value}" in coercion expansions.
|
|
358
|
+
*/
|
|
359
|
+
function findCoercionProperty(node) {
|
|
360
|
+
if (!node.coercions || node.coercions.length === 0)
|
|
361
|
+
return null;
|
|
362
|
+
for (const alt of node.coercions) {
|
|
363
|
+
for (const [key, value] of Object.entries(alt.expansion)) {
|
|
364
|
+
if (value === "{value}") {
|
|
365
|
+
return key;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
// ============================================================================
|
|
372
|
+
// Enum collection
|
|
373
|
+
// ============================================================================
|
|
374
|
+
/**
|
|
375
|
+
* Collect unique enum definitions from all fields across all types in a file.
|
|
376
|
+
* Deduplicates by enum name — same-named enums with the same values share one definition.
|
|
377
|
+
*/
|
|
378
|
+
function collectEnums(types) {
|
|
379
|
+
const seen = new Map();
|
|
380
|
+
// Collect discriminator field names to skip — these are handled by polymorphic dispatch
|
|
381
|
+
const discriminatorFields = new Set();
|
|
382
|
+
for (const type of types) {
|
|
383
|
+
if (type.polymorphicDispatch) {
|
|
384
|
+
discriminatorFields.add(type.polymorphicDispatch.discriminatorField);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (const type of types) {
|
|
388
|
+
for (const field of type.fields) {
|
|
389
|
+
// Skip discriminator fields — they use the polymorphic Kind enum instead
|
|
390
|
+
if (discriminatorFields.has(field.name))
|
|
391
|
+
continue;
|
|
392
|
+
if (field.enumName && field.allowedValues.length > 0 && !seen.has(field.enumName)) {
|
|
393
|
+
seen.set(field.enumName, {
|
|
394
|
+
name: field.enumName,
|
|
395
|
+
values: field.allowedValues,
|
|
396
|
+
isOpen: field.isOpenEnum,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return Array.from(seen.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
402
|
+
}
|
|
403
|
+
// ============================================================================
|
|
404
|
+
// Import resolution
|
|
405
|
+
// ============================================================================
|
|
406
|
+
/**
|
|
407
|
+
* Resolve file-level imports from type references.
|
|
408
|
+
* Groups imports by module: each module maps to the symbols imported from it.
|
|
409
|
+
*/
|
|
410
|
+
function resolveImports(rootNode, types, registry) {
|
|
411
|
+
// Types defined in this file (excluded from imports)
|
|
412
|
+
const definedInFile = new Set([
|
|
413
|
+
rootNode.typeName.name,
|
|
414
|
+
...rootNode.childTypes.map(c => c.typeName.name),
|
|
415
|
+
]);
|
|
416
|
+
const importMap = new Map();
|
|
417
|
+
const addImport = (typeName, module) => {
|
|
418
|
+
if (definedInFile.has(typeName))
|
|
419
|
+
return;
|
|
420
|
+
// Determine which module this type lives in
|
|
421
|
+
const refNode = registry.get(typeName);
|
|
422
|
+
const mod = module ?? (refNode?.base ? refNode.base.name : typeName);
|
|
423
|
+
if (!importMap.has(mod))
|
|
424
|
+
importMap.set(mod, new Set());
|
|
425
|
+
importMap.get(mod).add(typeName);
|
|
426
|
+
};
|
|
427
|
+
// Collect import refs from all properties across all types in this file
|
|
428
|
+
for (const type of types) {
|
|
429
|
+
for (const field of type.fields) {
|
|
430
|
+
// Only import non-scalar, non-dict types
|
|
431
|
+
if (field.category.kind === "complex" || field.category.kind === "collection_complex") {
|
|
432
|
+
addImport(field.typeName.name);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Factory-referenced imports (may include child types like TextPart)
|
|
436
|
+
for (const factory of type.factories) {
|
|
437
|
+
for (const ref of collectExprTypeRefs(factory.body)) {
|
|
438
|
+
if (definedInFile.has(ref.name))
|
|
439
|
+
continue;
|
|
440
|
+
addImport(ref.name);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// Protocol method type references (param types and return types)
|
|
444
|
+
for (const method of type.methods) {
|
|
445
|
+
for (const typeName of extractMethodTypeRefs(method)) {
|
|
446
|
+
addImport(typeName);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return Array.from(importMap.entries())
|
|
451
|
+
.map(([module, names]) => {
|
|
452
|
+
// Look up the group of the module's root node in the registry
|
|
453
|
+
const modNode = registry.get(module);
|
|
454
|
+
const group = modNode?.group ?? "";
|
|
455
|
+
return { module, names: Array.from(names).sort(), group };
|
|
456
|
+
})
|
|
457
|
+
.sort((a, b) => a.module.localeCompare(b.module));
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Extract type names referenced in method parameter types and return type.
|
|
461
|
+
* Handles formats like "Prompty", "Message[]", "Record<unknown>", "string", "unknown".
|
|
462
|
+
*/
|
|
463
|
+
function extractMethodTypeRefs(method) {
|
|
464
|
+
const SCALARS = new Set(["string", "int32", "float32", "float64", "boolean", "unknown"]);
|
|
465
|
+
const refs = [];
|
|
466
|
+
const extract = (typeStr) => {
|
|
467
|
+
// Strip nullable suffix and array suffix: "string?" → "string", "Message[]" → "Message"
|
|
468
|
+
const base = typeStr.replace(/\?$/, "").replace(/\[\]$/, "");
|
|
469
|
+
// Skip scalars, Record<>, and generic types
|
|
470
|
+
if (SCALARS.has(base) || base.startsWith("Record<"))
|
|
471
|
+
return;
|
|
472
|
+
refs.push(base);
|
|
473
|
+
};
|
|
474
|
+
extract(method.returns);
|
|
475
|
+
for (const pType of Object.values(method.params)) {
|
|
476
|
+
extract(pType);
|
|
477
|
+
}
|
|
478
|
+
return refs;
|
|
479
|
+
}
|
|
480
|
+
//# sourceMappingURL=lower.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const scalarValue: Record<string, string>;
|
|
2
|
+
/**
|
|
3
|
+
* Convert PascalCase to snake_case.
|
|
4
|
+
* Used for idiomatic file naming in Python and Go.
|
|
5
|
+
*/
|
|
6
|
+
export declare function toSnakeCase(str: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Convert PascalCase to kebab-case.
|
|
9
|
+
* Used for idiomatic file naming in TypeScript.
|
|
10
|
+
*/
|
|
11
|
+
export declare function toKebabCase(str: string): string;
|
|
12
|
+
export declare const getCombinations: (arrays: any[][]) => any[][];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const scalarValue = {
|
|
2
|
+
"boolean": 'False',
|
|
3
|
+
"float": "3.14",
|
|
4
|
+
"float32": "3.14",
|
|
5
|
+
"float64": "3.14",
|
|
6
|
+
"number": "3.14",
|
|
7
|
+
"int32": "3",
|
|
8
|
+
"int64": "3",
|
|
9
|
+
"integer": "3",
|
|
10
|
+
"string": '"example"',
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Convert PascalCase to snake_case.
|
|
14
|
+
* Used for idiomatic file naming in Python and Go.
|
|
15
|
+
*/
|
|
16
|
+
export function toSnakeCase(str) {
|
|
17
|
+
return str
|
|
18
|
+
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
19
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
20
|
+
.toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Convert PascalCase to kebab-case.
|
|
24
|
+
* Used for idiomatic file naming in TypeScript.
|
|
25
|
+
*/
|
|
26
|
+
export function toKebabCase(str) {
|
|
27
|
+
return str
|
|
28
|
+
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
29
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
|
30
|
+
.toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
export const getCombinations = (arrays) => {
|
|
33
|
+
if (arrays.length === 0)
|
|
34
|
+
return [[]];
|
|
35
|
+
const [firstArray, ...restArrays] = arrays;
|
|
36
|
+
const combinationsOfRest = getCombinations(restArrays);
|
|
37
|
+
return firstArray.flatMap(item => combinationsOfRest.map(combination => [item, ...combination]));
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=utilities.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expression visitor interface and shared helpers.
|
|
3
|
+
*
|
|
4
|
+
* The ExprVisitor interface is the contract between the shared IR and
|
|
5
|
+
* per-language code emitters. Each language implements its own visitor
|
|
6
|
+
* in `languages/<lang>/visitor.ts`.
|
|
7
|
+
*/
|
|
8
|
+
import { Expr, Construct, VariantConstruct, TypeRegistry } from "./expansion.js";
|
|
9
|
+
export interface ExprVisitor {
|
|
10
|
+
visitExpr(expr: Expr): string;
|
|
11
|
+
/** Optional type registry for wire format generation and type-aware codegen. */
|
|
12
|
+
registry?: TypeRegistry;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Render a Construct expression's fields as a plain object literal.
|
|
16
|
+
* Used by TypeScript, Python, C#, Go coercions where the template wraps
|
|
17
|
+
* the literal in language-specific loading logic (e.g., `TypeName.load({...}, ctx)`).
|
|
18
|
+
*
|
|
19
|
+
* @param expr - A Construct or VariantConstruct expression
|
|
20
|
+
* @param visitor - The language-specific visitor for rendering field values
|
|
21
|
+
* @param format - How to format the fields:
|
|
22
|
+
* - "js": `{ field: value }` (TypeScript, Go)
|
|
23
|
+
* - "py": `{"field": value}` (Python)
|
|
24
|
+
*/
|
|
25
|
+
export declare function renderObjectLiteral(expr: Construct | VariantConstruct, visitor: ExprVisitor, format?: "js" | "py"): string;
|
|
26
|
+
/** camelCase → PascalCase (first char uppercase). */
|
|
27
|
+
export declare function toPascalCase(name: string): string;
|
|
28
|
+
/** Exhaustive check helper — TypeScript enforces all Expr.kind arms are handled. */
|
|
29
|
+
export declare function assertNever(x: never): never;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expression visitor interface and shared helpers.
|
|
3
|
+
*
|
|
4
|
+
* The ExprVisitor interface is the contract between the shared IR and
|
|
5
|
+
* per-language code emitters. Each language implements its own visitor
|
|
6
|
+
* in `languages/<lang>/visitor.ts`.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Render a Construct expression's fields as a plain object literal.
|
|
10
|
+
* Used by TypeScript, Python, C#, Go coercions where the template wraps
|
|
11
|
+
* the literal in language-specific loading logic (e.g., `TypeName.load({...}, ctx)`).
|
|
12
|
+
*
|
|
13
|
+
* @param expr - A Construct or VariantConstruct expression
|
|
14
|
+
* @param visitor - The language-specific visitor for rendering field values
|
|
15
|
+
* @param format - How to format the fields:
|
|
16
|
+
* - "js": `{ field: value }` (TypeScript, Go)
|
|
17
|
+
* - "py": `{"field": value}` (Python)
|
|
18
|
+
*/
|
|
19
|
+
export function renderObjectLiteral(expr, visitor, format = "js") {
|
|
20
|
+
const fields = expr.fields.map(f => {
|
|
21
|
+
const val = visitor.visitExpr(f.value);
|
|
22
|
+
switch (format) {
|
|
23
|
+
case "js":
|
|
24
|
+
return `${f.propertyName}: ${val}`;
|
|
25
|
+
case "py":
|
|
26
|
+
return `"${f.propertyName}": ${val}`;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return `{ ${fields.join(", ")} }`;
|
|
30
|
+
}
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Naming helpers — reusable across visitors
|
|
33
|
+
// ============================================================================
|
|
34
|
+
/** camelCase → PascalCase (first char uppercase). */
|
|
35
|
+
export function toPascalCase(name) {
|
|
36
|
+
// Handle snake_case first
|
|
37
|
+
if (name.includes("_")) {
|
|
38
|
+
return name
|
|
39
|
+
.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
40
|
+
.replace(/^(.)/, (_, char) => char.toUpperCase());
|
|
41
|
+
}
|
|
42
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
43
|
+
}
|
|
44
|
+
/** Exhaustive check helper — TypeScript enforces all Expr.kind arms are handled. */
|
|
45
|
+
export function assertNever(x) {
|
|
46
|
+
throw new Error(`Unexpected expression kind: ${x.kind}`);
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=visitor.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { EmitContext } from "@typespec/compiler";
|
|
2
|
+
import { EmitTarget, TypraEmitterOptions } from "../../lib.js";
|
|
3
|
+
import { TypeNode } from "../../ir/ast.js";
|
|
4
|
+
import { GeneratorOptions } from "../../emitter.js";
|
|
5
|
+
export declare const generateCsharp: (context: EmitContext<TypraEmitterOptions>, node: TypeNode, emitTarget: EmitTarget, options?: GeneratorOptions) => Promise<void>;
|