preserves/implementations/javascript/src/record.ts

147 lines
5.9 KiB
TypeScript

import { Tag } from "./constants";
import { Encoder } from "./codec";
import { PreserveOn } from "./symbols";
import { DefaultPointer, is, Value } from "./values";
import { Tuple, TupleMap } from "./tuple";
export const IsPreservesRecord = Symbol.for('IsPreservesRecord');
export type Record<LabelType extends Value<T>, FieldsType extends Tuple<Value<T>>, T extends object = DefaultPointer>
= { readonly label: LabelType } & FieldsType & RecordImpl<T>;
export interface RecordConstructor<LabelType extends Value<T>, FieldsType extends Tuple<Value<T>>, T extends object = DefaultPointer> {
(...fields: FieldsType): Record<LabelType, FieldsType, T>;
constructorInfo: RecordConstructorInfo<LabelType, T>;
isClassOf(v: any): v is Record<LabelType, FieldsType, T>;
_: { [getter: string]: (r: any) => Value<T> | undefined };
}
export interface RecordConstructorInfo<LabelType extends Value<T>, T extends object = DefaultPointer> {
label: LabelType;
arity: number;
}
export function Record<LabelType extends Value<T>, FieldsType extends Tuple<Value<T>>, T extends object = DefaultPointer>
(label: LabelType, fields: FieldsType)
: Record<LabelType, FieldsType, T>
{
return new RecordImpl(label, fields) as unknown as Record<LabelType, FieldsType, T>;
}
export namespace Record {
export function fallbackToString(_f: Value<any>): string {
return '<unprintable_preserves_field_value>';
}
export function makeConstructor<T extends object = DefaultPointer>(): <LabelType extends Value<T>, FieldNamesType extends Tuple<string>, FieldsType extends TupleMap<FieldNamesType, Value<T>>>(label: LabelType, fieldNames: FieldNamesType) => RecordConstructor<LabelType, FieldsType, T> {
return <LabelType extends Value<T>, FieldNamesType extends Tuple<string>, FieldsType extends TupleMap<FieldNamesType, Value<T>>>(label: LabelType, fieldNames: FieldNamesType) => {
const arity = fieldNames.length;
const ctor: RecordConstructor<LabelType, FieldsType, T> = (...fields: FieldsType): Record<LabelType, FieldsType, T> => {
if (fields.length !== arity) {
throw new Error("Record: cannot instantiate " + (label && (label as any).toString()) +
" expecting " + arity + " fields with " + fields.length + " fields");
}
return Record(label, fields);
};
const constructorInfo = { label, arity };
ctor.constructorInfo = constructorInfo;
ctor.isClassOf = (v: any): v is Record<LabelType, FieldsType, 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 as any).toString()+"."+name+
" from non-"+(label as any).toString()+": "+(r && r.toString()));
}
return r[i];
};
});
return ctor;
};
}
export function isRecord<T extends object = DefaultPointer>(x: any): x is Record<Value<T>, Tuple<Value<T>>, T> {
return !!x?.[IsPreservesRecord];
}
export function isClassOf<LabelType extends Value<T>, FieldsType extends Tuple<Value<T>>, T extends object = DefaultPointer>(ci: RecordConstructorInfo<LabelType, T>, v: any): v is Record<LabelType, FieldsType, T> {
return (Record.isRecord(v)) && is(ci.label, v.label) && (ci.arity === v.length);
}
}
export class RecordImpl<T extends object = DefaultPointer> extends Array<Value<T>> {
readonly label: Value<T>;
constructor(label: Value<T>, fields: Array<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;
}
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>): this {
return <this> new RecordImpl(this.label, this.map((f, i) => (i === index) ? newValue : f));
}
getConstructorInfo(): RecordConstructorInfo<Value<T>, 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[i]));
}
// hashCode(): number {
// let h = hash(this.label);
// this.forEach((f) => h = ((31 * h) + hash(f)) | 0);
// return h;
// }
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(', ') + ')';
}
[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;
}
}