enso/app/gui2/parser-codegen/codegen.ts
Kaz Wesley 9fd1ab9092
Parser TS bindings (#7881)
Generate TS bindings and lazy deserialization for the parser types.

# Important Notes
- The new API is imported into `ffi.ts`, but not yet used.
- I have tested the generated code in isolation, but cannot commit tests as we are not currently able to load WASM modules when running in `vitest`.
2023-10-11 13:04:38 +00:00

414 lines
12 KiB
TypeScript

/**
* 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 }
}