diff --git a/app/gui2/parser-codegen/codegen.ts b/app/gui2/parser-codegen/codegen.ts index a60f7dac04..5bd4e77cb5 100644 --- a/app/gui2/parser-codegen/codegen.ts +++ b/app/gui2/parser-codegen/codegen.ts @@ -12,9 +12,10 @@ import * as Schema from './schema.js' import { Type, abstractTypeDeserializer, + abstractTypeVariants, fieldDeserializer, fieldDynValue, - seekCursor, + seekViewDyn, support, supportImports, } from './serialization.js' @@ -29,11 +30,17 @@ import { } from './util.js' const tsf = 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 }) + const printer = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed, + omitTrailingSemicolon: true, + }) let output = '// *** THIS FILE GENERATED BY `parser-codegen` ***\n' function emit(data: ts.Node) { @@ -72,10 +79,6 @@ export function implement(schema: Schema.Schema): string { // 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 } @@ -158,31 +161,16 @@ function makeGetter(field: Field): ts.GetAccessorDeclaration { 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])), - ]), + forwardToSuper(viewIdent, support.DataView), + makeReadMethod( + ident, + addressIdent, + viewIdent, + tsf.createNewExpression(ident, [], [seekViewDyn(viewIdent, addressIdent)]), ), ], id, @@ -190,6 +178,73 @@ function makeConcreteType(id: string, schema: Schema.Schema): ts.ClassDeclaratio ) } +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 makeDebugFunction(fields: Field[], typeName?: string): ts.MethodDeclaration { const ident = tsf.createIdentifier('fields') const fieldAssignments = fields.map((field) => @@ -219,16 +274,19 @@ function makeDebugFunction(fields: Field[], typeName?: string): ts.MethodDeclara tsf.createTypeReferenceNode(`[string, ${support.DynValue}][]`), tsf.createBlock([ tsf.createReturnStatement( - tsf.createArrayLiteralExpression([ - tsf.createSpreadElement( - tsf.createCallExpression( - tsf.createPropertyAccessExpression(tsf.createSuper(), ident), - undefined, - undefined, + tsf.createArrayLiteralExpression( + [ + tsf.createSpreadElement( + tsf.createCallExpression( + tsf.createPropertyAccessExpression(tsf.createSuper(), ident), + undefined, + undefined, + ), ), - ), - ...fieldAssignments, - ]), + ...fieldAssignments, + ], + true, + ), ), ]), ) @@ -268,7 +326,7 @@ function makeClass( type ChildType = { definition: ts.ClassDeclaration - reference: ts.TypeNode + name: ts.Identifier enumMember: ts.EnumMember case: ts.CaseClause } @@ -283,15 +341,9 @@ function makeChildType( 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 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( @@ -308,7 +360,7 @@ function makeChildType( [modifiers.readonly], 'type', undefined, - tsf.createTypeReferenceNode('Type.' + name), + tsf.createTypeReferenceNode(tsf.createQualifiedName(typeIdent, name)), undefined, ), tsf.createConstructorDeclaration( @@ -317,42 +369,34 @@ function makeChildType( tsf.createParameterDeclaration( [], undefined, - cursorIdent, + viewIdent, undefined, - support.Cursor, + support.DataView, undefined, ), ], tsf.createBlock([ tsf.createExpressionStatement( - tsf.createCallExpression(tsf.createIdentifier('super'), [], [cursorIdent]), + tsf.createCallExpression(tsf.createSuper(), [], [viewIdent]), ), assignmentStatement( - tsf.createPropertyAccessExpression(tsf.createIdentifier('this'), 'type'), - tsf.createPropertyAccessExpression(tsf.createIdentifier('Type'), name), + tsf.createPropertyAccessExpression(tsf.createThis(), 'type'), + tsf.createPropertyAccessExpression(typeIdent, name), ), ]), ), - tsf.createMethodDeclaration( - [modifiers.static], - undefined, - 'read', - undefined, - [], - [cursorParam], - tsf.createTypeReferenceNode(ident), - tsf.createBlock([ - tsf.createReturnStatement(tsf.createNewExpression(ident, [], [cursorIdent])), - ]), + makeReadMethod( + ident, + addressIdent, + viewIdent, + tsf.createNewExpression(ident, [], [seekViewDyn(viewIdent, addressIdent)]), ), ...makeGetters(id, schema, name), ], ), - reference: tsf.createTypeReferenceNode(name), + name: tsf.createIdentifier(name), enumMember: tsf.createEnumMember(toPascal(ty.name), discriminantInt), - case: tsf.createCaseClause(discriminantInt, [ - tsf.createReturnStatement(tsf.createNewExpression(ident, [], [seekCursor(cursorIdent, 4)])), - ]), + case: tsf.createCaseClause(discriminantInt, [tsf.createReturnStatement(viewIdent)]), } } @@ -373,7 +417,7 @@ function makeAbstractType( 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, @@ -381,7 +425,7 @@ function makeAbstractType( makeClass( [modifiers.abstract], baseIdent, - [forwardToSuper(cursorIdent, support.Cursor, [modifiers.protected])], + [forwardToSuper(viewIdent, support.DataView, [modifiers.protected])], id, schema, ), @@ -395,11 +439,14 @@ function makeAbstractType( [modifiers.export], ident, undefined, - tsf.createUnionTypeNode(childTypes.map((child) => child.reference)), + tsf.createUnionTypeNode(childTypes.map((child) => tsf.createTypeReferenceNode(child.name))), ), - abstractTypeDeserializer( + abstractTypeVariants(childTypes.map((child) => child.name)), + makeReadFunction( ident, - childTypes.map((child) => child.case), + addressIdent, + viewIdent, + abstractTypeDeserializer(ident, viewIdent, addressIdent), ), ]), ) @@ -407,7 +454,7 @@ function makeAbstractType( [modifiers.export], ident, undefined, - tsf.createTypeReferenceNode(name + '.' + name), + tsf.createTypeReferenceNode(tsf.createQualifiedName(tsf.createIdentifier(name), name)), ) return { module: moduleDecl, export: abstractTypeExport } } diff --git a/app/gui2/parser-codegen/serialization.ts b/app/gui2/parser-codegen/serialization.ts index add65487dc..99cd04fb04 100644 --- a/app/gui2/parser-codegen/serialization.ts +++ b/app/gui2/parser-codegen/serialization.ts @@ -1,43 +1,69 @@ /** Generates code lazily deserializing from an application-specific binary format. */ import ts from 'typescript' -import { casesOrThrow, modifiers } from './util.js' +import { makeArrow } from './util' const { factory: tsf } = ts // === Definitions === const noneType = tsf.createTypeReferenceNode('undefined') -const cursorFieldIdent = tsf.createIdentifier('lazyObjectData') +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, - Cursor: false, Result: true, DynValue: true, Dyn: false, + 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, } as const export const support = { LazyObject: tsf.createIdentifier('LazyObject'), - Cursor: tsf.createTypeReferenceNode(tsf.createIdentifier('Cursor')), + DataView: tsf.createTypeReferenceNode(tsf.createIdentifier('DataView')), Result: (t0: ts.TypeNode, t1: ts.TypeNode) => tsf.createTypeReferenceNode(tsf.createIdentifier('Result'), [t0, t1]), DynValue: 'DynValue', Dyn: tsf.createIdentifier('Dyn'), + 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'), } as const -const cursorMethods = { - readString: primitiveReader('readString'), - readBool: primitiveReader('readBool'), - readU32: primitiveReader('readU32'), - readI32: primitiveReader('readI32'), - readU64: primitiveReader('readU64'), - readI64: primitiveReader('readI64'), - readPointer: primitiveReader('readPointer'), - readSequence: readerTransformerSized('readSequence'), - readOption: readerTransformer('readOption'), - readResult: readerTransformerTwoTyped('readResult'), +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 const dynBuilders = { Primitive: dynReader('Primitive'), @@ -47,20 +73,20 @@ const dynBuilders = { Object: dynReader('Object'), } as const -type ExpressionTransformer = (expression: ts.Expression) => ts.Expression +type ReadApplicator = (cursor: ts.Expression, offset: AccessOffset) => ts.Expression // === Public API === export class Type { readonly type: ts.TypeNode - readonly reader: ExpressionTransformer - readonly dynReader: ExpressionTransformer + readonly reader: ReadApplicator + readonly dynReader: ReadApplicator readonly size: number private constructor( type: ts.TypeNode, - reader: ExpressionTransformer, - dynReader: ExpressionTransformer, + reader: ReadApplicator, + dynReader: ReadApplicator, size: number, ) { this.type = type @@ -70,7 +96,7 @@ export class Type { } static Abstract(name: string): Type { - const valueReader = abstractTypeReader(name) + const valueReader = callRead(name) return new Type( tsf.createTypeReferenceNode(name), valueReader, @@ -80,7 +106,7 @@ export class Type { } static Concrete(name: string, size: number): Type { - const valueReader = concreteTypeReader(name) + const valueReader = callRead(name) return new Type( tsf.createTypeReferenceNode(name), valueReader, @@ -92,8 +118,8 @@ export class Type { static Sequence(element: Type): Type { return new Type( tsf.createTypeReferenceNode('Iterable', [element.type]), - cursorMethods.readSequence(element.reader, element.size), - dynBuilders.Sequence(cursorMethods.readSequence(element.dynReader, element.size)), + createSequenceReader(element.size, element.reader), + dynBuilders.Sequence(createSequenceReader(element.size, element.dynReader)), POINTER_SIZE, ) } @@ -101,8 +127,8 @@ export class Type { static Option(element: Type): Type { return new Type( tsf.createUnionTypeNode([element.type, noneType]), - cursorMethods.readOption(element.reader), - dynBuilders.Option(cursorMethods.readOption(element.dynReader)), + baseReaders.readOption(element.reader), + dynBuilders.Option(baseReaders.readOption(element.dynReader)), POINTER_SIZE + 1, ) } @@ -110,102 +136,102 @@ export class Type { static Result(ok: Type, err: Type): Type { return new Type( support.Result(ok.type, err.type), - cursorMethods.readResult(ok.reader, err.reader), - dynBuilders.Result(cursorMethods.readResult(ok.dynReader, err.dynReader)), + baseReaders.readResult(ok.reader, err.reader), + dynBuilders.Result(baseReaders.readResult(ok.dynReader, err.dynReader)), POINTER_SIZE, ) } static Boolean: Type = new Type( tsf.createTypeReferenceNode('boolean'), - cursorMethods.readBool, - dynBuilders.Primitive(cursorMethods.readBool), + baseReaders.readBool, + dynBuilders.Primitive(baseReaders.readBool), 1, ) static UInt32: Type = new Type( tsf.createTypeReferenceNode('number'), - cursorMethods.readU32, - dynBuilders.Primitive(cursorMethods.readU32), + baseReaders.readU32, + dynBuilders.Primitive(baseReaders.readU32), 4, ) static Int32: Type = new Type( tsf.createTypeReferenceNode('number'), - cursorMethods.readI32, - dynBuilders.Primitive(cursorMethods.readI32), + baseReaders.readI32, + dynBuilders.Primitive(baseReaders.readI32), 4, ) static UInt64: Type = new Type( tsf.createTypeReferenceNode('bigint'), - cursorMethods.readU64, - dynBuilders.Primitive(cursorMethods.readU64), + baseReaders.readU64, + dynBuilders.Primitive(baseReaders.readU64), 8, ) static Int64: Type = new Type( tsf.createTypeReferenceNode('bigint'), - cursorMethods.readI64, - dynBuilders.Primitive(cursorMethods.readI64), + baseReaders.readI64, + dynBuilders.Primitive(baseReaders.readI64), 8, ) static Char: Type = new Type( tsf.createTypeReferenceNode('number'), - cursorMethods.readU32, - dynBuilders.Primitive(cursorMethods.readU32), + baseReaders.readU32, + dynBuilders.Primitive(baseReaders.readU32), 4, ) static String: Type = new Type( tsf.createTypeReferenceNode('string'), - cursorMethods.readString, - dynBuilders.Primitive(cursorMethods.readString), + baseReaders.readString, + dynBuilders.Primitive(baseReaders.readString), POINTER_SIZE, ) } -export function seekCursor(cursor: ts.Expression, offset: number): ts.Expression { - if (offset === 0) { - return cursor +export function seekView(view: ts.Expression, address: number): ts.Expression { + if (address === 0) { + return view } else { - return tsf.createCallExpression( - tsf.createPropertyAccessExpression(cursor, 'seek'), - [], - [tsf.createNumericLiteral(offset)], - ) + 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, - cases: ts.CaseClause[], -): ts.FunctionDeclaration { - const cursorIdent = tsf.createIdentifier('cursor') - return tsf.createFunctionDeclaration( - [modifiers.export], - undefined, - 'read', - [], - [ - tsf.createParameterDeclaration( - [], - undefined, - cursorIdent, - undefined, - support.Cursor, - undefined, - ), - ], - tsf.createTypeReferenceNode(ident), - tsf.createBlock([ - tsf.createSwitchStatement( - cursorMethods.readU32(cursorIdent), - casesOrThrow(cases, 'Unexpected discriminant while deserializing.'), - ), - ]), + 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, - offset: number, + address: number, ): ts.GetAccessorDeclaration { return tsf.createGetAccessorDeclaration( [], @@ -214,30 +240,26 @@ export function fieldDeserializer( type.type, tsf.createBlock([ tsf.createReturnStatement( - type.reader( - seekCursor( - tsf.createPropertyAccessExpression(tsf.createThis(), cursorFieldIdent), - offset, - ), - ), + type.reader(thisAccess(viewFieldIdent), makeConstantAddress(address)), ), ]), ) } -export function fieldDynValue(type: Type, offset: number): ts.Expression { - return type.dynReader( - seekCursor(tsf.createPropertyAccessExpression(tsf.createThis(), cursorFieldIdent), offset), - ) +export function fieldDynValue(type: Type, address: number): ts.Expression { + return type.dynReader(thisAccess(viewFieldIdent), makeConstantAddress(address)) +} + +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(name: string): ExpressionTransformer { - return (cursor) => - tsf.createCallExpression(tsf.createPropertyAccessExpression(cursor, name), [], []) +function primitiveReader(func: ts.Identifier): ReadApplicator { + return (view, address) => tsf.createCallExpression(func, [], [view, materializeAddress(address)]) } /** @@ -250,123 +272,121 @@ function primitiveReader(name: string): ExpressionTransformer { * 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( - name: string, -): (readElement: ExpressionTransformer) => ExpressionTransformer { - const innerParameter = tsf.createIdentifier('element') - return (readElement: ExpressionTransformer) => (cursor: ts.Expression) => { +function readerTransformer(func: ts.Identifier): (readElement: ReadApplicator) => ReadApplicator { + return (readElement) => (view, offset) => { return tsf.createCallExpression( - tsf.createPropertyAccessExpression(cursor, name), + func, [], - [ - tsf.createArrowFunction( - [], - [], - [ - tsf.createParameterDeclaration( - [], - undefined, - innerParameter, - undefined, - support.Cursor, - undefined, - ), - ], - undefined, - undefined, - readElement(innerParameter), - ), - ], + [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( - name: string, -): (readOk: ExpressionTransformer, readErr: ExpressionTransformer) => ExpressionTransformer { - function makeArrow(reader: ExpressionTransformer, data: ts.Identifier) { - return tsf.createArrowFunction( - [], - [], - [tsf.createParameterDeclaration([], undefined, data, undefined, support.Cursor, undefined)], - undefined, - undefined, - reader(data), - ) - } - - const okData = tsf.createIdentifier('okData') - const errData = tsf.createIdentifier('errData') - return (readOk: ExpressionTransformer, readErr: ExpressionTransformer) => - (cursor: ts.Expression) => { - return tsf.createCallExpression( - tsf.createPropertyAccessExpression(cursor, name), - [], - [makeArrow(readOk, okData), makeArrow(readErr, errData)], - ) - } -} - -/** Similar to [`readerTransformer`], but for deserialization-transformers are parameterized by the size of their - * element. */ -function readerTransformerSized( - name: string, -): (readElement: ExpressionTransformer, size: number) => ExpressionTransformer { - const innerParameter = tsf.createIdentifier('element') - return (readElement: ExpressionTransformer, size: number) => (cursor: ts.Expression) => { + func: ts.Identifier, +): (readOk: ReadApplicator, readErr: ReadApplicator) => ReadApplicator { + return (readOk: ReadApplicator, readErr: ReadApplicator) => (view, offset) => { return tsf.createCallExpression( - tsf.createPropertyAccessExpression(cursor, name), + func, [], - [ - tsf.createArrowFunction( - [], - [], - [ - tsf.createParameterDeclaration( - [], - undefined, - innerParameter, - undefined, - support.Cursor, - undefined, - ), - ], - undefined, - undefined, - readElement(innerParameter), - ), - tsf.createNumericLiteral(size), - ], + [view, materializeAddress(offset), readerClosure(readOk), readerClosure(readErr)], ) } } -function dynReader(name: string): (readValue: ExpressionTransformer) => ExpressionTransformer { - return (readValue: ExpressionTransformer) => (cursor: ts.Expression) => { +function dynReader(name: string): (readValue: ReadApplicator) => ReadApplicator { + return (readValue) => (view, address) => { return tsf.createCallExpression( tsf.createPropertyAccessExpression(support.Dyn, name), [], - [readValue(cursor)], + [readValue(view, address)], ) } } -function abstractTypeReader(name: string): ExpressionTransformer { - return (cursor: ts.Expression) => +export function callRead(ident: string): ReadApplicator { + return (view, address) => tsf.createCallExpression( - tsf.createPropertyAccessExpression(tsf.createIdentifier(name), 'read'), + tsf.createPropertyAccessExpression(tsf.createIdentifier(ident), 'read'), [], - [cursorMethods.readPointer(cursor)], + [view, materializeAddress(address)], ) } -function concreteTypeReader(name: string): ExpressionTransformer { - return (cursor: ts.Expression) => +export function createSequenceReader(size: number, reader: ReadApplicator): ReadApplicator { + const sizeLiteral = tsf.createNumericLiteral(size) + const closure = readerClosure(reader) + return (view, address) => tsf.createCallExpression( - tsf.createPropertyAccessExpression(tsf.createIdentifier(name), 'read'), + support.readSequence, [], - [cursor], + [view, materializeAddress(address), sizeLiteral, closure], ) } + +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(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 +} diff --git a/app/gui2/parser-codegen/util.ts b/app/gui2/parser-codegen/util.ts index 11ea74b4db..c8bf973f6d 100644 --- a/app/gui2/parser-codegen/util.ts +++ b/app/gui2/parser-codegen/util.ts @@ -76,16 +76,22 @@ export function forwardToSuper( } export function casesOrThrow(cases: ts.CaseClause[], error: string): ts.CaseBlock { - return tsf.createCaseBlock([ - ...cases, - tsf.createDefaultClause([ - tsf.createThrowStatement( - tsf.createNewExpression( - tsf.createIdentifier('Error'), - [], - [tsf.createStringLiteral(error)], - ), - ), - ]), - ]) + return tsf.createCaseBlock([...cases, tsf.createDefaultClause([throwError(error)])]) +} + +export function throwError(error: string): ts.Statement { + return tsf.createThrowStatement( + tsf.createNewExpression(tsf.createIdentifier('Error'), [], [tsf.createStringLiteral(error)]), + ) +} + +export function makeArrow(params: ts.BindingName[], expr: ts.Expression) { + return tsf.createArrowFunction( + [], + [], + params.map((ident) => tsf.createParameterDeclaration([], undefined, ident)), + undefined, + undefined, + expr, + ) } diff --git a/app/gui2/src/util/ast.ts b/app/gui2/src/util/ast.ts index c096a8bf51..3886cfff39 100644 --- a/app/gui2/src/util/ast.ts +++ b/app/gui2/src/util/ast.ts @@ -6,7 +6,7 @@ export { Ast } export function parseEnso(code: string): Ast.Tree { const blob = parse(code) - return Ast.deserializeTree(blob.buffer) + return Ast.Tree.read(new DataView(blob.buffer), blob.byteLength - 4) } if (import.meta.vitest) { diff --git a/app/gui2/src/util/parserSupport.ts b/app/gui2/src/util/parserSupport.ts index 72f47b5305..f789c9a69c 100644 --- a/app/gui2/src/util/parserSupport.ts +++ b/app/gui2/src/util/parserSupport.ts @@ -2,6 +2,7 @@ export { type Result } from '@/util/result' import { Err, Error, Ok, type Result } from '@/util/result' +import { bail } from './assert' export type Primitive = { type: 'primitive' @@ -24,6 +25,7 @@ export type DynObject = { type: 'object' getFields: () => [string, DynValue][] } + export const Dyn = { Primitive: (value: boolean | number | bigint | string): DynValue => ({ type: 'primitive', @@ -40,10 +42,10 @@ export const Dyn = { /** Base class for objects that lazily deserialize fields when accessed. */ export abstract class LazyObject { - protected readonly lazyObjectData: Cursor + protected readonly _v: DataView - protected constructor(data: Cursor) { - this.lazyObjectData = data + protected constructor(view: DataView) { + this._v = view } fields(): [string, DynValue][] { @@ -51,109 +53,102 @@ export abstract class LazyObject { } } -export const builtin = { - Array: Array, -} as const +type Reader = (view: DataView, address: number) => T -export class Cursor { - private readonly blob: DataView +function makeDataView(buffer: ArrayBuffer, address: number) { + return new DataView(buffer, address) +} - constructor(buffer: ArrayBuffer, address: number) { - this.blob = new DataView(buffer, address) +export function readU8(view: DataView, address: number) { + return view.getUint8(address) +} + +export function readU32(view: DataView, address: number) { + return view.getUint32(address, true) +} + +export function readI32(view: DataView, address: number) { + return view.getInt32(address, true) +} + +export function readU64(view: DataView, address: number) { + return view.getBigUint64(address, true) +} + +export function readI64(view: DataView, address: number) { + return view.getBigInt64(address, true) +} + +export function readBool(view: DataView, address: number) { + return readU8(view, address) !== 0 +} + +export function readOffset(view: DataView, offset: number) { + return makeDataView(view.buffer, view.byteOffset + offset) +} + +export function readPointer(view: DataView, address: number): DataView { + return makeDataView(view.buffer, readU32(view, address)) +} + +const textDecoder = new TextDecoder() + +export function readOption( + view: DataView, + address: number, + readElement: Reader, +): T | undefined { + const discriminant = readU8(view, address) + switch (discriminant) { + case 0: + return undefined + case 1: + return readElement(readPointer(view, address + 1), 0) + default: + throw new Error(`Invalid Option discriminant: 0x${discriminant.toString(16)}.`) } +} - *readSequence(readElement: (cursor: Cursor) => T, elementSize: number): Iterable { - const data = this.readPointer() - let count = data.readU32() - let offset = 4 - while (count > 0) { - yield readElement(data.seek(offset)) - count-- - offset += elementSize - } +export function readResult( + view: DataView, + address: number, + readOk: Reader, + readErr: Reader, +): Result { + const data = readPointer(view, address) + const discriminant = readU32(data, 0) + switch (discriminant) { + case 0: + return Ok(readOk(data, 4)) + case 1: + return Err(readErr(data, 4)) + default: + throw new Error(`Invalid Result discriminant: 0x${discriminant.toString(16)}.`) } +} +export function* readSequence(view: DataView, address: number, size: number, reader: Reader) { + const data = readPointer(view, address) + let count = readU32(data, 0) + let offset = 4 + while (count > 0) { + yield reader(data, offset) + count-- + offset += size + } +} - readOption(readElement: (cursor: Cursor) => T): T | undefined { - const discriminant = this.readU8() - switch (discriminant) { - case 0: - return undefined - case 1: - return readElement(this.seek(1).readPointer()) - default: - throw new Error(`Invalid Option discriminant: 0x${discriminant.toString(16)}.`) - } - } +export function readString(view: DataView, address: number): string { + const data = readPointer(view, address) + const len = readU32(data, 0) + const bytes = new Uint8Array(data.buffer, data.byteOffset + 4, len) + return textDecoder.decode(bytes) +} - readResult( - readOk: (cursor: Cursor) => Ok, - readErr: (cursor: Cursor) => Err, - ): Result { - const data = this.readPointer() - const discriminant = data.readU32() - switch (discriminant) { - case 0: - return Ok(readOk(data.seek(4))) - case 1: - return Err(readErr(data.seek(4))) - default: - throw new Error(`Invalid Result discriminant: 0x${discriminant.toString(16)}.`) - } - } - - readPointer(): Cursor { - const pointee = this.readU32() - return new Cursor(this.blob.buffer, pointee) - } - - readU8(): number { - return this.blob.getUint8(0) - } - - readU32(): number { - return this.blob.getUint32(0, true) - } - - readI32(): number { - return this.blob.getInt32(0, true) - } - - readU64(): bigint { - return this.blob.getBigUint64(0, true) - } - - readI64(): bigint { - return this.blob.getBigInt64(0, true) - } - - readBool(): boolean { - const value = this.readU8() - switch (value) { - case 0: - return false - case 1: - return true - default: - throw new Error( - `Invalid boolean: 0x${value.toString(16)} @ 0x${this.blob.byteOffset.toString(16)}.`, - ) - } - } - - readString(): string { - const data = this.readPointer() - const len = data.readU32() - const bytes = data.blob.buffer.slice(data.blob.byteOffset + 4, data.blob.byteOffset + 4 + len) - return new TextDecoder().decode(bytes) - } - - seek(offset: number): Cursor { - return new Cursor(this.blob.buffer, this.blob.byteOffset + offset) - } - - address(): number { - return this.blob.byteOffset - } +export function readEnum(readers: Reader[], view: DataView, address: number): T { + const data = readPointer(view, address) + const discriminant = readU32(data, 0) + const reader = readers[discriminant] ?? bail(`Invalid enum discriminant: ${discriminant}`) + return reader(data, 4) } export function debug(obj: LazyObject): any {