Different approach to pointer codec; support custom schema-driven decode

This commit is contained in:
Tony Garnock-Jones 2021-03-12 20:41:35 +01:00
parent 4ee9f99529
commit 1cc0325007
9 changed files with 839 additions and 71 deletions

View File

@ -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<T> {
export interface DecoderOptions {
includeAnnotations?: boolean;
decodePointer?: (v: Value<T>) => T;
}
export class Decoder<T> {
packet: Uint8Array;
index: number;
options: DecoderOptions<T>;
export interface DecoderPointerOptions<T> extends DecoderOptions {
decodePointer?(d: TypedDecoder<T>): T | undefined;
}
constructor(packet: BytesLike = new Uint8Array(0), options: DecoderOptions<T> = {}) {
export interface TypedDecoder<T> {
atEnd(): boolean;
mark(): any;
restoreMark(m: any): void;
skip(): void;
next(): Value<T>;
withPointerDecoder<S, R>(decodePointer: (d: TypedDecoder<S>) => S | undefined,
body: (d: TypedDecoder<S>) => 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<T, E extends Value<T>>(actual: Value<T>, expected: E): E | undefined {
return is(actual, expected) ? expected : void 0;
}
function _defaultDecodePointer<T>(_d: TypedDecoder<T>): T | undefined {
throw new DecodeError("No decodePointer function supplied");
}
export class Decoder<T = never> implements TypedDecoder<T> {
packet: Uint8Array;
index = 0;
options: DecoderOptions;
decodePointer: ((d: TypedDecoder<T>) => 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<T> {
}
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<T> {
}
peekend(): boolean {
const matched = this.nextbyte() === Tag.End;
if (!matched) this.index--;
return matched;
return (this.peekbyte() === Tag.End) && (this.advance(), true);
}
nextvalues(): Value<T>[] {
@ -112,11 +158,11 @@ export class Decoder<T> {
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<T> {
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<T> | 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<R>(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<T> | 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<S>(decodePointer: (d: TypedDecoder<S>) => S | undefined): Decoder<S> {
const replacement = new Decoder<S>(this.packet, this.options);
replacement.index = this.index;
replacement.decodePointer = decodePointer;
this.packet = new Uint8Array();
this.index = 0;
this.decodePointer = _defaultDecodePointer;
return replacement;
}
withPointerDecoder<S, R>(decodePointer: (d: TypedDecoder<S>) => S | undefined,
body: (d: TypedDecoder<S>) => R): R
{
const oldDecodePointer = this.decodePointer;
const disguised = this as unknown as Decoder<S>;
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<T>(bs: BytesLike, options?: DecoderOptions<T>) {
return new Decoder(bs, options).next();
export function decode<T>(bs: BytesLike, options: DecoderPointerOptions<T> = {}): Value<T> {
return new Decoder<T>(bs, options).withPointerDecoder<T, Value<T>>(
options.decodePointer ?? _defaultDecodePointer,
d => d.next());
}
export function decodeWithAnnotations<T>(bs: BytesLike, options: DecoderOptions<T> = {}): Annotated<T> {
export function decodeWithAnnotations<T>(bs: BytesLike,
options: DecoderPointerOptions<T> = {}): Annotated<T> {
return decode(bs, { ... options, includeAnnotations: true }) as Annotated<T>;
}

View File

@ -16,10 +16,13 @@ export function isPreservable<T>(v: any): v is Preservable<T> {
return typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function';
}
export interface EncoderOptions<T> {
export interface EncoderOptions {
canonical?: boolean;
includeAnnotations?: boolean;
encodePointer?: (v: T) => Value<T>;
}
export interface EncoderPointerOptions<T> extends EncoderOptions {
encodePointer?: (e: Encoder<T>, v: T) => void;
}
function chunkStr(bs: Uint8Array): string {
@ -30,19 +33,38 @@ function isIterable<T>(v: any): v is Iterable<T> {
return typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function';
}
function _defaultEncodePointer<T>(e: Encoder<T>, v: T): void {
e.push(pointerId(v));
}
export class Encoder<T> {
chunks: Array<Uint8Array>;
view: DataView;
index: number;
options: EncoderOptions<T>;
options: EncoderOptions;
encodePointer: ((e: Encoder<T>, v: T) => void) = _defaultEncodePointer;
constructor(options: EncoderOptions<T> = {}) {
constructor(options: EncoderOptions = {}) {
this.chunks = [];
this.view = new DataView(new ArrayBuffer(256));
this.index = 0;
this.options = options;
}
withPointerEncoder<S>(encodePointer: (e: Encoder<S>, v: S) => void,
body: (e: Encoder<S>) => void): this
{
const oldEncodePointer = this.encodePointer;
const disguised = this as unknown as Encoder<S>;
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<T> {
this.encodevalues(Tag.Sequence, v as Iterable<Value<T>>);
}
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<T>(v: Encodable<T>, options?: EncoderOptions<T>): Bytes {
return new Encoder(options).push(v).contents();
export function encode<T>(v: Encodable<T>, options: EncoderPointerOptions<T> = {}): Bytes {
return new Encoder(options).withPointerEncoder<T>(
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<any>, options?: EncoderOptions<any>): Bytes {
export function canonicalEncode(v: Encodable<any>, options?: EncoderPointerOptions<any>): Bytes
{
if (options === void 0 && !_usingCanonicalEncoder) {
_usingCanonicalEncoder = true;
const bs = _canonicalEncoder.push(v).contents();
@ -220,9 +244,17 @@ export function canonicalEncode(v: Encodable<any>, options?: EncoderOptions<any>
}
export function canonicalString(v: Encodable<any>): 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<T>(v: Encodable<T>, options: EncoderOptions<T> = {}): Bytes {
export function encodeWithAnnotations<T>(v: Encodable<T>,
options: EncoderPointerOptions<T> = {}): Bytes {
return encode(v, { ... options, includeAnnotations: true });
}

View File

@ -168,7 +168,7 @@ export class Reader<T> {
if (!Bytes.isBytes(bs)) this.error('ByteString must follow #=',
startPos);
return decode<T>(bs, {
decodePointer: this.options.decodePointer,
decodePointer: d => this.options.decodePointer?.(d.next()),
includeAnnotations: this.options.includeAnnotations,
});
}

View File

@ -1,4 +1,4 @@
import { Value } from './values';
import type { Value } from './values';
export function stringify(x: any): string {
if (typeof x?.asPreservesText === 'function') {

View File

@ -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<object> {
encodePointer(e: Encoder<object>, 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>): object {
decodePointer(d: TypedDecoder<object>): 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<number, object>();
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<Pointer>(
Record<symbol, any, Pointer>(Symbol.for('R'),
[annotate<Pointer>(Symbol.for('f'),
Symbol.for('af'))]),
Record<symbol, any>(Symbol.for('R'),
[annotate<Pointer>(Symbol.for('f'),
Symbol.for('af'))]),
Symbol.for('ar')),
back: Record<Value<Pointer>, any, Pointer>(Symbol.for('R'), [Symbol.for('f')])
back: Record<Value<Pointer>, any>(Symbol.for('R'), [Symbol.for('f')])
},
annotation6: {
forward: Record<Value<Pointer>, any, Pointer>(
forward: Record<Value<Pointer>, any>(
annotate<Pointer>(Symbol.for('R'),
Symbol.for('ar')),
[annotate<Pointer>(Symbol.for('f'),
Symbol.for('af'))]),
back: Record<symbol, any, Pointer>(Symbol.for('R'), [Symbol.for('f')])
back: Record<symbol, any>(Symbol.for('R'), [Symbol.for('f')])
},
annotation7: {
forward: annotate<Pointer>([], Symbol.for('a'), Symbol.for('b'), Symbol.for('c')),

View File

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

View File

@ -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>): Pointer {
export function readPointer(v: Value<Pointer>): Pointer {
return new Pointer(strip(v));
}
export function encodePointer(w: Pointer): Value<Pointer> {
return w.v;
export function decodePointer(d: TypedDecoder<Pointer>): Pointer {
return readPointer(d.next());
}
export function encodePointer(e: Encoder<Pointer>, w: Pointer): void {
e.push(w.v);
}

View File

@ -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`);

View File

@ -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<symbol, Pattern, _ptr>;
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<symbol, Pattern, _ptr>;
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<Pattern> = [];
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<Pattern>];
})())) as any;
if (Fs === void 0) { d.restoreMark(M); return void 0; };
return _.Record<(typeof $or), [Array<Pattern>]>(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<Pattern> = [];
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<Pattern>];
})())) as any;
if (Fs === void 0) { d.restoreMark(M); return void 0; };
return _.Record<(typeof $and), [Array<Pattern>]>(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<NamedPattern> = [];
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<NamedPattern>];
})())) as any;
if (Fs === void 0) { d.restoreMark(M); return void 0; };
return _.Record<(typeof $tuple), [Array<NamedPattern>]>(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<NamedPattern> = [];
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>, NamedPattern];
})())) as any;
if (Fs === void 0) { d.restoreMark(M); return void 0; };
return _.Record<(typeof $tuple_STAR_), [Array<NamedPattern>, 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<symbol> = [];
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"); };