Split out values.ts; this makes circular dependencies worse, so I will likely pull it all back together again soon
This commit is contained in:
parent
83b09d9406
commit
407e8778a1
|
@ -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<T extends object = DefaultPointer> {
|
||||
readonly annotations: Array<Value<T>>;
|
||||
readonly item: Value<T>;
|
||||
|
||||
constructor(item: Value<T>) {
|
||||
this.annotations = [];
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
[AsPreserve](): Value<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
[PreserveOn](encoder: Encoder<T>) {
|
||||
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<T extends object = DefaultPointer>(x: any): x is Annotated<T> {
|
||||
return !!x?.[IsPreservesAnnotated];
|
||||
}
|
||||
}
|
||||
|
||||
export function peel<T extends object = DefaultPointer>(v: Value<T>): Value<T> {
|
||||
return strip(v, 1);
|
||||
}
|
||||
|
||||
export function strip<T extends object = DefaultPointer>(v: Value<T>, depth: number = Infinity): Value<T> {
|
||||
function step(v: Value<T>, depth: number): Value<T> {
|
||||
if (depth === 0) return v;
|
||||
if (!Annotated.isAnnotated<T>(v)) return v;
|
||||
|
||||
const nextDepth = depth - 1;
|
||||
function walk(v: Value<T>): Value<T> { return step(v, nextDepth); }
|
||||
|
||||
if (Record.isRecord<T>(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<T>(v.item)) {
|
||||
return v.item.map(walk);
|
||||
} else if (Dictionary.isDictionary<Value<T>, 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<T extends object = DefaultPointer>(v0: Value<T>, ...anns: Value<T>[]): Annotated<T> {
|
||||
const v = Annotated.isAnnotated<T>(v0) ? v0 : new Annotated(v0);
|
||||
anns.forEach((a) => v.annotations.push(a));
|
||||
return v;
|
||||
}
|
|
@ -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<never> {
|
||||
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]<T extends object = DefaultPointer>(): Value<T> {
|
||||
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<never>) {
|
||||
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<number>;
|
||||
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<number>;
|
||||
|
||||
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<number>;
|
||||
}
|
||||
|
||||
(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](); };
|
||||
})();
|
|
@ -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<V, T extends object = DefaultPointer> extends FlexMap<Value<T>, V> {
|
||||
get [DictionaryType](): DictionaryType {
|
||||
return 'Dictionary';
|
||||
}
|
||||
|
||||
static isDictionary<V, T extends object = DefaultPointer>(x: any): x is Dictionary<V, T> {
|
||||
return x?.[DictionaryType] === 'Dictionary';
|
||||
}
|
||||
|
||||
static fromJS<V extends object = DefaultPointer, T extends object = DefaultPointer>(x: object): Dictionary<Value<V>, T> {
|
||||
if (Dictionary.isDictionary<V, T>(x)) return x as Dictionary<Value<V>, T>;
|
||||
const d = new Dictionary<Value<V>, T>();
|
||||
Object.entries(x).forEach(([key, value]) => d.set(key, fromJS(value)));
|
||||
return d;
|
||||
}
|
||||
|
||||
constructor(items?: Iterable<readonly [any, V]>) {
|
||||
const iter = items?.[Symbol.iterator]();
|
||||
super(canonicalString, iter === void 0 ? void 0 : _iterMap(iter, ([k,v]) => [fromJS(k), v]));
|
||||
}
|
||||
|
||||
mapEntries<W, R extends object = DefaultPointer>(f: (entry: [Value<T>, V]) => [Value<R>, W]): Dictionary<W, R> {
|
||||
const result = new Dictionary<W, R>();
|
||||
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<V, T> {
|
||||
return new Dictionary(this);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.asPreservesText();
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag]() { return 'Dictionary'; }
|
||||
|
||||
[PreserveOn](encoder: Encoder<T>) {
|
||||
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<T>); // Suuuuuuuper unsound
|
||||
});
|
||||
encoder.emitbyte(Tag.End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Set<T extends object = DefaultPointer> extends FlexSet<Value<T>> {
|
||||
get [DictionaryType](): DictionaryType {
|
||||
return 'Set';
|
||||
}
|
||||
|
||||
static isSet<T extends object = DefaultPointer>(x: any): x is Set<T> {
|
||||
return x?.[DictionaryType] === 'Set';
|
||||
}
|
||||
|
||||
constructor(items?: Iterable<any>) {
|
||||
const iter = items?.[Symbol.iterator]();
|
||||
super(canonicalString, iter === void 0 ? void 0 : _iterMap<any, Value<T>>(iter, fromJS));
|
||||
}
|
||||
|
||||
map<R extends object = DefaultPointer>(f: (value: Value<T>) => Value<R>): Set<R> {
|
||||
return new Set(_iterMap(this[Symbol.iterator](), f));
|
||||
}
|
||||
|
||||
filter(f: (value: Value<T>) => boolean): Set<T> {
|
||||
const result = new Set<T>();
|
||||
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<T> {
|
||||
return new Set(this);
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag]() { return 'Set'; }
|
||||
|
||||
[PreserveOn](encoder: Encoder<T>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<never> {
|
||||
[AsPreserve]<T extends object = DefaultPointer>(): Value<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
[PreserveOn](encoder: Encoder<never>) {
|
||||
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<never> {
|
||||
[AsPreserve]<T extends object = DefaultPointer>(): Value<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
[PreserveOn](encoder: Encoder<never>) {
|
||||
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);
|
||||
}
|
|
@ -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<T extends object = DefaultPointer> extends Array<Value<T>> {
|
||||
readonly label: Value<T>;
|
||||
|
||||
constructor(label: Value<T>, 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<T>): Value<T> | undefined {
|
||||
return (index < this.length) ? this[index] : defaultValue;
|
||||
}
|
||||
|
||||
set(index: number, newValue: Value<T>): Record<T> {
|
||||
return new Record(this.label, this.map((f, i) => (i === index) ? newValue : f));
|
||||
}
|
||||
|
||||
getConstructorInfo(): RecordConstructorInfo<T> {
|
||||
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<any>) => string = (_f) => '<unprintable_preserves_field_value>';
|
||||
|
||||
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<T extends object = DefaultPointer>(labelSymbolText: string, fieldNames: string[]): RecordConstructor<T> {
|
||||
return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames);
|
||||
}
|
||||
|
||||
static makeBasicConstructor<T extends object = DefaultPointer>(label0: any, fieldNames: string[]): RecordConstructor<T> {
|
||||
const label = fromJS<T>(label0);
|
||||
const arity = fieldNames.length;
|
||||
const ctor: RecordConstructor<T> = (...fields: any[]): Record<T> => {
|
||||
if (fields.length !== arity) {
|
||||
throw new Error("Record: cannot instantiate " + (label && label.toString()) +
|
||||
" expecting " + arity + " fields with " + fields.length + " fields");
|
||||
}
|
||||
return new Record<T>(label, fields);
|
||||
};
|
||||
const constructorInfo = { label, arity };
|
||||
ctor.constructorInfo = constructorInfo;
|
||||
ctor.isClassOf = (v: any): v is Record<T> => Record.isClassOf(constructorInfo, v);
|
||||
ctor._ = {};
|
||||
fieldNames.forEach((name, i) => {
|
||||
ctor._[name] = function (r: any): Value<T> | 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<T>) {
|
||||
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<T extends object = DefaultPointer>(x: any): x is Record<T> {
|
||||
return !!x?.[IsPreservesRecord];
|
||||
}
|
||||
|
||||
static isClassOf<T extends object = DefaultPointer>(ci: RecordConstructorInfo<T>, v: any): v is Record<T> {
|
||||
return (Record.isRecord(v)) && is(ci.label, v.label) && (ci.arity === v.length);
|
||||
}
|
||||
}
|
||||
|
||||
export interface RecordConstructor<T extends object = DefaultPointer> {
|
||||
(...fields: any[]): Record<T>;
|
||||
constructorInfo: RecordConstructorInfo<T>;
|
||||
isClassOf(v: any): v is Record<T>;
|
||||
_: { [getter: string]: (r: any) => Value<T> | undefined };
|
||||
}
|
||||
|
||||
export interface RecordConstructorInfo<T extends object = DefaultPointer> {
|
||||
label: Value<T>;
|
||||
arity: number;
|
||||
}
|
|
@ -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<T extends object = DefaultPointer> = Atom | Compound<T> | T |
|
|||
export type Atom = boolean | SingleFloat | DoubleFloat | number | string | Bytes | symbol;
|
||||
export type Compound<T extends object = DefaultPointer> = Record<T> | Array<Value<T>> | Set<T> | Dictionary<Value<T>, T>;
|
||||
|
||||
export const IsPreservesRecord = Symbol.for('IsPreservesRecord');
|
||||
export const IsPreservesBytes = Symbol.for('IsPreservesBytes');
|
||||
export const IsPreservesAnnotated = Symbol.for('IsPreservesAnnotated');
|
||||
|
||||
export function fromJS<T extends object = DefaultPointer>(x: any): Value<T> {
|
||||
switch (typeof x) {
|
||||
case 'number':
|
||||
|
@ -63,278 +63,21 @@ export function fromJS<T extends object = DefaultPointer>(x: any): Value<T> {
|
|||
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<never> {
|
||||
[AsPreserve]<T extends object = DefaultPointer>(): Value<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
[PreserveOn](encoder: Encoder<never>) {
|
||||
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<never> {
|
||||
[AsPreserve]<T extends object = DefaultPointer>(): Value<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
[PreserveOn](encoder: Encoder<never>) {
|
||||
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<never> {
|
||||
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]<T extends object = DefaultPointer>(): Value<T> {
|
||||
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<never>) {
|
||||
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<any>) => 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<number>;
|
||||
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<number>;
|
||||
|
||||
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<number>;
|
||||
}
|
||||
|
||||
(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<T extends object = DefaultPointer> extends Array<Value<T>> {
|
||||
readonly label: Value<T>;
|
||||
|
||||
constructor(label: Value<T>, 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<T>): Value<T> | undefined {
|
||||
return (index < this.length) ? this[index] : defaultValue;
|
||||
}
|
||||
|
||||
set(index: number, newValue: Value<T>): Record<T> {
|
||||
return new Record(this.label, this.map((f, i) => (i === index) ? newValue : f));
|
||||
}
|
||||
|
||||
getConstructorInfo(): RecordConstructorInfo<T> {
|
||||
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<any>) => string = (_f) => '<unprintable_preserves_field_value>';
|
||||
|
||||
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<T extends object = DefaultPointer>(labelSymbolText: string, fieldNames: string[]): RecordConstructor<T> {
|
||||
return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames);
|
||||
}
|
||||
|
||||
static makeBasicConstructor<T extends object = DefaultPointer>(label0: any, fieldNames: string[]): RecordConstructor<T> {
|
||||
const label = fromJS<T>(label0);
|
||||
const arity = fieldNames.length;
|
||||
const ctor: RecordConstructor<T> = (...fields: any[]): Record<T> => {
|
||||
if (fields.length !== arity) {
|
||||
throw new Error("Record: cannot instantiate " + (label && label.toString()) +
|
||||
" expecting " + arity + " fields with " + fields.length + " fields");
|
||||
}
|
||||
return new Record<T>(label, fields);
|
||||
};
|
||||
const constructorInfo = { label, arity };
|
||||
ctor.constructorInfo = constructorInfo;
|
||||
ctor.isClassOf = (v: any): v is Record<T> => Record.isClassOf(constructorInfo, v);
|
||||
ctor._ = {};
|
||||
fieldNames.forEach((name, i) => {
|
||||
ctor._[name] = function (r: any): Value<T> | 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<T>) {
|
||||
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<T extends object = DefaultPointer>(x: any): x is Record<T> {
|
||||
return !!x?.[IsPreservesRecord];
|
||||
}
|
||||
|
||||
static isClassOf<T extends object = DefaultPointer>(ci: RecordConstructorInfo<T>, v: any): v is Record<T> {
|
||||
return (Record.isRecord(v)) && is(ci.label, v.label) && (ci.arity === v.length);
|
||||
}
|
||||
}
|
||||
|
||||
export interface RecordConstructor<T extends object = DefaultPointer> {
|
||||
(...fields: any[]): Record<T>;
|
||||
constructorInfo: RecordConstructorInfo<T>;
|
||||
isClassOf(v: any): v is Record<T>;
|
||||
_: { [getter: string]: (r: any) => Value<T> | undefined };
|
||||
}
|
||||
|
||||
export interface RecordConstructorInfo<T extends object = DefaultPointer> {
|
||||
label: Value<T>;
|
||||
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<V, T extends object = DefaultPointer> extends FlexMap<Value<T>, V> {
|
||||
get [DictionaryType](): DictionaryType {
|
||||
return 'Dictionary';
|
||||
}
|
||||
|
||||
static isDictionary<V, T extends object = DefaultPointer>(x: any): x is Dictionary<V, T> {
|
||||
return x?.[DictionaryType] === 'Dictionary';
|
||||
}
|
||||
|
||||
static fromJS<V extends object = DefaultPointer, T extends object = DefaultPointer>(x: object): Dictionary<Value<V>, T> {
|
||||
if (Dictionary.isDictionary<V, T>(x)) return x as Dictionary<Value<V>, T>;
|
||||
const d = new Dictionary<Value<V>, T>();
|
||||
Object.entries(x).forEach(([key, value]) => d.set(key, fromJS(value)));
|
||||
return d;
|
||||
}
|
||||
|
||||
constructor(items?: Iterable<readonly [any, V]>) {
|
||||
const iter = items?.[Symbol.iterator]();
|
||||
super(canonicalString, iter === void 0 ? void 0 : _iterMap(iter, ([k,v]) => [fromJS(k), v]));
|
||||
}
|
||||
|
||||
mapEntries<W, R extends object = DefaultPointer>(f: (entry: [Value<T>, V]) => [Value<R>, W]): Dictionary<W, R> {
|
||||
const result = new Dictionary<W, R>();
|
||||
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<V, T> {
|
||||
return new Dictionary(this);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.asPreservesText();
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag]() { return 'Dictionary'; }
|
||||
|
||||
[PreserveOn](encoder: Encoder<T>) {
|
||||
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<T>); // Suuuuuuuper unsound
|
||||
});
|
||||
encoder.emitbyte(Tag.End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Set<T extends object = DefaultPointer> extends FlexSet<Value<T>> {
|
||||
get [DictionaryType](): DictionaryType {
|
||||
return 'Set';
|
||||
}
|
||||
|
||||
static isSet<T extends object = DefaultPointer>(x: any): x is Set<T> {
|
||||
return x?.[DictionaryType] === 'Set';
|
||||
}
|
||||
|
||||
constructor(items?: Iterable<any>) {
|
||||
const iter = items?.[Symbol.iterator]();
|
||||
super(canonicalString, iter === void 0 ? void 0 : _iterMap<any, Value<T>>(iter, fromJS));
|
||||
}
|
||||
|
||||
map<R extends object = DefaultPointer>(f: (value: Value<T>) => Value<R>): Set<R> {
|
||||
return new Set(_iterMap(this[Symbol.iterator](), f));
|
||||
}
|
||||
|
||||
filter(f: (value: Value<T>) => boolean): Set<T> {
|
||||
const result = new Set<T>();
|
||||
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<T> {
|
||||
return new Set(this);
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag]() { return 'Set'; }
|
||||
|
||||
[PreserveOn](encoder: Encoder<T>) {
|
||||
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<T extends object = DefaultPointer> {
|
||||
readonly annotations: Array<Value<T>>;
|
||||
readonly item: Value<T>;
|
||||
|
||||
constructor(item: Value<T>) {
|
||||
this.annotations = [];
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
[AsPreserve](): Value<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
[PreserveOn](encoder: Encoder<T>) {
|
||||
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<T extends object = DefaultPointer>(x: any): x is Annotated<T> {
|
||||
return !!x?.[IsPreservesAnnotated];
|
||||
}
|
||||
}
|
||||
|
||||
export function peel<T extends object = DefaultPointer>(v: Value<T>): Value<T> {
|
||||
return strip(v, 1);
|
||||
}
|
||||
|
||||
export function strip<T extends object = DefaultPointer>(v: Value<T>, depth: number = Infinity): Value<T> {
|
||||
function step(v: Value<T>, depth: number): Value<T> {
|
||||
if (depth === 0) return v;
|
||||
if (!Annotated.isAnnotated<T>(v)) return v;
|
||||
|
||||
const nextDepth = depth - 1;
|
||||
function walk(v: Value<T>): Value<T> { return step(v, nextDepth); }
|
||||
|
||||
if (Record.isRecord<T>(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<T>(v.item)) {
|
||||
return v.item.map(walk);
|
||||
} else if (Dictionary.isDictionary<Value<T>, 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<T extends object = DefaultPointer>(v0: Value<T>, ...anns: Value<T>[]): Annotated<T> {
|
||||
const v = Annotated.isAnnotated<T>(v0) ? v0 : new Annotated(v0);
|
||||
anns.forEach((a) => v.annotations.push(a));
|
||||
return v;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue