From 1cc03250073bf3dbd4056c98ebc797bc264a5abe Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Fri, 12 Mar 2021 20:41:35 +0100 Subject: [PATCH] Different approach to pointer codec; support custom schema-driven decode --- .../javascript/packages/core/src/decoder.ts | 296 ++++++++++++-- .../javascript/packages/core/src/encoder.ts | 54 ++- .../javascript/packages/core/src/reader.ts | 2 +- .../javascript/packages/core/src/text.ts | 2 +- .../packages/core/test/codec.test.ts | 23 +- .../packages/core/test/reader.test.ts | 16 +- .../packages/core/test/test-utils.ts | 12 +- .../packages/schema/src/compiler.ts | 131 +++++- .../packages/schema/src/gen/schema.ts | 374 ++++++++++++++++++ 9 files changed, 839 insertions(+), 71 deletions(-) diff --git a/implementations/javascript/packages/core/src/decoder.ts b/implementations/javascript/packages/core/src/decoder.ts index b8eb46e..8d62eff 100644 --- a/implementations/javascript/packages/core/src/decoder.ts +++ b/implementations/javascript/packages/core/src/decoder.ts @@ -6,20 +6,60 @@ import { DoubleFloat, SingleFloat } from "./float"; import { Record } from "./record"; import { Bytes, BytesLike, underlying } from "./bytes"; import { Value } from "./values"; +import { is } from "./is"; -export interface DecoderOptions { +export interface DecoderOptions { includeAnnotations?: boolean; - decodePointer?: (v: Value) => T; } -export class Decoder { - packet: Uint8Array; - index: number; - options: DecoderOptions; +export interface DecoderPointerOptions extends DecoderOptions { + decodePointer?(d: TypedDecoder): T | undefined; +} - constructor(packet: BytesLike = new Uint8Array(0), options: DecoderOptions = {}) { +export interface TypedDecoder { + atEnd(): boolean; + + mark(): any; + restoreMark(m: any): void; + + skip(): void; + next(): Value; + withPointerDecoder(decodePointer: (d: TypedDecoder) => S | undefined, + body: (d: TypedDecoder) => R): R; + + nextBoolean(): boolean | undefined; + nextFloat(): SingleFloat | undefined; + nextDouble(): DoubleFloat | undefined; + nextPointer(): T | undefined; + nextSignedInteger(): number | undefined; + nextString(): string | undefined; + nextByteString(): Bytes | undefined; + nextSymbol(): symbol | undefined; + + openRecord(): boolean; + openSequence(): boolean; + openSet(): boolean; + openDictionary(): boolean; + + closeCompound(): boolean; +} + +export function checkIs>(actual: Value, expected: E): E | undefined { + return is(actual, expected) ? expected : void 0; +} + +function _defaultDecodePointer(_d: TypedDecoder): T | undefined { + throw new DecodeError("No decodePointer function supplied"); +} + +export class Decoder implements TypedDecoder { + packet: Uint8Array; + index = 0; + options: DecoderOptions; + decodePointer: ((d: TypedDecoder) => T | undefined) = _defaultDecodePointer; + + constructor(packet: BytesLike = new Uint8Array(0), options: DecoderOptions = {}) { this.packet = underlying(packet); - this.index = 0; this.options = options; } @@ -37,16 +77,24 @@ export class Decoder { } nextbyte(): number { - if (this.index >= this.packet.length) throw new ShortPacket("Short packet"); - // ^ NOTE: greater-than-or-equal-to, not greater-than. - return this.packet[this.index++]; + if (this.atEnd()) throw new ShortPacket("Short packet"); + return this.packet[this.advance()]; + } + + peekbyte(): number { + if (this.atEnd()) throw new ShortPacket("Short packet"); + return this.packet[this.index]; + } + + advance(): number { + return this.index++; } nextbytes(n: number): DataView { const start = this.index; this.index += n; if (this.index > this.packet.length) throw new ShortPacket("Short packet"); - // ^ NOTE: greater-than, not greater-than-or-equal-to. + // ^ NOTE: greater-than, not greater-than-or-equal-to - this makes atEnd() inappropriate return new DataView(this.packet.buffer, this.packet.byteOffset + start, n); } @@ -58,9 +106,7 @@ export class Decoder { } peekend(): boolean { - const matched = this.nextbyte() === Tag.End; - if (!matched) this.index--; - return matched; + return (this.peekbyte() === Tag.End) && (this.advance(), true); } nextvalues(): Value[] { @@ -112,11 +158,11 @@ export class Decoder { return this.unshiftAnnotation(a, v); } case Tag.Pointer: { - const d = this.options.decodePointer; - if (d === void 0) { - throw new DecodeError("No decodePointer function supplied"); + const v = this.decodePointer(this); + if (v === void 0) { + throw new DecodeError("decodePointer function failed"); } - return this.wrap(d(this.next())); + return this.wrap(v); } case Tag.SignedInteger: return this.wrap(this.nextint(this.varint())); case Tag.String: return this.wrap(Bytes.from(this.nextbytes(this.varint())).fromUtf8()); @@ -131,41 +177,219 @@ export class Decoder { case Tag.Set: return this.wrap(new Set(this.nextvalues())); case Tag.Dictionary: return this.wrap(Decoder.dictionaryFromArray(this.nextvalues())); default: { - if (tag >= Tag.SmallInteger_lo && tag <= Tag.SmallInteger_lo + 15) { - const v = tag - Tag.SmallInteger_lo; - return this.wrap(v > 12 ? v - 16 : v); + const v = this.nextSmallOrMediumInteger(tag); + if (v === void 0) { + throw new DecodeError("Unsupported Preserves tag: " + tag); } - if (tag >= Tag.MediumInteger_lo && tag <= Tag.MediumInteger_lo + 15) { - const n = tag - Tag.MediumInteger_lo; - return this.wrap(this.nextint(n + 1)); - } - throw new DecodeError("Unsupported Preserves tag: " + tag); + return this.wrap(v); } } } - try_next(): Value | undefined { - const start = this.index; + nextSmallOrMediumInteger(tag: number): number | undefined { + if (tag >= Tag.SmallInteger_lo && tag <= Tag.SmallInteger_lo + 15) { + const v = tag - Tag.SmallInteger_lo; + return v > 12 ? v - 16 : v; + } + if (tag >= Tag.MediumInteger_lo && tag <= Tag.MediumInteger_lo + 15) { + const n = tag - Tag.MediumInteger_lo; + return this.nextint(n + 1); + } + return void 0; + } - if (start >= this.packet.length) return void 0; + shortGuard(body: () => R, short: () => R): R { + if (this.atEnd()) return short(); // ^ important somewhat-common case optimization - avoid the exception + const start = this.mark(); try { - return this.next(); + return body(); } catch (e) { if (ShortPacket.isShortPacket(e)) { - this.index = start; - return void 0; + this.restoreMark(start); + return short(); } throw e; } } + + try_next(): Value | undefined { + return this.shortGuard(() => this.next(), () => void 0); + } + + atEnd(): boolean { + return this.index >= this.packet.length; + } + + mark(): number { + return this.index; + } + + restoreMark(m: number): void { + this.index = m; + } + + skip(): void { + // TODO: be more efficient + this.next(); + } + + replacePointerDecoder(decodePointer: (d: TypedDecoder) => S | undefined): Decoder { + const replacement = new Decoder(this.packet, this.options); + replacement.index = this.index; + replacement.decodePointer = decodePointer; + this.packet = new Uint8Array(); + this.index = 0; + this.decodePointer = _defaultDecodePointer; + return replacement; + } + + withPointerDecoder(decodePointer: (d: TypedDecoder) => S | undefined, + body: (d: TypedDecoder) => R): R + { + const oldDecodePointer = this.decodePointer; + const disguised = this as unknown as Decoder; + disguised.decodePointer = decodePointer; + try { + return body(disguised); + } finally { + this.decodePointer = oldDecodePointer; + } + } + + skipAnnotations(): void { + while (this.peekbyte() === Tag.Annotation) { + this.advance(); + this.skip(); + } + } + + nextBoolean(): boolean | undefined { + this.skipAnnotations(); + switch (this.peekbyte()) { + case Tag.False: this.advance(); return false; + case Tag.True: this.advance(); return true; + default: return void 0; + } + } + + nextFloat(): SingleFloat | undefined { + this.skipAnnotations(); + switch (this.peekbyte()) { + case Tag.Float: + this.advance(); + return new SingleFloat(this.nextbytes(4).getFloat32(0, false)); + default: + return void 0; + } + } + + nextDouble(): DoubleFloat | undefined { + this.skipAnnotations(); + switch (this.peekbyte()) { + case Tag.Double: + this.advance(); + return new DoubleFloat(this.nextbytes(8).getFloat64(0, false)); + default: + return void 0; + } + } + + nextPointer(): T | undefined { + this.skipAnnotations(); + switch (this.peekbyte()) { + case Tag.Pointer: { + this.advance(); + const M = this.mark(); + const v = this.decodePointer(this); + if (v === void 0) this.restoreMark(M); + return v; + } + default: + return void 0; + } + } + + nextSignedInteger(): number | undefined { + this.skipAnnotations(); + const b = this.nextbyte(); + switch (b) { + case Tag.SignedInteger: + return this.nextint(this.varint()); + default: { + const v = this.nextSmallOrMediumInteger(b); + if (v === void 0) this.index--; // ugh + return v; + } + } + } + + nextString(): string | undefined { + this.skipAnnotations(); + switch (this.peekbyte()) { + case Tag.String: + this.advance(); + return Bytes.from(this.nextbytes(this.varint())).fromUtf8(); + default: + return void 0; + } + } + + nextByteString(): Bytes | undefined { + this.skipAnnotations(); + switch (this.peekbyte()) { + case Tag.ByteString: + this.advance(); + return Bytes.from(this.nextbytes(this.varint())); + default: + return void 0; + } + } + + nextSymbol(): symbol | undefined { + this.skipAnnotations(); + switch (this.peekbyte()) { + case Tag.Symbol: + this.advance(); + return Symbol.for(Bytes.from(this.nextbytes(this.varint())).fromUtf8()); + default: + return void 0; + } + } + + openRecord(): boolean { + this.skipAnnotations(); + return (this.peekbyte() === Tag.Record) && (this.advance(), true); + } + + openSequence(): boolean { + this.skipAnnotations(); + return (this.peekbyte() === Tag.Sequence) && (this.advance(), true); + } + + openSet(): boolean { + this.skipAnnotations(); + return (this.peekbyte() === Tag.Set) && (this.advance(), true); + } + + openDictionary(): boolean { + this.skipAnnotations(); + return (this.peekbyte() === Tag.Dictionary) && (this.advance(), true); + } + + closeCompound(): boolean { + return this.peekend(); + } } -export function decode(bs: BytesLike, options?: DecoderOptions) { - return new Decoder(bs, options).next(); +export function decode(bs: BytesLike, options: DecoderPointerOptions = {}): Value { + return new Decoder(bs, options).withPointerDecoder>( + options.decodePointer ?? _defaultDecodePointer, + d => d.next()); } -export function decodeWithAnnotations(bs: BytesLike, options: DecoderOptions = {}): Annotated { +export function decodeWithAnnotations(bs: BytesLike, + options: DecoderPointerOptions = {}): Annotated { return decode(bs, { ... options, includeAnnotations: true }) as Annotated; } diff --git a/implementations/javascript/packages/core/src/encoder.ts b/implementations/javascript/packages/core/src/encoder.ts index e1eeec8..888f2d5 100644 --- a/implementations/javascript/packages/core/src/encoder.ts +++ b/implementations/javascript/packages/core/src/encoder.ts @@ -16,10 +16,13 @@ export function isPreservable(v: any): v is Preservable { return typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function'; } -export interface EncoderOptions { +export interface EncoderOptions { canonical?: boolean; includeAnnotations?: boolean; - encodePointer?: (v: T) => Value; +} + +export interface EncoderPointerOptions extends EncoderOptions { + encodePointer?: (e: Encoder, v: T) => void; } function chunkStr(bs: Uint8Array): string { @@ -30,19 +33,38 @@ function isIterable(v: any): v is Iterable { return typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function'; } +function _defaultEncodePointer(e: Encoder, v: T): void { + e.push(pointerId(v)); +} + export class Encoder { chunks: Array; view: DataView; index: number; - options: EncoderOptions; + options: EncoderOptions; + encodePointer: ((e: Encoder, v: T) => void) = _defaultEncodePointer; - constructor(options: EncoderOptions = {}) { + constructor(options: EncoderOptions = {}) { this.chunks = []; this.view = new DataView(new ArrayBuffer(256)); this.index = 0; this.options = options; } + withPointerEncoder(encodePointer: (e: Encoder, v: S) => void, + body: (e: Encoder) => void): this + { + const oldEncodePointer = this.encodePointer; + const disguised = this as unknown as Encoder; + disguised.encodePointer = encodePointer; + try { + body(disguised); + return this; + } finally { + this.encodePointer = oldEncodePointer; + } + } + get canonical(): boolean { return this.options.canonical ?? true; } @@ -183,16 +205,17 @@ export class Encoder { this.encodevalues(Tag.Sequence, v as Iterable>); } else { - const e = this.options.encodePointer ?? pointerId; this.emitbyte(Tag.Pointer); - this.push(e(v)); + this.encodePointer(this, v); } return this; // for chaining } } -export function encode(v: Encodable, options?: EncoderOptions): Bytes { - return new Encoder(options).push(v).contents(); +export function encode(v: Encodable, options: EncoderPointerOptions = {}): Bytes { + return new Encoder(options).withPointerEncoder( + options.encodePointer ?? _defaultEncodePointer, + e => e.push(v)).contents(); } let _nextId = 0; @@ -208,7 +231,8 @@ export function pointerId(v: any): number { const _canonicalEncoder = new Encoder({ canonical: true }); let _usingCanonicalEncoder = false; -export function canonicalEncode(v: Encodable, options?: EncoderOptions): Bytes { +export function canonicalEncode(v: Encodable, options?: EncoderPointerOptions): Bytes +{ if (options === void 0 && !_usingCanonicalEncoder) { _usingCanonicalEncoder = true; const bs = _canonicalEncoder.push(v).contents(); @@ -220,9 +244,17 @@ export function canonicalEncode(v: Encodable, options?: EncoderOptions } export function canonicalString(v: Encodable): string { - return _canonicalEncoder.push(v).contentsString(); + if (!_usingCanonicalEncoder) { + _usingCanonicalEncoder = true; + const s = _canonicalEncoder.push(v).contentsString(); + _usingCanonicalEncoder = false; + return s; + } else { + return new Encoder({ canonical: true }).push(v).contentsString(); + } } -export function encodeWithAnnotations(v: Encodable, options: EncoderOptions = {}): Bytes { +export function encodeWithAnnotations(v: Encodable, + options: EncoderPointerOptions = {}): Bytes { return encode(v, { ... options, includeAnnotations: true }); } diff --git a/implementations/javascript/packages/core/src/reader.ts b/implementations/javascript/packages/core/src/reader.ts index 79b137e..4cc6467 100644 --- a/implementations/javascript/packages/core/src/reader.ts +++ b/implementations/javascript/packages/core/src/reader.ts @@ -168,7 +168,7 @@ export class Reader { if (!Bytes.isBytes(bs)) this.error('ByteString must follow #=', startPos); return decode(bs, { - decodePointer: this.options.decodePointer, + decodePointer: d => this.options.decodePointer?.(d.next()), includeAnnotations: this.options.includeAnnotations, }); } diff --git a/implementations/javascript/packages/core/src/text.ts b/implementations/javascript/packages/core/src/text.ts index d0c5eb3..d232d44 100644 --- a/implementations/javascript/packages/core/src/text.ts +++ b/implementations/javascript/packages/core/src/text.ts @@ -1,4 +1,4 @@ -import { Value } from './values'; +import type { Value } from './values'; export function stringify(x: any): string { if (typeof x?.asPreservesText === 'function') { diff --git a/implementations/javascript/packages/core/test/codec.test.ts b/implementations/javascript/packages/core/test/codec.test.ts index 1aea17f..63a2dd6 100644 --- a/implementations/javascript/packages/core/test/codec.test.ts +++ b/implementations/javascript/packages/core/test/codec.test.ts @@ -9,6 +9,8 @@ import { preserves, fromJS, Constants, + TypedDecoder, + Encoder, } from '../src/index'; const { Tag } = Constants; import './test-utils'; @@ -104,9 +106,9 @@ describe('encoding and decoding pointers', () => { expect(encode( [A, B], { - encodePointer(v: object): Value { + encodePointer(e: Encoder, v: object): void { objects.push(v); - return objects.length - 1; + e.push(objects.length - 1); } })).is(Bytes.from([Tag.Sequence, Tag.Pointer, Tag.SmallInteger_lo, @@ -124,7 +126,8 @@ describe('encoding and decoding pointers', () => { Tag.Pointer, Tag.SmallInteger_lo + 1, Tag.End ]), { - decodePointer(v: Value): object { + decodePointer(d: TypedDecoder): object { + const v = d.next(); if (typeof v !== 'number' || v < 0 || v >= objects.length) { throw new Error("Unknown pointer target"); } @@ -135,7 +138,7 @@ describe('encoding and decoding pointers', () => { it('should store pointers embedded in map keys correctly', () => { const A1 = ({a: 1}); const A2 = ({a: 1}); - const m = new Dictionary(); + const m = new Dictionary(); m.set([A1], 1); m.set([A2], 2); expect(m.get(A1)).toBeUndefined(); @@ -179,19 +182,19 @@ describe('common test suite', () => { back: 5 }, annotation5: { forward: annotate( - Record(Symbol.for('R'), - [annotate(Symbol.for('f'), - Symbol.for('af'))]), + Record(Symbol.for('R'), + [annotate(Symbol.for('f'), + Symbol.for('af'))]), Symbol.for('ar')), - back: Record, any, Pointer>(Symbol.for('R'), [Symbol.for('f')]) + back: Record, any>(Symbol.for('R'), [Symbol.for('f')]) }, annotation6: { - forward: Record, any, Pointer>( + forward: Record, any>( annotate(Symbol.for('R'), Symbol.for('ar')), [annotate(Symbol.for('f'), Symbol.for('af'))]), - back: Record(Symbol.for('R'), [Symbol.for('f')]) + back: Record(Symbol.for('R'), [Symbol.for('f')]) }, annotation7: { forward: annotate([], Symbol.for('a'), Symbol.for('b'), Symbol.for('c')), diff --git a/implementations/javascript/packages/core/test/reader.test.ts b/implementations/javascript/packages/core/test/reader.test.ts index 603abc5..90e76e9 100644 --- a/implementations/javascript/packages/core/test/reader.test.ts +++ b/implementations/javascript/packages/core/test/reader.test.ts @@ -1,6 +1,6 @@ import { Bytes, Decoder, encode, Reader } from '../src/index'; import './test-utils'; -import { decodePointer, encodePointer } from './test-utils'; +import { decodePointer, encodePointer, readPointer } from './test-utils'; import * as fs from 'fs'; @@ -9,19 +9,23 @@ describe('reading common test suite', () => { const samples_txt = fs.readFileSync(__dirname + '/../../../../../tests/samples.txt', 'utf-8'); it('should read equal to decoded binary without annotations', () => { - const s1 = new Reader(samples_txt, { decodePointer, includeAnnotations: false }).next(); - const s2 = new Decoder(samples_bin, { decodePointer, includeAnnotations: false }).next(); + const s1 = new Reader(samples_txt, { decodePointer: readPointer, includeAnnotations: false }).next(); + const s2 = new Decoder(samples_bin, { includeAnnotations: false }).withPointerDecoder( + decodePointer, + d => d.next()); expect(s1).is(s2); }); it('should read equal to decoded binary with annotations', () => { - const s1 = new Reader(samples_txt, { decodePointer, includeAnnotations: true }).next(); - const s2 = new Decoder(samples_bin, { decodePointer, includeAnnotations: true }).next(); + const s1 = new Reader(samples_txt, { decodePointer: readPointer, includeAnnotations: true }).next(); + const s2 = new Decoder(samples_bin, { includeAnnotations: true }).withPointerDecoder( + decodePointer, + d => d.next()); expect(s1).is(s2); }); it('should read and encode back to binary with annotations', () => { - const s = new Reader(samples_txt, { decodePointer, includeAnnotations: true }).next(); + const s = new Reader(samples_txt, { decodePointer: readPointer, includeAnnotations: true }).next(); const bs = Bytes.toIO(encode(s, { encodePointer, includeAnnotations: true, diff --git a/implementations/javascript/packages/core/test/test-utils.ts b/implementations/javascript/packages/core/test/test-utils.ts index 5b51377..f6b1655 100644 --- a/implementations/javascript/packages/core/test/test-utils.ts +++ b/implementations/javascript/packages/core/test/test-utils.ts @@ -1,4 +1,4 @@ -import { Value, is, preserves, strip } from '../src/index'; +import { Value, is, preserves, strip, TypedDecoder, Encoder } from '../src/index'; import '../src/node_support'; declare global { @@ -51,10 +51,14 @@ export class Pointer { } } -export function decodePointer(v: Value): Pointer { +export function readPointer(v: Value): Pointer { return new Pointer(strip(v)); } -export function encodePointer(w: Pointer): Value { - return w.v; +export function decodePointer(d: TypedDecoder): Pointer { + return readPointer(d.next()); +} + +export function encodePointer(e: Encoder, w: Pointer): void { + e.push(w.v); } diff --git a/implementations/javascript/packages/schema/src/compiler.ts b/implementations/javascript/packages/schema/src/compiler.ts index 3d6a9cf..f6407f3 100644 --- a/implementations/javascript/packages/schema/src/compiler.ts +++ b/implementations/javascript/packages/schema/src/compiler.ts @@ -40,6 +40,119 @@ export function compile(env: Environment, schema: Schema, options: CompilerOptio return varname; } + function decoderFor(p: Pattern, recordFields = false): Item { + switch (p.label) { + case M.$atom: + switch (p[0]) { + case M.$Boolean: return `d.nextBoolean()`; + case M.$Float: return `d.nextFloat()`; + case M.$Double: return `d.nextDouble()`; + case M.$SignedInteger: return `d.nextSignedInteger()`; + case M.$String: return `d.nextString()`; + case M.$ByteString: return `d.nextByteString()`; + case M.$Symbol: return `d.nextSymbol()`; + } + case M.$lit: + switch (typeof p[0]) { + case 'boolean': return `_.checkIs(d.nextBoolean(), ${literal(p[0])})`; + case 'string': return `_.checkIs(d.nextString(), ${literal(p[0])})`; + case 'number': return `_.checkIs(d.nextSignedInteger(), ${literal(p[0])})`; + case 'symbol': return `_.checkIs(d.nextSymbol(), ${literal(p[0])})`; + default: return `_.checkIs(d.next(), ${literal(p[0])})`; + } + case M.$ref: + return lookup(refPosition(p), p, env, + (_p) => `decode${p[1].description!}(d)`, + (p) => decoderFor(p), + (mod, modPath,_p) => { + imports.add([mod, modPath]); + return `${mod}.decode${p[1].description!}(d)`; + }); + case M.$or: + return opseq('void 0', ' ?? ', ... p[0].map(pp => decoderFor(pp))); + case M.$and: + switch (p[0].length) { + case 0: return `d.next()`; + case 1: return decoderFor(p[0][0]); + default: { + const tmp = gentemp(); + const [pp0, ... ppN] = p[0]; + const otherChecks = + opseq('false', ' && ', ... ppN.map(pp => predicateFor(tmp, pp))); + return seq(`((${tmp} = `, decoderFor(pp0), `) != void 0) && `, + otherChecks, ` ? ${tmp} : void 0`); + } + } + case M.$pointer: + return `_decodePtr(d)`; + case M.$rec: + return fnblock( + seq(`const M = d.mark()`), + seq(`if (!d.openRecord()) return void 0`), + seq(`const L = `, decoderFor(p[0])), + seq(`if (L === void 0) { d.restoreMark(M); return void 0; }`), + seq(`const Fs = (`, decoderFor(p[1], true), `) as any`), + seq(`if (Fs === void 0) { d.restoreMark(M); return void 0; }`), + seq(`return _.Record`, + anglebrackets(typeFor(p[0]), typeFor(p[1])), + parens(`L`, `Fs`))); + case M.$tuple: + return fnblock( + seq(`const M = d.mark()`), + ... recordFields ? [] : [seq(`if (!d.openSequence()) return void 0`)], + ... p[0].map((pp, i) => + seq(`const v${i} = `, decoderFor(unname(pp)), `; `, + `if (v${i} === void 0) { d.restoreMark(M); return void 0; }`)), + seq(`if (!d.closeCompound()) { d.restoreMark(M); return void 0; }`), + seq(`return [${p[0].map((_pp, i) => `v${i}`).join(', ')}] as `, typeFor(p))); + case M.$tuple_STAR_: + return fnblock( + seq(`const M = d.mark()`), + ... recordFields ? [] : [seq(`if (!d.openSequence()) return void 0`)], + ... p[0].map((pp, i) => + seq(`const v${i} = `, decoderFor(unname(pp)), `; `, + `if (v${i} === void 0) { d.restoreMark(M); return void 0; }`)), + seq(`const vN: Array<`, typeFor(unname(p[1])), `> = []`), + seq(`let tmp: undefined | `, typeFor(unname(p[1]))), + seq(`while ((tmp = `, decoderFor(unname(p[1])), `) !== void 0) vN.push(tmp)`), + seq(`if (!d.closeCompound()) { d.restoreMark(M); return void 0; }`), + (p[0].length === 0 + ? seq(`return vN`) + : seq(`return [${p[0].map((_pp, i) => `v${i}`).join(', ')}, ... vN] as `, + typeFor(p)))); + case M.$setof: + return fnblock( + seq(`const M = d.mark()`), + seq(`if (!d.openSet()) return void 0`), + seq(`const r: `, typeFor(p), ` = new _.KeyedSet()`), + seq(`let tmp: undefined | `, typeFor(p[0])), + seq(`while ((tmp = `, decoderFor(p[0]), `) !== void 0) r.add(tmp)`), + seq(`if (!d.closeCompound()) { d.restoreMark(M); return void 0; }`), + seq(`return r`)); + case M.$dictof: + return fnblock( + seq(`const M = d.mark()`), + seq(`if (!d.openDictionary()) return void 0`), + seq(`const r: `, typeFor(p), ` = new _.KeyedDictionary()`), + seq(`let K: undefined | `, typeFor(p[0])), + seq(`while ((K = `, decoderFor(p[0]), `) !== void 0) `, block( + seq(`const V = `, decoderFor(p[1])), + seq(`if (V === void 0) { d.restoreMark(M); return void 0; }`), + seq(`r.set(K, V)`))), + seq(`if (!d.closeCompound()) { d.restoreMark(M); return void 0; }`), + seq(`return r`)); + case M.$dict: + return fnblock( + seq(`const M = d.mark()`), + seq(`const r = d.next()`), + seq(`if (!(`, predicateFor('r', p), `)) { d.restoreMark(M); return void 0; }`), + seq(`return r`)); + default: + ((_p: never) => {})(p); + throw new Error("Unreachable"); + } + } + function typeFor(p: Pattern): Item { switch (p.label) { case M.$atom: @@ -205,23 +318,37 @@ export function compile(env: Environment, schema: Schema, options: CompilerOptio types.push( seq(`export type ${name.description!} = `, typeFor(pattern), `;`)); functions.push( - seq('export function ', `is${name.description!}`, + seq(`export function is${name.description!}`, '(v: any): v is ', name.description!, ' ', block( ... temps.length > 0 ? [seq('let ', commas(... temps), ': any')] : [], seq('return ', recognizer)))); functions.push( - seq('export function ', `as${name.description!}`, + seq(`export function as${name.description!}`, '(v: any): ', name.description!, ' ', block( seq(`if (!is${name.description!}(v)) `, block(`throw new TypeError(\`Invalid ${name.description!}: \${_.stringify(v)}\`)`), ' else ', block(`return v`))))); + + temps = []; + const decoder = decoderFor(pattern); + functions.push( + seq(`export function decode${name.description!}`, + `(d: _.TypedDecoder<_ptr>): ${name.description!} | undefined `, + block( + ... temps.length > 0 ? [seq('let ', commas(... temps), ': any')] : [], + seq(`return `, decoder)))); } types.push(seq('export type _ptr = ', pointerName === false ? 'never' : typeFor(pointerName), `;`)); types.push(`export type _val = _.Value<_ptr>;`); + functions.push(seq(`export const _decodePtr = `, + (pointerName === false + ? '() => { throw new _.DecodeError("Pointers forbidden"); }' + : seq(`(d: _.TypedDecoder<_ptr>) => `, decoderFor(pointerName))), + `;`)); const f = new Formatter(); f.write(`import * as _ from ${JSON.stringify(options.preservesModule ?? '@preserves/core')};\n`); diff --git a/implementations/javascript/packages/schema/src/gen/schema.ts b/implementations/javascript/packages/schema/src/gen/schema.ts index ac53a40..def579e 100644 --- a/implementations/javascript/packages/schema/src/gen/schema.ts +++ b/implementations/javascript/packages/schema/src/gen/schema.ts @@ -137,18 +137,87 @@ export function asSchema(v: any): Schema { if (!isSchema(v)) {throw new TypeError(`Invalid Schema: ${_.stringify(v)}`);} else {return v;}; } +export function decodeSchema(d: _.TypedDecoder<_ptr>): Schema | undefined { + let _tmp0, _tmp1, _tmp2: any; + return ((() => { + const M = d.mark(); + if (!d.openRecord()) return void 0; + const L = _.checkIs(d.nextSymbol(), $schema); + if (L === void 0) { d.restoreMark(M); return void 0; }; + const Fs = (((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const v0 = ((() => { + const M = d.mark(); + const r = d.next(); + if (!(( + _.Dictionary.isDictionary<_val, _ptr>(r) && + ((_tmp0 = r.get($version)) !== void 0 && isVersion(_tmp0)) && + ((_tmp1 = r.get($pointer)) !== void 0 && isPointerName(_tmp1)) && + ( + (_tmp2 = r.get($definitions)) !== void 0 && ( + _.Dictionary.isDictionary<_val, _ptr>(_tmp2) && + ((() => { + for (const e of _tmp2) { + if (!(typeof e[0] === 'symbol')) return false; + if (!(isPattern(e[1]))) return false; + }; + return true; + })()) + ) + ) + ))) { d.restoreMark(M); return void 0; }; + return r; + })()); if (v0 === void 0) { d.restoreMark(M); return void 0; }; + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return [v0] as [ + ( + { + get(k: typeof $version): Version; + get(k: typeof $pointer): PointerName; + get(k: typeof $definitions): _.KeyedDictionary; + has(k: typeof $version): true; + has(k: typeof $pointer): true; + has(k: typeof $definitions): true; + } & _.Dictionary<_val, _ptr> + ) + ]; + })())) as any; + if (Fs === void 0) { d.restoreMark(M); return void 0; }; + return _.Record< + (typeof $schema), + [ + ( + { + get(k: typeof $version): Version; + get(k: typeof $pointer): PointerName; + get(k: typeof $definitions): _.KeyedDictionary; + has(k: typeof $version): true; + has(k: typeof $pointer): true; + has(k: typeof $definitions): true; + } & _.Dictionary<_val, _ptr> + ) + ] + >(L, Fs); + })()); +} + export function isVersion(v: any): v is Version {return _.is(v, $1);} export function asVersion(v: any): Version { if (!isVersion(v)) {throw new TypeError(`Invalid Version: ${_.stringify(v)}`);} else {return v;}; } +export function decodeVersion(d: _.TypedDecoder<_ptr>): Version | undefined {return _.checkIs(d.nextSignedInteger(), $1);} + export function isPointerName(v: any): v is PointerName {return (isRef(v) || _.is(v, __lit5));} export function asPointerName(v: any): PointerName { if (!isPointerName(v)) {throw new TypeError(`Invalid PointerName: ${_.stringify(v)}`);} else {return v;}; } +export function decodePointerName(d: _.TypedDecoder<_ptr>): PointerName | undefined {return (decodeRef(d) ?? _.checkIs(d.nextBoolean(), __lit5));} + export function isPattern(v: any): v is Pattern { return ( ( @@ -267,6 +336,254 @@ export function asPattern(v: any): Pattern { if (!isPattern(v)) {throw new TypeError(`Invalid Pattern: ${_.stringify(v)}`);} else {return v;}; } +export function decodePattern(d: _.TypedDecoder<_ptr>): Pattern | undefined { + return ( + ((() => { + const M = d.mark(); + if (!d.openRecord()) return void 0; + const L = _.checkIs(d.nextSymbol(), $atom); + if (L === void 0) { d.restoreMark(M); return void 0; }; + const Fs = (((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const v0 = ( + _.checkIs(d.nextSymbol(), $Boolean) ?? + _.checkIs(d.nextSymbol(), $Float) ?? + _.checkIs(d.nextSymbol(), $Double) ?? + _.checkIs(d.nextSymbol(), $SignedInteger) ?? + _.checkIs(d.nextSymbol(), $String) ?? + _.checkIs(d.nextSymbol(), $ByteString) ?? + _.checkIs(d.nextSymbol(), $Symbol) + ); if (v0 === void 0) { d.restoreMark(M); return void 0; }; + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return [v0] as [ + ( + (typeof $Boolean) | + (typeof $Float) | + (typeof $Double) | + (typeof $SignedInteger) | + (typeof $String) | + (typeof $ByteString) | + (typeof $Symbol) + ) + ]; + })())) as any; + if (Fs === void 0) { d.restoreMark(M); return void 0; }; + return _.Record< + (typeof $atom), + [ + ( + (typeof $Boolean) | + (typeof $Float) | + (typeof $Double) | + (typeof $SignedInteger) | + (typeof $String) | + (typeof $ByteString) | + (typeof $Symbol) + ) + ] + >(L, Fs); + })()) ?? + ((() => { + const M = d.mark(); + if (!d.openRecord()) return void 0; + const L = _.checkIs(d.nextSymbol(), $pointer); + if (L === void 0) { d.restoreMark(M); return void 0; }; + const Fs = (((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return [] as []; + })())) as any; + if (Fs === void 0) { d.restoreMark(M); return void 0; }; + return _.Record<(typeof $pointer), []>(L, Fs); + })()) ?? + ((() => { + const M = d.mark(); + if (!d.openRecord()) return void 0; + const L = _.checkIs(d.nextSymbol(), $lit); + if (L === void 0) { d.restoreMark(M); return void 0; }; + const Fs = (((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const v0 = d.next(); if (v0 === void 0) { d.restoreMark(M); return void 0; }; + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return [v0] as [_val]; + })())) as any; + if (Fs === void 0) { d.restoreMark(M); return void 0; }; + return _.Record<(typeof $lit), [_val]>(L, Fs); + })()) ?? + decodeRef(d) ?? + ((() => { + const M = d.mark(); + if (!d.openRecord()) return void 0; + const L = _.checkIs(d.nextSymbol(), $or); + if (L === void 0) { d.restoreMark(M); return void 0; }; + const Fs = (((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const v0 = ((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const vN: Array = []; + let tmp: undefined | Pattern; + while ((tmp = decodePattern(d)) !== void 0) vN.push(tmp); + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return vN; + })()); if (v0 === void 0) { d.restoreMark(M); return void 0; }; + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return [v0] as [Array]; + })())) as any; + if (Fs === void 0) { d.restoreMark(M); return void 0; }; + return _.Record<(typeof $or), [Array]>(L, Fs); + })()) ?? + ((() => { + const M = d.mark(); + if (!d.openRecord()) return void 0; + const L = _.checkIs(d.nextSymbol(), $and); + if (L === void 0) { d.restoreMark(M); return void 0; }; + const Fs = (((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const v0 = ((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const vN: Array = []; + let tmp: undefined | Pattern; + while ((tmp = decodePattern(d)) !== void 0) vN.push(tmp); + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return vN; + })()); if (v0 === void 0) { d.restoreMark(M); return void 0; }; + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return [v0] as [Array]; + })())) as any; + if (Fs === void 0) { d.restoreMark(M); return void 0; }; + return _.Record<(typeof $and), [Array]>(L, Fs); + })()) ?? + ((() => { + const M = d.mark(); + if (!d.openRecord()) return void 0; + const L = _.checkIs(d.nextSymbol(), $rec); + if (L === void 0) { d.restoreMark(M); return void 0; }; + const Fs = (((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const v0 = decodePattern(d); if (v0 === void 0) { d.restoreMark(M); return void 0; }; + const v1 = decodePattern(d); if (v1 === void 0) { d.restoreMark(M); return void 0; }; + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return [v0, v1] as [Pattern, Pattern]; + })())) as any; + if (Fs === void 0) { d.restoreMark(M); return void 0; }; + return _.Record<(typeof $rec), [Pattern, Pattern]>(L, Fs); + })()) ?? + ((() => { + const M = d.mark(); + if (!d.openRecord()) return void 0; + const L = _.checkIs(d.nextSymbol(), $tuple); + if (L === void 0) { d.restoreMark(M); return void 0; }; + const Fs = (((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const v0 = ((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const vN: Array = []; + let tmp: undefined | NamedPattern; + while ((tmp = decodeNamedPattern(d)) !== void 0) vN.push(tmp); + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return vN; + })()); if (v0 === void 0) { d.restoreMark(M); return void 0; }; + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return [v0] as [Array]; + })())) as any; + if (Fs === void 0) { d.restoreMark(M); return void 0; }; + return _.Record<(typeof $tuple), [Array]>(L, Fs); + })()) ?? + ((() => { + const M = d.mark(); + if (!d.openRecord()) return void 0; + const L = _.checkIs(d.nextSymbol(), $tuple_STAR_); + if (L === void 0) { d.restoreMark(M); return void 0; }; + const Fs = (((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const v0 = ((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const vN: Array = []; + let tmp: undefined | NamedPattern; + while ((tmp = decodeNamedPattern(d)) !== void 0) vN.push(tmp); + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return vN; + })()); if (v0 === void 0) { d.restoreMark(M); return void 0; }; + const v1 = decodeNamedPattern(d); if (v1 === void 0) { d.restoreMark(M); return void 0; }; + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return [v0, v1] as [Array, NamedPattern]; + })())) as any; + if (Fs === void 0) { d.restoreMark(M); return void 0; }; + return _.Record<(typeof $tuple_STAR_), [Array, NamedPattern]>(L, Fs); + })()) ?? + ((() => { + const M = d.mark(); + if (!d.openRecord()) return void 0; + const L = _.checkIs(d.nextSymbol(), $setof); + if (L === void 0) { d.restoreMark(M); return void 0; }; + const Fs = (((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const v0 = decodePattern(d); if (v0 === void 0) { d.restoreMark(M); return void 0; }; + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return [v0] as [Pattern]; + })())) as any; + if (Fs === void 0) { d.restoreMark(M); return void 0; }; + return _.Record<(typeof $setof), [Pattern]>(L, Fs); + })()) ?? + ((() => { + const M = d.mark(); + if (!d.openRecord()) return void 0; + const L = _.checkIs(d.nextSymbol(), $dictof); + if (L === void 0) { d.restoreMark(M); return void 0; }; + const Fs = (((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const v0 = decodePattern(d); if (v0 === void 0) { d.restoreMark(M); return void 0; }; + const v1 = decodePattern(d); if (v1 === void 0) { d.restoreMark(M); return void 0; }; + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return [v0, v1] as [Pattern, Pattern]; + })())) as any; + if (Fs === void 0) { d.restoreMark(M); return void 0; }; + return _.Record<(typeof $dictof), [Pattern, Pattern]>(L, Fs); + })()) ?? + ((() => { + const M = d.mark(); + if (!d.openRecord()) return void 0; + const L = _.checkIs(d.nextSymbol(), $dict); + if (L === void 0) { d.restoreMark(M); return void 0; }; + const Fs = (((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const v0 = ((() => { + const M = d.mark(); + if (!d.openDictionary()) return void 0; + const r: _.KeyedDictionary<_val, Pattern, _ptr> = new _.KeyedDictionary(); + let K: undefined | _val; + while ((K = d.next()) !== void 0) { + const V = decodePattern(d); + if (V === void 0) { d.restoreMark(M); return void 0; }; + r.set(K, V); + }; + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return r; + })()); if (v0 === void 0) { d.restoreMark(M); return void 0; }; + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return [v0] as [_.KeyedDictionary<_val, Pattern, _ptr>]; + })())) as any; + if (Fs === void 0) { d.restoreMark(M); return void 0; }; + return _.Record<(typeof $dict), [_.KeyedDictionary<_val, Pattern, _ptr>]>(L, Fs); + })()) + ); +} + export function isNamedPattern(v: any): v is NamedPattern { return ( ( @@ -282,6 +599,28 @@ export function asNamedPattern(v: any): NamedPattern { if (!isNamedPattern(v)) {throw new TypeError(`Invalid NamedPattern: ${_.stringify(v)}`);} else {return v;}; } +export function decodeNamedPattern(d: _.TypedDecoder<_ptr>): NamedPattern | undefined { + return ( + ((() => { + const M = d.mark(); + if (!d.openRecord()) return void 0; + const L = _.checkIs(d.nextSymbol(), $named); + if (L === void 0) { d.restoreMark(M); return void 0; }; + const Fs = (((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const v0 = d.nextSymbol(); if (v0 === void 0) { d.restoreMark(M); return void 0; }; + const v1 = decodePattern(d); if (v1 === void 0) { d.restoreMark(M); return void 0; }; + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return [v0, v1] as [symbol, Pattern]; + })())) as any; + if (Fs === void 0) { d.restoreMark(M); return void 0; }; + return _.Record<(typeof $named), [symbol, Pattern]>(L, Fs); + })()) ?? + decodePattern(d) + ); +} + export function isRef(v: any): v is Ref { return ( _.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && @@ -294,12 +633,33 @@ export function asRef(v: any): Ref { if (!isRef(v)) {throw new TypeError(`Invalid Ref: ${_.stringify(v)}`);} else {return v;}; } +export function decodeRef(d: _.TypedDecoder<_ptr>): Ref | undefined { + return ((() => { + const M = d.mark(); + if (!d.openRecord()) return void 0; + const L = _.checkIs(d.nextSymbol(), $ref); + if (L === void 0) { d.restoreMark(M); return void 0; }; + const Fs = (((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const v0 = decodeModuleRef(d); if (v0 === void 0) { d.restoreMark(M); return void 0; }; + const v1 = d.nextSymbol(); if (v1 === void 0) { d.restoreMark(M); return void 0; }; + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return [v0, v1] as [ModuleRef, symbol]; + })())) as any; + if (Fs === void 0) { d.restoreMark(M); return void 0; }; + return _.Record<(typeof $ref), [ModuleRef, symbol]>(L, Fs); + })()); +} + export function isModuleRef(v: any): v is ModuleRef {return (_.is(v, $thisModule) || isModulePath(v));} export function asModuleRef(v: any): ModuleRef { if (!isModuleRef(v)) {throw new TypeError(`Invalid ModuleRef: ${_.stringify(v)}`);} else {return v;}; } +export function decodeModuleRef(d: _.TypedDecoder<_ptr>): ModuleRef | undefined {return (_.checkIs(d.nextSymbol(), $thisModule) ?? decodeModulePath(d));} + export function isModulePath(v: any): v is ModulePath { return ( _.Array.isArray(v) && @@ -313,3 +673,17 @@ export function asModulePath(v: any): ModulePath { if (!isModulePath(v)) {throw new TypeError(`Invalid ModulePath: ${_.stringify(v)}`);} else {return v;}; } +export function decodeModulePath(d: _.TypedDecoder<_ptr>): ModulePath | undefined { + return ((() => { + const M = d.mark(); + if (!d.openSequence()) return void 0; + const vN: Array = []; + let tmp: undefined | symbol; + while ((tmp = d.nextSymbol()) !== void 0) vN.push(tmp); + if (!d.closeCompound()) { d.restoreMark(M); return void 0; }; + return vN; + })()); +} + +export const _decodePtr = () => { throw new _.DecodeError("Pointers forbidden"); }; +