enso/app/gui2/parser-codegen/serialization.ts
Paweł Grabarz e492d3f260
[GUI2] Render widget arguments (#8339)
Fixes #8256 #8257

Implemented displaying node argument placeholders. Refactored widgets to allow arbitrary input types, and split widget matching into two phases, separating input type validation step and allowing it to influence the derived TypeScript type of widget component props.

Connecting an edge to the placeholder is not implemented yet. Only connections to existing arguments are supported for now.

https://github.com/enso-org/enso/assets/919491/ad3bf254-5f56-4f2f-8c0d-616a7195c6d8


Nested argument lists are also supported:
<img width="396" alt="image" src="https://github.com/enso-org/enso/assets/919491/b61f6870-0f01-48c3-9b45-c7c6bc464638">
2023-11-22 14:43:58 +00:00

451 lines
14 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
}