diff --git a/implementations/javascript/packages/core/src/annotated.ts b/implementations/javascript/packages/core/src/annotated.ts index 054f570..d97bcf8 100644 --- a/implementations/javascript/packages/core/src/annotated.ts +++ b/implementations/javascript/packages/core/src/annotated.ts @@ -1,10 +1,10 @@ import { Encoder } from "./encoder"; import { Tag } from "./constants"; -import { AsPreserve, PreserveOn } from "./symbols"; import { Value } from "./values"; import { is, isAnnotated, IsPreservesAnnotated } from "./is"; import { stringify } from "./text"; import { GenericEmbedded } from "./embedded"; +import type { Preservable } from "./encoder"; export interface Position { line?: number; @@ -53,7 +53,7 @@ export function formatPosition(p: Position | null | string): string { } } -export class Annotated { +export class Annotated implements Preservable { readonly annotations: Array>; readonly pos: Position | null; readonly item: Value; @@ -64,11 +64,15 @@ export class Annotated { this.item = item; } - [AsPreserve](): Value { + __as__preserve__(): Value { return this; } - [PreserveOn](encoder: Encoder) { + static __from_preserve__(v: Value): undefined | Annotated { + return isAnnotated(v) ? v : void 0; + } + + __preserve_on__(encoder: Encoder): void { if (encoder.includeAnnotations) { for (const a of this.annotations) { encoder.state.emitbyte(Tag.Annotation); diff --git a/implementations/javascript/packages/core/src/bytes.ts b/implementations/javascript/packages/core/src/bytes.ts index 6acafba..b7769e0 100644 --- a/implementations/javascript/packages/core/src/bytes.ts +++ b/implementations/javascript/packages/core/src/bytes.ts @@ -1,5 +1,4 @@ import { Tag } from './constants'; -import { AsPreserve, PreserveOn } from './symbols'; import { Encoder, Preservable } from './encoder'; import { Value } from './values'; import { GenericEmbedded } from './embedded'; @@ -127,10 +126,14 @@ export class Bytes implements Preservable { return this.asPreservesText(); } - [AsPreserve](): Value { + __as_preserve__(): Value { return this; } + static __from_preserve__(v: Value): undefined | Bytes { + return Bytes.isBytes(v) ? v : void 0; + } + asPreservesText(): string { return '#"' + this.__asciify() + '"'; } @@ -165,7 +168,7 @@ export class Bytes implements Preservable { return this.toHex(); } - [PreserveOn](encoder: Encoder) { + __preserve_on__(encoder: Encoder) { encoder.state.emitbyte(Tag.ByteString); encoder.state.varint(this.length); encoder.state.emitbytes(this._view); diff --git a/implementations/javascript/packages/core/src/dictionary.ts b/implementations/javascript/packages/core/src/dictionary.ts index de89d46..ccc4e66 100644 --- a/implementations/javascript/packages/core/src/dictionary.ts +++ b/implementations/javascript/packages/core/src/dictionary.ts @@ -1,16 +1,16 @@ import { Encoder, canonicalEncode, canonicalString } from "./encoder"; import { Tag } from "./constants"; import { FlexMap, FlexSet, _iterMap } from "./flex"; -import { PreserveOn } from "./symbols"; import { stringify } from "./text"; import { Value } from "./values"; import { Bytes } from './bytes'; import { GenericEmbedded } from "./embedded"; +import type { Preservable } from "./encoder"; export type DictionaryType = 'Dictionary' | 'Set'; export const DictionaryType = Symbol.for('DictionaryType'); -export class KeyedDictionary, V, T = GenericEmbedded> extends FlexMap { +export class KeyedDictionary, V, T = GenericEmbedded> extends FlexMap implements Preservable { get [DictionaryType](): DictionaryType { return 'Dictionary'; } @@ -51,7 +51,7 @@ export class KeyedDictionary, V, T = GenericEmbedded> extends get [Symbol.toStringTag]() { return 'Dictionary'; } - [PreserveOn](encoder: Encoder) { + __preserve_on__(encoder: Encoder) { if (encoder.canonical) { const entries = Array.from(this); const pieces = entries.map<[Bytes, number]>(([k, _v], i) => [canonicalEncode(k), i]); @@ -78,9 +78,13 @@ export class Dictionary> extends KeyedDictiona static isDictionary>(x: any): x is Dictionary { return x?.[DictionaryType] === 'Dictionary'; } + + static __from_preserve__(v: Value): undefined | Dictionary { + return Dictionary.isDictionary(v) ? v : void 0; + } } -export class KeyedSet, T = GenericEmbedded> extends FlexSet { +export class KeyedSet, T = GenericEmbedded> extends FlexSet implements Preservable { get [DictionaryType](): DictionaryType { return 'Set'; } @@ -119,7 +123,7 @@ export class KeyedSet, T = GenericEmbedded> extends FlexSet) { + __preserve_on__(encoder: Encoder) { if (encoder.canonical) { const pieces = Array.from(this).map<[Bytes, K]>(k => [canonicalEncode(k), k]); pieces.sort((a, b) => Bytes.compare(a[0], b[0])); @@ -134,4 +138,8 @@ export class Set extends KeyedSet, T> { static isSet(x: any): x is Set { return x?.[DictionaryType] === 'Set'; } + + static __from_preserve__(v: Value): undefined | Set { + return Set.isSet(v) ? v : void 0; + } } diff --git a/implementations/javascript/packages/core/src/embedded.ts b/implementations/javascript/packages/core/src/embedded.ts index e0bf7c2..bfbebdf 100644 --- a/implementations/javascript/packages/core/src/embedded.ts +++ b/implementations/javascript/packages/core/src/embedded.ts @@ -33,6 +33,14 @@ export class Embedded { return '#!' + (this.embeddedValue as any).toString(); } } + + __as_preserve__(): T extends R ? Value : never { + return this as any; + } + + static __from_preserve__(v: Value): undefined | Embedded { + return isEmbedded(v) ? v : void 0; + } } export function embed(embeddedValue: T): Embedded { diff --git a/implementations/javascript/packages/core/src/encoder.ts b/implementations/javascript/packages/core/src/encoder.ts index 0e222bd..fc0ccf6 100644 --- a/implementations/javascript/packages/core/src/encoder.ts +++ b/implementations/javascript/packages/core/src/encoder.ts @@ -1,7 +1,6 @@ import { Tag } from "./constants"; import { Bytes } from "./bytes"; import { Value } from "./values"; -import { PreserveOn } from "./symbols"; import { EncodeError } from "./codec"; import { Record, Tuple } from "./record"; import { GenericEmbedded, EmbeddedTypeEncode } from "./embedded"; @@ -10,11 +9,11 @@ export type Encodable = Value | Preservable | Iterable> | ArrayBufferView; export interface Preservable { - [PreserveOn](encoder: Encoder): void; + __preserve_on__(encoder: Encoder): void; } export function isPreservable(v: any): v is Preservable { - return typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function'; + return typeof v === 'object' && v !== null && '__preserve_on__' in v && typeof v.__preserve_on__ === 'function'; } export interface EncoderOptions { @@ -211,11 +210,11 @@ export class Encoder { } push(v: Encodable) { - if (isPreservable(v)) { - v[PreserveOn](this as unknown as Encoder); + if (isPreservable(v)) { + v.__preserve_on__(this); } else if (isPreservable(v)) { - v[PreserveOn](this); + v.__preserve_on__(this); } else if (typeof v === 'boolean') { this.state.emitbyte(v ? Tag.True : Tag.False); diff --git a/implementations/javascript/packages/core/src/float.ts b/implementations/javascript/packages/core/src/float.ts index a2f0980..1b8c8c3 100644 --- a/implementations/javascript/packages/core/src/float.ts +++ b/implementations/javascript/packages/core/src/float.ts @@ -1,6 +1,5 @@ import { Encoder, Preservable } from "./encoder"; import { Tag } from "./constants"; -import { AsPreserve, PreserveOn } from "./symbols"; import { Value } from "./values"; import { GenericEmbedded } from "./embedded"; @@ -44,12 +43,16 @@ export function floatValue(f: any): number { } } -export class SingleFloat extends Float implements Preservable { - [AsPreserve](): Value { +export class SingleFloat extends Float implements Preservable { + __as_preserve__(): Value { return this; } - [PreserveOn](encoder: Encoder) { + static __from_preserve__(v: Value): undefined | SingleFloat { + return Float.isSingle(v) ? v : void 0; + } + + __preserve_on__(encoder: Encoder) { encoder.state.emitbyte(Tag.Float); encoder.state.makeroom(4); encoder.state.view.setFloat32(encoder.state.index, this.value, false); @@ -69,12 +72,16 @@ export function Single(value: number | Float): SingleFloat { return new SingleFloat(value); } -export class DoubleFloat extends Float implements Preservable { - [AsPreserve](): Value { +export class DoubleFloat extends Float implements Preservable { + __as_preserve__(): Value { return this; } - [PreserveOn](encoder: Encoder) { + static __from_preserve__(v: Value): undefined | DoubleFloat { + return Float.isDouble(v) ? v : void 0; + } + + __preserve_on__(encoder: Encoder) { encoder.state.emitbyte(Tag.Double); encoder.state.makeroom(8); encoder.state.view.setFloat64(encoder.state.index, this.value, false); diff --git a/implementations/javascript/packages/core/src/fromjs.ts b/implementations/javascript/packages/core/src/fromjs.ts index 587d846..c676152 100644 --- a/implementations/javascript/packages/core/src/fromjs.ts +++ b/implementations/javascript/packages/core/src/fromjs.ts @@ -1,7 +1,6 @@ import { embed, GenericEmbedded } from "./embedded"; import { Bytes } from "./bytes"; import { Record, Tuple } from "./record"; -import { AsPreserve } from "./symbols"; import { Value } from "./values"; import { Dictionary, Set } from "./dictionary"; @@ -27,8 +26,8 @@ export function fromJS(x: any): Value { if (x === null) { break; } - if (typeof x[AsPreserve] === 'function') { - return x[AsPreserve](); + if (typeof x.__as_preserve__ === 'function') { + return x.__as_preserve__(); } if (Record.isRecord, Tuple>, T>(x)) { return x; diff --git a/implementations/javascript/packages/core/src/index.ts b/implementations/javascript/packages/core/src/index.ts index ca99e59..09aa58e 100644 --- a/implementations/javascript/packages/core/src/index.ts +++ b/implementations/javascript/packages/core/src/index.ts @@ -1,6 +1,18 @@ export * from './runtime'; export * as Constants from './constants'; +import type { Value } from './values'; + +declare global { + interface ArrayConstructor { + __from_preserve__(v: Value): undefined | Array>; + } +} + +Array.__from_preserve__ = (v: Value) => { + return Array.isArray(v) ? v : void 0; +}; + const _Array = Array; type _Array = Array; export { _Array as Array }; diff --git a/implementations/javascript/packages/core/src/runtime.ts b/implementations/javascript/packages/core/src/runtime.ts index eaab6cc..ed9da23 100644 --- a/implementations/javascript/packages/core/src/runtime.ts +++ b/implementations/javascript/packages/core/src/runtime.ts @@ -16,6 +16,5 @@ export * from './merge'; export * from './reader'; export * from './record'; export * from './strip'; -export * from './symbols'; export * from './text'; export * from './values'; diff --git a/implementations/javascript/packages/core/src/symbols.ts b/implementations/javascript/packages/core/src/symbols.ts index 85dce93..a51f0a5 100644 --- a/implementations/javascript/packages/core/src/symbols.ts +++ b/implementations/javascript/packages/core/src/symbols.ts @@ -1,5 +1,23 @@ // Symbols for various Preserves protocols. -export const PreserveOn = Symbol.for('PreserveOn'); -export const AsPreserve = Symbol.for('AsPreserve'); - +// Previously, we had the following: +// +// export const PreserveOn = Symbol.for('PreserveOn'); +// export const AsPreserve = Symbol.for('AsPreserve'); +// export const FromPreserve = Symbol.for('FromPreserve'); +// +// ... and used them to define methods [PreserveOn] and [AsPreserve] +// and functions [FromPreserve] in various namespaces. The methods +// worked well, but the functions didn't work with TypeScript as of TS +// 4.5 because there's no way to export a symbol-named property from a +// `namespace`. See +// https://github.com/microsoft/TypeScript/issues/36813. +// +// So, instead, I've converted them as follows: +// +// [PreserveOn] ==> __preserve_on__ +// [AsPreserve] ==> __as_preserve__ +// [FromPreserve] ==> __from_preserve__ +// +// and emphasised use of functions `fromJS` and `toJS` to get away +// from the `__fiddly_details__`.