diff --git a/implementations/javascript/src/annotated.ts b/implementations/javascript/src/annotated.ts new file mode 100644 index 0000000..b43b88d --- /dev/null +++ b/implementations/javascript/src/annotated.ts @@ -0,0 +1,92 @@ +import { Encoder } from "./codec"; +import { Tag } from "./constants"; +import { AsPreserve, PreserveOn } from "./symbols"; +import { DefaultPointer, is, Value } from "./values"; +import { Record } from './record'; +import { Dictionary, Set } from './dictionary'; + +export const IsPreservesAnnotated = Symbol.for('IsPreservesAnnotated'); + +export class Annotated { + readonly annotations: Array>; + readonly item: Value; + + constructor(item: Value) { + this.annotations = []; + this.item = item; + } + + [AsPreserve](): Value { + return this; + } + + [PreserveOn](encoder: Encoder) { + if (encoder.includeAnnotations) { + for (const a of this.annotations) { + encoder.emitbyte(Tag.Annotation); + encoder.push(a); + } + } + encoder.push(this.item); + } + + equals(other: any): boolean { + return is(this.item, Annotated.isAnnotated(other) ? other.item : other); + } + + // hashCode(): number { + // return hash(this.item); + // } + + toString(): string { + return this.asPreservesText(); + } + + asPreservesText(): string { + const anns = this.annotations.map((a) => '@' + a.asPreservesText()).join(' '); + return (anns ? anns + ' ' : anns) + this.item.asPreservesText(); + } + + get [IsPreservesAnnotated](): boolean { + return true; + } + + static isAnnotated(x: any): x is Annotated { + return !!x?.[IsPreservesAnnotated]; + } +} + +export function peel(v: Value): Value { + return strip(v, 1); +} + +export function strip(v: Value, depth: number = Infinity): Value { + function step(v: Value, depth: number): Value { + if (depth === 0) return v; + if (!Annotated.isAnnotated(v)) return v; + + const nextDepth = depth - 1; + function walk(v: Value): Value { return step(v, nextDepth); } + + if (Record.isRecord(v.item)) { + return new Record(step(v.item.label, depth), v.item.map(walk)); + } else if (Array.isArray(v.item)) { + return v.item.map(walk); + } else if (Set.isSet(v.item)) { + return v.item.map(walk); + } else if (Dictionary.isDictionary, T>(v.item)) { + return v.item.mapEntries((e) => [walk(e[0]), walk(e[1])]); + } else if (Annotated.isAnnotated(v.item)) { + throw new Error("Improper annotation structure"); + } else { + return v.item; + } + } + return step(v, depth); +} + +export function annotate(v0: Value, ...anns: Value[]): Annotated { + const v = Annotated.isAnnotated(v0) ? v0 : new Annotated(v0); + anns.forEach((a) => v.annotations.push(a)); + return v; +} diff --git a/implementations/javascript/src/bytes.ts b/implementations/javascript/src/bytes.ts new file mode 100644 index 0000000..d62ac8e --- /dev/null +++ b/implementations/javascript/src/bytes.ts @@ -0,0 +1,258 @@ +import { Tag } from './constants'; +import { AsPreserve, PreserveOn } from './symbols'; +import { Encoder, Preservable } from './codec'; +import { DefaultPointer, Value } from './values'; + +const textEncoder = new TextEncoder(); +const textDecoder = new TextDecoder(); + +export const IsPreservesBytes = Symbol.for('IsPreservesBytes'); + +export type BytesLike = Bytes | Uint8Array; + +export class Bytes implements Preservable { + readonly _view: Uint8Array; + + constructor(maybeByteIterable: any = new Uint8Array()) { + if (Bytes.isBytes(maybeByteIterable)) { + this._view = maybeByteIterable._view; + } else if (ArrayBuffer.isView(maybeByteIterable)) { + this._view = new Uint8Array(maybeByteIterable.buffer, + maybeByteIterable.byteOffset, + maybeByteIterable.byteLength); + } else if (maybeByteIterable instanceof ArrayBuffer) { + this._view = new Uint8Array(maybeByteIterable); + } else if (typeof maybeByteIterable === 'string') { + this._view = textEncoder.encode(maybeByteIterable); + } else if (typeof maybeByteIterable === 'number') { + this._view = new Uint8Array(maybeByteIterable); + } else if (typeof maybeByteIterable.length === 'number') { + this._view = Uint8Array.from(maybeByteIterable); + } else { + throw new TypeError("Attempt to initialize Bytes from unsupported value: " + + maybeByteIterable); + } + } + + get length(): number { + return this._view.length; + } + + static from(x: any): Bytes { + return new Bytes(x); + } + + static of(...bytes: number[]): Bytes { + return new Bytes(Uint8Array.of(...bytes)); + } + + static fromHex(s: string): Bytes { + if (s.length & 1) throw new Error("Cannot decode odd-length hexadecimal string"); + const len = s.length >> 1; + const result = new Bytes(len); + for (let i = 0; i < len; i++) { + result._view[i] = + (unhexDigit(s.charCodeAt(i << 1)) << 4) | unhexDigit(s.charCodeAt((i << 1) + 1)); + } + return result; + } + + static fromIO(io: string | BytesLike): string | Bytes { + if (typeof io === 'string') return io; + if (Bytes.isBytes(io)) return io; + return new Bytes(io); + } + + static toIO(b : string | BytesLike): string | Uint8Array { + if (typeof b === 'string') return b; + if (Bytes.isBytes(b)) return b._view; + return b; + } + + static concat = function (bss: BytesLike[]): Bytes { + let len = 0; + for (let i = 0; i < bss.length; i++) { len += underlying(bss[i]).length; } + + const result = new Bytes(len); + let index = 0; + for (let i = 0; i < bss.length; i++) { + const bs = underlying(bss[i]); + result._view.set(bs, index); + index += bs.length; + } + return result; + } + + get(index: number): number { + return this._view[index]; + } + + equals(other: any): boolean { + if (!Bytes.isBytes(other)) return false; + if (other.length !== this.length) return false; + const va = this._view; + const vb = other._view; + for (let i = 0; i < va.length; i++) { + if (va[i] !== vb[i]) return false; + } + return true; + } + + hashCode(): number { + // Immutable.js uses this function for strings. + const v = this._view; + let hash = 0; + for (let i = 0; i < v.length; i++) { + hash = ((31 * hash) + v[i]) | 0; + } + return hash; + } + + static compare(a: Bytes, b: Bytes): number { + if (a < b) return -1; + if (b < a) return 1; + return 0; + } + + static decodeUtf8(bs: Bytes | Uint8Array): string { + return textDecoder.decode(underlying(bs)); + } + + fromUtf8(): string { + return textDecoder.decode(this._view); + } + + toString(): string { + return this.asPreservesText(); + } + + [AsPreserve](): Value { + return this; + } + + asPreservesText(): string { + return '#"' + this.__asciify() + '"'; + } + + __asciify(): string { + const pieces = []; + const v = this._view; + for (let i = 0; i < v.length; i++) { + const b = v[i]; + if (b === 92 || b === 34) { + pieces.push('\\' + String.fromCharCode(b)); + } else if (b >= 32 && b <= 126) { + pieces.push(String.fromCharCode(b)); + } else { + pieces.push('\\x' + hexDigit(b >> 4) + hexDigit(b & 15)); + } + } + return pieces.join(''); + } + + toHex(): string { + var nibbles = []; + for (let i = 0; i < this.length; i++) { + nibbles.push(hexDigit(this._view[i] >> 4)); + nibbles.push(hexDigit(this._view[i] & 15)); + } + return nibbles.join(''); + } + + [PreserveOn](encoder: Encoder) { + encoder.emitbyte(Tag.ByteString); + encoder.varint(this.length); + encoder.emitbytes(this._view); + } + + get [IsPreservesBytes](): boolean { + return true; + } + + static isBytes(x: any): x is Bytes { + return !!x?.[IsPreservesBytes]; + } +} + +export function hexDigit(n: number): string { + return '0123456789abcdef'[n]; +} + +export function unhexDigit(asciiCode: number) { + if (asciiCode >= 48 && asciiCode <= 57) return asciiCode - 48; + if (asciiCode >= 97 && asciiCode <= 102) return asciiCode - 97 + 10; + if (asciiCode >= 65 && asciiCode <= 70) return asciiCode - 65 + 10; + throw new Error("Invalid hex digit: " + String.fromCharCode(asciiCode)); +} + +export function underlying(b: Bytes | Uint8Array): Uint8Array { + return (b instanceof Uint8Array) ? b : b._view; +} + +// Uint8Array / TypedArray methods + +export interface Bytes { + entries(): IterableIterator<[number, number]>; + every(predicate: (value: number, index: number, array: Uint8Array) => unknown, + thisArg?: any): boolean; + find(predicate: (value: number, index: number, obj: Uint8Array) => boolean, + thisArg?: any): number; + findIndex(predicate: (value: number, index: number, obj: Uint8Array) => boolean, + thisArg?: any): number; + forEach(callbackfn: (value: number, index: number, array: Uint8Array) => void, + thisArg?: any): void; + includes(searchElement: number, fromIndex?: number): boolean; + indexOf(searchElement: number, fromIndex?: number): number; + join(separator?: string): string; + keys(): IterableIterator; + lastIndexOf(searchElement: number, fromIndex?: number): number; + reduce(callbackfn: (previousValue: number, + currentValue: number, + currentIndex: number, + array: Uint8Array) => number, + initialValue?: number): number; + reduceRight(callbackfn: (previousValue: number, + currentValue: number, + currentIndex: number, + array: Uint8Array) => number, + initialValue?: number): number; + some(predicate: (value: number, index: number, array: Uint8Array) => unknown, + thisArg?: any): boolean; + toLocaleString(): string; + values(): IterableIterator; + + filter(predicate: (value: number, index: number, array: Uint8Array) => any, + thisArg?: any): Bytes; + map(callbackfn: (value: number, index: number, array: Uint8Array) => number, + thisArg?: any): Bytes; + slice(start?: number, end?: number): Bytes; + subarray(begin?: number, end?: number): Bytes; + + reverse(): Bytes; + sort(compareFn?: (a: number, b: number) => number): Bytes; + + [Symbol.iterator](): IterableIterator; +} + +(function () { + for (const k of `entries every find findIndex forEach includes indexOf join +keys lastIndexOf reduce reduceRight some toLocaleString values`.split(/\s+/)) + { + (Bytes as any).prototype[k] = + function (...args: any[]) { return this._view[k](...args); }; + } + + for (const k of `filter map slice subarray`.split(/\s+/)) + { + (Bytes as any).prototype[k] = + function (...args: any[]) { return new Bytes(this._view[k](...args)); }; + } + + for (const k of `reverse sort`.split(/\s+/)) + { + (Bytes as any).prototype[k] = + function (...args: any[]) { return new Bytes(this._view.slice()[k](...args)); }; + } + + Bytes.prototype[Symbol.iterator] = function () { return this._view[Symbol.iterator](); }; +})(); diff --git a/implementations/javascript/src/dictionary.ts b/implementations/javascript/src/dictionary.ts new file mode 100644 index 0000000..4dbf567 --- /dev/null +++ b/implementations/javascript/src/dictionary.ts @@ -0,0 +1,125 @@ +import { canonicalEncode, canonicalString, Encoder } from "./codec"; +import { Tag } from "./constants"; +import { FlexMap, FlexSet, _iterMap } from "./flex"; +import { PreserveOn } from "./symbols"; +import { stringify } from "./text"; +import { DefaultPointer, fromJS, Value } from "./values"; +import { Bytes } from './bytes'; + +export type DictionaryType = 'Dictionary' | 'Set'; +export const DictionaryType = Symbol.for('DictionaryType'); + +export class Dictionary extends FlexMap, V> { + get [DictionaryType](): DictionaryType { + return 'Dictionary'; + } + + static isDictionary(x: any): x is Dictionary { + return x?.[DictionaryType] === 'Dictionary'; + } + + static fromJS(x: object): Dictionary, T> { + if (Dictionary.isDictionary(x)) return x as Dictionary, T>; + const d = new Dictionary, T>(); + Object.entries(x).forEach(([key, value]) => d.set(key, fromJS(value))); + return d; + } + + constructor(items?: Iterable) { + const iter = items?.[Symbol.iterator](); + super(canonicalString, iter === void 0 ? void 0 : _iterMap(iter, ([k,v]) => [fromJS(k), v])); + } + + mapEntries(f: (entry: [Value, V]) => [Value, W]): Dictionary { + const result = new Dictionary(); + for (let oldEntry of this.entries()) { + const newEntry = f(oldEntry); + result.set(newEntry[0], newEntry[1]) + } + return result; + } + + 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(); + } + + get [Symbol.toStringTag]() { return 'Dictionary'; } + + [PreserveOn](encoder: Encoder) { + if (encoder.canonical) { + const pieces = Array.from(this).map(([k, v]) => + Bytes.concat([canonicalEncode(k), canonicalEncode(v)])); + pieces.sort(Bytes.compare); + encoder.encoderawvalues(Tag.Dictionary, pieces); + } else { + encoder.emitbyte(Tag.Dictionary); + this.forEach((v, k) => { + encoder.push(k); + encoder.push(v as unknown as Value); // Suuuuuuuper unsound + }); + encoder.emitbyte(Tag.End); + } + } +} + +export class Set extends FlexSet> { + get [DictionaryType](): DictionaryType { + return 'Set'; + } + + static isSet(x: any): x is Set { + return x?.[DictionaryType] === 'Set'; + } + + constructor(items?: Iterable) { + const iter = items?.[Symbol.iterator](); + super(canonicalString, iter === void 0 ? void 0 : _iterMap>(iter, fromJS)); + } + + map(f: (value: Value) => Value): Set { + return new Set(_iterMap(this[Symbol.iterator](), f)); + } + + filter(f: (value: Value) => boolean): Set { + const result = new Set(); + for (let k of this) if (f(k)) result.add(k); + return result; + } + + toString(): string { + return this.asPreservesText(); + } + + asPreservesText(): string { + return '#{' + + Array.from(_iterMap(this.values(), v => v.asPreservesText())).join(', ') + + '}'; + } + + clone(): Set { + return new Set(this); + } + + get [Symbol.toStringTag]() { return 'Set'; } + + [PreserveOn](encoder: Encoder) { + if (encoder.canonical) { + const pieces = Array.from(this).map(k => canonicalEncode(k)); + pieces.sort(Bytes.compare); + encoder.encoderawvalues(Tag.Set, pieces); + } else { + encoder.encodevalues(Tag.Set, this); + } + } +} diff --git a/implementations/javascript/src/float.ts b/implementations/javascript/src/float.ts new file mode 100644 index 0000000..4aa759f --- /dev/null +++ b/implementations/javascript/src/float.ts @@ -0,0 +1,94 @@ +import { Tag } from "./constants"; +import { AsPreserve, PreserveOn } from "./symbols"; +import { DefaultPointer, Value } from "./values"; +import { Encoder, Preservable } from "./codec"; + +export type FloatType = 'Single' | 'Double'; +export const FloatType = Symbol.for('FloatType'); + +export abstract class Float { + readonly value: number; + + constructor(value: number | Float) { + this.value = typeof value === 'number' ? value : value.value; + } + + toString() { + return this.asPreservesText(); + } + + equals(other: any): boolean { + return Object.is(other.constructor, this.constructor) && (other.value === this.value); + } + + hashCode(): number { + return (this.value | 0); // TODO: something better? + } + + abstract asPreservesText(): string; + abstract get [FloatType](): FloatType; + + static isFloat = (x: any): x is Float => x?.[FloatType] !== void 0; + static isSingle = (x: any): x is SingleFloat => x?.[FloatType] === 'Single'; + static isDouble = (x: any): x is DoubleFloat => x?.[FloatType] === 'Double'; +} + +export function floatValue(f: any): number { + if (typeof f === 'number') { + return f; + } else if (Float.isFloat(f)) { + return f.value; + } else { + return NaN; + } +} + +export class SingleFloat extends Float implements Preservable { + [AsPreserve](): Value { + return this; + } + + [PreserveOn](encoder: Encoder) { + encoder.emitbyte(Tag.Float); + encoder.makeroom(4); + encoder.view.setFloat32(encoder.index, this.value, false); + encoder.index += 4; + } + + get [FloatType](): 'Single' { + return 'Single'; + } + + asPreservesText(): string { + return '' + this.value + 'f'; + } +} + +export function Single(value: number | Float): SingleFloat { + return new SingleFloat(value); +} + +export class DoubleFloat extends Float implements Preservable { + [AsPreserve](): Value { + return this; + } + + [PreserveOn](encoder: Encoder) { + encoder.emitbyte(Tag.Double); + encoder.makeroom(8); + encoder.view.setFloat64(encoder.index, this.value, false); + encoder.index += 8; + } + + get [FloatType](): 'Double' { + return 'Double'; + } + + asPreservesText(): string { + return '' + this.value; + } +} + +export function Double(value: number | Float): DoubleFloat { + return new DoubleFloat(value); +} diff --git a/implementations/javascript/src/record.ts b/implementations/javascript/src/record.ts new file mode 100644 index 0000000..2a02039 --- /dev/null +++ b/implementations/javascript/src/record.ts @@ -0,0 +1,134 @@ +import { Tag } from "./constants"; +import { Encoder } from "./codec"; +import { PreserveOn } from "./symbols"; +import { DefaultPointer, fromJS, is, Value } from "./values"; + +export const IsPreservesRecord = Symbol.for('IsPreservesRecord'); + +export class Record extends Array> { + readonly label: Value; + + constructor(label: Value, fieldsJS: any[]) { + if (arguments.length === 1) { + // Using things like someRecord.map() involves the runtime + // apparently instantiating instances of this.constructor + // as if it were just plain old Array, so we have to be + // somewhat calling-convention-compatible. This is + // something that isn't part of the user-facing API. + super(label); + this.label = label; // needed just to keep the typechecker happy + return; + } + + super(fieldsJS.length); + fieldsJS.forEach((f, i) => this[i] = fromJS(f)); + this.label = label; + Object.freeze(this); + } + + get(index: number, defaultValue?: Value): Value | undefined { + return (index < this.length) ? this[index] : defaultValue; + } + + set(index: number, newValue: Value): Record { + return new Record(this.label, this.map((f, i) => (i === index) ? newValue : f)); + } + + getConstructorInfo(): RecordConstructorInfo { + return { label: this.label, arity: this.length }; + } + + equals(other: any): boolean { + return Record.isRecord(other) && + is(this.label, other.label) && + this.every((f, i) => is(f, other.get(i))); + } + + // hashCode(): number { + // let h = hash(this.label); + // this.forEach((f) => h = ((31 * h) + hash(f)) | 0); + // return h; + // } + + static fallbackToString: (f: Value) => string = (_f) => ''; + + toString(): string { + return this.asPreservesText(); + } + + asPreservesText(): string { + if (!('label' in this)) { + // A quasi-Array from someRecord.map() or similar. See constructor. + return super.toString(); + } + return this.label.asPreservesText() + + '(' + this.map((f) => { + try { + return f.asPreservesText(); + } catch (e) { + return Record.fallbackToString(f); + } + }).join(', ') + ')'; + } + + static makeConstructor(labelSymbolText: string, fieldNames: string[]): RecordConstructor { + return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames); + } + + static makeBasicConstructor(label0: any, fieldNames: string[]): RecordConstructor { + const label = fromJS(label0); + const arity = fieldNames.length; + const ctor: RecordConstructor = (...fields: any[]): Record => { + if (fields.length !== arity) { + throw new Error("Record: cannot instantiate " + (label && label.toString()) + + " expecting " + arity + " fields with " + fields.length + " fields"); + } + return new Record(label, fields); + }; + const constructorInfo = { label, arity }; + ctor.constructorInfo = constructorInfo; + ctor.isClassOf = (v: any): v is Record => Record.isClassOf(constructorInfo, v); + ctor._ = {}; + fieldNames.forEach((name, i) => { + ctor._[name] = function (r: any): Value | undefined { + if (!ctor.isClassOf(r)) { + throw new Error("Record: attempt to retrieve field "+label.toString()+"."+name+ + " from non-"+label.toString()+": "+(r && r.toString())); + } + return r.get(i); + }; + }); + return ctor; + } + + [PreserveOn](encoder: Encoder) { + encoder.emitbyte(Tag.Record); + encoder.push(this.label); + this.forEach((f) => encoder.push(f)); + encoder.emitbyte(Tag.End); + } + + get [IsPreservesRecord](): boolean { + return true; + } + + static isRecord(x: any): x is Record { + return !!x?.[IsPreservesRecord]; + } + + static isClassOf(ci: RecordConstructorInfo, v: any): v is Record { + return (Record.isRecord(v)) && is(ci.label, v.label) && (ci.arity === v.length); + } +} + +export interface RecordConstructor { + (...fields: any[]): Record; + constructorInfo: RecordConstructorInfo; + isClassOf(v: any): v is Record; + _: { [getter: string]: (r: any) => Value | undefined }; +} + +export interface RecordConstructorInfo { + label: Value; + arity: number; +} diff --git a/implementations/javascript/src/values.ts b/implementations/javascript/src/values.ts index 84bf60d..87ee80b 100644 --- a/implementations/javascript/src/values.ts +++ b/implementations/javascript/src/values.ts @@ -1,13 +1,17 @@ // Preserves Values. -import { PreserveOn, AsPreserve } from './symbols'; -import { Tag } from './constants'; -import { Encoder, canonicalEncode, canonicalString, Preservable } from './codec'; -import { stringify } from './text'; -import { _iterMap, FlexMap, FlexSet } from './flex'; +import { AsPreserve } from './symbols'; +import { Bytes } from './bytes'; +import { DoubleFloat, SingleFloat } from './float'; +import { Record } from './record'; +import { Annotated } from './annotated'; +import { Set, Dictionary } from './dictionary'; -const textEncoder = new TextEncoder(); -const textDecoder = new TextDecoder(); +export * from './bytes'; +export * from './float'; +export * from './record'; +export * from './annotated'; +export * from './dictionary'; export type DefaultPointer = object @@ -15,10 +19,6 @@ export type Value = Atom | Compound | T | export type Atom = boolean | SingleFloat | DoubleFloat | number | string | Bytes | symbol; export type Compound = Record | Array> | Set | Dictionary, T>; -export const IsPreservesRecord = Symbol.for('IsPreservesRecord'); -export const IsPreservesBytes = Symbol.for('IsPreservesBytes'); -export const IsPreservesAnnotated = Symbol.for('IsPreservesAnnotated'); - export function fromJS(x: any): Value { switch (typeof x) { case 'number': @@ -63,278 +63,21 @@ export function fromJS(x: any): Value { throw new TypeError("Cannot represent JavaScript value as Preserves: " + x); } -export type FloatType = 'Single' | 'Double'; -export const FloatType = Symbol.for('FloatType'); - -export abstract class Float { - readonly value: number; - - constructor(value: number | Float) { - this.value = typeof value === 'number' ? value : value.value; - } - - toString() { - return this.asPreservesText(); - } - - equals(other: any): boolean { - return Object.is(other.constructor, this.constructor) && (other.value === this.value); - } - - hashCode(): number { - return (this.value | 0); // TODO: something better? - } - - abstract asPreservesText(): string; - abstract get [FloatType](): FloatType; - - static isFloat(x: any, t: FloatType): x is Float { - return (x?.[FloatType] === t); - } - - static isSingle = (x: any): x is SingleFloat => Float.isFloat(x, 'Single'); - static isDouble = (x: any): x is DoubleFloat => Float.isFloat(x, 'Double'); -} - -export function floatValue(f: any): number { - if (typeof f === 'number') { - return f; - } else if (f?.[FloatType] !== void 0) { - return f.value; - } else { - return NaN; - } -} - -export class SingleFloat extends Float implements Preservable { - [AsPreserve](): Value { - return this; - } - - [PreserveOn](encoder: Encoder) { - encoder.emitbyte(Tag.Float); - encoder.makeroom(4); - encoder.view.setFloat32(encoder.index, this.value, false); - encoder.index += 4; - } - - get [FloatType](): FloatType { - return 'Single'; - } - - asPreservesText(): string { - return '' + this.value + 'f'; - } -} - -export function Single(value: number | Float) { - return new SingleFloat(value); -} - -export class DoubleFloat extends Float implements Preservable { - [AsPreserve](): Value { - return this; - } - - [PreserveOn](encoder: Encoder) { - encoder.emitbyte(Tag.Double); - encoder.makeroom(8); - encoder.view.setFloat64(encoder.index, this.value, false); - encoder.index += 8; - } - - get [FloatType](): FloatType { - return 'Double'; - } - - asPreservesText(): string { - return '' + this.value; - } -} - -export function Double(value: number | Float) { - return new DoubleFloat(value); -} - -export type BytesLike = Bytes | Uint8Array; - -export class Bytes implements Preservable { - readonly _view: Uint8Array; - - constructor(maybeByteIterable: any = new Uint8Array()) { - if (Bytes.isBytes(maybeByteIterable)) { - this._view = maybeByteIterable._view; - } else if (ArrayBuffer.isView(maybeByteIterable)) { - this._view = new Uint8Array(maybeByteIterable.buffer, - maybeByteIterable.byteOffset, - maybeByteIterable.byteLength); - } else if (maybeByteIterable instanceof ArrayBuffer) { - this._view = new Uint8Array(maybeByteIterable); - } else if (typeof maybeByteIterable === 'string') { - this._view = textEncoder.encode(maybeByteIterable); - } else if (typeof maybeByteIterable === 'number') { - this._view = new Uint8Array(maybeByteIterable); - } else if (typeof maybeByteIterable.length === 'number') { - this._view = Uint8Array.from(maybeByteIterable); - } else { - throw new TypeError("Attempt to initialize Bytes from unsupported value: " + - maybeByteIterable); +export function is(a: any, b: any): boolean { + if (Annotated.isAnnotated(a)) a = a.item; + if (Annotated.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 && typeof a.equals === 'function') 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; + return true; } } - - get length(): number { - return this._view.length; - } - - static from(x: any): Bytes { - return new Bytes(x); - } - - static of(...bytes: number[]): Bytes { - return new Bytes(Uint8Array.of(...bytes)); - } - - static fromHex(s: string): Bytes { - if (s.length & 1) throw new Error("Cannot decode odd-length hexadecimal string"); - const len = s.length >> 1; - const result = new Bytes(len); - for (let i = 0; i < len; i++) { - result._view[i] = - (unhexDigit(s.charCodeAt(i << 1)) << 4) | unhexDigit(s.charCodeAt((i << 1) + 1)); - } - return result; - } - - static fromIO(io: string | BytesLike): string | Bytes { - if (typeof io === 'string') return io; - if (Bytes.isBytes(io)) return io; - return new Bytes(io); - } - - static toIO(b : string | BytesLike): string | Uint8Array { - if (typeof b === 'string') return b; - if (Bytes.isBytes(b)) return b._view; - return b; - } - - static concat = function (bss: BytesLike[]): Bytes { - let len = 0; - for (let i = 0; i < bss.length; i++) { len += underlying(bss[i]).length; } - - const result = new Bytes(len); - let index = 0; - for (let i = 0; i < bss.length; i++) { - const bs = underlying(bss[i]); - result._view.set(bs, index); - index += bs.length; - } - return result; - } - - get(index: number): number { - return this._view[index]; - } - - equals(other: any): boolean { - if (!Bytes.isBytes(other)) return false; - if (other.length !== this.length) return false; - const va = this._view; - const vb = other._view; - for (let i = 0; i < va.length; i++) { - if (va[i] !== vb[i]) return false; - } - return true; - } - - hashCode(): number { - // Immutable.js uses this function for strings. - const v = this._view; - let hash = 0; - for (let i = 0; i < v.length; i++) { - hash = ((31 * hash) + v[i]) | 0; - } - return hash; - } - - static compare(a: Bytes, b: Bytes): number { - if (a < b) return -1; - if (b < a) return 1; - return 0; - } - - static decodeUtf8(bs: Bytes | Uint8Array): string { - return textDecoder.decode(underlying(bs)); - } - - fromUtf8(): string { - return textDecoder.decode(this._view); - } - - toString(): string { - return this.asPreservesText(); - } - - [AsPreserve](): Value { - return this; - } - - asPreservesText(): string { - return '#"' + this.__asciify() + '"'; - } - - __asciify(): string { - const pieces = []; - const v = this._view; - for (let i = 0; i < v.length; i++) { - const b = v[i]; - if (b === 92 || b === 34) { - pieces.push('\\' + String.fromCharCode(b)); - } else if (b >= 32 && b <= 126) { - pieces.push(String.fromCharCode(b)); - } else { - pieces.push('\\x' + hexDigit(b >> 4) + hexDigit(b & 15)); - } - } - return pieces.join(''); - } - - toHex(): string { - var nibbles = []; - for (let i = 0; i < this.length; i++) { - nibbles.push(hexDigit(this._view[i] >> 4)); - nibbles.push(hexDigit(this._view[i] & 15)); - } - return nibbles.join(''); - } - - [PreserveOn](encoder: Encoder) { - encoder.emitbyte(Tag.ByteString); - encoder.varint(this.length); - encoder.emitbytes(this._view); - } - - get [IsPreservesBytes](): boolean { - return true; - } - - static isBytes(x: any): x is Bytes { - return !!x?.[IsPreservesBytes]; - } -} - -export function hexDigit(n: number): string { - return '0123456789abcdef'[n]; -} - -export function unhexDigit(asciiCode: number) { - if (asciiCode >= 48 && asciiCode <= 57) return asciiCode - 48; - if (asciiCode >= 97 && asciiCode <= 102) return asciiCode - 97 + 10; - if (asciiCode >= 65 && asciiCode <= 70) return asciiCode - 65 + 10; - throw new Error("Invalid hex digit: " + String.fromCharCode(asciiCode)); -} - -export function underlying(b: Bytes | Uint8Array): Uint8Array { - return (b instanceof Uint8Array) ? b : b._view; + return false; } declare global { @@ -367,418 +110,3 @@ Symbol.prototype.asPreservesText = function (): string { Array.prototype.asPreservesText = function (): string { return '[' + this.map((i: Value) => i.asPreservesText()).join(', ') + ']'; }; - -// Uint8Array / TypedArray methods - -export interface Bytes { - entries(): IterableIterator<[number, number]>; - every(predicate: (value: number, index: number, array: Uint8Array) => unknown, - thisArg?: any): boolean; - find(predicate: (value: number, index: number, obj: Uint8Array) => boolean, - thisArg?: any): number; - findIndex(predicate: (value: number, index: number, obj: Uint8Array) => boolean, - thisArg?: any): number; - forEach(callbackfn: (value: number, index: number, array: Uint8Array) => void, - thisArg?: any): void; - includes(searchElement: number, fromIndex?: number): boolean; - indexOf(searchElement: number, fromIndex?: number): number; - join(separator?: string): string; - keys(): IterableIterator; - lastIndexOf(searchElement: number, fromIndex?: number): number; - reduce(callbackfn: (previousValue: number, - currentValue: number, - currentIndex: number, - array: Uint8Array) => number, - initialValue?: number): number; - reduceRight(callbackfn: (previousValue: number, - currentValue: number, - currentIndex: number, - array: Uint8Array) => number, - initialValue?: number): number; - some(predicate: (value: number, index: number, array: Uint8Array) => unknown, - thisArg?: any): boolean; - toLocaleString(): string; - values(): IterableIterator; - - filter(predicate: (value: number, index: number, array: Uint8Array) => any, - thisArg?: any): Bytes; - map(callbackfn: (value: number, index: number, array: Uint8Array) => number, - thisArg?: any): Bytes; - slice(start?: number, end?: number): Bytes; - subarray(begin?: number, end?: number): Bytes; - - reverse(): Bytes; - sort(compareFn?: (a: number, b: number) => number): Bytes; - - [Symbol.iterator](): IterableIterator; -} - -(function () { - for (const k of `entries every find findIndex forEach includes indexOf join -keys lastIndexOf reduce reduceRight some toLocaleString values`.split(/\s+/)) - { - (Bytes as any).prototype[k] = - function (...args: any[]) { return this._view[k](...args); }; - } - - for (const k of `filter map slice subarray`.split(/\s+/)) - { - (Bytes as any).prototype[k] = - function (...args: any[]) { return new Bytes(this._view[k](...args)); }; - } - - for (const k of `reverse sort`.split(/\s+/)) - { - (Bytes as any).prototype[k] = - function (...args: any[]) { return new Bytes(this._view.slice()[k](...args)); }; - } - - Bytes.prototype[Symbol.iterator] = function () { return this._view[Symbol.iterator](); }; -})(); - -export class Record extends Array> { - readonly label: Value; - - constructor(label: Value, fieldsJS: any[]) { - if (arguments.length === 1) { - // Using things like someRecord.map() involves the runtime - // apparently instantiating instances of this.constructor - // as if it were just plain old Array, so we have to be - // somewhat calling-convention-compatible. This is - // something that isn't part of the user-facing API. - super(label); - this.label = label; // needed just to keep the typechecker happy - return; - } - - super(fieldsJS.length); - fieldsJS.forEach((f, i) => this[i] = fromJS(f)); - this.label = label; - Object.freeze(this); - } - - get(index: number, defaultValue?: Value): Value | undefined { - return (index < this.length) ? this[index] : defaultValue; - } - - set(index: number, newValue: Value): Record { - return new Record(this.label, this.map((f, i) => (i === index) ? newValue : f)); - } - - getConstructorInfo(): RecordConstructorInfo { - return { label: this.label, arity: this.length }; - } - - equals(other: any): boolean { - return Record.isRecord(other) && - is(this.label, other.label) && - this.every((f, i) => is(f, other.get(i))); - } - - // hashCode(): number { - // let h = hash(this.label); - // this.forEach((f) => h = ((31 * h) + hash(f)) | 0); - // return h; - // } - - static fallbackToString: (f: Value) => string = (_f) => ''; - - toString(): string { - return this.asPreservesText(); - } - - asPreservesText(): string { - if (!('label' in this)) { - // A quasi-Array from someRecord.map() or similar. See constructor. - return super.toString(); - } - return this.label.asPreservesText() + - '(' + this.map((f) => { - try { - return f.asPreservesText(); - } catch (e) { - return Record.fallbackToString(f); - } - }).join(', ') + ')'; - } - - static makeConstructor(labelSymbolText: string, fieldNames: string[]): RecordConstructor { - return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames); - } - - static makeBasicConstructor(label0: any, fieldNames: string[]): RecordConstructor { - const label = fromJS(label0); - const arity = fieldNames.length; - const ctor: RecordConstructor = (...fields: any[]): Record => { - if (fields.length !== arity) { - throw new Error("Record: cannot instantiate " + (label && label.toString()) + - " expecting " + arity + " fields with " + fields.length + " fields"); - } - return new Record(label, fields); - }; - const constructorInfo = { label, arity }; - ctor.constructorInfo = constructorInfo; - ctor.isClassOf = (v: any): v is Record => Record.isClassOf(constructorInfo, v); - ctor._ = {}; - fieldNames.forEach((name, i) => { - ctor._[name] = function (r: any): Value | undefined { - if (!ctor.isClassOf(r)) { - throw new Error("Record: attempt to retrieve field "+label.toString()+"."+name+ - " from non-"+label.toString()+": "+(r && r.toString())); - } - return r.get(i); - }; - }); - return ctor; - } - - [PreserveOn](encoder: Encoder) { - encoder.emitbyte(Tag.Record); - encoder.push(this.label); - this.forEach((f) => encoder.push(f)); - encoder.emitbyte(Tag.End); - } - - get [IsPreservesRecord](): boolean { - return true; - } - - static isRecord(x: any): x is Record { - return !!x?.[IsPreservesRecord]; - } - - static isClassOf(ci: RecordConstructorInfo, v: any): v is Record { - return (Record.isRecord(v)) && is(ci.label, v.label) && (ci.arity === v.length); - } -} - -export interface RecordConstructor { - (...fields: any[]): Record; - constructorInfo: RecordConstructorInfo; - isClassOf(v: any): v is Record; - _: { [getter: string]: (r: any) => Value | undefined }; -} - -export interface RecordConstructorInfo { - label: Value; - arity: number; -} - -export function is(a: any, b: any): boolean { - if (Annotated.isAnnotated(a)) a = a.item; - if (Annotated.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 && typeof a.equals === 'function') 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; - return true; - } - } - return false; -} - -export type DictionaryType = 'Dictionary' | 'Set'; -export const DictionaryType = Symbol.for('DictionaryType'); - -export class Dictionary extends FlexMap, V> { - get [DictionaryType](): DictionaryType { - return 'Dictionary'; - } - - static isDictionary(x: any): x is Dictionary { - return x?.[DictionaryType] === 'Dictionary'; - } - - static fromJS(x: object): Dictionary, T> { - if (Dictionary.isDictionary(x)) return x as Dictionary, T>; - const d = new Dictionary, T>(); - Object.entries(x).forEach(([key, value]) => d.set(key, fromJS(value))); - return d; - } - - constructor(items?: Iterable) { - const iter = items?.[Symbol.iterator](); - super(canonicalString, iter === void 0 ? void 0 : _iterMap(iter, ([k,v]) => [fromJS(k), v])); - } - - mapEntries(f: (entry: [Value, V]) => [Value, W]): Dictionary { - const result = new Dictionary(); - for (let oldEntry of this.entries()) { - const newEntry = f(oldEntry); - result.set(newEntry[0], newEntry[1]) - } - return result; - } - - 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(); - } - - get [Symbol.toStringTag]() { return 'Dictionary'; } - - [PreserveOn](encoder: Encoder) { - if (encoder.canonical) { - const pieces = Array.from(this).map(([k, v]) => - Bytes.concat([canonicalEncode(k), canonicalEncode(v)])); - pieces.sort(Bytes.compare); - encoder.encoderawvalues(Tag.Dictionary, pieces); - } else { - encoder.emitbyte(Tag.Dictionary); - this.forEach((v, k) => { - encoder.push(k); - encoder.push(v as unknown as Value); // Suuuuuuuper unsound - }); - encoder.emitbyte(Tag.End); - } - } -} - -export class Set extends FlexSet> { - get [DictionaryType](): DictionaryType { - return 'Set'; - } - - static isSet(x: any): x is Set { - return x?.[DictionaryType] === 'Set'; - } - - constructor(items?: Iterable) { - const iter = items?.[Symbol.iterator](); - super(canonicalString, iter === void 0 ? void 0 : _iterMap>(iter, fromJS)); - } - - map(f: (value: Value) => Value): Set { - return new Set(_iterMap(this[Symbol.iterator](), f)); - } - - filter(f: (value: Value) => boolean): Set { - const result = new Set(); - for (let k of this) if (f(k)) result.add(k); - return result; - } - - toString(): string { - return this.asPreservesText(); - } - - asPreservesText(): string { - return '#{' + - Array.from(_iterMap(this.values(), v => v.asPreservesText())).join(', ') + - '}'; - } - - clone(): Set { - return new Set(this); - } - - get [Symbol.toStringTag]() { return 'Set'; } - - [PreserveOn](encoder: Encoder) { - if (encoder.canonical) { - const pieces = Array.from(this).map(k => canonicalEncode(k)); - pieces.sort(Bytes.compare); - encoder.encoderawvalues(Tag.Set, pieces); - } else { - encoder.encodevalues(Tag.Set, this); - } - } -} - -export class Annotated { - readonly annotations: Array>; - readonly item: Value; - - constructor(item: Value) { - this.annotations = []; - this.item = item; - } - - [AsPreserve](): Value { - return this; - } - - [PreserveOn](encoder: Encoder) { - if (encoder.includeAnnotations) { - for (const a of this.annotations) { - encoder.emitbyte(Tag.Annotation); - encoder.push(a); - } - } - encoder.push(this.item); - } - - equals(other: any): boolean { - return is(this.item, Annotated.isAnnotated(other) ? other.item : other); - } - - // hashCode(): number { - // return hash(this.item); - // } - - toString(): string { - return this.asPreservesText(); - } - - asPreservesText(): string { - const anns = this.annotations.map((a) => '@' + a.asPreservesText()).join(' '); - return (anns ? anns + ' ' : anns) + this.item.asPreservesText(); - } - - get [IsPreservesAnnotated](): boolean { - return true; - } - - static isAnnotated(x: any): x is Annotated { - return !!x?.[IsPreservesAnnotated]; - } -} - -export function peel(v: Value): Value { - return strip(v, 1); -} - -export function strip(v: Value, depth: number = Infinity): Value { - function step(v: Value, depth: number): Value { - if (depth === 0) return v; - if (!Annotated.isAnnotated(v)) return v; - - const nextDepth = depth - 1; - function walk(v: Value): Value { return step(v, nextDepth); } - - if (Record.isRecord(v.item)) { - return new Record(step(v.item.label, depth), v.item.map(walk)); - } else if (Array.isArray(v.item)) { - return v.item.map(walk); - } else if (Set.isSet(v.item)) { - return v.item.map(walk); - } else if (Dictionary.isDictionary, T>(v.item)) { - return v.item.mapEntries((e) => [walk(e[0]), walk(e[1])]); - } else if (Annotated.isAnnotated(v.item)) { - throw new Error("Improper annotation structure"); - } else { - return v.item; - } - } - return step(v, depth); -} - -export function annotate(v0: Value, ...anns: Value[]): Annotated { - const v = Annotated.isAnnotated(v0) ? v0 : new Annotated(v0); - anns.forEach((a) => v.annotations.push(a)); - return v; -} diff --git a/implementations/javascript/test/values.test.ts b/implementations/javascript/test/values.test.ts index e846973..e47d8bc 100644 --- a/implementations/javascript/test/values.test.ts +++ b/implementations/javascript/test/values.test.ts @@ -3,12 +3,12 @@ import './test-utils'; describe('Single', () => { it('should print reasonably', () => { - expect(new Single(123.45).toString()).toEqual("123.45f"); + expect(Single(123.45).toString()).toEqual("123.45f"); }); }); describe('Double', () => { it('should print reasonably', () => { - expect(new Double(123.45).toString()).toEqual("123.45"); + expect(Double(123.45).toString()).toEqual("123.45q"); }); });