[GUI2] use DataView directly in generated parser code (#8028)

- Removed `Cursor` and moved all accessors to standalone functions, reducing the generated code size significantly (especially after minification).
- Removed unnecessary `seek`s from everywhere. Now the offseting is part of each `read` implementation.
- Removed unnecessary differences between `read`s on abstract and concrete types. Now abstract types do the pointer access internally in their `read`.
This commit is contained in:
Paweł Grabarz 2023-10-11 23:28:29 +02:00 committed by GitHub
parent 34f34cf0ad
commit bf76be6e6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 431 additions and 363 deletions

View File

@ -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 }
}

View File

@ -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<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
}

View File

@ -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,
)
}

View File

@ -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) {

View File

@ -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<T> = (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<T>(
view: DataView,
address: number,
readElement: Reader<T>,
): 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<T>(readElement: (cursor: Cursor) => T, elementSize: number): Iterable<T> {
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<Ok, Err>(
view: DataView,
address: number,
readOk: Reader<Ok>,
readErr: Reader<Err>,
): Result<Ok, Err> {
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<T>(view: DataView, address: number, size: number, reader: Reader<T>) {
const data = readPointer(view, address)
let count = readU32(data, 0)
let offset = 4
while (count > 0) {
yield reader(data, offset)
count--
offset += size
}
}
readOption<T>(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<Ok, Err>(
readOk: (cursor: Cursor) => Ok,
readErr: (cursor: Cursor) => Err,
): Result<Ok, Err> {
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<T>(readers: Reader<T>[], 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 {