preserves/implementations/javascript/src/record.ts

134 lines
4.8 KiB
TypeScript
Raw Normal View History

import { Tag } from "./constants";
import { Encoder } from "./codec";
import { PreserveOn } from "./symbols";
2021-02-22 19:00:15 +00:00
import { DefaultPointer, 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>;
2021-02-22 19:00:15 +00:00
constructor(label: Value<T>, fields: Value<T>[]) {
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;
}
2021-02-22 19:00:15 +00:00
super(fields.length);
fields.forEach((f, i) => this[i] = 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 = any>(labelSymbolText: string, fieldNames: string[]): RecordConstructor<T> {
2021-02-22 19:00:15 +00:00
return Record.makeBasicConstructor<T>(Symbol.for(labelSymbolText), fieldNames);
}
static makeBasicConstructor<T extends object = any>(label: Value<T>, fieldNames: string[]): RecordConstructor<T> {
const arity = fieldNames.length;
2021-02-22 19:03:09 +00:00
const ctor: RecordConstructor<T> = (...fields: Value<T>[]): 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) => {
2021-02-22 19:03:09 +00:00
ctor._[name] = function (r: Value<T>): 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> {
2021-02-22 19:03:09 +00:00
(...fields: Value<T>[]): Record<T>;
constructorInfo: RecordConstructorInfo<T>;
isClassOf(v: any): v is Record<T>;
2021-02-22 19:03:09 +00:00
_: { [getter: string]: (r: Value<T>) => Value<T> | undefined };
}
export interface RecordConstructorInfo<T extends object = DefaultPointer> {
label: Value<T>;
arity: number;
}