From 8442718f961e33bc470354abfd137439c27f8c05 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Sun, 25 Apr 2021 10:42:21 +0200 Subject: [PATCH] Fix module cycles (largely by splitting PointerType in two) --- .../javascript/packages/core/src/decoder.ts | 40 +++++---- .../packages/core/src/dictionary.ts | 8 -- .../javascript/packages/core/src/encoder.ts | 43 +++++++--- .../javascript/packages/core/src/fromjs.ts | 13 +++ .../javascript/packages/core/src/pointer.ts | 82 +++---------------- .../packages/core/src/pointerTypes.ts | 50 +++++++++++ .../javascript/packages/core/src/reader.ts | 32 +++++--- .../javascript/packages/core/src/runtime.ts | 1 + .../packages/core/test/codec.test.ts | 15 ++-- .../packages/core/test/reader.test.ts | 12 +-- .../packages/core/test/values.test.ts | 6 ++ 11 files changed, 172 insertions(+), 130 deletions(-) create mode 100644 implementations/javascript/packages/core/src/pointerTypes.ts diff --git a/implementations/javascript/packages/core/src/decoder.ts b/implementations/javascript/packages/core/src/decoder.ts index 51b61f8..59db2cc 100644 --- a/implementations/javascript/packages/core/src/decoder.ts +++ b/implementations/javascript/packages/core/src/decoder.ts @@ -7,14 +7,14 @@ import { Record } from "./record"; import { Bytes, BytesLike, underlying } from "./bytes"; import { Value } from "./values"; import { is } from "./is"; -import { embed, neverPointerType, Pointer, PointerType } from "./pointer"; +import { embed, GenericPointer, Pointer, PointerTypeDecode } from "./pointer"; export interface DecoderOptions { includeAnnotations?: boolean; } export interface DecoderPointerOptions extends DecoderOptions { - pointerType?: PointerType; + pointerDecode?: PointerTypeDecode; } export interface TypedDecoder { @@ -25,8 +25,8 @@ export interface TypedDecoder { skip(): void; next(): Value; - withPointerType( - pointerType: PointerType, + withPointerDecode( + pointerDecode: PointerTypeDecode, body: (d: TypedDecoder) => R): R; nextBoolean(): boolean | undefined; @@ -161,23 +161,33 @@ export class DecoderState { } } +export const neverPointerTypeDecode: PointerTypeDecode = { + decode(_s: DecoderState): never { + throw new Error("Pointers not permitted at this point in Preserves document"); + }, + + fromValue(_v: Value): never { + throw new Error("Pointers not permitted at this point in Preserves document"); + }, +}; + export class Decoder implements TypedDecoder { state: DecoderState; - pointerType: PointerType; + pointerDecode: PointerTypeDecode; - constructor(state: DecoderState, pointerType?: PointerType); + constructor(state: DecoderState, pointerDecode?: PointerTypeDecode); constructor(packet?: BytesLike, options?: DecoderPointerOptions); constructor( packet_or_state: (DecoderState | BytesLike) = new Uint8Array(0), - options_or_pointerType?: (DecoderPointerOptions | PointerType)) + options_or_pointerDecode?: (DecoderPointerOptions | PointerTypeDecode)) { if (packet_or_state instanceof DecoderState) { this.state = packet_or_state; - this.pointerType = (options_or_pointerType as PointerType) ?? neverPointerType; + this.pointerDecode = (options_or_pointerDecode as PointerTypeDecode) ?? neverPointerTypeDecode; } else { - const options = (options_or_pointerType as DecoderPointerOptions) ?? {}; + const options = (options_or_pointerDecode as DecoderPointerOptions) ?? {}; this.state = new DecoderState(packet_or_state, options); - this.pointerType = options.pointerType ?? neverPointerType; + this.pointerDecode = options.pointerDecode ?? neverPointerTypeDecode; } } @@ -213,7 +223,7 @@ export class Decoder implements TypedDecoder { const v = this.next() as Annotated; return this.state.unshiftAnnotation(a, v); } - case Tag.Pointer: return this.state.wrap(embed(this.pointerType.decode(this.state))); + case Tag.Pointer: return this.state.wrap(embed(this.pointerDecode.decode(this.state))); case Tag.SignedInteger: return this.state.wrap(this.state.nextint(this.state.varint())); case Tag.String: return this.state.wrap(Bytes.from(this.state.nextbytes(this.state.varint())).fromUtf8()); case Tag.ByteString: return this.state.wrap(Bytes.from(this.state.nextbytes(this.state.varint()))); @@ -257,11 +267,11 @@ export class Decoder implements TypedDecoder { this.next(); } - withPointerType( - pointerType: PointerType, + withPointerDecode( + pointerDecode: PointerTypeDecode, body: (d: TypedDecoder) => R): R { - return body(new Decoder(this.state, pointerType)); + return body(new Decoder(this.state, pointerDecode)); } skipAnnotations(): void { @@ -299,7 +309,7 @@ export class Decoder implements TypedDecoder { nextPointer(): Pointer | undefined { this.skipAnnotations(); switch (this.state.nextbyte()) { - case Tag.Pointer: return embed(this.pointerType.decode(this.state)); + case Tag.Pointer: return embed(this.pointerDecode.decode(this.state)); default: return void 0; } } diff --git a/implementations/javascript/packages/core/src/dictionary.ts b/implementations/javascript/packages/core/src/dictionary.ts index c9593ee..e53cd0c 100644 --- a/implementations/javascript/packages/core/src/dictionary.ts +++ b/implementations/javascript/packages/core/src/dictionary.ts @@ -5,7 +5,6 @@ import { PreserveOn } from "./symbols"; import { stringify } from "./text"; import { Value } from "./values"; import { Bytes } from './bytes'; -import { fromJS } from "./fromjs"; import { GenericPointer } from "./pointer"; export type DictionaryType = 'Dictionary' | 'Set'; @@ -79,13 +78,6 @@ export class Dictionary> extends KeyedDictionar static isDictionary>(x: any): x is Dictionary { return x?.[DictionaryType] === 'Dictionary'; } - - static fromJS(x: object): Dictionary> { - if (Dictionary.isDictionary>(x)) return x; - const d = new Dictionary>(); - Object.entries(x).forEach(([key, value]) => d.set(key, fromJS(value))); - return d; - } } export class KeyedSet, T = GenericPointer> extends FlexSet { diff --git a/implementations/javascript/packages/core/src/encoder.ts b/implementations/javascript/packages/core/src/encoder.ts index 54b07ff..d00f9c5 100644 --- a/implementations/javascript/packages/core/src/encoder.ts +++ b/implementations/javascript/packages/core/src/encoder.ts @@ -4,7 +4,7 @@ import { Value } from "./values"; import { PreserveOn } from "./symbols"; import { EncodeError } from "./codec"; import { Record, Tuple } from "./record"; -import { identityPointerType, PointerType } from "./pointer"; +import { GenericPointer, PointerTypeEncode } from "./pointer"; export type Encodable = Value | Preservable | Iterable> | ArrayBufferView; @@ -23,7 +23,7 @@ export interface EncoderOptions { } export interface EncoderPointerOptions extends EncoderOptions { - pointerType?: PointerType; + pointerEncode?: PointerTypeEncode; } export function asLatin1(bs: Uint8Array): string { @@ -34,6 +34,27 @@ function isIterable(v: any): v is Iterable { return typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function'; } +let _nextId = 0; +const _registry = new WeakMap(); +export function pointerId(v: any): number { + let id = _registry.get(v); + if (id === void 0) { + id = _nextId++; + _registry.set(v, id); + } + return id; +} + +export const identityPointerTypeEncode: PointerTypeEncode = { + encode(s: EncoderState, v: any): void { + new Encoder(s, this).push(pointerId(v)); + }, + + toValue(v: any): Value { + return pointerId(v); + } +}; + export class EncoderState { chunks: Array; view: DataView; @@ -138,28 +159,28 @@ export class EncoderState { export class Encoder { state: EncoderState; - pointerType: PointerType | undefined; + pointerEncode: PointerTypeEncode; constructor(options: EncoderPointerOptions); - constructor(state: EncoderState, pointerType?: PointerType); + constructor(state: EncoderState, pointerEncode?: PointerTypeEncode); constructor( state_or_options: (EncoderState | EncoderPointerOptions) = {}, - pointerType?: PointerType) + pointerEncode?: PointerTypeEncode) { if (state_or_options instanceof EncoderState) { this.state = state_or_options; - this.pointerType = pointerType; + this.pointerEncode = pointerEncode ?? identityPointerTypeEncode; } else { this.state = new EncoderState(state_or_options); - this.pointerType = state_or_options.pointerType; + this.pointerEncode = state_or_options.pointerEncode ?? identityPointerTypeEncode; } } - withPointerType( - pointerType: PointerType, + withPointerEncode( + pointerEncode: PointerTypeEncode, body: (e: Encoder) => void): this { - body(new Encoder(this.state, pointerType)); + body(new Encoder(this.state, pointerEncode)); return this; } @@ -232,7 +253,7 @@ export class Encoder { } else { this.state.emitbyte(Tag.Pointer); - (this.pointerType ?? identityPointerType).encode(this.state, v.embeddedValue); + this.pointerEncode.encode(this.state, v.embeddedValue); } return this; // for chaining } diff --git a/implementations/javascript/packages/core/src/fromjs.ts b/implementations/javascript/packages/core/src/fromjs.ts index 43a8276..d22ca4a 100644 --- a/implementations/javascript/packages/core/src/fromjs.ts +++ b/implementations/javascript/packages/core/src/fromjs.ts @@ -58,3 +58,16 @@ export function fromJS(x: any): Value { throw new TypeError("Cannot represent JavaScript value as Preserves: " + x); } + +declare module "./dictionary" { + namespace Dictionary { + export function fromJS(x: object): Dictionary>; + } +} + +Dictionary.fromJS = function (x: object): Dictionary> { + if (Dictionary.isDictionary>(x)) return x; + const d = new Dictionary>(); + Object.entries(x).forEach(([key, value]) => d.set(key, fromJS(value))); + return d; +}; diff --git a/implementations/javascript/packages/core/src/pointer.ts b/implementations/javascript/packages/core/src/pointer.ts index 6dd16c5..40653a7 100644 --- a/implementations/javascript/packages/core/src/pointer.ts +++ b/implementations/javascript/packages/core/src/pointer.ts @@ -1,16 +1,19 @@ -import { Encoder, EncoderState } from "./encoder"; -import { Decoder, DecoderState } from "./decoder"; +import type { EncoderState } from "./encoder"; +import type { DecoderState } from "./decoder"; import type { Value } from "./values"; -import { strip } from "./strip"; -export type PointerType = { - decode(s: DecoderState): T; +export type PointerTypeEncode = { encode(s: EncoderState, v: T): void; - - fromValue(v: Value): T; toValue(v: T): Value; } +export type PointerTypeDecode = { + decode(s: DecoderState): T; + fromValue(v: Value): T; +} + +export type PointerType = PointerTypeEncode & PointerTypeDecode; + export class Pointer { embeddedValue: T; @@ -50,68 +53,3 @@ export class GenericPointer { return this.generic.asPreservesText(); } } - -export const genericPointerType: PointerType = { - decode(s: DecoderState): GenericPointer { - return new GenericPointer(new Decoder(s, this).next()); - }, - - encode(s: EncoderState, v: GenericPointer): void { - new Encoder(s, this).push(v.generic); - }, - - fromValue(v: Value): GenericPointer { - return new GenericPointer(strip(v)); - }, - - toValue(v: GenericPointer): Value { - return v.generic; - } -}; - -export const neverPointerType: PointerType = { - decode(_s: DecoderState): never { - throw new Error("Pointers not permitted at this point in Preserves document"); - }, - - encode(_s: EncoderState, _v: never): void { - throw new Error("Pointers not permitted encoding Preserves document"); - }, - - fromValue(_v: Value): never { - throw new Error("Pointers not permitted at this point in Preserves document"); - }, - - toValue(_v: never): Value { - throw new Error("Pointers not permitted encoding Preserves document"); - } -}; - -let _nextId = 0; -const _registry = new WeakMap(); -export function pointerId(v: any): number { - let id = _registry.get(v); - if (id === void 0) { - id = _nextId++; - _registry.set(v, id); - } - return id; -} - -export const identityPointerType: PointerType = { - decode(_s: DecoderState): any { - throw new Error("Cannot decode identityPointerType"); - }, - - encode(s: EncoderState, v: any): void { - new Encoder(s, this).push(pointerId(v)); - }, - - fromValue(_v: Value): any { - throw new Error("Cannot decode identityPointerType"); - }, - - toValue(v: any): Value { - return pointerId(v); - } -}; diff --git a/implementations/javascript/packages/core/src/pointerTypes.ts b/implementations/javascript/packages/core/src/pointerTypes.ts new file mode 100644 index 0000000..2ccf4f9 --- /dev/null +++ b/implementations/javascript/packages/core/src/pointerTypes.ts @@ -0,0 +1,50 @@ +import { GenericPointer, PointerType, PointerTypeDecode, PointerTypeEncode } from "./pointer"; +import { Encoder, EncoderState, identityPointerTypeEncode } from "./encoder"; +import { genericPointerTypeDecode } from "./reader"; +import { Value } from "./values"; +import { DecoderState, neverPointerTypeDecode } from "./decoder"; + +export const genericPointerTypeEncode: PointerTypeEncode = { + encode(s: EncoderState, v: GenericPointer): void { + new Encoder(s, this).push(v.generic); + }, + + toValue(v: GenericPointer): Value { + return v.generic; + } +}; + +export const genericPointerType: PointerType = + Object.assign({}, + genericPointerTypeDecode, + genericPointerTypeEncode); + +export const neverPointerTypeEncode: PointerTypeEncode = { + encode(_s: EncoderState, _v: never): void { + throw new Error("Pointers not permitted encoding Preserves document"); + }, + + toValue(_v: never): Value { + throw new Error("Pointers not permitted encoding Preserves document"); + } +}; + +export const neverPointerType: PointerType = + Object.assign({}, + neverPointerTypeDecode, + neverPointerTypeEncode); + +export const identityPointerTypeDecode: PointerTypeDecode = { + decode(_s: DecoderState): any { + throw new Error("Cannot decode identityPointerType"); + }, + + fromValue(_v: Value): any { + throw new Error("Cannot decode identityPointerType"); + }, +}; + +export const identityPointerType: PointerType = + Object.assign({}, + identityPointerTypeDecode, + identityPointerTypeEncode); diff --git a/implementations/javascript/packages/core/src/reader.ts b/implementations/javascript/packages/core/src/reader.ts index f9496d7..a5983dc 100644 --- a/implementations/javascript/packages/core/src/reader.ts +++ b/implementations/javascript/packages/core/src/reader.ts @@ -3,14 +3,14 @@ import type { Value } from './values'; import { DecodeError, ShortPacket } from './codec'; import { Dictionary, Set } from './dictionary'; -import { unannotate } from './strip'; +import { strip, unannotate } from './strip'; import { Bytes, unhexDigit } from './bytes'; -import { decode } from './decoder'; +import { decode, Decoder, DecoderState, neverPointerTypeDecode } from './decoder'; import { Record } from './record'; import { Annotated, newPosition, Position, updatePosition } from './annotated'; import { Double, DoubleFloat, Single, SingleFloat } from './float'; import { stringify } from './text'; -import { embed, GenericPointer, genericPointerType, neverPointerType, PointerType } from './pointer'; +import { embed, GenericPointer, PointerTypeDecode } from './pointer'; export interface ReaderStateOptions { includeAnnotations?: boolean; @@ -18,7 +18,7 @@ export interface ReaderStateOptions { } export interface ReaderOptions extends ReaderStateOptions { - pointerType?: PointerType; + pointerDecode?: PointerTypeDecode; } type IntOrFloat = 'int' | 'float'; @@ -275,23 +275,33 @@ export class ReaderState { } } +export const genericPointerTypeDecode: PointerTypeDecode = { + decode(s: DecoderState): GenericPointer { + return new GenericPointer(new Decoder(s, this).next()); + }, + + fromValue(v: Value): GenericPointer { + return new GenericPointer(strip(v)); + }, +}; + export class Reader { state: ReaderState; - pointerType: PointerType; + pointerType: PointerTypeDecode; - constructor(state: ReaderState, pointerType: PointerType); + constructor(state: ReaderState, pointerType: PointerTypeDecode); constructor(buffer: string, options?: ReaderOptions); constructor( state_or_buffer: (ReaderState | string) = '', - pointerType_or_options?: (PointerType | ReaderOptions)) + pointerType_or_options?: (PointerTypeDecode | ReaderOptions)) { if (state_or_buffer instanceof ReaderState) { this.state = state_or_buffer; - this.pointerType = pointerType_or_options as PointerType; + this.pointerType = pointerType_or_options as PointerTypeDecode; } else { const options = (pointerType_or_options as ReaderOptions) ?? {}; this.state = new ReaderState(state_or_buffer, options); - this.pointerType = options.pointerType ?? neverPointerType; + this.pointerType = options.pointerDecode ?? neverPointerTypeDecode; } } @@ -378,12 +388,12 @@ export class Reader { if (!Bytes.isBytes(bs)) this.state.error('ByteString must follow #=', startPos); return decode(bs, { - pointerType: this.pointerType, + pointerDecode: this.pointerType, includeAnnotations: this.state.options.includeAnnotations, }); } case '!': return embed(this.pointerType.fromValue( - new Reader(this.state, genericPointerType).next())); + new Reader(this.state, genericPointerTypeDecode).next())); default: this.state.error(`Invalid # syntax: ${c}`, startPos); } diff --git a/implementations/javascript/packages/core/src/runtime.ts b/implementations/javascript/packages/core/src/runtime.ts index 9befa66..2a16ca8 100644 --- a/implementations/javascript/packages/core/src/runtime.ts +++ b/implementations/javascript/packages/core/src/runtime.ts @@ -12,6 +12,7 @@ export * from './fromjs'; export * from './is'; export * from './merge'; export * from './pointer'; +export * from './pointerTypes'; export * from './reader'; export * from './record'; export * from './strip'; diff --git a/implementations/javascript/packages/core/test/codec.test.ts b/implementations/javascript/packages/core/test/codec.test.ts index 9531dcf..ab4c5a6 100644 --- a/implementations/javascript/packages/core/test/codec.test.ts +++ b/implementations/javascript/packages/core/test/codec.test.ts @@ -17,7 +17,8 @@ import { Decoder, Pointer, embed, - genericPointerType, + genericPointerTypeDecode, + genericPointerTypeEncode, } from '../src/index'; const { Tag } = Constants; import './test-utils'; @@ -144,7 +145,7 @@ describe('encoding and decoding pointers', () => { const pt = new LookasidePointerType(objects); const A = embed({a: 1}); const B = embed({b: 2}); - expect(encode([A, B], { pointerType: pt })).is( + expect(encode([A, B], { pointerEncode: pt })).is( Bytes.from([Tag.Sequence, Tag.Pointer, Tag.SmallInteger_lo, Tag.Pointer, Tag.SmallInteger_lo + 1, @@ -163,7 +164,7 @@ describe('encoding and decoding pointers', () => { Tag.Pointer, Tag.SmallInteger_lo, Tag.Pointer, Tag.SmallInteger_lo + 1, Tag.End - ]), { pointerType: pt })).is([X, Y]); + ]), { pointerDecode: pt })).is([X, Y]); }); it('should store pointers embedded in map keys correctly', () => { const A1a = {a: 1}; @@ -183,7 +184,7 @@ describe('encoding and decoding pointers', () => { describe('common test suite', () => { const samples_bin = fs.readFileSync(__dirname + '/../../../../../tests/samples.bin'); - const samples = decodeWithAnnotations(samples_bin, { pointerType: genericPointerType }); + const samples = decodeWithAnnotations(samples_bin, { pointerDecode: genericPointerTypeDecode }); const TestCases = Record.makeConstructor<{ cases: Dictionary @@ -191,13 +192,13 @@ describe('common test suite', () => { type TestCases = ReturnType; function DS(bs: Bytes) { - return decode(bs, { pointerType: genericPointerType }); + return decode(bs, { pointerDecode: genericPointerTypeDecode }); } function D(bs: Bytes) { - return decodeWithAnnotations(bs, { pointerType: genericPointerType }); + return decodeWithAnnotations(bs, { pointerDecode: genericPointerTypeDecode }); } function E(v: Value) { - return encodeWithAnnotations(v, { pointerType: genericPointerType }); + return encodeWithAnnotations(v, { pointerEncode: genericPointerTypeEncode }); } interface ExpectedValues { diff --git a/implementations/javascript/packages/core/test/reader.test.ts b/implementations/javascript/packages/core/test/reader.test.ts index 0f4a67e..c6b4380 100644 --- a/implementations/javascript/packages/core/test/reader.test.ts +++ b/implementations/javascript/packages/core/test/reader.test.ts @@ -8,21 +8,21 @@ describe('reading common test suite', () => { const samples_pr = fs.readFileSync(__dirname + '/../../../../../tests/samples.pr', 'utf-8'); it('should read equal to decoded binary without annotations', () => { - const s1 = new Reader(samples_pr, { pointerType: genericPointerType, includeAnnotations: false }).next(); - const s2 = new Decoder(samples_bin, { pointerType: genericPointerType, includeAnnotations: false }).next(); + const s1 = new Reader(samples_pr, { pointerDecode: genericPointerType, includeAnnotations: false }).next(); + const s2 = new Decoder(samples_bin, { pointerDecode: genericPointerType, includeAnnotations: false }).next(); expect(s1).is(s2); }); it('should read equal to decoded binary with annotations', () => { - const s1 = new Reader(samples_pr, { pointerType: genericPointerType, includeAnnotations: true }).next(); - const s2 = new Decoder(samples_bin, { pointerType: genericPointerType, includeAnnotations: true }).next(); + const s1 = new Reader(samples_pr, { pointerDecode: genericPointerType, includeAnnotations: true }).next(); + const s2 = new Decoder(samples_bin, { pointerDecode: genericPointerType, includeAnnotations: true }).next(); expect(s1).is(s2); }); it('should read and encode back to binary with annotations', () => { - const s = new Reader(samples_pr, { pointerType: genericPointerType, includeAnnotations: true }).next(); + const s = new Reader(samples_pr, { pointerDecode: genericPointerType, includeAnnotations: true }).next(); const bs = Bytes.toIO(encode(s, { - pointerType: genericPointerType, + pointerEncode: genericPointerType, includeAnnotations: true, canonical: true, })); diff --git a/implementations/javascript/packages/core/test/values.test.ts b/implementations/javascript/packages/core/test/values.test.ts index ab60c99..5b41ee7 100644 --- a/implementations/javascript/packages/core/test/values.test.ts +++ b/implementations/javascript/packages/core/test/values.test.ts @@ -36,3 +36,9 @@ describe('fold', () => { expect(mapPointers(v, _t => embed(w1))).is(v1); }); }); + +describe('fromJS', () => { + it('should map integers to themselves', () => { + expect(fromJS(1)).toBe(1); + }); +});