skir-kotlin-gen 0.0.2

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/src/index.ts ADDED
@@ -0,0 +1,1010 @@
1
+ // TODO: add comments
2
+
3
+ import {
4
+ type CodeGenerator,
5
+ type Constant,
6
+ Doc,
7
+ type Field,
8
+ type Method,
9
+ type Module,
10
+ type RecordKey,
11
+ type RecordLocation,
12
+ type ResolvedType,
13
+ convertCase,
14
+ simpleHash,
15
+ } from "skir-internal";
16
+ import { z } from "zod";
17
+ import { Namer, toEnumConstantName } from "./naming.js";
18
+ import { TypeSpeller } from "./type_speller.js";
19
+
20
+ const Config = z.object({
21
+ packagePrefix: z
22
+ .string()
23
+ .regex(/^([a-z_$][a-z0-9_$]*\.)*$/)
24
+ .optional(),
25
+ });
26
+
27
+ type Config = z.infer<typeof Config>;
28
+
29
+ class KotlinCodeGenerator implements CodeGenerator<Config> {
30
+ readonly id = "kotlin";
31
+ readonly configType = Config;
32
+ readonly version = "1.0.0";
33
+
34
+ generateCode(input: CodeGenerator.Input<Config>): CodeGenerator.Output {
35
+ const { recordMap, config } = input;
36
+ const outputFiles: CodeGenerator.OutputFile[] = [];
37
+ for (const module of input.modules) {
38
+ outputFiles.push({
39
+ path: module.path.replace(/\.skir$/, ".kt"),
40
+ code: new KotlinSourceFileGenerator(
41
+ module,
42
+ recordMap,
43
+ config,
44
+ ).generate(),
45
+ });
46
+ }
47
+ return { files: outputFiles };
48
+ }
49
+ }
50
+
51
+ // Generates the code for one Kotlin file.
52
+ class KotlinSourceFileGenerator {
53
+ constructor(
54
+ private readonly inModule: Module,
55
+ recordMap: ReadonlyMap<RecordKey, RecordLocation>,
56
+ config: Config,
57
+ ) {
58
+ this.packagePrefix = config.packagePrefix ?? "";
59
+ this.namer = new Namer(this.packagePrefix);
60
+ this.typeSpeller = new TypeSpeller(recordMap, this.namer);
61
+ }
62
+
63
+ generate(): string {
64
+ // http://patorjk.com/software/taag/#f=Doom&t=Do%20not%20edit
65
+ this.push(
66
+ `@file:Suppress("ktlint")
67
+
68
+ // ______ _ _ _ _
69
+ // | _ \\ | | | |(_)| |
70
+ // | | | | ___ _ __ ___ | |_ ___ __| | _ | |_
71
+ // | | | | / _ \\ | '_ \\ / _ \\ | __| / _ \\ / _\` || || __|
72
+ // | |/ / | (_) | | | | || (_) || |_ | __/| (_| || || |_
73
+ // |___/ \\___/ |_| |_| \\___/ \\__| \\___| \\__,_||_| \\__|
74
+ //
75
+
76
+ // To install the Soia client library, add:
77
+ // implementation("build.skir:skir-client:latest.release")
78
+ // to your build.gradle.kts file
79
+
80
+ `,
81
+ `package ${this.packagePrefix}skirout.`,
82
+ this.inModule.path.replace(/\.skir$/, "").replace("/", "."),
83
+ ";\n\n",
84
+ "import build.skir.internal.MustNameArguments as _MustNameArguments;\n",
85
+ "import build.skir.internal.UnrecognizedFields as _UnrecognizedFields;\n",
86
+ "import build.skir.internal.UnrecognizedVariant as _UnrecognizedVariant;\n\n",
87
+ );
88
+
89
+ this.writeClassesForRecords(
90
+ this.inModule.records.filter(
91
+ // Only retain top-level records.
92
+ // Nested records will be processed from within their ancestors.
93
+ (r: RecordLocation) => r.recordAncestors.length === 1,
94
+ ),
95
+ );
96
+
97
+ for (const method of this.inModule.methods) {
98
+ this.writeMethod(method);
99
+ }
100
+
101
+ for (const constant of this.inModule.constants) {
102
+ this.writeConstant(constant);
103
+ }
104
+
105
+ return this.joinLinesAndFixFormatting();
106
+ }
107
+
108
+ private writeClassesForRecords(
109
+ recordLocations: readonly RecordLocation[],
110
+ ): void {
111
+ for (const record of recordLocations) {
112
+ const { recordType } = record.record;
113
+ this.pushEol();
114
+ if (recordType === "struct") {
115
+ this.writeClassesForStruct(record);
116
+ } else {
117
+ this.writeClassForEnum(record);
118
+ }
119
+ }
120
+ }
121
+
122
+ private writeClassesForStruct(struct: RecordLocation): void {
123
+ const { namer, typeSpeller } = this;
124
+ const { recordMap } = typeSpeller;
125
+ const { fields } = struct.record;
126
+ const className = namer.getClassName(struct);
127
+ const { qualifiedName } = className;
128
+ this.push(`sealed interface ${className.name}_OrMutable {\n`);
129
+ for (const field of fields) {
130
+ const fieldName = namer.structFieldToKotlinName(field);
131
+ const allRecordsFrozen = field.isRecursive === "hard";
132
+ const type = typeSpeller.getKotlinType(
133
+ field.type!,
134
+ "maybe-mutable",
135
+ allRecordsFrozen,
136
+ );
137
+ this.push(commentify(docToCommentText(field.doc)));
138
+ this.push(`val ${fieldName}: ${type};\n`);
139
+ }
140
+ this.push(`\nfun toFrozen(): ${qualifiedName};\n`);
141
+ this.push(
142
+ "}\n\n", // class _OrMutable
143
+ commentify([docToCommentText(struct.record.doc), "\nDeeply immutable."]),
144
+ '@kotlin.Suppress("UNUSED_PARAMETER")\n',
145
+ `class ${className.name} private constructor(\n`,
146
+ );
147
+ for (const field of fields) {
148
+ const fieldName = namer.structFieldToKotlinName(field);
149
+ const type = typeSpeller.getKotlinType(field.type!, "frozen");
150
+ if (field.isRecursive === "hard") {
151
+ this.push(`private val __${fieldName}: ${type}?,\n`);
152
+ } else {
153
+ this.push(`override val ${fieldName}: ${type},\n`);
154
+ }
155
+ }
156
+ this.push(
157
+ `private val _unrecognizedFields: _UnrecognizedFields<${qualifiedName}>? =\n`,
158
+ "null,\n",
159
+ `): ${qualifiedName}_OrMutable {\n`,
160
+ );
161
+ for (const field of fields) {
162
+ if (field.isRecursive === "hard") {
163
+ const fieldName = namer.structFieldToKotlinName(field);
164
+ const defaultExpr = this.getDefaultExpression(field.type!);
165
+ this.push(
166
+ `override val ${fieldName} get() = __${fieldName} ?: ${defaultExpr};\n`,
167
+ );
168
+ }
169
+ }
170
+ this.pushEol();
171
+ this.push(
172
+ "constructor(\n",
173
+ "_mustNameArguments: _MustNameArguments =\n_MustNameArguments,\n",
174
+ );
175
+ for (const field of fields) {
176
+ const fieldName = namer.structFieldToKotlinName(field);
177
+ const type = typeSpeller.getKotlinType(field.type!, "initializer");
178
+ this.push(`${fieldName}: ${type},\n`);
179
+ }
180
+ this.push(
181
+ `_unrecognizedFields: _UnrecognizedFields<${qualifiedName}>? =\n`,
182
+ "null,\n",
183
+ "): this(\n",
184
+ );
185
+ for (const field of fields) {
186
+ const fieldName = namer.structFieldToKotlinName(field);
187
+ this.push(this.toFrozenExpression(fieldName, field.type!), ",\n");
188
+ }
189
+ this.push(
190
+ "_unrecognizedFields,\n",
191
+ ") {}\n\n",
192
+ '@kotlin.Deprecated("Already frozen", kotlin.ReplaceWith("this"))\n',
193
+ "override fun toFrozen() = this;\n\n",
194
+ "/** Returns a mutable shallow copy of this instance */\n",
195
+ `fun toMutable() = Mutable(\n`,
196
+ );
197
+ for (const field of fields) {
198
+ const fieldName = namer.structFieldToKotlinName(field);
199
+ this.push(`${fieldName} = this.${fieldName},\n`);
200
+ }
201
+ this.push(");\n\n");
202
+
203
+ if (fields.length) {
204
+ this.push(
205
+ "/** Returns a shallow copy of this instance with the specified fields replaced. */\n",
206
+ "fun copy(\n",
207
+ "_mustNameArguments: _MustNameArguments =\n_MustNameArguments,\n",
208
+ );
209
+ for (const field of fields) {
210
+ const fieldName = namer.structFieldToKotlinName(field);
211
+ const type = typeSpeller.getKotlinType(field.type!, "initializer");
212
+ this.push(`${fieldName}: ${type} =\nthis.${fieldName},\n`);
213
+ }
214
+ this.push(`) = ${qualifiedName}(\n`);
215
+ for (const field of fields) {
216
+ const fieldName = namer.structFieldToKotlinName(field);
217
+ this.push(this.toFrozenExpression(fieldName, field.type!), ",\n");
218
+ }
219
+ this.push(
220
+ "this._unrecognizedFields,\n",
221
+ ");\n\n",
222
+ '@kotlin.Deprecated("No point in creating an exact copy of an immutable object", kotlin.ReplaceWith("this"))\n',
223
+ "fun copy() = this;\n\n",
224
+ );
225
+ }
226
+ this.push(
227
+ "override fun equals(other: kotlin.Any?): kotlin.Boolean {\n",
228
+ `return this === other || (other is ${qualifiedName}`,
229
+ fields
230
+ .map(
231
+ (f) =>
232
+ ` && this.${namer.structFieldToKotlinName(f)} == other.${namer.structFieldToKotlinName(f)}`,
233
+ )
234
+ .join(""),
235
+ ");\n",
236
+ "}\n\n",
237
+ "override fun hashCode(): kotlin.Int {\n",
238
+ "return kotlin.collections.listOf<kotlin.Any?>(",
239
+ fields.map((f) => `this.${namer.structFieldToKotlinName(f)}`).join(", "),
240
+ ").hashCode();\n",
241
+ "}\n\n",
242
+ "override fun toString(): kotlin.String {\n",
243
+ "return build.skir.internal.toStringImpl(\n",
244
+ "this,\n",
245
+ `${qualifiedName}.serializerImpl,\n`,
246
+ ")\n",
247
+ "}\n\n",
248
+ );
249
+ this.push(
250
+ `/** Mutable version of [${className.name}]. */\n`,
251
+ `class Mutable internal constructor(\n`,
252
+ "_mustNameArguments: _MustNameArguments =\n_MustNameArguments,\n",
253
+ );
254
+ for (const field of fields) {
255
+ const fieldName = namer.structFieldToKotlinName(field);
256
+ const allRecordsFrozen = !!field.isRecursive;
257
+ const type = typeSpeller.getKotlinType(
258
+ field.type!,
259
+ "maybe-mutable",
260
+ allRecordsFrozen,
261
+ );
262
+ const defaultExpr = this.getDefaultExpression(field.type!);
263
+ this.push(`override var ${fieldName}: ${type} =\n${defaultExpr},\n`);
264
+ }
265
+ this.push(
266
+ `internal var _unrecognizedFields: _UnrecognizedFields<${qualifiedName}>? =\n`,
267
+ "null,\n",
268
+ `): ${qualifiedName}_OrMutable {\n`,
269
+ "/** Returns a deeply immutable copy of this instance */\n",
270
+ `override fun toFrozen() = ${qualifiedName}(\n`,
271
+ );
272
+ for (const field of fields) {
273
+ const fieldName = namer.structFieldToKotlinName(field);
274
+ this.push(`${fieldName} = this.${fieldName},\n`);
275
+ }
276
+ this.push(
277
+ "_unrecognizedFields = this._unrecognizedFields,\n", //
278
+ `);\n\n`,
279
+ );
280
+ this.writeMutableGetters(fields);
281
+ this.push(
282
+ "}\n\n",
283
+ "companion object {\n",
284
+ "private val default =\n",
285
+ `${qualifiedName}(\n`,
286
+ );
287
+ for (const field of fields) {
288
+ this.push(
289
+ field.isRecursive === "hard"
290
+ ? "null"
291
+ : this.getDefaultExpression(field.type!),
292
+ ",\n",
293
+ );
294
+ }
295
+ this.push(
296
+ ");\n\n",
297
+ "/** Returns an instance with all fields set to their default values. */\n",
298
+ "fun partial() = default;\n\n",
299
+ "/**\n",
300
+ ` * Creates a new instance of [${className.name}].\n`,
301
+ " * Unlike the constructor, does not require all fields to be specified.\n",
302
+ " * Missing fields will be set to their default values.\n",
303
+ " */\n",
304
+ "fun partial(\n",
305
+ "_mustNameArguments: _MustNameArguments =\n_MustNameArguments,\n",
306
+ );
307
+ for (const field of fields) {
308
+ const fieldName = namer.structFieldToKotlinName(field);
309
+ const type = typeSpeller.getKotlinType(field.type!, "initializer");
310
+ const defaultExpr = this.getDefaultExpression(field.type!);
311
+ this.push(`${fieldName}: ${type} =\n${defaultExpr},\n`);
312
+ }
313
+ this.push(`) = ${qualifiedName}(\n`);
314
+ for (const field of fields) {
315
+ const fieldName = namer.structFieldToKotlinName(field);
316
+ this.push(`${fieldName} = ${fieldName},\n`);
317
+ }
318
+ this.push(
319
+ "_unrecognizedFields = null,\n",
320
+ ");\n\n",
321
+ "private val serializerImpl = build.skir.internal.StructSerializer(\n",
322
+ `recordId = "${getRecordId(struct)}",\n`,
323
+ `doc = ${toKotlinStringLiteral(struct.record.doc.text)},\n`,
324
+ "defaultInstance = default,\n",
325
+ "newMutableFn = { it?.toMutable() ?: Mutable() },\n",
326
+ "toFrozenFn = { it.toFrozen() },\n",
327
+ "getUnrecognizedFields = { it._unrecognizedFields },\n",
328
+ "setUnrecognizedFields = { m, u -> m._unrecognizedFields = u },\n",
329
+ ");\n\n",
330
+ `/** Serializer for [${className.name}] instances. */\n`,
331
+ "val serializer = build.skir.internal.makeSerializer(serializerImpl);\n\n",
332
+ `/** Describes the [${className.name}] type. Provides runtime introspection capabilities. */\n`,
333
+ "val typeDescriptor get() = serializerImpl.typeDescriptor;\n\n",
334
+ "init {\n",
335
+ );
336
+ for (const field of fields) {
337
+ const fieldName = namer.structFieldToKotlinName(field);
338
+ this.push(
339
+ "serializerImpl.addField(\n",
340
+ `"${field.name.text}",\n`,
341
+ `"${fieldName}",\n`,
342
+ `${field.number},\n`,
343
+ `${typeSpeller.getSerializerExpression(field.type!)},\n`,
344
+ `${toKotlinStringLiteral(field.doc.text)},\n`,
345
+ `{ it.${fieldName} },\n`,
346
+ `{ mut, v -> mut.${fieldName} = v },\n`,
347
+ ");\n",
348
+ );
349
+ }
350
+ for (const removedNumber of struct.record.removedNumbers) {
351
+ this.push(`serializerImpl.addRemovedNumber(${removedNumber});\n`);
352
+ }
353
+ this.push("serializerImpl.finalizeStruct();\n", "}\n", "}\n");
354
+
355
+ // Write the classes for the records nested in `record`.
356
+ const nestedRecords = struct.record.nestedRecords.map(
357
+ (r) => recordMap.get(r.key)!,
358
+ );
359
+ this.writeClassesForRecords(nestedRecords);
360
+
361
+ this.push("}\n\n");
362
+ }
363
+
364
+ private writeMutableGetters(fields: readonly Field[]): void {
365
+ const { namer, typeSpeller } = this;
366
+ for (const field of fields) {
367
+ if (field.isRecursive) {
368
+ continue;
369
+ }
370
+ const type = field.type!;
371
+ const fieldName = namer.structFieldToKotlinName(field);
372
+ const mutableGetterName =
373
+ "mutable" + convertCase(field.name.text, "UpperCamel");
374
+ const mutableType = typeSpeller.getKotlinType(field.type!, "mutable");
375
+ const accessor = `this.${fieldName}`;
376
+ let bodyLines: string[] = [];
377
+ if (type.kind === "array") {
378
+ bodyLines = [
379
+ "return when (value) {\n",
380
+ "is build.skir.internal.MutableList -> value;\n",
381
+ "else -> {\n",
382
+ "value = build.skir.internal.MutableList(value);\n",
383
+ `${accessor} = value;\n`,
384
+ "value;\n",
385
+ "}\n",
386
+ "}\n",
387
+ ];
388
+ } else if (type.kind === "record") {
389
+ const record = this.typeSpeller.recordMap.get(type.key)!;
390
+ if (record.record.recordType === "struct") {
391
+ const structQualifiedName = namer.getClassName(record).qualifiedName;
392
+ bodyLines = [
393
+ "return when (value) {\n",
394
+ `is ${structQualifiedName} -> {\n`,
395
+ "value = value.toMutable();\n",
396
+ `${accessor} = value;\n`,
397
+ "return value;\n",
398
+ "}\n",
399
+ `is ${structQualifiedName}.Mutable -> value;\n`,
400
+ "}\n",
401
+ ];
402
+ }
403
+ }
404
+ if (bodyLines.length) {
405
+ this.push(
406
+ "/**\n",
407
+ ` * If the value of [${fieldName}] is already mutable, returns it as-is.\n`,
408
+ ` * Otherwise, makes a mutable copy, assigns it back to [${fieldName}] and returns it.\n`,
409
+ ` */\n`,
410
+ `val ${mutableGetterName}: ${mutableType} get() {\n`,
411
+ `var value = ${accessor};\n`,
412
+ );
413
+ for (const line of bodyLines) {
414
+ this.push(line);
415
+ }
416
+ this.push("}\n\n");
417
+ }
418
+ }
419
+ }
420
+
421
+ private writeClassForEnum(record: RecordLocation): void {
422
+ const { namer, typeSpeller } = this;
423
+ const { recordMap } = typeSpeller;
424
+ const { fields: variants } = record.record;
425
+ const constantVariants = variants.filter((v) => !v.type);
426
+ const wrapperVariants = variants.filter((v) => v.type);
427
+ const className = namer.getClassName(record);
428
+ const qualifiedName = className.qualifiedName;
429
+ this.push(
430
+ commentify([docToCommentText(record.record.doc), "\nDeeply immutable."]),
431
+ `sealed class ${className.name} private constructor() {\n`,
432
+ `/** The kind of variant held by a \`${className.name}\`. */\n`,
433
+ "enum class Kind {\n", //
434
+ "UNKNOWN,\n",
435
+ );
436
+ for (const variant of constantVariants) {
437
+ this.push(`${variant.name.text}_CONST,\n`);
438
+ }
439
+ for (const variant of wrapperVariants) {
440
+ this.push(
441
+ convertCase(variant.name.text, "UPPER_UNDERSCORE"),
442
+ "_WRAPPER,\n",
443
+ );
444
+ }
445
+ this.push(
446
+ "}\n\n",
447
+ 'class Unknown @kotlin.Deprecated("For internal use", kotlin.ReplaceWith("',
448
+ qualifiedName,
449
+ '.UNKNOWN")) internal constructor(\n',
450
+ `internal val _unrecognized: _UnrecognizedVariant<${qualifiedName}>?,\n`,
451
+ `) : ${qualifiedName}() {\n`,
452
+ "override val kind get() = Kind.UNKNOWN;\n\n",
453
+ "override fun equals(other: kotlin.Any?): kotlin.Boolean {\n",
454
+ "return other is Unknown;\n",
455
+ "}\n\n",
456
+ "override fun hashCode(): kotlin.Int {\n",
457
+ "return -900601970;\n",
458
+ "}\n\n",
459
+ "}\n\n", // class Unknown
460
+ );
461
+ for (const constantVariant of constantVariants) {
462
+ const kindExpr = `Kind.${constantVariant.name.text}_CONST`;
463
+ const constantName = toEnumConstantName(constantVariant);
464
+ this.push(
465
+ commentify(docToCommentText(constantVariant.doc)),
466
+ `object ${constantName} : ${qualifiedName}() {\n`,
467
+ `override val kind get() = ${kindExpr};\n\n`,
468
+ "init {\n",
469
+ "_maybeFinalizeSerializer();\n",
470
+ "}\n",
471
+ `}\n\n`, // object
472
+ );
473
+ }
474
+ for (const wrapperVariant of wrapperVariants) {
475
+ const valueType = wrapperVariant.type!;
476
+ const wrapperClassName =
477
+ convertCase(wrapperVariant.name.text, "UpperCamel") + "Wrapper";
478
+ const initializerType = typeSpeller
479
+ .getKotlinType(valueType, "initializer")
480
+ .toString();
481
+ const frozenType = typeSpeller
482
+ .getKotlinType(valueType, "frozen")
483
+ .toString();
484
+ this.pushEol();
485
+ this.push(commentify(docToCommentText(wrapperVariant.doc)));
486
+ if (initializerType === frozenType) {
487
+ this.push(
488
+ `class ${wrapperClassName}(\n`,
489
+ `val value: ${initializerType},\n`,
490
+ `) : ${qualifiedName}() {\n`,
491
+ );
492
+ } else {
493
+ this.push(
494
+ `class ${wrapperClassName} private constructor (\n`,
495
+ `val value: ${frozenType},\n`,
496
+ `) : ${qualifiedName}() {\n`,
497
+ "constructor(\n",
498
+ `value: ${initializerType},\n`,
499
+ `): this(${this.toFrozenExpression("value", valueType)}) {}\n\n`,
500
+ );
501
+ }
502
+ const kindExpr =
503
+ "Kind." +
504
+ convertCase(wrapperVariant.name.text, "UPPER_UNDERSCORE") +
505
+ "_WRAPPER";
506
+ this.push(
507
+ `override val kind get() = ${kindExpr};\n\n`,
508
+ "override fun equals(other: kotlin.Any?): kotlin.Boolean {\n",
509
+ `return other is ${qualifiedName}.${wrapperClassName} && value == other.value;\n`,
510
+ "}\n\n",
511
+ "override fun hashCode(): kotlin.Int {\n",
512
+ "return this.value.hashCode() + ",
513
+ String(simpleHash(wrapperVariant.name.text) | 0),
514
+ ";\n",
515
+ "}\n\n",
516
+ "}\n\n", // class
517
+ );
518
+ }
519
+
520
+ this.push(
521
+ "abstract val kind: Kind;\n\n",
522
+ "override fun toString(): kotlin.String {\n",
523
+ "return build.skir.internal.toStringImpl(\n",
524
+ "this,\n",
525
+ `${qualifiedName}._serializerImpl,\n`,
526
+ ")\n",
527
+ "}\n\n",
528
+ "companion object {\n",
529
+ commentify([
530
+ `Constant indicating an unknown [${className.name}].`,
531
+ `Default value for fields of type [${className.name}].`,
532
+ ]),
533
+ 'val UNKNOWN = @kotlin.Suppress("DEPRECATION") Unknown(null);\n\n',
534
+ );
535
+ for (const wrapperVariant of wrapperVariants) {
536
+ const type = wrapperVariant.type!;
537
+ if (type.kind !== "record") {
538
+ continue;
539
+ }
540
+ const structLocation = typeSpeller.recordMap.get(type.key)!;
541
+ const struct = structLocation.record;
542
+ if (struct.recordType !== "struct") {
543
+ continue;
544
+ }
545
+ const structClassName = namer.getClassName(structLocation);
546
+ const createFunName =
547
+ "create" + convertCase(wrapperVariant.name.text, "UpperCamel");
548
+ const wrapperClassName =
549
+ convertCase(wrapperVariant.name.text, "UpperCamel") + "Wrapper";
550
+ this.push(
551
+ '@kotlin.Suppress("UNUSED_PARAMETER")\n',
552
+ `fun ${createFunName}(\n`,
553
+ "_mustNameArguments: _MustNameArguments =\n_MustNameArguments,\n",
554
+ );
555
+ for (const field of struct.fields) {
556
+ const fieldName = namer.structFieldToKotlinName(field);
557
+ const type = typeSpeller.getKotlinType(field.type!, "initializer");
558
+ this.push(`${fieldName}: ${type},\n`);
559
+ }
560
+ this.push(
561
+ `) = ${wrapperClassName}(\n`,
562
+ `${structClassName.qualifiedName}(\n`,
563
+ );
564
+ for (const field of struct.fields) {
565
+ const fieldName = namer.structFieldToKotlinName(field);
566
+ this.push(`${fieldName} = ${fieldName},\n`);
567
+ }
568
+ this.push(")\n", ");\n\n");
569
+ }
570
+ this.push(
571
+ "private val _serializerImpl =\n",
572
+ `build.skir.internal.EnumSerializer.create<${qualifiedName}, Unknown>(\n`,
573
+ `recordId = "${getRecordId(record)}",\n`,
574
+ `doc = ${toKotlinStringLiteral(record.record.doc.text)},\n`,
575
+ "getKindOrdinal = { it.kind.ordinal },\n",
576
+ "kindCount = Kind.values().size,\n",
577
+ "unknownInstance = UNKNOWN,\n",
578
+ 'wrapUnrecognized = { @kotlin.Suppress("DEPRECATION") Unknown(it) },\n',
579
+ "getUnrecognized = { it._unrecognized },\n)",
580
+ ";\n\n",
581
+ `/** Serializer for [${className.name}] instances. */\n`,
582
+ "val serializer = build.skir.internal.makeSerializer(_serializerImpl);\n\n",
583
+ `/** Describes the [${className.name}] type. Provides runtime introspection capabilities. */\n`,
584
+ "val typeDescriptor get() = _serializerImpl.typeDescriptor;\n\n",
585
+ "init {\n",
586
+ );
587
+ for (const constantVariant of constantVariants) {
588
+ this.push(toEnumConstantName(constantVariant), ";\n");
589
+ }
590
+ this.push("_maybeFinalizeSerializer();\n");
591
+ this.push(
592
+ "}\n\n", // init
593
+ `private var _finalizationCounter = 0;\n\n`,
594
+ "private fun _maybeFinalizeSerializer() {\n",
595
+ "_finalizationCounter += 1;\n",
596
+ `if (_finalizationCounter == ${constantVariants.length + 1}) {\n`,
597
+ );
598
+ for (const variant of constantVariants) {
599
+ this.push(
600
+ "_serializerImpl.addConstantVariant(\n",
601
+ `${variant.number},\n`,
602
+ `"${variant.name.text}",\n`,
603
+ `Kind.${variant.name.text}_CONST.ordinal,\n`,
604
+ `${toKotlinStringLiteral(variant.doc.text)},\n`,
605
+ `${toEnumConstantName(variant)},\n`,
606
+ ");\n",
607
+ );
608
+ }
609
+ for (const variant of wrapperVariants) {
610
+ const serializerExpression = typeSpeller.getSerializerExpression(
611
+ variant.type!,
612
+ );
613
+ const wrapperClassName =
614
+ convertCase(variant.name.text, "UpperCamel") + "Wrapper";
615
+ const kindConstName =
616
+ convertCase(variant.name.text, "UPPER_UNDERSCORE") + "_WRAPPER";
617
+ this.push(
618
+ "_serializerImpl.addWrapperVariant(\n",
619
+ `${variant.number},\n`,
620
+ `"${variant.name.text}",\n`,
621
+ `Kind.${kindConstName}.ordinal,\n`,
622
+ `${serializerExpression},\n`,
623
+ `${toKotlinStringLiteral(variant.doc.text)},\n`,
624
+ `{ ${wrapperClassName}(it) },\n`,
625
+ ") { it.value };\n",
626
+ );
627
+ }
628
+ for (const removedNumber of record.record.removedNumbers) {
629
+ this.push(`_serializerImpl.addRemovedNumber(${removedNumber});\n`);
630
+ }
631
+ this.push(
632
+ "_serializerImpl.finalizeEnum();\n",
633
+ "}\n",
634
+ "}\n", // maybeFinalizeSerializer
635
+ "}\n\n", // companion object
636
+ );
637
+
638
+ // Write the classes for the records nested in `record`.
639
+ const nestedRecords = record.record.nestedRecords.map(
640
+ (r) => recordMap.get(r.key)!,
641
+ );
642
+ this.writeClassesForRecords(nestedRecords);
643
+ this.push("}\n\n");
644
+ }
645
+
646
+ private writeMethod(method: Method): void {
647
+ const { typeSpeller } = this;
648
+ const methodName = method.name.text;
649
+ const requestType = typeSpeller.getKotlinType(
650
+ method.requestType!,
651
+ "frozen",
652
+ );
653
+ const requestSerializerExpr = typeSpeller.getSerializerExpression(
654
+ method.requestType!,
655
+ );
656
+ const responseType = typeSpeller.getKotlinType(
657
+ method.responseType!,
658
+ "frozen",
659
+ );
660
+ const responseSerializerExpr = typeSpeller.getSerializerExpression(
661
+ method.responseType!,
662
+ );
663
+ this.push(
664
+ commentify(docToCommentText(method.doc)),
665
+ `val ${methodName}: build.skir.service.Method<\n${requestType},\n${responseType},\n> by kotlin.lazy {\n`,
666
+ "build.skir.service.Method(\n",
667
+ `"${methodName}",\n`,
668
+ `${method.number},\n`,
669
+ requestSerializerExpr + ",\n",
670
+ responseSerializerExpr + ",\n",
671
+ toKotlinStringLiteral(method.doc.text) + ",\n",
672
+ ")\n",
673
+ "}\n\n",
674
+ );
675
+ }
676
+
677
+ private writeConstant(constant: Constant): void {
678
+ const { typeSpeller } = this;
679
+ const name = constant.name.text;
680
+ const type = constant.type!;
681
+ const kotlinType = typeSpeller.getKotlinType(type, "frozen");
682
+ const tryGetKotlinConstLiteral: () => string | undefined = () => {
683
+ if (type.kind !== "primitive") {
684
+ return undefined;
685
+ }
686
+ const { valueAsDenseJson } = constant;
687
+ switch (type.primitive) {
688
+ case "bool":
689
+ return JSON.stringify(!!valueAsDenseJson);
690
+ case "int32":
691
+ case "string":
692
+ return JSON.stringify(valueAsDenseJson);
693
+ case "int64":
694
+ return `${valueAsDenseJson}L`;
695
+ case "uint64":
696
+ return `${valueAsDenseJson}UL`;
697
+ case "float32": {
698
+ if (valueAsDenseJson === "NaN") {
699
+ return "Float.NaN";
700
+ } else if (valueAsDenseJson === "Infinity") {
701
+ return "Float.POSITIVE_INFINITY";
702
+ } else if (valueAsDenseJson === "-Infinity") {
703
+ return "Float.NEGATIVE_INFINITY";
704
+ } else {
705
+ return JSON.stringify(valueAsDenseJson) + "F";
706
+ }
707
+ }
708
+ case "float64": {
709
+ if (valueAsDenseJson === "NaN") {
710
+ return "Double.NaN";
711
+ } else if (valueAsDenseJson === "Infinity") {
712
+ return "Double.POSITIVE_INFINITY";
713
+ } else if (valueAsDenseJson === "-Infinity") {
714
+ return "Double.NEGATIVE_INFINITY";
715
+ } else {
716
+ return JSON.stringify(valueAsDenseJson);
717
+ }
718
+ }
719
+ default:
720
+ return undefined;
721
+ }
722
+ };
723
+ this.push(commentify(docToCommentText(constant.doc)));
724
+ const kotlinConstLiteral = tryGetKotlinConstLiteral();
725
+ if (kotlinConstLiteral !== undefined) {
726
+ this.push(
727
+ `const val ${name}: ${kotlinType} = ${kotlinConstLiteral};\n\n`,
728
+ );
729
+ } else {
730
+ const serializerExpression = typeSpeller.getSerializerExpression(
731
+ constant.type!,
732
+ );
733
+ const jsonStringLiteral = JSON.stringify(
734
+ JSON.stringify(constant.valueAsDenseJson),
735
+ );
736
+ this.push(
737
+ `val ${name}: ${kotlinType} by kotlin.lazy {\n`,
738
+ serializerExpression,
739
+ `.fromJsonCode(${jsonStringLiteral})\n`,
740
+ "}\n\n",
741
+ );
742
+ }
743
+ }
744
+
745
+ private getDefaultExpression(type: ResolvedType): string {
746
+ switch (type.kind) {
747
+ case "primitive": {
748
+ switch (type.primitive) {
749
+ case "bool":
750
+ return "false";
751
+ case "int32":
752
+ case "int64":
753
+ case "uint64":
754
+ return "0";
755
+ case "float32":
756
+ return "0.0f";
757
+ case "float64":
758
+ return "0.0";
759
+ case "timestamp":
760
+ return "java.time.Instant.EPOCH";
761
+ case "string":
762
+ return '""';
763
+ case "bytes":
764
+ return "okio.ByteString.EMPTY";
765
+ }
766
+ break;
767
+ }
768
+ case "array": {
769
+ const itemType = this.typeSpeller.getKotlinType(type.item, "frozen");
770
+ if (type.key) {
771
+ const { keyType } = type.key;
772
+ let kotlinKeyType = this.typeSpeller.getKotlinType(keyType, "frozen");
773
+ if (keyType.kind === "record") {
774
+ kotlinKeyType += ".Kind";
775
+ }
776
+ return `build.skir.internal.emptyKeyedList<${itemType}, ${kotlinKeyType}>()`;
777
+ } else {
778
+ return `build.skir.internal.emptyFrozenList<${itemType}>()`;
779
+ }
780
+ }
781
+ case "optional": {
782
+ return "null";
783
+ }
784
+ case "record": {
785
+ const record = this.typeSpeller.recordMap.get(type.key)!;
786
+ const kotlinType = this.typeSpeller.getKotlinType(type, "frozen");
787
+ switch (record.record.recordType) {
788
+ case "struct": {
789
+ return `${kotlinType}.partial()`;
790
+ }
791
+ case "enum": {
792
+ return `${kotlinType}.UNKNOWN`;
793
+ }
794
+ }
795
+ break;
796
+ }
797
+ }
798
+ }
799
+
800
+ private toFrozenExpression(inputExpr: string, type: ResolvedType): string {
801
+ const { namer } = this;
802
+ switch (type.kind) {
803
+ case "primitive": {
804
+ return inputExpr;
805
+ }
806
+ case "array": {
807
+ const itemToFrozenExpr = this.toFrozenExpression("it", type.item);
808
+ if (type.key) {
809
+ const path = type.key.path
810
+ .map((f) => namer.structFieldToKotlinName(f.name.text))
811
+ .join(".");
812
+ if (itemToFrozenExpr === "it") {
813
+ return `build.skir.internal.toKeyedList(${inputExpr}, "${path}", { it.${path} })`;
814
+ } else {
815
+ return `build.skir.internal.toKeyedList(${inputExpr}, "${path}", { it.${path} }, { ${itemToFrozenExpr} })`;
816
+ }
817
+ } else {
818
+ if (itemToFrozenExpr === "it") {
819
+ return `build.skir.internal.toFrozenList(${inputExpr})`;
820
+ } else {
821
+ return `build.skir.internal.toFrozenList(${inputExpr}, { ${itemToFrozenExpr} })`;
822
+ }
823
+ }
824
+ }
825
+ case "optional": {
826
+ const otherExpr = this.toFrozenExpression(inputExpr, type.other);
827
+ if (otherExpr === inputExpr) {
828
+ return otherExpr;
829
+ } else {
830
+ return `if (${inputExpr} != null) ${otherExpr} else null`;
831
+ }
832
+ }
833
+ case "record": {
834
+ const record = this.typeSpeller.recordMap.get(type.key)!;
835
+ if (record.record.recordType === "struct") {
836
+ return `${inputExpr}.toFrozen()`;
837
+ } else {
838
+ return inputExpr;
839
+ }
840
+ }
841
+ }
842
+ }
843
+
844
+ private push(...code: string[]): void {
845
+ this.code += code.join("");
846
+ }
847
+
848
+ private pushEol(): void {
849
+ this.code += "\n";
850
+ }
851
+
852
+ private joinLinesAndFixFormatting(): string {
853
+ const indentUnit = " ";
854
+ let result = "";
855
+ // The indent at every line is obtained by repeating indentUnit N times,
856
+ // where N is the length of this array.
857
+ const contextStack: Array<"{" | "(" | "[" | "<" | ":" | "."> = [];
858
+ // Returns the last element in `contextStack`.
859
+ const peakTop = (): string => contextStack.at(-1)!;
860
+ const getMatchingLeftBracket = (r: "}" | ")" | "]" | ">"): string => {
861
+ switch (r) {
862
+ case "}":
863
+ return "{";
864
+ case ")":
865
+ return "(";
866
+ case "]":
867
+ return "[";
868
+ case ">":
869
+ return "<";
870
+ }
871
+ };
872
+ for (let line of this.code.split("\n")) {
873
+ line = line.trim();
874
+ if (line.length <= 0) {
875
+ // Don't indent empty lines.
876
+ result += "\n";
877
+ continue;
878
+ }
879
+
880
+ const firstChar = line[0];
881
+ switch (firstChar) {
882
+ case "}":
883
+ case ")":
884
+ case "]":
885
+ case ">": {
886
+ const left = getMatchingLeftBracket(firstChar);
887
+ while (contextStack.pop() !== left) {
888
+ if (contextStack.length <= 0) {
889
+ throw Error();
890
+ }
891
+ }
892
+ break;
893
+ }
894
+ case ".": {
895
+ if (peakTop() !== ".") {
896
+ contextStack.push(".");
897
+ }
898
+ break;
899
+ }
900
+ }
901
+ const indent =
902
+ indentUnit.repeat(contextStack.length) +
903
+ (line.startsWith("*") ? " " : "");
904
+ result += `${indent}${line.trimEnd()}\n`;
905
+ if (line.startsWith("/") || line.startsWith("*")) {
906
+ // A comment.
907
+ continue;
908
+ }
909
+ const lastChar = line.slice(-1);
910
+ switch (lastChar) {
911
+ case "{":
912
+ case "(":
913
+ case "[":
914
+ case "<": {
915
+ // The next line will be indented
916
+ contextStack.push(lastChar);
917
+ break;
918
+ }
919
+ case ":":
920
+ case "=": {
921
+ if (peakTop() !== ":") {
922
+ contextStack.push(":");
923
+ }
924
+ break;
925
+ }
926
+ case ";":
927
+ case ",": {
928
+ if (peakTop() === "." || peakTop() === ":") {
929
+ contextStack.pop();
930
+ }
931
+ }
932
+ }
933
+ }
934
+
935
+ return (
936
+ result
937
+ // Remove spaces enclosed within curly brackets if that's all there is.
938
+ .replace(/\{\s+\}/g, "{}")
939
+ // Remove spaces enclosed within round brackets if that's all there is.
940
+ .replace(/\(\s+\)/g, "()")
941
+ // Remove spaces enclosed within square brackets if that's all there is.
942
+ .replace(/\[\s+\]/g, "[]")
943
+ // Remove empty line following an open curly bracket.
944
+ .replace(/(\{\n *)\n/g, "$1")
945
+ // Remove empty line preceding a closed curly bracket.
946
+ .replace(/\n(\n *\})/g, "$1")
947
+ // Coalesce consecutive empty lines.
948
+ .replace(/\n\n\n+/g, "\n\n")
949
+ .replace(/\n\n$/g, "\n")
950
+ );
951
+ }
952
+
953
+ private readonly typeSpeller: TypeSpeller;
954
+ private readonly packagePrefix: string;
955
+ private readonly namer: Namer;
956
+ private code = "";
957
+ }
958
+
959
+ function getRecordId(struct: RecordLocation): string {
960
+ const modulePath = struct.modulePath;
961
+ const qualifiedRecordName = struct.recordAncestors
962
+ .map((r) => r.name.text)
963
+ .join(".");
964
+ return `${modulePath}:${qualifiedRecordName}`;
965
+ }
966
+
967
+ function toKotlinStringLiteral(input: string): string {
968
+ // Escape special characters for Kotlin string literals
969
+ const escaped = input
970
+ .replace(/\\/g, "\\\\") // Escape backslashes
971
+ .replace(/"/g, '\\"') // Escape double quotes
972
+ .replace(/\n/g, "\\n") // Escape newlines
973
+ .replace(/\r/g, "\\r") // Escape carriage returns
974
+ .replace(/\t/g, "\\t") // Escape tabs
975
+ .replace(/\$/g, "\\$"); // Escape $ to prevent unwanted interpolation
976
+ return `"${escaped}"`;
977
+ }
978
+
979
+ function commentify(textOrLines: string | readonly string[]): string {
980
+ const text = (
981
+ typeof textOrLines === "string" ? textOrLines : textOrLines.join("\n")
982
+ )
983
+ .trim()
984
+ .replace(/\n{3,}/g, "\n\n")
985
+ .replace("*/", "* /");
986
+ if (text.length <= 0) {
987
+ return "";
988
+ }
989
+ const lines = text.split("\n");
990
+ if (lines.length === 1) {
991
+ return `/** ${text} */\n`;
992
+ } else {
993
+ return ["/**\n", ...lines.map((line) => ` * ${line}\n`), " */\n"].join("");
994
+ }
995
+ }
996
+
997
+ function docToCommentText(doc: Doc): string {
998
+ return doc.pieces
999
+ .map((p) => {
1000
+ switch (p.kind) {
1001
+ case "text":
1002
+ return p.text;
1003
+ case "reference":
1004
+ return "`" + p.referenceRange.text.slice(1, -1) + "`";
1005
+ }
1006
+ })
1007
+ .join("");
1008
+ }
1009
+
1010
+ export const GENERATOR = new KotlinCodeGenerator();