forked from syndicate-lang/preserves
147 lines
5.9 KiB
TypeScript
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;
|
|
}
|
|
}
|