enso/app/ydoc-shared/parser-codegen/serialization.ts
Paweł Grabarz b286adaae4
Split ydoc server into separate module (#10735)
# Important Notes
The command to run the gui dev environment has been changed. Invoking the old command will print a message about that.
From now on, use `pnpm dev:gui2` in repository root.
2024-08-08 12:12:05 +00:00

451 lines
13 KiB
TypeScript

/** Generates code lazily deserializing from an application-specific binary format. */
import ts from 'typescript'
import { makeArrow } from './util'
const { factory: tsf } = ts
// === Definitions ===
const noneType = tsf.createTypeReferenceNode('undefined')
const viewFieldIdent = tsf.createIdentifier('_v')
const variantReadersIdent = tsf.createIdentifier('VARIANT_READERS')
const POINTER_SIZE: number = 4
// Symbols exported by the `parserSupport` module.
export const supportImports = {
LazyObject: false,
ObjectVisitor: true,
ObjectAddressVisitor: true,
Result: true,
readU8: false,
readU32: false,
readI32: false,
readU64: false,
readI64: false,
readBool: false,
readOffset: false,
readPointer: false,
readOption: false,
readResult: false,
readEnum: false,
readSequence: false,
readString: false,
visitSequence: false,
visitOption: false,
visitResult: false,
} as const
export const support = {
LazyObject: tsf.createIdentifier('LazyObject'),
ObjectVisitor: tsf.createTypeReferenceNode(tsf.createIdentifier('ObjectVisitor')),
ObjectAddressVisitor: tsf.createTypeReferenceNode(tsf.createIdentifier('ObjectAddressVisitor')),
DataView: tsf.createTypeReferenceNode(tsf.createIdentifier('DataView')),
Result: (t0: ts.TypeNode, t1: ts.TypeNode) =>
tsf.createTypeReferenceNode(tsf.createIdentifier('Result'), [t0, t1]),
readU8: tsf.createIdentifier('readU8'),
readU32: tsf.createIdentifier('readU32'),
readI32: tsf.createIdentifier('readI32'),
readU64: tsf.createIdentifier('readU64'),
readI64: tsf.createIdentifier('readI64'),
readBool: tsf.createIdentifier('readBool'),
readOffset: tsf.createIdentifier('readOffset'),
readPointer: tsf.createIdentifier('readPointer'),
readOption: tsf.createIdentifier('readOption'),
readResult: tsf.createIdentifier('readResult'),
readEnum: tsf.createIdentifier('readEnum'),
readSequence: tsf.createIdentifier('readSequence'),
readString: tsf.createIdentifier('readString'),
visitSequence: tsf.createIdentifier('visitSequence'),
visitOption: tsf.createIdentifier('visitOption'),
visitResult: tsf.createIdentifier('visitResult'),
} as const
const baseReaders = {
readString: primitiveReader(support.readString),
readBool: primitiveReader(support.readBool),
readU32: primitiveReader(support.readU32),
readI32: primitiveReader(support.readI32),
readU64: primitiveReader(support.readU64),
readI64: primitiveReader(support.readI64),
readPointer: primitiveReader(support.readPointer),
readOffset: primitiveReader(support.readOffset),
readOption: readerTransformer(support.readOption),
readResult: readerTransformerTwoTyped(support.readResult),
} as const
type ReadApplicator = (cursor: ts.Expression, offset: AccessOffset) => ts.Expression
type VisitorApplicator = (cursor: ts.Expression, offset: AccessOffset) => ts.Expression
// === Public API ===
export class Type {
readonly type: ts.TypeNode
readonly reader: ReadApplicator
readonly visitor: VisitorApplicator | undefined | 'visitValue'
readonly size: number
private constructor(
type: ts.TypeNode,
reader: ReadApplicator,
visitor: VisitorApplicator | undefined | 'visitValue',
size: number,
) {
this.type = type
this.reader = reader
this.visitor = visitor
this.size = size
}
static Abstract(name: string): Type {
const valueReader = callRead(name)
return new Type(tsf.createTypeReferenceNode(name), valueReader, 'visitValue', POINTER_SIZE)
}
static Concrete(name: string, size: number): Type {
const valueReader = callRead(name)
return new Type(tsf.createTypeReferenceNode(name), valueReader, 'visitValue', size)
}
static Sequence(element: Type): Type {
return new Type(
tsf.createTypeReferenceNode('IterableIterator', [element.type]),
createSequenceReader(element.size, element.reader),
createSequenceVisitor(element.size, visitorClosure(element.visitor, element.reader)),
POINTER_SIZE,
)
}
static Option(element: Type): Type {
return new Type(
tsf.createUnionTypeNode([element.type, noneType]),
baseReaders.readOption(element.reader),
createOptionVisitor(visitorClosure(element.visitor, element.reader)),
POINTER_SIZE + 1,
)
}
static Result(ok: Type, err: Type): Type {
return new Type(
support.Result(ok.type, err.type),
baseReaders.readResult(ok.reader, err.reader),
createResultVisitor(
visitorClosure(ok.visitor, ok.reader),
visitorClosure(err.visitor, err.reader),
),
POINTER_SIZE,
)
}
static Boolean: Type = new Type(
tsf.createTypeReferenceNode('boolean'),
baseReaders.readBool,
undefined,
1,
)
static UInt32: Type = new Type(
tsf.createTypeReferenceNode('number'),
baseReaders.readU32,
undefined,
4,
)
static Int32: Type = new Type(
tsf.createTypeReferenceNode('number'),
baseReaders.readI32,
undefined,
4,
)
static UInt64: Type = new Type(
tsf.createTypeReferenceNode('bigint'),
baseReaders.readU64,
undefined,
8,
)
static Int64: Type = new Type(
tsf.createTypeReferenceNode('bigint'),
baseReaders.readI64,
undefined,
8,
)
static Char: Type = new Type(
tsf.createTypeReferenceNode('number'),
baseReaders.readU32,
undefined,
4,
)
static String: Type = new Type(
tsf.createTypeReferenceNode('string'),
baseReaders.readString,
undefined,
POINTER_SIZE,
)
}
export function seekView(view: ts.Expression, address: number): ts.Expression {
if (address === 0) {
return view
} else {
return seekViewDyn(view, tsf.createNumericLiteral(address))
}
}
export function seekViewDyn(view: ts.Expression, address: ts.Expression): ts.Expression {
return tsf.createCallExpression(support.readOffset, [], [view, address])
}
export function abstractTypeVariants(cases: ts.Identifier[]): ts.Statement {
const reads = cases.map(c => tsf.createPropertyAccessChain(c, undefined, 'read'))
return tsf.createVariableStatement(
[],
tsf.createVariableDeclarationList(
[
tsf.createVariableDeclaration(
variantReadersIdent,
undefined,
undefined,
tsf.createArrayLiteralExpression(reads),
),
],
ts.NodeFlags.Const,
),
)
}
export function abstractTypeDeserializer(
ident: ts.Identifier,
cursorIdent: ts.Identifier,
offsetIdent: ts.Identifier,
): ts.Expression {
return tsf.createCallExpression(
support.readEnum,
[tsf.createTypeReferenceNode(ident)],
[variantReadersIdent, cursorIdent, offsetIdent],
)
}
export function fieldDeserializer(
ident: ts.Identifier,
type: Type,
address: number,
): ts.GetAccessorDeclaration {
return tsf.createGetAccessorDeclaration(
[],
ident,
[],
type.type,
tsf.createBlock([
tsf.createReturnStatement(
type.reader(thisAccess(viewFieldIdent), makeConstantAddress(address)),
),
]),
)
}
export function fieldVisitor(
ident: ts.Identifier,
type: Type,
address: number,
): ts.ClassElement | undefined {
if (type.visitor == null || type.visitor === 'visitValue') return undefined
const value = type.visitor(thisAccess(viewFieldIdent), makeConstantAddress(address))
return tsf.createMethodDeclaration(
undefined,
undefined,
ident,
undefined,
undefined,
[
tsf.createParameterDeclaration(
undefined,
undefined,
'visitor',
undefined,
support.ObjectVisitor,
undefined,
),
],
tsf.createTypeReferenceNode('boolean'),
tsf.createBlock([tsf.createReturnStatement(value)]),
)
}
function thisAccess(ident: ts.Identifier): ts.PropertyAccessExpression {
return tsf.createPropertyAccessExpression(tsf.createThis(), ident)
}
// === Implementation ===
/** Returns a function that, given an expression evaluating to a [`Cursor`], returns an expression applying a
* deserialization method with the given name to the cursor. */
function primitiveReader(func: ts.Identifier): ReadApplicator {
return (view, address) => tsf.createCallExpression(func, [], [view, materializeAddress(address)])
}
/**
* Given the name of a runtime `Cursor` method that deserializes a derived type given a function to deserialize a
* base type, return a codegen-time function that generates a *reader* for a derived type from a *reader* for the base
* type, where a *reader* is a function producing a deserialization expression from an expression that evaluates to a
* `Cursor`.
*
* For example, if we have a reader produced by `primitiveReader('readU32')`, we can use it to create an expression
* representing the deserialization of a number from an expression that will evaluate to a location in the input. If we
* create a `readerTransformer('readOption')`, we can apply it to the number reader to yield an optional-number reader.
*/
function readerTransformer(func: ts.Identifier): (readElement: ReadApplicator) => ReadApplicator {
return readElement => (view, offset) => {
return tsf.createCallExpression(
func,
[],
[view, materializeAddress(offset), readerClosure(readElement)],
)
}
}
interface AccessOffset {
expression: ts.Expression | null
constant: number
}
function makeConstantAddress(constant: number): AccessOffset {
return { expression: null, constant }
}
function makeDynAddress(expression: ts.Expression, constant = 0): AccessOffset {
return { expression, constant }
}
function materializeAddress(offset: AccessOffset): ts.Expression {
if (offset.expression == null) {
return tsf.createNumericLiteral(offset.constant)
} else if (offset.constant == 0) {
return offset.expression
} else {
return tsf.createAdd(offset.expression, tsf.createNumericLiteral(offset.constant))
}
}
/** Similar to [`readerTransformer`], but for deserialization-transformers that produce a reader by combining two input
* readers. */
function readerTransformerTwoTyped(
func: ts.Identifier,
): (readOk: ReadApplicator, readErr: ReadApplicator) => ReadApplicator {
return (readOk: ReadApplicator, readErr: ReadApplicator) => (view, offset) => {
return tsf.createCallExpression(
func,
[],
[view, materializeAddress(offset), readerClosure(readOk), readerClosure(readErr)],
)
}
}
export function callRead(ident: string): ReadApplicator {
return (view, address) =>
tsf.createCallExpression(
tsf.createPropertyAccessExpression(tsf.createIdentifier(ident), 'read'),
[],
[view, materializeAddress(address)],
)
}
export function createSequenceReader(size: number, reader: ReadApplicator): ReadApplicator {
const sizeLiteral = tsf.createNumericLiteral(size)
const closure = readerClosure(reader)
return (view, address) =>
tsf.createCallExpression(
support.readSequence,
[],
[view, materializeAddress(address), sizeLiteral, closure],
)
}
function createSequenceVisitor(
size: number,
closure: ts.Expression | undefined,
): VisitorApplicator | undefined {
if (closure == null) return undefined
const sizeLiteral = tsf.createNumericLiteral(size)
return (view, address) =>
tsf.createCallExpression(
support.visitSequence,
[],
[thisAccess(viewFieldIdent), materializeAddress(address), sizeLiteral, closure],
)
}
function createOptionVisitor(closure: ts.Expression | undefined): VisitorApplicator | undefined {
if (closure == null) return undefined
return (view, address) =>
tsf.createCallExpression(support.visitOption, [], [view, materializeAddress(address), closure])
}
function createResultVisitor(
ok: ts.Expression | undefined,
err: ts.Expression | undefined,
): VisitorApplicator | undefined {
if (ok == null && err == null) return undefined
const none = tsf.createNull()
return (view, address) =>
tsf.createCallExpression(
support.visitResult,
[],
[view, materializeAddress(address), ok ?? none, err ?? none],
)
}
export function visitorClosure(
visitor: VisitorApplicator | 'visitValue' | undefined,
reader: ReadApplicator,
): ts.Expression | undefined {
if (visitor == null) return undefined
const view = tsf.createIdentifier('view')
const address = tsf.createIdentifier('address')
const addressValue = makeDynAddress(address)
if (visitor === 'visitValue') {
const read = reader(view, addressValue)
const readAndVisit = tsf.createCallExpression(tsf.createIdentifier('visitor'), [], [read])
return makeArrow([view, address], readAndVisit)
} else {
return makeArrow([view, address], visitor(view, addressValue))
}
}
export function readerClosure(reader: ReadApplicator): ts.Expression {
const view = tsf.createIdentifier('view')
const address = tsf.createIdentifier('address')
const read = reader(view, makeDynAddress(address))
if (isSimpleRead(read)) {
return read.expression
} else {
return makeArrow([view, address], read)
}
}
function isSimpleRead(reader: ts.Expression): reader is ts.CallExpression {
return (
ts.isCallExpression(reader) &&
ts.isPropertyAccessExpression(reader.expression) &&
reader.expression.name.text === 'read' &&
reader.arguments.length === 2
)
}
function _dbg<T extends ts.Node | undefined>(node: T): T {
if (node == null) {
console.log(node)
return node
}
const printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed,
omitTrailingSemicolon: true,
})
console.log(
ts.SyntaxKind[node.kind],
':',
printer.printNode(
ts.EmitHint.Unspecified,
node,
ts.createSourceFile('dbg.ts', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS),
),
)
return node
}