794 lines
23 KiB
TypeScript
794 lines
23 KiB
TypeScript
// Preserves Values.
|
|
|
|
import { PreserveOn, AsPreserve } from './symbols';
|
|
import { Tag } from './constants';
|
|
import { Encoder, encode } from './codec';
|
|
|
|
const textEncoder = new TextEncoder();
|
|
const textDecoder = new TextDecoder();
|
|
|
|
export type Value = Atom | Compound | Annotated;
|
|
export type Atom = boolean | Single | Double | number | string | Bytes | symbol;
|
|
export type Compound = Record | Array<Value> | Set | Dictionary;
|
|
|
|
export function isRecord(x: any): x is Record {
|
|
return Array.isArray(x) && 'label' in x;
|
|
}
|
|
|
|
export function fromJS(x: any): Value {
|
|
switch (typeof x) {
|
|
case 'number':
|
|
if (!Number.isInteger(x)) {
|
|
// We require that clients be explicit about integer vs. non-integer types.
|
|
throw new TypeError("Refusing to autoconvert non-integer number to Single or Double");
|
|
}
|
|
// FALL THROUGH
|
|
case 'string':
|
|
case 'symbol':
|
|
case 'boolean':
|
|
return x;
|
|
|
|
case 'undefined':
|
|
case 'function':
|
|
break;
|
|
|
|
case 'object':
|
|
if (x === null) {
|
|
break;
|
|
}
|
|
if (typeof x[AsPreserve] === 'function') {
|
|
return x[AsPreserve]();
|
|
}
|
|
if (isRecord(x)) {
|
|
return x;
|
|
}
|
|
if (Array.isArray(x)) {
|
|
return x.map(fromJS);
|
|
}
|
|
if (ArrayBuffer.isView(x) || x instanceof ArrayBuffer) {
|
|
return Bytes.from(x);
|
|
}
|
|
return Dictionary.fromJS(x);
|
|
}
|
|
|
|
throw new TypeError("Cannot represent JavaScript value as Preserves: " + x);
|
|
}
|
|
|
|
export type FloatType = 'Single' | 'Double';
|
|
|
|
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;
|
|
}
|
|
|
|
export class Single extends Float {
|
|
[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;
|
|
}
|
|
|
|
_floatType(): FloatType {
|
|
return 'Single';
|
|
}
|
|
|
|
asPreservesText(): string {
|
|
return '' + this.value + 'f';
|
|
}
|
|
}
|
|
|
|
export class Double extends Float {
|
|
[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;
|
|
}
|
|
|
|
_floatType(): FloatType {
|
|
return 'Double';
|
|
}
|
|
|
|
asPreservesText(): string {
|
|
return '' + this.value;
|
|
}
|
|
}
|
|
|
|
function isFloat(x: any, t: FloatType): x is Float {
|
|
return typeof x === 'object' && x !== null &&
|
|
'value' in x && typeof x.value === 'number' &&
|
|
'_floatType' in x && x._floatType() === t;
|
|
}
|
|
|
|
export const isSingle = (x: any): x is Single => isFloat(x, 'Single');
|
|
export const isDouble = (x: any): x is Double => isFloat(x, 'Double');
|
|
|
|
export type BytesLike = Bytes | Uint8Array;
|
|
|
|
export class Bytes {
|
|
readonly _view: Uint8Array;
|
|
|
|
constructor(maybeByteIterable: any = new Uint8Array()) {
|
|
if (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.slice(0));
|
|
} 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 (isBytes(io)) return io;
|
|
if (io instanceof Uint8Array) return new Bytes(io);
|
|
}
|
|
|
|
static toIO(b : string | BytesLike): string | Uint8Array {
|
|
if (typeof b === 'string') return b;
|
|
if (isBytes(b)) return b._view;
|
|
if (b instanceof Uint8Array) 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 (!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);
|
|
}
|
|
}
|
|
|
|
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 isBytes(x: any): x is Bytes {
|
|
return typeof x === 'object' && x !== null &&
|
|
'_view' in x &&
|
|
x._view instanceof Uint8Array;
|
|
}
|
|
|
|
export function underlying(b: Bytes | Uint8Array): Uint8Array {
|
|
return (b instanceof Uint8Array) ? b : b._view;
|
|
}
|
|
|
|
declare global {
|
|
interface Boolean { asPreservesText(): string; }
|
|
interface Number { asPreservesText(): string; }
|
|
interface String { asPreservesText(): string; }
|
|
interface Symbol { asPreservesText(): string; }
|
|
interface Array<T> { asPreservesText(): string; }
|
|
}
|
|
|
|
Boolean.prototype.asPreservesText = function (): string {
|
|
return this ? '#t' : '#f';
|
|
};
|
|
|
|
Number.prototype.asPreservesText = function (): string {
|
|
return '' + this;
|
|
};
|
|
|
|
String.prototype.asPreservesText = function (): string {
|
|
return JSON.stringify(this);
|
|
};
|
|
|
|
Symbol.prototype.asPreservesText = function (): string {
|
|
// TODO: escaping
|
|
return this.description;
|
|
};
|
|
|
|
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<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.prototype[k] = function (...args: any[]) { return this._view[k](...args); };
|
|
}
|
|
|
|
for (const k of `filter map slice subarray`.split(/\s+/))
|
|
{
|
|
Bytes.prototype[k] = function (...args: any[]) { return new Bytes(this._view[k](...args)); };
|
|
}
|
|
|
|
for (const k of `reverse sort`.split(/\s+/))
|
|
{
|
|
Bytes.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<Value> {
|
|
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);
|
|
return;
|
|
}
|
|
|
|
super(fieldsJS.length);
|
|
fieldsJS.forEach((f, i) => this[i] = fromJS(f));
|
|
this.label = label;
|
|
Object.freeze(this);
|
|
}
|
|
|
|
get(index: number, defaultValue?: Value): Value {
|
|
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 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) => '<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(labelSymbolText: string, fieldNames: string[]) {
|
|
return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames);
|
|
}
|
|
|
|
static makeBasicConstructor(label0: any, fieldNames: string[]): RecordConstructor {
|
|
const label = fromJS(label0);
|
|
const arity = fieldNames.length;
|
|
const ctor = (...fields: any[]) => {
|
|
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);
|
|
};
|
|
ctor.constructorInfo = { label, arity };
|
|
ctor.isClassOf =
|
|
(v: any): v is Record => (isRecord(v) && is(label, v.label) && v.length === arity);
|
|
ctor._ = {};
|
|
fieldNames.forEach((name, i) => {
|
|
ctor._[name] = function (r: any): Value {
|
|
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);
|
|
}
|
|
}
|
|
|
|
export interface RecordConstructor {
|
|
(...fields: any[]): Record;
|
|
constructorInfo: RecordConstructorInfo;
|
|
isClassOf(v: any): v is Record;
|
|
_: { [getter: string]: (r: any) => Value };
|
|
}
|
|
|
|
export interface RecordConstructorInfo {
|
|
label: Value;
|
|
arity: number;
|
|
}
|
|
|
|
export function is(a: Value, b: Value): boolean {
|
|
if (Object.is(a, b)) return true;
|
|
if (typeof a !== typeof b) return false;
|
|
if (typeof a === 'object') {
|
|
if (a === null || b === null) return false;
|
|
if ('equals' in a) return a.equals(b);
|
|
if (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 function hash(a: Value): number {
|
|
throw new Error("shouldBeImplemented"); // TODO
|
|
}
|
|
|
|
export function isClassOf(ci: RecordConstructorInfo, v: any): v is Record {
|
|
return (isRecord(v)) && is(ci.label, v.label) && (ci.arity === v.length);
|
|
}
|
|
|
|
export type DictionaryType = 'Dictionary' | 'Set';
|
|
|
|
export function is_Dictionary(x: any, t: DictionaryType): x is _Dictionary {
|
|
return typeof x === 'object' && x !== null &&
|
|
'_items' in x &&
|
|
'_dictionaryType' in x &&
|
|
x._dictionaryType() === t;
|
|
}
|
|
|
|
export const isDictionary = (x: any): x is Dictionary => is_Dictionary(x, 'Dictionary');
|
|
export const isSet = (x: any): x is Set => is_Dictionary(x, 'Set');
|
|
|
|
export type DictionaryEntry = [Value, Value];
|
|
|
|
export abstract class _Dictionary {
|
|
_items: { [key: string]: DictionaryEntry } = {};
|
|
|
|
_key(key: Value): string {
|
|
const bs = encode(key, { canonical: true })._view;
|
|
const s = String.fromCharCode.apply(null, bs);
|
|
return s;
|
|
}
|
|
|
|
_lookup(key: Value): DictionaryEntry | null {
|
|
const k = this._key(key);
|
|
return k in this._items ? this._items[k] : null;
|
|
}
|
|
|
|
_set(key: Value, value: Value) {
|
|
this._items[this._key(key)] = [key, value];
|
|
}
|
|
|
|
_get(key: Value, defaultValue?: Value): Value {
|
|
const k = this._key(key);
|
|
return k in this._items ? this._items[k][1] : defaultValue;
|
|
}
|
|
|
|
_delete(key: Value) {
|
|
delete this._items[this._key(key)];
|
|
}
|
|
|
|
_forEach(f: (value: Value, key: Value) => void) {
|
|
for (let ks in this._items) {
|
|
const [k, v] = this._items[ks];
|
|
f(v, k);
|
|
}
|
|
}
|
|
|
|
_map<T>(f: (value: Value, key: Value) => T): Array<T> {
|
|
const result = [];
|
|
for (let ks in this._items) {
|
|
const [k, v] = this._items[ks];
|
|
result.push(f(v, k));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
equals(other: any): boolean {
|
|
if (!Object.is(other.constructor, this.constructor)) return false;
|
|
const es1 = Object.entries(this._items);
|
|
if (es1.length !== Object.entries(other._items).length) return false;
|
|
for (let [ks1, e1] of es1) {
|
|
const e2 = other._items[ks1];
|
|
if (!is(e1[1], e2[1])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
toString(): string {
|
|
return this.asPreservesText();
|
|
}
|
|
|
|
abstract asPreservesText(): string;
|
|
abstract _dictionaryType(): DictionaryType;
|
|
}
|
|
|
|
export class Dictionary extends _Dictionary {
|
|
static fromJS(x: object): Dictionary {
|
|
if (isDictionary(x)) return x;
|
|
const d = new Dictionary();
|
|
for (let key in x) {
|
|
const value = x[key];
|
|
d._set(key, fromJS(value));
|
|
}
|
|
return d;
|
|
}
|
|
|
|
_dictionaryType(): DictionaryType {
|
|
return 'Dictionary';
|
|
}
|
|
|
|
set(key: Value, value: Value) {
|
|
this._set(key, value);
|
|
}
|
|
|
|
get(key: Value, defaultValue?: Value): Value {
|
|
return this._get(key, defaultValue);
|
|
}
|
|
|
|
delete(key: Value) {
|
|
this._delete(key);
|
|
}
|
|
|
|
mapEntries(f: (entry: DictionaryEntry) => DictionaryEntry): Dictionary {
|
|
const result = new Dictionary();
|
|
for (let ks in this._items) {
|
|
const oldEntry = this._items[ks];
|
|
const newEntry = f(oldEntry);
|
|
result._set(newEntry[0], newEntry[1])
|
|
}
|
|
return result;
|
|
}
|
|
|
|
forEach(f: (value: Value, key: Value) => void) {
|
|
this._forEach(f);
|
|
}
|
|
|
|
asPreservesText(): string {
|
|
return '{' +
|
|
this._map((v, k) => k.asPreservesText() + ': ' + v.asPreservesText()).join(', ') +
|
|
'}';
|
|
}
|
|
}
|
|
|
|
export class Set extends _Dictionary implements Iterable<Value> {
|
|
constructor(items: Iterable<any> = []) {
|
|
super();
|
|
for (let item of items) this.add(fromJS(item));
|
|
}
|
|
|
|
_dictionaryType(): DictionaryType {
|
|
return 'Set';
|
|
}
|
|
|
|
add(v: Value) {
|
|
this._set(v, true);
|
|
}
|
|
|
|
delete(v: Value) {
|
|
this._delete(v);
|
|
}
|
|
|
|
includes(key: Value) {
|
|
return this._lookup(key) !== null;
|
|
}
|
|
|
|
forEach(f: (value: Value) => void) {
|
|
this._forEach((_v, k) => f(k));
|
|
}
|
|
|
|
map(f: (value: Value) => Value): Set {
|
|
const result = new Set();
|
|
for (let ks in this._items) {
|
|
const k = this._items[ks][0];
|
|
result._set(f(k), true);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[Symbol.iterator](): Iterator<Value> {
|
|
return this._map((_v, k) => k)[Symbol.iterator]();
|
|
}
|
|
|
|
asPreservesText(): string {
|
|
return '#{' +
|
|
this._map((_v, k) => k.asPreservesText()).join(', ') +
|
|
'}';
|
|
}
|
|
}
|
|
|
|
export function isAnnotated(x: any): x is Annotated {
|
|
return typeof x === 'object' && x !== null &&
|
|
'annotations' in x &&
|
|
'item' in x;
|
|
}
|
|
|
|
export class Annotated {
|
|
readonly annotations: Array<Value>;
|
|
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 isAnnotated(other) && is(this.item, other.item);
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
export function peel(v: Value): Value {
|
|
return strip(v, 1);
|
|
}
|
|
|
|
export function strip(v: Value, depth: number = Infinity) {
|
|
function step(v: Value, depth: number): Value {
|
|
if (depth === 0) return v;
|
|
if (!isAnnotated(v)) return v;
|
|
|
|
const nextDepth = depth - 1;
|
|
function walk(v: Value) { return step(v, nextDepth); }
|
|
|
|
if (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 (isSet(v.item)) {
|
|
return v.item.map(walk);
|
|
} else if (isDictionary(v.item)) {
|
|
return v.item.mapEntries((e) => [walk(e[0]), walk(e[1])]);
|
|
} else if (isAnnotated(v.item)) {
|
|
throw new Error("Improper annotation structure");
|
|
} else {
|
|
return v.item;
|
|
}
|
|
}
|
|
return step(v, depth);
|
|
}
|
|
|
|
export function annotate(v0: Value, ...anns: Value[]) {
|
|
const v = isAnnotated(v0) ? v0 : new Annotated(v0);
|
|
anns.forEach((a) => v.annotations.push(a));
|
|
return v;
|
|
}
|