skir-rust-gen 0.1.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/LICENSE +21 -0
- package/README.md +0 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +611 -0
- package/dist/index.js.map +1 -0
- package/dist/keyed_array_context.d.ts +23 -0
- package/dist/keyed_array_context.d.ts.map +1 -0
- package/dist/keyed_array_context.js +91 -0
- package/dist/keyed_array_context.js.map +1 -0
- package/dist/naming.d.ts +24 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/naming.js +109 -0
- package/dist/naming.js.map +1 -0
- package/dist/rust_module_spec.d.ts +17 -0
- package/dist/rust_module_spec.d.ts.map +1 -0
- package/dist/rust_module_spec.js +51 -0
- package/dist/rust_module_spec.js.map +1 -0
- package/dist/type_speller.d.ts +16 -0
- package/dist/type_speller.d.ts.map +1 -0
- package/dist/type_speller.js +185 -0
- package/dist/type_speller.js.map +1 -0
- package/package.json +61 -0
- package/src/index.ts +781 -0
- package/src/keyed_array_context.ts +135 -0
- package/src/naming.ts +119 -0
- package/src/rust_module_spec.ts +72 -0
- package/src/type_speller.ts +195 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CodeGenerator,
|
|
3
|
+
type Constant,
|
|
4
|
+
convertCase,
|
|
5
|
+
type Doc,
|
|
6
|
+
type Field,
|
|
7
|
+
type Method,
|
|
8
|
+
type RecordKey,
|
|
9
|
+
type RecordLocation,
|
|
10
|
+
type ResolvedType,
|
|
11
|
+
} from "skir-internal";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { KeyedArrayContext } from "./keyed_array_context.js";
|
|
14
|
+
import {
|
|
15
|
+
getTypeName,
|
|
16
|
+
isUpperCasedKeyword,
|
|
17
|
+
Namer,
|
|
18
|
+
toStructFieldName as toRustFieldName,
|
|
19
|
+
} from "./naming.js";
|
|
20
|
+
import { collectRustModuleSpecs, RustModuleSpec } from "./rust_module_spec.js";
|
|
21
|
+
import { skirDefaultIsRustDefault, TypeSpeller } from "./type_speller.js";
|
|
22
|
+
|
|
23
|
+
const Config = z.strictObject({});
|
|
24
|
+
|
|
25
|
+
type Config = z.infer<typeof Config>;
|
|
26
|
+
|
|
27
|
+
class RustCodeGenerator implements CodeGenerator<Config> {
|
|
28
|
+
readonly id = "skir-rust-gen";
|
|
29
|
+
readonly configType = Config;
|
|
30
|
+
|
|
31
|
+
generateCode(input: CodeGenerator.Input<Config>): CodeGenerator.Output {
|
|
32
|
+
const { recordMap, config } = input;
|
|
33
|
+
const keyedArrayContext = new KeyedArrayContext(input.modules);
|
|
34
|
+
const rustModuleSpecs = collectRustModuleSpecs(input.modules);
|
|
35
|
+
const outputFiles = rustModuleSpecs.map((moduleSpec) => ({
|
|
36
|
+
path: moduleSpec.path,
|
|
37
|
+
code: new RustSourceFileGenerator(
|
|
38
|
+
moduleSpec,
|
|
39
|
+
recordMap,
|
|
40
|
+
keyedArrayContext,
|
|
41
|
+
config,
|
|
42
|
+
).generate(),
|
|
43
|
+
}));
|
|
44
|
+
return { files: outputFiles };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Generates the code for one Rust file.
|
|
49
|
+
class RustSourceFileGenerator {
|
|
50
|
+
constructor(
|
|
51
|
+
private readonly moduleSpec: RustModuleSpec,
|
|
52
|
+
recordMap: ReadonlyMap<RecordKey, RecordLocation>,
|
|
53
|
+
private readonly keyedArrayContext: KeyedArrayContext,
|
|
54
|
+
private readonly config: Config,
|
|
55
|
+
) {
|
|
56
|
+
this.namer = new Namer(moduleSpec.skirModule);
|
|
57
|
+
this.typeSpeller = new TypeSpeller(recordMap, this.namer);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
generate(): string {
|
|
61
|
+
// const packageName = skirModulePathToSkiroutPath(this.inModule.path);
|
|
62
|
+
|
|
63
|
+
// http://patorjk.com/software/taag/#f=Doom&t=Do%20not%20edit
|
|
64
|
+
this.push(
|
|
65
|
+
`#![allow(nonstandard_style)]
|
|
66
|
+
|
|
67
|
+
// ______ _ _ _ _
|
|
68
|
+
// | _ \\ | | | |(_)| |
|
|
69
|
+
// | | | | ___ _ __ ___ | |_ ___ __| | _ | |_
|
|
70
|
+
// | | | | / _ \\ | '_ \\ / _ \\ | __| / _ \\ / _\` || || __|
|
|
71
|
+
// | |/ / | (_) | | | | || (_) || |_ | __/| (_| || || |_
|
|
72
|
+
// |___/ \\___/ |_| |_| \\___/ \\__| \\___| \\__,_||_| \\__|
|
|
73
|
+
//
|
|
74
|
+
// Generated by skir-rust-gen
|
|
75
|
+
// Home: https://github.com/gepheum/skir-rust-gen
|
|
76
|
+
//
|
|
77
|
+
// To install the Skir client library, run:
|
|
78
|
+
// cargo add skir-rust-client
|
|
79
|
+
`,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
for (const childModuleName of this.moduleSpec.childModuleNames) {
|
|
83
|
+
this.push(`pub mod ${childModuleName};\n`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.push("\n");
|
|
87
|
+
|
|
88
|
+
const skirModule = this.moduleSpec.skirModule;
|
|
89
|
+
if (skirModule) {
|
|
90
|
+
const { constants, methods, records } = skirModule;
|
|
91
|
+
|
|
92
|
+
if (records.length) {
|
|
93
|
+
for (const record of records) {
|
|
94
|
+
const { recordType } = record.record;
|
|
95
|
+
if (recordType === "struct") {
|
|
96
|
+
this.writeStruct(record);
|
|
97
|
+
} else {
|
|
98
|
+
this.writeEnum(record);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
this.writeInitializeModuleSerializersFn(records);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (methods.length) {
|
|
105
|
+
this.pushSeparator("Methods");
|
|
106
|
+
for (const method of methods) {
|
|
107
|
+
this.writeMethod(method);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (constants.length) {
|
|
112
|
+
this.pushSeparator("Constants");
|
|
113
|
+
for (const constant of constants) {
|
|
114
|
+
this.writeConstant(constant);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return this.joinLinesAndFixFormatting();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private writeStruct(struct: RecordLocation): void {
|
|
123
|
+
const { namer, typeSpeller } = this;
|
|
124
|
+
|
|
125
|
+
this.pushSeparator(
|
|
126
|
+
"struct ".concat(
|
|
127
|
+
struct.recordAncestors.map((r) => r.name.text).join("."),
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
const typeName = getTypeName(struct);
|
|
131
|
+
const allFieldsUseRustDefault = struct.record.fields.every(
|
|
132
|
+
(f) => f.isRecursive === "hard" || skirDefaultIsRustDefault(f.type!),
|
|
133
|
+
);
|
|
134
|
+
const deriveList = allFieldsUseRustDefault
|
|
135
|
+
? `${namer.clone}, ${namer.debug}, ${namer.partialEq}, ${namer.default}`
|
|
136
|
+
: `${namer.clone}, ${namer.debug}, ${namer.partialEq}`;
|
|
137
|
+
this.push(commentify(docToCommentText(struct.record.doc)));
|
|
138
|
+
this.push(`#[derive(${deriveList})]\n`);
|
|
139
|
+
this.push(`pub struct ${typeName} {\n`);
|
|
140
|
+
for (const field of struct.record.fields) {
|
|
141
|
+
const fieldType = typeSpeller.getRustType(field.type!);
|
|
142
|
+
const fieldName = toRustFieldName(field.name.text);
|
|
143
|
+
if (field.isRecursive === "hard") {
|
|
144
|
+
const recFieldName = `_${field.name.text}_rec`;
|
|
145
|
+
const boxedType = `${namer.option}<${namer.box}<${fieldType}>>`;
|
|
146
|
+
this.push(
|
|
147
|
+
commentify([
|
|
148
|
+
docToCommentText(field.doc),
|
|
149
|
+
"Recursive field. Noxed and optional to avoid infinite size.",
|
|
150
|
+
"None is equivalent to the default value.",
|
|
151
|
+
`Use \`${fieldName}()\` to read this field without having to handle the Option,`,
|
|
152
|
+
"but be careful not to call it from a recursive function as it may cause",
|
|
153
|
+
"infinite recursion.",
|
|
154
|
+
]),
|
|
155
|
+
);
|
|
156
|
+
this.push(`pub ${recFieldName}: ${boxedType},\n`);
|
|
157
|
+
} else {
|
|
158
|
+
this.push(commentify(docToCommentText(field.doc)));
|
|
159
|
+
this.push(`pub ${fieldName}: ${fieldType},\n`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this.push(commentify("Set this to None when you're creating a struct."));
|
|
163
|
+
this.push(
|
|
164
|
+
`pub _unrecognized: ${namer.option}<crate::skir_client::UnrecognizedFields<${typeName}>>,\n`,
|
|
165
|
+
);
|
|
166
|
+
this.push("}\n\n");
|
|
167
|
+
|
|
168
|
+
// impl block: default_ref() + getters for hard-recursive fields
|
|
169
|
+
const hardRecursiveFields = struct.record.fields.filter(
|
|
170
|
+
(f) => f.isRecursive === "hard",
|
|
171
|
+
);
|
|
172
|
+
this.push(`impl ${typeName} {\n`);
|
|
173
|
+
|
|
174
|
+
// default_ref()
|
|
175
|
+
this.push(`pub fn default_ref() -> &'static ${typeName} {\n`);
|
|
176
|
+
this.push(
|
|
177
|
+
`static D: std::sync::LazyLock<${typeName}> = std::sync::LazyLock::new(${typeName}::default);\n`,
|
|
178
|
+
);
|
|
179
|
+
this.push("&D\n");
|
|
180
|
+
this.push("}\n");
|
|
181
|
+
|
|
182
|
+
// Getters for hard-recursive fields
|
|
183
|
+
for (const field of hardRecursiveFields) {
|
|
184
|
+
const getterName = toRustFieldName(field.name.text);
|
|
185
|
+
const fieldType = typeSpeller.getRustType(field.type!);
|
|
186
|
+
this.push(commentify(docToCommentText(field.doc)));
|
|
187
|
+
this.push(`pub fn ${getterName}(&self) -> &${fieldType} {\n`);
|
|
188
|
+
this.push(`match &self._${field.name.text}_rec {\n`);
|
|
189
|
+
this.push(`Some(boxed) => boxed.as_ref(),\n`);
|
|
190
|
+
this.push(`None => ${fieldType}::default_ref(),\n`);
|
|
191
|
+
this.push("}\n");
|
|
192
|
+
this.push("}\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.push("}\n\n");
|
|
196
|
+
|
|
197
|
+
// Manual Default impl — only needed when #[derive(Default)] can't be used.
|
|
198
|
+
if (!allFieldsUseRustDefault) {
|
|
199
|
+
this.push(`impl ${namer.default} for ${typeName} {\n`);
|
|
200
|
+
this.push(`fn default() -> Self {\n`);
|
|
201
|
+
this.push(`${typeName} {\n`);
|
|
202
|
+
for (const field of struct.record.fields) {
|
|
203
|
+
if (field.isRecursive === "hard") {
|
|
204
|
+
this.push(`_${field.name.text}_rec: None,\n`);
|
|
205
|
+
} else {
|
|
206
|
+
const fieldName = toRustFieldName(field.name.text);
|
|
207
|
+
const defaultExpr = typeSpeller.getDefaultExpr(field.type!);
|
|
208
|
+
this.push(`${fieldName}: ${defaultExpr},\n`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
this.push(`_unrecognized: None,\n`);
|
|
212
|
+
this.push("}\n");
|
|
213
|
+
this.push("}\n");
|
|
214
|
+
this.push("}\n\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Write a KeyedVecSpec impl for each keyed array that has this struct as item type.
|
|
218
|
+
for (const keySpec of this.keyedArrayContext.getKeySpecsForItemStruct(
|
|
219
|
+
struct.record,
|
|
220
|
+
typeSpeller,
|
|
221
|
+
)) {
|
|
222
|
+
this.push(`pub struct ${keySpec.rustSpecName};\n\n`);
|
|
223
|
+
this.push(
|
|
224
|
+
`impl crate::skir_client::KeyedVecSpec for ${keySpec.rustSpecName} {\n`,
|
|
225
|
+
);
|
|
226
|
+
this.push(`type Item = ${typeName};\n`);
|
|
227
|
+
this.push(`type StorageKey = ${keySpec.rustKeyType};\n`);
|
|
228
|
+
this.push(
|
|
229
|
+
`type Lookup = crate::skir_client::internal::${keySpec.lookupImpl};\n`,
|
|
230
|
+
);
|
|
231
|
+
this.push(`fn get_key(item: &${typeName}) -> ${keySpec.rustKeyType} {\n`);
|
|
232
|
+
this.push(`${keySpec.rustKeyExpr}\n`);
|
|
233
|
+
this.push("}\n");
|
|
234
|
+
this.push(`fn key_extractor() -> &'static str {\n`);
|
|
235
|
+
this.push(`"${keySpec.keyExtractor}"\n`);
|
|
236
|
+
this.push("}\n");
|
|
237
|
+
this.push(`fn default_item() -> &'static ${typeName} {\n`);
|
|
238
|
+
this.push(`${typeName}::default_ref()\n`);
|
|
239
|
+
this.push("}\n");
|
|
240
|
+
this.push("}\n\n");
|
|
241
|
+
}
|
|
242
|
+
const structModulePath = this.moduleSpec.skirModule!.path;
|
|
243
|
+
const structQualifiedName = struct.recordAncestors
|
|
244
|
+
.map((r) => r.name.text)
|
|
245
|
+
.join(".");
|
|
246
|
+
this.push(`impl ${typeName} {\n`);
|
|
247
|
+
this.push(
|
|
248
|
+
`fn _adapter() -> &'static crate::skir_client::internal::StructAdapter<${typeName}> {\n`,
|
|
249
|
+
);
|
|
250
|
+
this.push(
|
|
251
|
+
`static ADAPTER: std::sync::LazyLock<crate::skir_client::internal::StructAdapter<${typeName}>> =\n`,
|
|
252
|
+
);
|
|
253
|
+
this.push(`std::sync::LazyLock::new(|| {\n`);
|
|
254
|
+
this.push(`crate::skir_client::internal::StructAdapter::new(\n`);
|
|
255
|
+
this.push(`"${structModulePath}",\n`);
|
|
256
|
+
this.push(`"${structQualifiedName}",\n`);
|
|
257
|
+
this.push(`${toRustStringLiteral(docToCommentText(struct.record.doc))},\n`);
|
|
258
|
+
this.push(`|x: &${typeName}| &x._unrecognized,\n`);
|
|
259
|
+
this.push(`|x: &mut ${typeName}, u| x._unrecognized = u,\n`);
|
|
260
|
+
this.push(`)\n`);
|
|
261
|
+
this.push(`});\n`);
|
|
262
|
+
this.push(`&*ADAPTER\n`);
|
|
263
|
+
this.push("}\n");
|
|
264
|
+
this.push(
|
|
265
|
+
`pub fn serializer() -> crate::skir_client::Serializer<${typeName}> {\n`,
|
|
266
|
+
);
|
|
267
|
+
this.push(`initialize_module_serializers();\n`);
|
|
268
|
+
this.push(
|
|
269
|
+
`crate::skir_client::internal::struct_serializer_from_static(${typeName}::_adapter())\n`,
|
|
270
|
+
);
|
|
271
|
+
this.push("}\n");
|
|
272
|
+
this.push("}\n\n");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private writeEnum(record: RecordLocation): void {
|
|
276
|
+
const { namer, typeSpeller } = this;
|
|
277
|
+
this.pushSeparator(
|
|
278
|
+
"enum ".concat(record.recordAncestors.map((r) => r.name.text).join(".")),
|
|
279
|
+
);
|
|
280
|
+
const typeName = getTypeName(record);
|
|
281
|
+
this.push(commentify(docToCommentText(record.record.doc)));
|
|
282
|
+
this.push(
|
|
283
|
+
`#[derive(${namer.debug}, ${namer.clone}, ${namer.partialEq})]\n`,
|
|
284
|
+
);
|
|
285
|
+
this.push(`pub enum ${typeName} {\n`);
|
|
286
|
+
this.push(
|
|
287
|
+
`Unknown(${namer.option}<crate::skir_client::UnrecognizedVariant<${typeName}>>),\n`,
|
|
288
|
+
);
|
|
289
|
+
const variantNamesNeedSuffix = doVariantNamesNeedSuffix(
|
|
290
|
+
record.record.fields,
|
|
291
|
+
);
|
|
292
|
+
for (const variant of record.record.fields) {
|
|
293
|
+
const variantName = convertCase(variant.name.text, "UpperCamel").concat(
|
|
294
|
+
variantNamesNeedSuffix ? (variant.type ? "Wrapper" : "Const") : "",
|
|
295
|
+
);
|
|
296
|
+
this.push(commentify(docToCommentText(variant.doc)));
|
|
297
|
+
if (variant.type) {
|
|
298
|
+
const variantType = variant.type!;
|
|
299
|
+
let valueRustType = typeSpeller.getRustType(variantType);
|
|
300
|
+
if (doesWrapperVariantNeedBoxing(variantType)) {
|
|
301
|
+
valueRustType = `${namer.box}<${valueRustType}>`;
|
|
302
|
+
}
|
|
303
|
+
this.push(`${variantName}(${valueRustType}),\n`);
|
|
304
|
+
} else {
|
|
305
|
+
this.push(`${variantName},\n`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
this.push("}\n\n");
|
|
309
|
+
|
|
310
|
+
this.push(`impl ${namer.default} for ${typeName} {\n`);
|
|
311
|
+
this.push(`fn default() -> Self {\n`);
|
|
312
|
+
this.push(`${typeName}::Unknown(None)\n`);
|
|
313
|
+
this.push("}\n");
|
|
314
|
+
this.push("}\n\n");
|
|
315
|
+
if (this.keyedArrayContext.isEnumUsedAsKey(record.record)) {
|
|
316
|
+
// Write the _kind enum.
|
|
317
|
+
this.push(
|
|
318
|
+
`#[derive(${namer.clone}, ${namer.copy}, ${namer.debug}, ${namer.eq}, ${namer.hash}, ${namer.partialEq})]\n`,
|
|
319
|
+
);
|
|
320
|
+
this.push(`pub enum ${typeName}_kind {\n`);
|
|
321
|
+
this.push("Unknown,\n");
|
|
322
|
+
for (const variant of record.record.fields) {
|
|
323
|
+
const variantName = convertCase(variant.name.text, "UpperCamel").concat(
|
|
324
|
+
variantNamesNeedSuffix ? (variant.type ? "Wrapper" : "Const") : "",
|
|
325
|
+
);
|
|
326
|
+
this.push(`${variantName},\n`);
|
|
327
|
+
}
|
|
328
|
+
this.push("}\n\n");
|
|
329
|
+
|
|
330
|
+
// Write the kind() getter on the main enum.
|
|
331
|
+
this.push(`impl ${typeName} {\n`);
|
|
332
|
+
this.push(`pub fn kind(&self) -> ${typeName}_kind {\n`);
|
|
333
|
+
this.push(`match self {\n`);
|
|
334
|
+
this.push(`${typeName}::Unknown(_) => ${typeName}_kind::Unknown,\n`);
|
|
335
|
+
for (const variant of record.record.fields) {
|
|
336
|
+
const variantName = convertCase(variant.name.text, "UpperCamel").concat(
|
|
337
|
+
variantNamesNeedSuffix ? (variant.type ? "Wrapper" : "Const") : "",
|
|
338
|
+
);
|
|
339
|
+
if (variant.type) {
|
|
340
|
+
this.push(
|
|
341
|
+
`${typeName}::${variantName}(_) => ${typeName}_kind::${variantName},\n`,
|
|
342
|
+
);
|
|
343
|
+
} else {
|
|
344
|
+
this.push(
|
|
345
|
+
`${typeName}::${variantName} => ${typeName}_kind::${variantName},\n`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
this.push("}\n");
|
|
350
|
+
this.push("}\n");
|
|
351
|
+
this.push("}\n\n");
|
|
352
|
+
}
|
|
353
|
+
const enumModulePath = this.moduleSpec.skirModule!.path;
|
|
354
|
+
const enumQualifiedName = record.recordAncestors
|
|
355
|
+
.map((r) => r.name.text)
|
|
356
|
+
.join(".");
|
|
357
|
+
this.push(`impl ${typeName} {\n`);
|
|
358
|
+
this.push(
|
|
359
|
+
`fn _adapter() -> &'static crate::skir_client::internal::EnumAdapter<${typeName}> {\n`,
|
|
360
|
+
);
|
|
361
|
+
this.push(
|
|
362
|
+
`static ADAPTER: std::sync::LazyLock<crate::skir_client::internal::EnumAdapter<${typeName}>> =\n`,
|
|
363
|
+
);
|
|
364
|
+
this.push(`std::sync::LazyLock::new(|| {\n`);
|
|
365
|
+
this.push(`crate::skir_client::internal::EnumAdapter::new(\n`);
|
|
366
|
+
this.push(`|x: &${typeName}| match x {\n`);
|
|
367
|
+
this.push(`${typeName}::Unknown(_) => 0,\n`);
|
|
368
|
+
let kindOrdinal = 1;
|
|
369
|
+
for (const variant of record.record.fields) {
|
|
370
|
+
const variantName = convertCase(variant.name.text, "UpperCamel").concat(
|
|
371
|
+
variantNamesNeedSuffix ? (variant.type ? "Wrapper" : "Const") : "",
|
|
372
|
+
);
|
|
373
|
+
if (variant.type) {
|
|
374
|
+
this.push(`${typeName}::${variantName}(_) => ${kindOrdinal},\n`);
|
|
375
|
+
} else {
|
|
376
|
+
this.push(`${typeName}::${variantName} => ${kindOrdinal},\n`);
|
|
377
|
+
}
|
|
378
|
+
kindOrdinal++;
|
|
379
|
+
}
|
|
380
|
+
this.push(`},\n`);
|
|
381
|
+
this.push(`|u| ${typeName}::Unknown(Some(u)),\n`);
|
|
382
|
+
this.push(
|
|
383
|
+
`|x: &${typeName}| match x { ${typeName}::Unknown(Some(u)) => Some(u.as_ref()), _ => None },\n`,
|
|
384
|
+
);
|
|
385
|
+
this.push(`"${enumModulePath}",\n`);
|
|
386
|
+
this.push(`"${enumQualifiedName}",\n`);
|
|
387
|
+
this.push(`${toRustStringLiteral(docToCommentText(record.record.doc))},\n`);
|
|
388
|
+
this.push(`)\n`);
|
|
389
|
+
this.push(`});\n`);
|
|
390
|
+
this.push(`&*ADAPTER\n`);
|
|
391
|
+
this.push("}\n");
|
|
392
|
+
this.push(
|
|
393
|
+
`pub fn serializer() -> crate::skir_client::Serializer<${typeName}> {\n`,
|
|
394
|
+
);
|
|
395
|
+
this.push(`initialize_module_serializers();\n`);
|
|
396
|
+
this.push(
|
|
397
|
+
`crate::skir_client::internal::enum_serializer_from_static(${typeName}::_adapter())\n`,
|
|
398
|
+
);
|
|
399
|
+
this.push("}\n");
|
|
400
|
+
this.push("}\n\n");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private writeInitializeModuleSerializersFn(
|
|
404
|
+
records: readonly RecordLocation[],
|
|
405
|
+
): void {
|
|
406
|
+
const { typeSpeller } = this;
|
|
407
|
+
this.pushSeparator("initialize_module_serializers()");
|
|
408
|
+
this.push("fn initialize_module_serializers() {\n");
|
|
409
|
+
this.push("static INIT: std::sync::LazyLock<()> =\n");
|
|
410
|
+
this.push("std::sync::LazyLock::new(|| {\n");
|
|
411
|
+
for (const record of records) {
|
|
412
|
+
const typeName = getTypeName(record);
|
|
413
|
+
if (record.record.recordType === "struct") {
|
|
414
|
+
this.push("unsafe {\n");
|
|
415
|
+
this.push(
|
|
416
|
+
`let a: *mut crate::skir_client::internal::StructAdapter<${typeName}> = ${typeName}::_adapter() as *const _ as *mut _;\n`,
|
|
417
|
+
);
|
|
418
|
+
for (const removedNumber of record.record.removedNumbers) {
|
|
419
|
+
this.push(`(*a).add_removed_number(${removedNumber});\n`);
|
|
420
|
+
}
|
|
421
|
+
for (const field of record.record.fields) {
|
|
422
|
+
const fieldName = toRustFieldName(field.name.text);
|
|
423
|
+
let serializerExpr = typeSpeller.getSerializerExpression(
|
|
424
|
+
field.type!,
|
|
425
|
+
"init",
|
|
426
|
+
);
|
|
427
|
+
if (field.isRecursive === "hard") {
|
|
428
|
+
serializerExpr = `crate::skir_client::internal::recursive_serializer(${serializerExpr})`;
|
|
429
|
+
}
|
|
430
|
+
const getter =
|
|
431
|
+
field.isRecursive === "hard"
|
|
432
|
+
? `|x: &${typeName}| &x._${field.name.text}_rec`
|
|
433
|
+
: `|x: &${typeName}| &x.${fieldName}`;
|
|
434
|
+
const setter =
|
|
435
|
+
field.isRecursive === "hard"
|
|
436
|
+
? `|x: &mut ${typeName}, v| x._${field.name.text}_rec = v`
|
|
437
|
+
: `|x: &mut ${typeName}, v| x.${fieldName} = v`;
|
|
438
|
+
this.push(
|
|
439
|
+
`(*a).add_field("${field.name.text}", ${field.number}, ${serializerExpr}, ${toRustStringLiteral(docToCommentText(field.doc))}, ${getter}, ${setter});\n`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
this.push("(*a).finalize();\n");
|
|
443
|
+
this.push("}\n");
|
|
444
|
+
} else {
|
|
445
|
+
const variantNamesNeedSuffix = doVariantNamesNeedSuffix(
|
|
446
|
+
record.record.fields,
|
|
447
|
+
);
|
|
448
|
+
this.push("unsafe {\n");
|
|
449
|
+
this.push(
|
|
450
|
+
`let a: *mut crate::skir_client::internal::EnumAdapter<${typeName}> = ${typeName}::_adapter() as *const _ as *mut _;\n`,
|
|
451
|
+
);
|
|
452
|
+
for (const removedNumber of record.record.removedNumbers) {
|
|
453
|
+
this.push(`(*a).add_removed_number(${removedNumber});\n`);
|
|
454
|
+
}
|
|
455
|
+
let kindOrdinal = 1;
|
|
456
|
+
for (const variant of record.record.fields) {
|
|
457
|
+
const variantName = convertCase(
|
|
458
|
+
variant.name.text,
|
|
459
|
+
"UpperCamel",
|
|
460
|
+
).concat(
|
|
461
|
+
variantNamesNeedSuffix ? (variant.type ? "Wrapper" : "Const") : "",
|
|
462
|
+
);
|
|
463
|
+
if (variant.type) {
|
|
464
|
+
const serializerExpr = typeSpeller.getSerializerExpression(
|
|
465
|
+
variant.type,
|
|
466
|
+
"init",
|
|
467
|
+
);
|
|
468
|
+
let wrapFn: string;
|
|
469
|
+
let getValueFn: string;
|
|
470
|
+
if (doesWrapperVariantNeedBoxing(variant.type)) {
|
|
471
|
+
wrapFn = `|v| ${typeName}::${variantName}(Box::new(v))`;
|
|
472
|
+
getValueFn = `|x| match x { ${typeName}::${variantName}(b) => b.as_ref(), _ => unreachable!() }`;
|
|
473
|
+
} else {
|
|
474
|
+
wrapFn = `|v| ${typeName}::${variantName}(v)`;
|
|
475
|
+
getValueFn = `|x| match x { ${typeName}::${variantName}(v) => v, _ => unreachable!() }`;
|
|
476
|
+
}
|
|
477
|
+
this.push(
|
|
478
|
+
`(*a).add_wrapper_variant("${variant.name.text}", ${variant.number}, ${kindOrdinal}, ${serializerExpr}, ${toRustStringLiteral(docToCommentText(variant.doc))}, ${wrapFn}, ${getValueFn});\n`,
|
|
479
|
+
);
|
|
480
|
+
} else {
|
|
481
|
+
this.push(
|
|
482
|
+
`(*a).add_constant_variant("${variant.name.text}", ${variant.number}, ${kindOrdinal}, ${toRustStringLiteral(docToCommentText(variant.doc))}, ${typeName}::${variantName});\n`,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
kindOrdinal++;
|
|
486
|
+
}
|
|
487
|
+
this.push("(*a).finalize();\n");
|
|
488
|
+
this.push("}\n");
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
this.push("});\n");
|
|
492
|
+
this.push("let _ = *INIT;\n");
|
|
493
|
+
this.push("}\n\n");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private writeMethod(method: Method): void {
|
|
497
|
+
const { typeSpeller } = this;
|
|
498
|
+
const rustName = convertCase(method.name.text, "lower_underscore").concat(
|
|
499
|
+
"_method",
|
|
500
|
+
);
|
|
501
|
+
const requestRustType = typeSpeller.getRustType(method.requestType!);
|
|
502
|
+
const responseRustType = typeSpeller.getRustType(method.responseType!);
|
|
503
|
+
const requestSerializerExpr = typeSpeller.getSerializerExpression(
|
|
504
|
+
method.requestType!,
|
|
505
|
+
null,
|
|
506
|
+
);
|
|
507
|
+
const responseSerializerExpr = typeSpeller.getSerializerExpression(
|
|
508
|
+
method.responseType!,
|
|
509
|
+
null,
|
|
510
|
+
);
|
|
511
|
+
const nameStr = toRustStringLiteral(method.name.text);
|
|
512
|
+
const docStr = toRustStringLiteral(docToCommentText(method.doc));
|
|
513
|
+
this.push(commentify(docToCommentText(method.doc)));
|
|
514
|
+
this.push(
|
|
515
|
+
`pub fn ${rustName}() -> &'static crate::skir_client::Method<${requestRustType}, ${responseRustType}> {\n`,
|
|
516
|
+
);
|
|
517
|
+
this.push(
|
|
518
|
+
`static METHOD: std::sync::LazyLock<crate::skir_client::Method<${requestRustType}, ${responseRustType}>> = std::sync::LazyLock::new(|| {\n`,
|
|
519
|
+
);
|
|
520
|
+
this.push(`crate::skir_client::Method {\n`);
|
|
521
|
+
this.push(`name: ${nameStr}.to_string(),\n`);
|
|
522
|
+
this.push(`number: ${method.number}_i64,\n`);
|
|
523
|
+
this.push(`request_serializer: ${requestSerializerExpr},\n`);
|
|
524
|
+
this.push(`response_serializer: ${responseSerializerExpr},\n`);
|
|
525
|
+
this.push(`doc: ${docStr}.to_string(),\n`);
|
|
526
|
+
this.push("}\n");
|
|
527
|
+
this.push("});\n");
|
|
528
|
+
this.push("&*METHOD\n");
|
|
529
|
+
this.push("}\n\n");
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private writeConstant(constant: Constant): void {
|
|
533
|
+
const { typeSpeller } = this;
|
|
534
|
+
const rustName = convertCase(constant.name.text, "lower_underscore").concat(
|
|
535
|
+
"_const",
|
|
536
|
+
);
|
|
537
|
+
const type = constant.type!;
|
|
538
|
+
this.push(commentify(docToCommentText(constant.doc)));
|
|
539
|
+
const rustLiteral = tryGetRustLiteral(constant);
|
|
540
|
+
if (rustLiteral !== null) {
|
|
541
|
+
// This type can be represented as a real Rust const.
|
|
542
|
+
// String primitives must use &'static str because String is not
|
|
543
|
+
// const-eligible in Rust.
|
|
544
|
+
const constType =
|
|
545
|
+
type.kind === "primitive" && type.primitive === "string"
|
|
546
|
+
? "&'static str"
|
|
547
|
+
: typeSpeller.getRustType(type);
|
|
548
|
+
this.push(`pub const ${rustName}: ${constType} = ${rustLiteral};\n\n`);
|
|
549
|
+
} else {
|
|
550
|
+
// Use LazyLock for lazy initialization from JSON.
|
|
551
|
+
const rustType = typeSpeller.getRustType(type);
|
|
552
|
+
const serializerExpr = typeSpeller.getSerializerExpression(type, null);
|
|
553
|
+
const jsonLiteral = toRustStringLiteral(
|
|
554
|
+
JSON.stringify(constant.valueAsDenseJson),
|
|
555
|
+
);
|
|
556
|
+
this.push(`pub fn ${rustName}() -> &'static ${rustType} {\n`);
|
|
557
|
+
this.push(
|
|
558
|
+
`static VALUE: std::sync::LazyLock<${rustType}> = std::sync::LazyLock::new(|| {\n`,
|
|
559
|
+
);
|
|
560
|
+
this.push(
|
|
561
|
+
`${serializerExpr}.from_json(${jsonLiteral}, crate::skir_client::UnrecognizedValues::Drop).unwrap()\n`,
|
|
562
|
+
);
|
|
563
|
+
this.push("});\n");
|
|
564
|
+
this.push("&*VALUE\n");
|
|
565
|
+
this.push("}\n\n");
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private pushSeparator(header: string): void {
|
|
570
|
+
this.push(`// ${"=".repeat(78)}\n`);
|
|
571
|
+
this.push(`// ${header}\n`);
|
|
572
|
+
this.push(`// ${"=".repeat(78)}\n\n`);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private push(...code: string[]): void {
|
|
576
|
+
this.code += code.join("");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private joinLinesAndFixFormatting(): string {
|
|
580
|
+
const indentUnit = " ";
|
|
581
|
+
let result = "";
|
|
582
|
+
// The indent at every line is obtained by repeating indentUnit N times,
|
|
583
|
+
// where N is the length of this array.
|
|
584
|
+
const contextStack: Array<"{" | "(" | "[" | "<" | ":" | "."> = [];
|
|
585
|
+
// Returns the last element in `contextStack`.
|
|
586
|
+
const peakTop = (): string => contextStack.at(-1)!;
|
|
587
|
+
const getMatchingLeftBracket = (r: "}" | ")" | "]" | ">"): string => {
|
|
588
|
+
switch (r) {
|
|
589
|
+
case "}":
|
|
590
|
+
return "{";
|
|
591
|
+
case ")":
|
|
592
|
+
return "(";
|
|
593
|
+
case "]":
|
|
594
|
+
return "[";
|
|
595
|
+
case ">":
|
|
596
|
+
return "<";
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
for (let line of this.code.split("\n")) {
|
|
600
|
+
line = line.trim();
|
|
601
|
+
if (line.length <= 0) {
|
|
602
|
+
// Don't indent empty lines.
|
|
603
|
+
result += "\n";
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const firstChar = line[0];
|
|
608
|
+
switch (firstChar) {
|
|
609
|
+
case "}":
|
|
610
|
+
case ")":
|
|
611
|
+
case "]":
|
|
612
|
+
case ">": {
|
|
613
|
+
const left = getMatchingLeftBracket(firstChar);
|
|
614
|
+
while (contextStack.pop() !== left) {
|
|
615
|
+
if (contextStack.length <= 0) {
|
|
616
|
+
throw Error();
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
case ".": {
|
|
622
|
+
if (peakTop() !== ".") {
|
|
623
|
+
contextStack.push(".");
|
|
624
|
+
}
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const indent =
|
|
629
|
+
indentUnit.repeat(contextStack.length) +
|
|
630
|
+
(line.startsWith("*") ? " " : "");
|
|
631
|
+
result += `${indent}${line.trimEnd()}\n`;
|
|
632
|
+
if (line.startsWith("/") || line.startsWith("*")) {
|
|
633
|
+
// A comment.
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
const lastChar = line.slice(-1);
|
|
637
|
+
switch (lastChar) {
|
|
638
|
+
case "{":
|
|
639
|
+
case "(":
|
|
640
|
+
case "[":
|
|
641
|
+
case "<": {
|
|
642
|
+
// The next line will be indented
|
|
643
|
+
contextStack.push(lastChar);
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
case ":":
|
|
647
|
+
case "=": {
|
|
648
|
+
if (peakTop() !== ":") {
|
|
649
|
+
contextStack.push(":");
|
|
650
|
+
}
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
case ";":
|
|
654
|
+
case ",": {
|
|
655
|
+
if (peakTop() === "." || peakTop() === ":") {
|
|
656
|
+
contextStack.pop();
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return (
|
|
663
|
+
result
|
|
664
|
+
// Remove spaces enclosed within curly brackets if that's all there is.
|
|
665
|
+
.replace(/\{\s+\}/g, "{}")
|
|
666
|
+
// Remove spaces enclosed within round brackets if that's all there is.
|
|
667
|
+
.replace(/\(\s+\)/g, "()")
|
|
668
|
+
// Remove spaces enclosed within square brackets if that's all there is.
|
|
669
|
+
.replace(/\[\s+\]/g, "[]")
|
|
670
|
+
// Remove empty line following an open curly bracket.
|
|
671
|
+
.replace(/(\{\n *)\n/g, "$1")
|
|
672
|
+
// Remove empty line preceding a closed curly bracket.
|
|
673
|
+
.replace(/\n(\n *\})/g, "$1")
|
|
674
|
+
// Coalesce consecutive empty lines.
|
|
675
|
+
.replace(/\n\n\n+/g, "\n\n")
|
|
676
|
+
.replace(/\n\n$/g, "\n")
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private readonly namer: Namer;
|
|
681
|
+
private readonly typeSpeller: TypeSpeller;
|
|
682
|
+
private code = "";
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function doVariantNamesNeedSuffix(variants: readonly Field[]): boolean {
|
|
686
|
+
const seenNames = new Set<string>();
|
|
687
|
+
for (const variant of variants) {
|
|
688
|
+
const name = convertCase(variant.name.text, "UpperCamel");
|
|
689
|
+
if (isUpperCasedKeyword(name) || seenNames.has(name)) {
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
seenNames.add(name);
|
|
693
|
+
}
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function doesWrapperVariantNeedBoxing(type: ResolvedType): boolean {
|
|
698
|
+
switch (type.kind) {
|
|
699
|
+
case "array":
|
|
700
|
+
return false;
|
|
701
|
+
case "optional":
|
|
702
|
+
return doesWrapperVariantNeedBoxing(type.other);
|
|
703
|
+
case "primitive":
|
|
704
|
+
return false;
|
|
705
|
+
case "record":
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function toRustStringLiteral(input: string): string {
|
|
711
|
+
const escaped = input
|
|
712
|
+
.replace(/\\/g, "\\\\") // Escape backslashes
|
|
713
|
+
.replace(/"/g, '\\"') // Escape double quotes
|
|
714
|
+
.replace(/\n/g, "\\n") // Escape newlines
|
|
715
|
+
.replace(/\r/g, "\\r") // Escape carriage returns
|
|
716
|
+
.replace(/\t/g, "\\t"); // Escape tabs
|
|
717
|
+
return `"${escaped}"`;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function tryGetRustLiteral(constant: Constant): string | null {
|
|
721
|
+
const type = constant.type!;
|
|
722
|
+
if (type.kind !== "primitive") {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
const valueAsDenseJson = constant.valueAsDenseJson!;
|
|
726
|
+
switch (type.primitive) {
|
|
727
|
+
case "bool":
|
|
728
|
+
return valueAsDenseJson ? "true" : "false";
|
|
729
|
+
case "int32":
|
|
730
|
+
case "int64":
|
|
731
|
+
case "hash64":
|
|
732
|
+
case "float32":
|
|
733
|
+
case "float64": {
|
|
734
|
+
const maybeQuoted = valueAsDenseJson.toString();
|
|
735
|
+
if (
|
|
736
|
+
maybeQuoted === "Infinity" ||
|
|
737
|
+
maybeQuoted === "-Infinity" ||
|
|
738
|
+
maybeQuoted === "NaN"
|
|
739
|
+
) {
|
|
740
|
+
return null;
|
|
741
|
+
} else {
|
|
742
|
+
return maybeQuoted.startsWith('"') && maybeQuoted.endsWith('"')
|
|
743
|
+
? maybeQuoted.slice(1, -1)
|
|
744
|
+
: maybeQuoted;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
case "string":
|
|
748
|
+
return toRustStringLiteral(valueAsDenseJson as string);
|
|
749
|
+
}
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function commentify(textOrLines: string | readonly string[]): string {
|
|
754
|
+
const text = (
|
|
755
|
+
typeof textOrLines === "string" ? textOrLines : textOrLines.join("\n")
|
|
756
|
+
)
|
|
757
|
+
.trim()
|
|
758
|
+
.replace(/\n{3,}/g, "\n\n");
|
|
759
|
+
if (text.length <= 0) {
|
|
760
|
+
return "";
|
|
761
|
+
}
|
|
762
|
+
return text
|
|
763
|
+
.split("\n")
|
|
764
|
+
.map((line) => (line.length > 0 ? `/// ${line}\n` : `///\n`))
|
|
765
|
+
.join("");
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function docToCommentText(doc: Doc): string {
|
|
769
|
+
return doc.pieces
|
|
770
|
+
.map((p) => {
|
|
771
|
+
switch (p.kind) {
|
|
772
|
+
case "text":
|
|
773
|
+
return p.text;
|
|
774
|
+
case "reference":
|
|
775
|
+
return "`" + p.referenceRange.text.slice(1, -1) + "`";
|
|
776
|
+
}
|
|
777
|
+
})
|
|
778
|
+
.join("");
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
export const GENERATOR = new RustCodeGenerator();
|