diff --git a/implementations/javascript/package.json b/implementations/javascript/package.json index 3d7000b..29e6fbb 100644 --- a/implementations/javascript/package.json +++ b/implementations/javascript/package.json @@ -9,6 +9,7 @@ }, "repository": "gitlab:preserves/preserves", "main": "lib/index.js", + "types": "lib/index.d.ts", "author": "Tony Garnock-Jones ", "devDependencies": { "@types/jest": "^26.0.19", diff --git a/implementations/javascript/src/codec.ts b/implementations/javascript/src/codec.ts index 7ee522e..c978a8b 100644 --- a/implementations/javascript/src/codec.ts +++ b/implementations/javascript/src/codec.ts @@ -4,7 +4,6 @@ import { underlying, Annotated, Dictionary, Set, Bytes, Record, Single, Double, - isSet, isDictionary, BytesLike, Value, } from './values'; @@ -117,8 +116,8 @@ export class Decoder { return this.includeAnnotations ? new Annotated(v) : v; } - static dictionaryFromArray(vs: Value[]): Dictionary { - const d = new Dictionary(); + static dictionaryFromArray(vs: Value[]): Dictionary { + const d = new Dictionary(); if (vs.length % 2) throw new DecodeError("Missing dictionary value"); for (let i = 0; i < vs.length; i += 2) { d.set(vs[i], vs[i+1]); @@ -294,7 +293,7 @@ export class Encoder { this.emitbyte(Tag.End); } - push(v: Value) { + push(v: any) { if (typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function') { v[PreserveOn](this); } @@ -327,30 +326,6 @@ export class Encoder { else if (Array.isArray(v)) { this.encodevalues(Tag.Sequence, v); } - else if (isSet(v)) { - if (this.canonical) { - const pieces = v._map((_v, k) => encode(k, { canonical: true })); - pieces.sort(Bytes.compare); - this.encoderawvalues(Tag.Set, pieces); - } else { - this.encodevalues(Tag.Set, v); - } - } - else if (isDictionary(v)) { - if (this.canonical) { - const pieces = v._map((v, k) => Bytes.concat([encode(k, { canonical: true }), - encode(v, { canonical: true })])); - pieces.sort(Bytes.compare); - this.encoderawvalues(Tag.Dictionary, pieces); - } else { - this.emitbyte(Tag.Dictionary); - v._forEach((v, k) => { - this.push(k); - this.push(v); - }); - this.emitbyte(Tag.End); - } - } else if (typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function') { this.encodevalues(Tag.Sequence, v as Iterable); } @@ -361,10 +336,10 @@ export class Encoder { } } -export function encode(v: Value, options?: EncoderOptions): Bytes { +export function encode(v: any, options?: EncoderOptions): Bytes { return new Encoder(options).push(v).contents(); } -export function encodeWithAnnotations(v: Value, options: EncoderOptions = {}): Bytes { +export function encodeWithAnnotations(v: any, options: EncoderOptions = {}): Bytes { return encode(v, { ... options, includeAnnotations: true }); } diff --git a/implementations/javascript/src/flex.ts b/implementations/javascript/src/flex.ts new file mode 100644 index 0000000..1d098d5 --- /dev/null +++ b/implementations/javascript/src/flex.ts @@ -0,0 +1,223 @@ +// FlexMap, FlexSet: like built-in Map and Set, but with a +// canonicalization function which gives us the possibility of a +// coarser equivalence than the identity equivalence used in Map and +// Set. + +// A Canonicalizer represents the equivalence you have in mind. For +// +// c: Canonicalizer +// eqv: Equivalence +// v1: V +// v2: V +// +// where `eqv` is the equivalence you want, +// +// eqv(v1, v2) ⇔ c(v1) === c(v2) +// +export type Canonicalizer = (v: V) => string; +export type Equivalence = (v1: V, v2: V) => boolean; + +export type IdentityMap = Map; +export type IdentitySet = Set; +export const IdentityMap = Map; +export const IdentitySet = Set; + +export function _iterMap(i: Iterator | undefined, f : (s: S) => T): IterableIterator { + if (!i) return void 0; + const _f = (r: IteratorResult): IteratorResult => { + const { done, value } = r; + return { done, value: done ? void 0 : f(value) }; + }; + return { + next: (v?: any): IteratorResult => _f(i.next(v)), + return: (v?: any): IteratorResult => _f(i.return(v)), + throw: (e?: any): IteratorResult => _f(i.throw(e)), + [Symbol.iterator]() { return this; }, + }; +} + +export class FlexMap implements Map { + readonly items: Map; + readonly canonicalizer: Canonicalizer; + + constructor(c: Canonicalizer, items?: Iterable) { + this.canonicalizer = c; + this.items = new Map((items === void 0) + ? void 0 + : _iterMap(items[Symbol.iterator](), ([k, v]) => [this._key(k), [k, v]])); + } + + _key(k: K): string { + return this.canonicalizer(k); + } + + get(k: K, defaultValue?: V): V | undefined { + const e = this.items.get(this._key(k)); + return (e === void 0) ? defaultValue : e[1]; + } + + set(k: K, v: V): this { + this.items.set(this._key(k), [k, v]); + return this; + } + + forEach(f: > (v: V, k: K, map: T) => void, thisArg?: any) { + this.items.forEach(([k, v]) => f.call(thisArg, v, k, this)); + } + + entries(): IterableIterator<[K, V]> { + return this.items.values(); + } + + keys(): IterableIterator { + return _iterMap(this.items.values(), ([k, _v]) => k); + } + + values(): IterableIterator { + return _iterMap(this.items.values(), ([_k, v]) => v); + } + + delete(k: K): boolean { + return this.items.delete(this._key(k)); + } + + clear() { + this.items.clear(); + } + + has(k: K): boolean { + return this.items.has(this._key(k)); + } + + get size(): number { + return this.items.size; + } + + [Symbol.iterator](): IterableIterator<[K, V]> { + return this.items.values(); + } + + [Symbol.toStringTag] = 'FlexMap'; + + equals(other: any, eqv: Equivalence = (v1, v2) => v1 === v2): boolean { + if (!('size' in other && 'has' in other && 'get' in other)) return false; + if (this.size !== other.size) return false; + for (let [k, v] of this.items.values()) { + if (!other.has(k)) return false; + if (!eqv(v, other.get(k))) return false; + } + return true; + } + + update(key: K, + f: (oldValue?: V) => V | undefined, + defaultValue?: V, + eqv: Equivalence = (v1, v2) => v1 === v2): number + { + const ks = this._key(key); + if (this.items.has(ks)) { + const oldValue = this.items.get(ks)[1]; + const newValue = f(oldValue); + if (newValue === void 0) { + this.items.delete(ks); + return -1; + } else { + if (!eqv(newValue, oldValue)) this.items.set(ks, [key, newValue]); + return 0; + } + } else { + const newValue = f(defaultValue); + if (newValue === void 0) { + return 0; + } else { + this.items.set(ks, [key, newValue]); + return 1; + } + } + } + + canonicalKeys(): IterableIterator { + return this.items.keys(); + } +} + +export class FlexSet implements Set { + readonly items: Map; + readonly canonicalizer: Canonicalizer; + + constructor(c: Canonicalizer, items?: Iterable) { + this.canonicalizer = c; + this.items = new Map((items === void 0) + ? void 0 + : _iterMap(items[Symbol.iterator](), (v) => [this._key(v), v])); + } + + _key(v: V): string { + return this.canonicalizer(v); + } + + has(v: V): boolean { + return this.items.has(this._key(v)); + } + + get(v: V): {item: V} | null { + const vs = this._key(v); + if (this.items.has(vs)) { + return {item: this.items[vs]}; + } else { + return null; + } + } + + add(v: V): this { + this.items[this._key(v)] = v; + return this; + } + + forEach(f: >(v: V, v2: V, set: T) => void, thisArg?: any) { + this.items.forEach((v) => f.call(thisArg, v, v, this)); + } + + entries(): IterableIterator<[V, V]> { + return _iterMap(this.items.values(), (v) => [v, v]); + } + + keys(): IterableIterator { + return this.items.values(); + } + + values(): IterableIterator { + return this.items.values(); + } + + delete(v: V): boolean { + return this.items.delete(this._key(v)); + } + + clear() { + this.items.clear(); + } + + get size(): number { + return this.items.size; + } + + [Symbol.iterator](): IterableIterator { + return this.items.values(); + } + + [Symbol.toStringTag] = 'FlexSet'; + + equals(other: any): boolean { + if (!('size' in other && 'has' in other)) return false; + if (this.size !== other.size) return false; + for (let v of this.items.values()) { + if (!other.has(v)) return false; + } + return true; + } + + canonicalValues(): IterableIterator { + return this.items.keys(); + } +} diff --git a/implementations/javascript/src/index.ts b/implementations/javascript/src/index.ts index 6926d5b..0f84416 100644 --- a/implementations/javascript/src/index.ts +++ b/implementations/javascript/src/index.ts @@ -1,3 +1,4 @@ +export * from './flex'; export * from './symbols'; export * from './codec'; export * from './values'; diff --git a/implementations/javascript/src/text.ts b/implementations/javascript/src/text.ts index fbbe98d..307b000 100644 --- a/implementations/javascript/src/text.ts +++ b/implementations/javascript/src/text.ts @@ -1,10 +1,14 @@ import { Value } from './values'; -export function stringify(x: Value): string { +export function stringify(x: any): string { if (typeof x === 'object' && x !== null && 'asPreservesText' in x) { return x.asPreservesText(); } else { - return JSON.stringify(x); + try { + return JSON.stringify(x); + } catch (_e) { + return ('' + x).asPreservesText(); + } } } diff --git a/implementations/javascript/src/values.ts b/implementations/javascript/src/values.ts index c48e358..090d081 100644 --- a/implementations/javascript/src/values.ts +++ b/implementations/javascript/src/values.ts @@ -3,13 +3,15 @@ import { PreserveOn, AsPreserve } from './symbols'; import { Tag } from './constants'; import { Encoder, encode } from './codec'; +import { stringify } from './text'; +import { _iterMap, FlexMap, FlexSet, IdentityMap, IdentitySet } from './flex'; const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); export type Value = Atom | Compound | Annotated; export type Atom = boolean | Single | Double | number | string | Bytes | symbol; -export type Compound = Record | Array | Set | Dictionary; +export type Compound = Record | Array | Set | Dictionary; export function isRecord(x: any): x is Record { return Array.isArray(x) && 'label' in x; @@ -517,12 +519,14 @@ export interface RecordConstructorInfo { arity: number; } -export function is(a: Value, b: Value): boolean { +export function is(a: any, b: any): boolean { + if (isAnnotated(a)) a = a.item; + if (isAnnotated(b)) b = b.item; if (Object.is(a, b)) return true; if (typeof a !== typeof b) return false; if (typeof a === 'object') { if (a === null || b === null) return false; - if ('equals' in a) return a.equals(b); + if ('equals' in a) return a.equals(b, is); if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (!is(a[i], b[i])) return false; @@ -542,176 +546,119 @@ export function isClassOf(ci: RecordConstructorInfo, v: any): v is Record { export type DictionaryType = 'Dictionary' | 'Set'; -export function is_Dictionary(x: any, t: DictionaryType): x is _Dictionary { - return typeof x === 'object' && x !== null && - '_items' in x && - '_dictionaryType' in x && - x._dictionaryType() === t; +export function is_Dictionary(x: any, t: DictionaryType): boolean { + return typeof x === 'object' && x !== null && x[Symbol.toStringTag] === t; } -export const isDictionary = (x: any): x is Dictionary => is_Dictionary(x, 'Dictionary'); +export const isDictionary = (x: any): x is Dictionary => is_Dictionary(x, 'Dictionary'); export const isSet = (x: any): x is Set => is_Dictionary(x, 'Set'); -export type DictionaryEntry = [Value, Value]; +export function _canonicalString(item: Value): string { + const bs = encode(item, { canonical: true })._view; + const s = String.fromCharCode.apply(null, bs); + return s; +} -export abstract class _Dictionary { - _items: { [key: string]: DictionaryEntry } = {}; - - _key(key: Value): string { - const bs = encode(key, { canonical: true })._view; - const s = String.fromCharCode.apply(null, bs); - return s; - } - - _lookup(key: Value): DictionaryEntry | null { - const k = this._key(key); - return k in this._items ? this._items[k] : null; - } - - _set(key: Value, value: Value) { - this._items[this._key(key)] = [key, value]; - } - - _get(key: Value, defaultValue?: Value): Value { - const k = this._key(key); - return k in this._items ? this._items[k][1] : defaultValue; - } - - _delete(key: Value) { - delete this._items[this._key(key)]; - } - - _forEach(f: (value: Value, key: Value) => void) { - for (let ks in this._items) { - const [k, v] = this._items[ks]; - f(v, k); +export class Dictionary extends FlexMap { + static fromJS(x: object): Dictionary { + if (isDictionary(x)) return x as Dictionary; + const d = new Dictionary(); + for (let key in x) { + const value = x[key]; + d.set(key, fromJS(value)); } + return d; } - _map(f: (value: Value, key: Value) => T): Array { - const result = []; - for (let ks in this._items) { - const [k, v] = this._items[ks]; - result.push(f(v, k)); + constructor(items?: Iterable) { + super(_canonicalString, _iterMap(items?.[Symbol.iterator](), ([k,v]) => [fromJS(k), v])); + } + + mapEntries(f: (entry: [Value, T]) => [Value, R]): Dictionary { + const result = new Dictionary(); + for (let oldEntry of this.entries()) { + const newEntry = f(oldEntry); + result.set(newEntry[0], newEntry[1]) } return result; } - equals(other: any): boolean { - if (!Object.is(other.constructor, this.constructor)) return false; - const es1 = Object.entries(this._items); - if (es1.length !== Object.entries(other._items).length) return false; - for (let [ks1, e1] of es1) { - const e2 = other._items[ks1]; - if (!is(e1[1], e2[1])) return false; - } - return true; + asPreservesText(): string { + return '{' + + Array.from(_iterMap(this.entries(), ([k, v]) => + k.asPreservesText() + ': ' + stringify(v))).join(', ') + + '}'; + } + + clone(): Dictionary { + return new Dictionary(this); } toString(): string { return this.asPreservesText(); } - abstract asPreservesText(): string; - abstract _dictionaryType(): DictionaryType; -} + [Symbol.toStringTag] = 'Dictionary'; -export class Dictionary extends _Dictionary { - static fromJS(x: object): Dictionary { - if (isDictionary(x)) return x; - const d = new Dictionary(); - for (let key in x) { - const value = x[key]; - d._set(key, fromJS(value)); + [PreserveOn](encoder: Encoder) { + if (encoder.canonical) { + const pieces = Array.from(this).map(([k, v]) => + Bytes.concat([encode(k, { canonical: true }), + encode(v, { canonical: true })])); + pieces.sort(Bytes.compare); + encoder.encoderawvalues(Tag.Dictionary, pieces); + } else { + encoder.emitbyte(Tag.Dictionary); + this.forEach((v, k) => { + encoder.push(k); + encoder.push(v); + }); + encoder.emitbyte(Tag.End); } - return d; - } - - _dictionaryType(): DictionaryType { - return 'Dictionary'; - } - - set(key: Value, value: Value) { - this._set(key, value); - } - - get(key: Value, defaultValue?: Value): Value { - return this._get(key, defaultValue); - } - - delete(key: Value) { - this._delete(key); - } - - mapEntries(f: (entry: DictionaryEntry) => DictionaryEntry): Dictionary { - const result = new Dictionary(); - for (let ks in this._items) { - const oldEntry = this._items[ks]; - const newEntry = f(oldEntry); - result._set(newEntry[0], newEntry[1]) - } - return result; - } - - forEach(f: (value: Value, key: Value) => void) { - this._forEach(f); - } - - asPreservesText(): string { - return '{' + - this._map((v, k) => k.asPreservesText() + ': ' + v.asPreservesText()).join(', ') + - '}'; } } -export class Set extends _Dictionary implements Iterable { - constructor(items: Iterable = []) { - super(); - for (let item of items) this.add(fromJS(item)); - } - - _dictionaryType(): DictionaryType { - return 'Set'; - } - - add(v: Value) { - this._set(v, true); - } - - delete(v: Value) { - this._delete(v); - } - - includes(key: Value) { - return this._lookup(key) !== null; - } - - forEach(f: (value: Value) => void) { - this._forEach((_v, k) => f(k)); +export class Set extends FlexSet { + constructor(items?: Iterable) { + super(_canonicalString, _iterMap(items?.[Symbol.iterator](), fromJS)); } map(f: (value: Value) => Value): Set { - const result = new Set(); - for (let ks in this._items) { - const k = this._items[ks][0]; - result._set(f(k), true); - } - return result; + return new Set(_iterMap(this[Symbol.iterator](), f)); } - [Symbol.iterator](): Iterator { - return this._map((_v, k) => k)[Symbol.iterator](); + filter(f: (value: Value) => boolean): Set { + const result = new Set(); + for (let k of this) if (f(k)) result.add(k); + return result; } asPreservesText(): string { return '#{' + - this._map((_v, k) => k.asPreservesText()).join(', ') + + Array.from(_iterMap(this.values(), v => v.asPreservesText())).join(', ') + '}'; } + + clone(): Set { + return new Set(this); + } + + [Symbol.toStringTag] = 'Set'; + + [PreserveOn](encoder: Encoder) { + if (encoder.canonical) { + const pieces = Array.from(this).map(k => encode(k, { canonical: true })); + pieces.sort(Bytes.compare); + encoder.encoderawvalues(Tag.Set, pieces); + } else { + encoder.encodevalues(Tag.Set, this); + } + } } export function isAnnotated(x: any): x is Annotated { return typeof x === 'object' && x !== null && + x.constructor.name === 'Annotated' && 'annotations' in x && 'item' in x; } @@ -740,7 +687,7 @@ export class Annotated { } equals(other: any): boolean { - return isAnnotated(other) && is(this.item, other.item); + return is(this.item, isAnnotated(other) ? other.item : other); } hashCode(): number { diff --git a/implementations/javascript/test/codec.test.ts b/implementations/javascript/test/codec.test.ts index 0147453..a132a79 100644 --- a/implementations/javascript/test/codec.test.ts +++ b/implementations/javascript/test/codec.test.ts @@ -132,7 +132,7 @@ describe('common test suite', () => { }); } - const tests = peel(TestCases._.cases(peel(samples))) as Dictionary; + const tests = peel(TestCases._.cases(peel(samples))) as Dictionary; tests.forEach((t0: Value, tName0: Value) => { const tName = Symbol.keyFor(strip(tName0) as symbol); const t = peel(t0) as Record;