2021-02-17 15:52:01 +00:00
|
|
|
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";
|
2021-02-17 15:52:01 +00:00
|
|
|
|
|
|
|
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>[]) {
|
2021-02-17 15:52:01 +00:00
|
|
|
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);
|
2021-02-17 15:52:01 +00:00
|
|
|
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> {
|
2021-02-22 19:00:15 +00:00
|
|
|
return Record.makeBasicConstructor<T>(Symbol.for(labelSymbolText), fieldNames);
|
2021-02-17 15:52:01 +00:00
|
|
|
}
|
|
|
|
|
2021-02-22 19:00:15 +00:00
|
|
|
static makeBasicConstructor<T extends object = DefaultPointer>(label: Value<T>, fieldNames: string[]): RecordConstructor<T> {
|
2021-02-17 15:52:01 +00:00
|
|
|
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;
|
|
|
|
}
|