import { Tag } from "./constants"; import { Encoder } from "./codec"; import { PreserveOn } from "./symbols"; import { DefaultPointer, fromJS, is, Value } from "./values"; export const IsPreservesRecord = Symbol.for('IsPreservesRecord'); export class Record extends Array> { 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); this.label = label; // needed just to keep the typechecker happy return; } super(fieldsJS.length); fieldsJS.forEach((f, i) => this[i] = fromJS(f)); this.label = label; Object.freeze(this); } get(index: number, defaultValue?: Value): Value | undefined { 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 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) => string = (_f) => ''; 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[]): RecordConstructor { return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames); } static makeBasicConstructor(label0: any, fieldNames: string[]): RecordConstructor { const label = fromJS(label0); const arity = fieldNames.length; const ctor: RecordConstructor = (...fields: any[]): Record => { 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); }; const constructorInfo = { label, arity }; ctor.constructorInfo = constructorInfo; ctor.isClassOf = (v: any): v is Record => Record.isClassOf(constructorInfo, v); ctor._ = {}; fieldNames.forEach((name, i) => { ctor._[name] = function (r: any): Value | 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) { 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(x: any): x is Record { return !!x?.[IsPreservesRecord]; } static isClassOf(ci: RecordConstructorInfo, v: any): v is Record { return (Record.isRecord(v)) && is(ci.label, v.label) && (ci.arity === v.length); } } export interface RecordConstructor { (...fields: any[]): Record; constructorInfo: RecordConstructorInfo; isClassOf(v: any): v is Record; _: { [getter: string]: (r: any) => Value | undefined }; } export interface RecordConstructorInfo { label: Value; arity: number; }