/** * Generates TypeScript bindings from a schema describing types and their serialization. * * Internally, the generated types deserialize their data on demand. This benefits performance: If we eagerly * deserialized a serialized tree to a tree of objects in memory, creating the tree would produce many heap-allocated * objects, and visiting the tree would require dereferencing chains of heap pointers. Deserializing while traversing * allows the optimizer to stack-allocate the temporary objects, saving time and reducing GC pressure. */ import ts from 'typescript' import * as Schema from './schema.js' import { Type, abstractTypeDeserializer, fieldDeserializer, fieldDynValue, seekCursor, support, supportImports, } from './serialization.js' import { assignmentStatement, forwardToSuper, mapIdent, modifiers, namespacedName, toCamel, toPascal, } from './util.js' const tsf = ts.factory // === Public API === export function implement(schema: Schema.Schema): string { const file = ts.createSourceFile('source.ts', '', ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS) const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }) let output = '// *** THIS FILE GENERATED BY `parser-codegen` ***\n' function emit(data: ts.Node) { output += printer.printNode(ts.EmitHint.Unspecified, data, file) output += '\n' } emit( tsf.createImportDeclaration( [], tsf.createImportClause( false, undefined, tsf.createNamedImports( Array.from(Object.entries(supportImports), ([name, isTypeOnly]) => tsf.createImportSpecifier(isTypeOnly, undefined, tsf.createIdentifier(name)), ), ), ), tsf.createStringLiteral('@/util/parserSupport', true), undefined, ), ) for (const id in schema.types) { const ty = schema.types[id] if (ty?.parent == null) { const discriminants = schema.serialization[id]?.discriminants if (discriminants == null) { emit(makeConcreteType(id, schema)) } else { const ty = makeAbstractType(id, discriminants, schema) emit(ty.module) emit(ty.export) } } else { // Ignore child types; they are generated when `makeAbstractType` processes the parent. } } output += `export function deserializeTree(data: ArrayBuffer): Tree { const cursor = new Cursor(data, data.byteLength - 4) return Tree.read(cursor.readPointer()) }` return output } // === Implementation === function makeType(ref: Schema.TypeRef, schema: Schema.Schema): Type { const c = ref.class switch (c) { case 'type': { const ty = schema.types[ref.id] if (!ty) throw new Error(`Invalid type ref: ${ref.id}`) const parent = ty.parent != null ? schema.types[ty.parent] : undefined const typeName = namespacedName(ty.name, parent?.name) const layout = schema.serialization[ref.id] if (!layout) throw new Error(`Invalid serialization ref: ${ref.id}`) if (layout.discriminants != null) { return Type.Abstract(typeName) } else { return Type.Concrete(typeName, layout.size) } } case 'primitive': { const p = ref.type switch (p) { case 'bool': return Type.Boolean case 'u32': return Type.UInt32 case 'i32': return Type.Int32 case 'u64': return Type.UInt64 case 'i64': return Type.Int64 case 'char': return Type.Char case 'string': return Type.String default: { const _ = p satisfies never throw new Error(`unreachable: PrimitiveType.type='${p}'`) } } } case 'sequence': return Type.Sequence(makeType(ref.type, schema)) case 'option': return Type.Option(makeType(ref.type, schema)) case 'result': return Type.Result(makeType(ref.type0, schema), makeType(ref.type1, schema)) default: { const _ = c satisfies never throw new Error(`unreachable: TypeRef.class='${c}' in ${JSON.stringify(ref)}`) } } } type Field = { name: string type: Type offset: number } function makeField( name: string, typeRef: Schema.TypeRef, offset: number, schema: Schema.Schema, ): Field { return { name: mapIdent(toCamel(name)), type: makeType(typeRef, schema), offset: offset, } } function makeGetter(field: Field): ts.GetAccessorDeclaration { return fieldDeserializer(tsf.createIdentifier(field.name), field.type, field.offset) } function makeConcreteType(id: string, schema: Schema.Schema): ts.ClassDeclaration { const ident = tsf.createIdentifier(toPascal(schema.types[id]!.name)) const paramIdent = tsf.createIdentifier('cursor') const cursorParam = tsf.createParameterDeclaration( [], undefined, paramIdent, undefined, support.Cursor, undefined, ) return makeClass( [modifiers.export], ident, [ forwardToSuper(paramIdent, support.Cursor), tsf.createMethodDeclaration( [modifiers.static], undefined, 'read', undefined, [], [cursorParam], tsf.createTypeReferenceNode(ident), tsf.createBlock([ tsf.createReturnStatement(tsf.createNewExpression(ident, [], [paramIdent])), ]), ), ], id, schema, ) } function makeDebugFunction(fields: Field[], typeName?: string): ts.MethodDeclaration { const ident = tsf.createIdentifier('fields') const fieldAssignments = fields.map((field) => tsf.createArrayLiteralExpression([ tsf.createStringLiteral(field.name), fieldDynValue(field.type, field.offset), ]), ) if (typeName != null) { fieldAssignments.push( tsf.createArrayLiteralExpression([ tsf.createStringLiteral('type'), tsf.createObjectLiteralExpression([ tsf.createPropertyAssignment('type', tsf.createStringLiteral('primitive')), tsf.createPropertyAssignment('value', tsf.createStringLiteral(typeName)), ]), ]), ) } return tsf.createMethodDeclaration( [], undefined, ident, undefined, [], [], tsf.createTypeReferenceNode(`[string, ${support.DynValue}][]`), tsf.createBlock([ tsf.createReturnStatement( tsf.createArrayLiteralExpression([ tsf.createSpreadElement( tsf.createCallExpression( tsf.createPropertyAccessExpression(tsf.createSuper(), ident), undefined, undefined, ), ), ...fieldAssignments, ]), ), ]), ) } function makeGetters(id: string, schema: Schema.Schema, typeName?: string): ts.ClassElement[] { const serialization = schema.serialization[id] const type = schema.types[id] if (serialization == null || type == null) throw new Error(`Invalid type id: ${id}`) const fields = serialization.fields.map(([name, offset]: [string, number]) => { const field = type.fields[name] if (field == null) throw new Error(`Invalid field name '${name}' for type '${type.name}'`) return makeField(name, field, offset, schema) }) return [...fields.map(makeGetter), makeDebugFunction(fields, typeName)] } function makeClass( modifiers: ts.Modifier[], name: ts.Identifier, members: ts.ClassElement[], id: string, schema: Schema.Schema, ): ts.ClassDeclaration { return tsf.createClassDeclaration( modifiers, name, undefined, [ tsf.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ tsf.createExpressionWithTypeArguments(support.LazyObject, []), ]), ], [...members, ...makeGetters(id, schema)], ) } type ChildType = { definition: ts.ClassDeclaration reference: ts.TypeNode enumMember: ts.EnumMember case: ts.CaseClause } function makeChildType( base: ts.Identifier, id: string, discriminant: string, schema: Schema.Schema, ): ChildType { const ty = schema.types[id] if (ty == null) throw new Error(`Invalid type id: ${id}`) const name = toPascal(ty.name) const ident = tsf.createIdentifier(name) const cursorIdent = tsf.createIdentifier('cursor') const cursorParam = tsf.createParameterDeclaration( [], undefined, cursorIdent, undefined, support.Cursor, undefined, ) const discriminantInt = tsf.createNumericLiteral(parseInt(discriminant, 10)) return { definition: tsf.createClassDeclaration( [modifiers.export], name, undefined, [ tsf.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ tsf.createExpressionWithTypeArguments(base, []), ]), ], [ tsf.createPropertyDeclaration( [modifiers.readonly], 'type', undefined, tsf.createTypeReferenceNode('Type.' + name), undefined, ), tsf.createConstructorDeclaration( [], [ tsf.createParameterDeclaration( [], undefined, cursorIdent, undefined, support.Cursor, undefined, ), ], tsf.createBlock([ tsf.createExpressionStatement( tsf.createCallExpression(tsf.createIdentifier('super'), [], [cursorIdent]), ), assignmentStatement( tsf.createPropertyAccessExpression(tsf.createIdentifier('this'), 'type'), tsf.createPropertyAccessExpression(tsf.createIdentifier('Type'), name), ), ]), ), tsf.createMethodDeclaration( [modifiers.static], undefined, 'read', undefined, [], [cursorParam], tsf.createTypeReferenceNode(ident), tsf.createBlock([ tsf.createReturnStatement(tsf.createNewExpression(ident, [], [cursorIdent])), ]), ), ...makeGetters(id, schema, name), ], ), reference: tsf.createTypeReferenceNode(name), enumMember: tsf.createEnumMember(toPascal(ty.name), discriminantInt), case: tsf.createCaseClause(discriminantInt, [ tsf.createReturnStatement(tsf.createNewExpression(ident, [], [seekCursor(cursorIdent, 4)])), ]), } } type AbstractType = { module: ts.ModuleDeclaration export: ts.TypeAliasDeclaration } function makeAbstractType( id: string, discriminants: Schema.DiscriminantMap, schema: Schema.Schema, ): AbstractType { const ty = schema.types[id]! const name = toPascal(ty.name) const ident = tsf.createIdentifier(name) const baseIdent = tsf.createIdentifier('AbstractBase') const childTypes = Array.from(Object.entries(discriminants), ([discrim, id]: [string, string]) => makeChildType(baseIdent, id, discrim, schema), ) const cursorIdent = tsf.createIdentifier('cursor') const moduleDecl = tsf.createModuleDeclaration( [modifiers.export], ident, tsf.createModuleBlock([ makeClass( [modifiers.abstract], baseIdent, [forwardToSuper(cursorIdent, support.Cursor, [modifiers.protected])], id, schema, ), tsf.createEnumDeclaration( [modifiers.export, modifiers.const], 'Type', childTypes.map((child) => child.enumMember), ), ...childTypes.map((child) => child.definition), tsf.createTypeAliasDeclaration( [modifiers.export], ident, undefined, tsf.createUnionTypeNode(childTypes.map((child) => child.reference)), ), abstractTypeDeserializer( ident, childTypes.map((child) => child.case), ), ]), ) const abstractTypeExport = tsf.createTypeAliasDeclaration( [modifiers.export], ident, undefined, tsf.createTypeReferenceNode(name + '.' + name), ) return { module: moduleDecl, export: abstractTypeExport } }