enso/app/gui2/parser-codegen/codegen.ts
Kaz Wesley 343a644051
Syntactic synchronization, automatic parentheses, metadata in Ast (#8893)
- Synchronize Y.Js clients by AST (implements #8237).
- Before committing an edit, insert any parentheses-nodes needed for the concrete syntax to reflect tree structure (fixes #8884).
- Move `externalId` and all node metadata into a Y.Map owned by each `Ast`. This allows including metadata changes in an edit, enables Y.Js merging of changes to different metadata fields, and will enable the use of Y.Js objects in metadata. (Implements #8804.)

### Important Notes

- Metadata is now set and retrieved through accessors on the `Ast` objects.
- Since some metadata edits need to take effect in real time (e.g. node dragging), new lower-overhead APIs (`commitDirect`, `skipTreeRepair`) are provided for careful use in certain cases.
- The client is now bundled as ESM.
- The build script cleans up git-untracked generated files in an outdated location, which fixes lint errors related to `src/generated` that may occur when switching branches.
2024-02-02 10:22:18 +01:00

525 lines
15 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,
abstractTypeVariants,
fieldDeserializer,
fieldVisitor,
seekViewDyn,
support,
supportImports,
} from './serialization.js'
import {
assignmentStatement,
forwardToSuper,
mapIdent,
modifiers,
namespacedName,
toCamel,
toPascal,
} from './util.js'
const tsf: ts.NodeFactory = ts.factory
const addressIdent = tsf.createIdentifier('address')
const viewIdent = tsf.createIdentifier('view')
// === 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,
omitTrailingSemicolon: true,
})
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('../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.
}
}
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))
return makeClass(
[modifiers.export],
ident,
[
forwardToSuper(viewIdent, support.DataView),
makeReadMethod(
ident,
addressIdent,
viewIdent,
tsf.createNewExpression(ident, [], [seekViewDyn(viewIdent, addressIdent)]),
),
],
id,
schema,
)
}
function makeReadMethod(
typeIdent: ts.Identifier,
addressIdent: ts.Identifier,
viewIdent: ts.Identifier,
returnValue: ts.Expression,
): ts.MethodDeclaration {
const offsetParam = tsf.createParameterDeclaration(
[],
undefined,
addressIdent,
undefined,
tsf.createTypeReferenceNode('number'),
undefined,
)
const cursorParam = tsf.createParameterDeclaration(
[],
undefined,
viewIdent,
undefined,
support.DataView,
undefined,
)
return tsf.createMethodDeclaration(
[modifiers.static],
undefined,
'read',
undefined,
[],
[cursorParam, offsetParam],
tsf.createTypeReferenceNode(typeIdent),
tsf.createBlock([tsf.createReturnStatement(returnValue)]),
)
}
function makeReadFunction(
typeIdent: ts.Identifier,
addressIdent: ts.Identifier,
viewIdent: ts.Identifier,
returnValue: ts.Expression,
): ts.FunctionDeclaration {
const offsetParam = tsf.createParameterDeclaration(
[],
undefined,
addressIdent,
undefined,
tsf.createTypeReferenceNode('number'),
undefined,
)
const cursorParam = tsf.createParameterDeclaration(
[],
undefined,
viewIdent,
undefined,
support.DataView,
undefined,
)
return tsf.createFunctionDeclaration(
[modifiers.export],
undefined,
'read',
[],
[cursorParam, offsetParam],
tsf.createTypeReferenceNode(typeIdent),
tsf.createBlock([tsf.createReturnStatement(returnValue)]),
)
}
function makeVisitFunction(fields: Field[]): ts.MethodDeclaration {
const ident = tsf.createIdentifier('visitChildren')
const visitorParam = tsf.createIdentifier('visitor')
const visitorParamDecl = tsf.createParameterDeclaration(
undefined,
undefined,
visitorParam,
undefined,
support.ObjectVisitor,
)
const visitSuperChildren = tsf.createCallExpression(
tsf.createPropertyAccessExpression(tsf.createSuper(), ident),
undefined,
[visitorParam],
)
const fieldVisitations: ts.Expression[] = []
for (const field of fields) {
if (field.type.visitor === 'visitValue') {
fieldVisitations.push(
tsf.createCallExpression(visitorParam, undefined, [
tsf.createPropertyAccessExpression(tsf.createThis(), field.name),
]),
)
} else if (field.type.visitor != null) {
fieldVisitations.push(
tsf.createCallExpression(
tsf.createPropertyAccessExpression(tsf.createThis(), toCamel('visit_' + field.name)),
undefined,
[visitorParam],
),
)
}
}
const toBool = (value: ts.Expression) =>
tsf.createPrefixUnaryExpression(
ts.SyntaxKind.ExclamationToken,
tsf.createPrefixUnaryExpression(ts.SyntaxKind.ExclamationToken, value),
)
const expression = fieldVisitations.reduce(
(lhs, rhs) => tsf.createBinaryExpression(lhs, ts.SyntaxKind.BarBarToken, toBool(rhs)),
visitSuperChildren,
)
return tsf.createMethodDeclaration(
undefined,
undefined,
ident,
undefined,
undefined,
[visitorParamDecl],
tsf.createTypeReferenceNode('boolean'),
tsf.createBlock([tsf.createReturnStatement(expression)]),
)
}
function makeGetters(id: string, schema: Schema.Schema): 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),
...fields.map(makeElementVisitor).filter((v): v is ts.ClassElement => v != null),
makeVisitFunction(fields),
]
}
function makeElementVisitor(field: Field): ts.ClassElement | undefined {
if (field.type.visitor == null) return undefined
const ident = tsf.createIdentifier(toCamel('visit_' + field.name))
return fieldVisitor(ident, field.type, field.offset)
}
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
name: ts.Identifier
enumMember: ts.EnumMember
}
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 typeIdent = tsf.createIdentifier('Type')
const addressIdent = tsf.createIdentifier('address')
const viewIdent = tsf.createIdentifier('view')
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(tsf.createQualifiedName(typeIdent, name)),
undefined,
),
tsf.createConstructorDeclaration(
[],
[
tsf.createParameterDeclaration(
[],
undefined,
viewIdent,
undefined,
support.DataView,
undefined,
),
],
tsf.createBlock([
tsf.createExpressionStatement(
tsf.createCallExpression(tsf.createSuper(), [], [viewIdent]),
),
assignmentStatement(
tsf.createPropertyAccessExpression(tsf.createThis(), 'type'),
tsf.createPropertyAccessExpression(typeIdent, name),
),
]),
),
makeReadMethod(
ident,
addressIdent,
viewIdent,
tsf.createNewExpression(ident, [], [seekViewDyn(viewIdent, addressIdent)]),
),
...makeGetters(id, schema),
],
),
name: tsf.createIdentifier(name),
enumMember: tsf.createEnumMember(name, discriminantInt),
}
}
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 type = tsf.createTypeReferenceNode(ident)
const baseIdent = tsf.createIdentifier('AbstractBase')
const childTypes = Array.from(Object.entries(discriminants), ([discrim, id]: [string, string]) =>
makeChildType(baseIdent, id, discrim, schema),
)
const moduleDecl = tsf.createModuleDeclaration(
[modifiers.export],
ident,
tsf.createModuleBlock([
makeClass(
[modifiers.export, modifiers.abstract],
baseIdent,
[forwardToSuper(viewIdent, support.DataView, [modifiers.protected])],
id,
schema,
),
tsf.createEnumDeclaration(
[modifiers.export, modifiers.const],
'Type',
childTypes.map((child) => child.enumMember),
),
makeExportConstVariable(
'typeNames',
tsf.createArrayLiteralExpression(
childTypes.map((child) => tsf.createStringLiteralFromNode(child.name)),
),
),
...childTypes.map((child) => child.definition),
tsf.createTypeAliasDeclaration(
[modifiers.export],
ident,
undefined,
tsf.createUnionTypeNode(childTypes.map((child) => tsf.createTypeReferenceNode(child.name))),
),
abstractTypeVariants(childTypes.map((child) => child.name)),
makeReadFunction(
ident,
addressIdent,
viewIdent,
abstractTypeDeserializer(ident, viewIdent, addressIdent),
),
makeIsInstance(type, baseIdent),
]),
)
const abstractTypeExport = tsf.createTypeAliasDeclaration(
[modifiers.export],
ident,
undefined,
tsf.createTypeReferenceNode(tsf.createQualifiedName(ident, ident)),
)
return { module: moduleDecl, export: abstractTypeExport }
}
function makeExportConstVariable(
varName: string,
initializer: ts.Expression,
): ts.VariableStatement {
return tsf.createVariableStatement(
[modifiers.export],
tsf.createVariableDeclarationList(
[
tsf.createVariableDeclaration(
varName,
undefined,
undefined,
tsf.createAsExpression(initializer, tsf.createTypeReferenceNode('const')),
),
],
ts.NodeFlags.Const,
),
)
}
function makeIsInstance(type: ts.TypeNode, baseIdent: ts.Identifier): ts.FunctionDeclaration {
const param = tsf.createIdentifier('obj')
const paramDecl = tsf.createParameterDeclaration(
undefined,
undefined,
param,
undefined,
tsf.createTypeReferenceNode('unknown'),
)
const returnValue = tsf.createBinaryExpression(param, ts.SyntaxKind.InstanceOfKeyword, baseIdent)
return tsf.createFunctionDeclaration(
[modifiers.export],
undefined,
'isInstance',
undefined,
[paramDecl],
tsf.createTypePredicateNode(undefined, param, type),
tsf.createBlock([tsf.createReturnStatement(returnValue)]),
)
}