mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 21:01:37 +03:00
414 lines
12 KiB
TypeScript
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 }
|
||
|
}
|