Typed Records

This commit is contained in:
Tony Garnock-Jones 2021-02-25 19:37:22 +01:00
parent 074fc5db98
commit 993689356b
7 changed files with 142 additions and 168 deletions

View File

@ -72,10 +72,10 @@ export function strip<T extends object = DefaultPointer>(v: Value<T>, depth: num
const nextDepth = depth - 1;
function walk(v: Value<T>): Value<T> { return step(v, nextDepth); }
if (Record.isRecord<T>(v.item)) {
return new Record(step(v.item.label, depth), v.item.map(walk));
if (Record.isRecord<Value<T>, T>(v.item)) {
return Record(step(v.item.label, depth), v.item.map(walk));
} else if (Array.isArray(v.item)) {
return v.item.map(walk);
return (v.item as Value<T>[]).map(walk);
} else if (Set.isSet<T>(v.item)) {
return v.item.map(walk);
} else if (Dictionary.isDictionary<Value<T>, T>(v.item)) {

View File

@ -178,7 +178,7 @@ export class Decoder<T extends object> {
case Tag.Record: {
const vs = this.nextvalues();
if (vs.length === 0) throw new DecodeError("Too few elements in encoded record");
return this.wrap(new Record(vs[0], vs.slice(1)));
return this.wrap(Record(vs[0], vs.slice(1)));
}
case Tag.Sequence: return this.wrap(this.nextvalues());
case Tag.Set: return this.wrap(new Set(this.nextvalues()));
@ -197,7 +197,7 @@ export class Decoder<T extends object> {
}
}
try_next() {
try_next(): Value<T> | undefined {
const start = this.index;
try {
return this.next();
@ -379,6 +379,12 @@ export class Encoder<T extends object> {
this.encodebytes(Tag.ByteString, bs);
}
}
else if (Record.isRecord<Value<T>, T>(v)) {
this.emitbyte(Tag.Record);
this.push(v.label);
for (let i of v) { this.push(i); }
this.emitbyte(Tag.End);
}
else if (Array.isArray(v)) {
this.encodevalues(Tag.Sequence, v);
}

View File

@ -11,7 +11,7 @@ export interface FoldMethods<T extends object, R> {
bytes(b: Bytes): R;
symbol(s: symbol): R;
record(r: Record<T>, k: Fold<T, R>): R;
record(r: Record<Value<T>, T>, k: Fold<T, R>): R;
array(a: Array<Value<T>>, k: Fold<T, R>): R;
set(s: Set<T>, k: Fold<T, R>): R;
dictionary(d: Dictionary<Value<T>, T>, k: Fold<T, R>): R;
@ -43,8 +43,8 @@ export abstract class ValueFold<T extends object, R extends object = T> implemen
symbol(s: symbol): Value<R> {
return s;
}
record(r: Record<T>, k: Fold<T, Value<R>>): Value<R> {
return new Record(k(r.label), r.map(k));
record(r: Record<Value<T>, T>, k: Fold<T, Value<R>>): Value<R> {
return Record(k(r.label), r.map(k));
}
array(a: Value<T>[], k: Fold<T, Value<R>>): Value<R> {
return a.map(k);
@ -99,7 +99,7 @@ export function fold<T extends object, R>(v: Value<T>, o: FoldMethods<T, R>): R
case 'symbol':
return o.symbol(v);
case 'object':
if (Record.isRecord<T>(v)) {
if (Record.isRecord<Value<T>, T>(v)) {
return o.record(v, walk);
} else if (Array.isArray(v)) {
return o.array(v, walk);

View File

@ -3,7 +3,7 @@
import * as util from 'util';
import { Record, Bytes, Annotated, Set, Dictionary } from './values';
[Bytes, Annotated, Record, Set, Dictionary].forEach((C) => {
[Bytes, Annotated, Set, Dictionary].forEach((C) => {
(C as any).prototype[util.inspect.custom] =
function (_depth: any, _options: any) {
return this.asPreservesText();

View File

@ -1,133 +1,71 @@
import { Tag } from "./constants";
import { Encoder } from "./codec";
import { PreserveOn } from "./symbols";
import { DefaultPointer, is, Value } from "./values";
export const IsPreservesRecord = Symbol.for('IsPreservesRecord');
export type Tuple<T> = Array<T> | [T];
export class Record<T extends object = DefaultPointer> extends Array<Value<T>> {
readonly label: Value<T>;
export type Record<LabelType extends Value<T>, T extends object = DefaultPointer>
= Array<Value<T>> & { label: LabelType };
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;
}
export type RecordGetters<L extends Value<T>, T extends object, Fs> = {
[K in string & keyof Fs]: (r: Record<L, T>) => Fs[K];
};
super(fields.length);
fields.forEach((f, i) => this[i] = f);
this.label = label;
Object.freeze(this);
}
export type CtorTypes<Fs, Names extends Tuple<keyof Fs>, T extends object> =
{ [K in keyof Names]: Fs[keyof Fs & Names[K]] } & any[];
get(index: number, defaultValue?: Value<T>): Value<T> | undefined {
return (index < this.length) ? this[index] : defaultValue;
}
export interface RecordConstructor<L extends Value<T>, Fs, Names extends Tuple<keyof Fs>, T extends object = DefaultPointer> {
(...fields: CtorTypes<Fs, Names, T>): Record<L, T>;
constructorInfo: RecordConstructorInfo<L, T>;
isClassOf(v: any): v is Record<L, T>;
_: RecordGetters<L, T, Fs>;
};
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> {
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;
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) => {
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> {
(...fields: Value<T>[]): Record<T>;
constructorInfo: RecordConstructorInfo<T>;
isClassOf(v: any): v is Record<T>;
_: { [getter: string]: (r: Value<T>) => Value<T> | undefined };
}
export interface RecordConstructorInfo<T extends object = DefaultPointer> {
label: Value<T>;
export interface RecordConstructorInfo<L extends Value<T>, T extends object = DefaultPointer> {
label: L;
arity: number;
}
export function Record<L extends Value<T>, T extends object = DefaultPointer>(
label: L, fields: Array<Value<T>>): Record<L, T>
{
(fields as any).label = label;
return fields as Record<L, T>;
}
export namespace Record {
export function isRecord<L extends Value<T>, T extends object = DefaultPointer>(x: any): x is Record<L, T> {
return Array.isArray(x) && 'label' in x;
}
export function fallbackToString (_f: Value<any>): string {
return '<unprintable_preserves_field_value>';
}
export function constructorInfo<L extends Value<T>, T extends object = DefaultPointer>(
r: Record<L, T>): RecordConstructorInfo<L, T>
{
return { label: r.label, arity: r.length };
}
export function isClassOf<L extends Value<T>, T extends object = DefaultPointer>(
ci: RecordConstructorInfo<L, T>, v: any): v is Record<L, T>
{
return (Record.isRecord(v)) && is(ci.label, v.label) && (ci.arity === v.length);
}
export function makeConstructor<Fs, T extends object = DefaultPointer>()
: (<L extends Value<T>, Names extends Tuple<keyof Fs>>(label: L, fieldNames: Names) =>
RecordConstructor<L, Fs, Names, T>)
{
return <L extends Value<T>, Names extends Tuple<keyof Fs>>(label: L, fieldNames: Names) => {
const ctor: RecordConstructor<L, Fs, Names, T> =
((...fields: CtorTypes<Fs, Names, T>) =>
Record(label, fields)) as RecordConstructor<L, Fs, Names, T>;
const constructorInfo = { label, arity: fieldNames.length };
ctor.constructorInfo = constructorInfo;
ctor.isClassOf = (v: any): v is Record<L, T> => Record.isClassOf(constructorInfo, v);
(ctor as any)._ = {};
fieldNames.forEach((name, i) => (ctor._ as any)[name] = (r: Record<L, T>) => r[i]);
return ctor;
};
}
}

View File

@ -13,11 +13,11 @@ export * from './record';
export * from './annotated';
export * from './dictionary';
export type DefaultPointer = object
export type DefaultPointer = object;
export type Value<T extends object = DefaultPointer> = Atom | Compound<T> | T | Annotated<T>;
export type Atom = boolean | SingleFloat | DoubleFloat | number | string | Bytes | symbol;
export type Compound<T extends object = DefaultPointer> = Record<T> | Array<Value<T>> | Set<T> | Dictionary<Value<T>, T>;
export type Compound<T extends object = DefaultPointer> = Record<any, T> | Array<Value<T>> | Set<T> | Dictionary<Value<T>, T>;
export function fromJS<T extends object = DefaultPointer>(x: any): Value<T> {
switch (typeof x) {
@ -44,11 +44,11 @@ export function fromJS<T extends object = DefaultPointer>(x: any): Value<T> {
if (typeof x[AsPreserve] === 'function') {
return x[AsPreserve]();
}
if (Record.isRecord<T>(x)) {
if (Record.isRecord<Value<T>, T>(x)) {
return x;
}
if (Array.isArray(x)) {
return (x as Array<Value<T>>).map<Value<T>>(fromJS);
return x.map<Value<T>>(fromJS);
}
if (ArrayBuffer.isView(x) || x instanceof ArrayBuffer) {
return Bytes.from(x);
@ -72,6 +72,9 @@ export function is(a: any, b: any): boolean {
if (a === null || b === null) return false;
if ('equals' in a && typeof a.equals === 'function') return a.equals(b, is);
if (Array.isArray(a) && Array.isArray(b)) {
const isRecord = 'label' in a;
if (isRecord !== 'label' in b) return false;
if (isRecord && !is((a as any).label, (b as any).label)) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) if (!is(a[i], b[i])) return false;
return true;
@ -108,5 +111,17 @@ Symbol.prototype.asPreservesText = function (): string {
};
Array.prototype.asPreservesText = function (): string {
return '[' + this.map((i: Value<any>) => i.asPreservesText()).join(', ') + ']';
if ('label' in (this as any)) {
const r = this as Record<any, any>;
return r.label.asPreservesText() +
'(' + r.map(f => {
try {
return f.asPreservesText();
} catch (e) {
return Record.fallbackToString(f);
}
}).join(', ') + ')';
} else {
return '[' + this.map(i => i.asPreservesText()).join(', ') + ']';
}
};

View File

@ -35,9 +35,12 @@ function encodePointer(w: Pointer): Value<Pointer> {
return w.v;
}
const Discard = Record.makeConstructor<Pointer>('discard', []);
const Capture = Record.makeConstructor<Pointer>('capture', ['pattern']);
const Observe = Record.makeConstructor<Pointer>('observe', ['pattern']);
const _discard = Symbol.for('discard');
const _capture = Symbol.for('capture');
const _observe = Symbol.for('observe');
const Discard = Record.makeConstructor<{}, Pointer>()(_discard, []);
const Capture = Record.makeConstructor<{pattern: Value<Pointer>}, Pointer>()(_capture, ['pattern']);
const Observe = Record.makeConstructor<{pattern: Value<Pointer>}, Pointer>()(_observe, ['pattern']);
describe('record constructors', () => {
it('should have constructorInfo', () => {
@ -51,8 +54,8 @@ describe('record constructors', () => {
})
describe('RecordConstructorInfo', () => {
const C1 = Record.makeBasicConstructor([1], ['x', 'y']);
const C2 = Record.makeBasicConstructor([1], ['z', 'w']);
const C1 = Record.makeConstructor<{x: number, y: number}>()([1], ['x', 'y']);
const C2 = Record.makeConstructor<{z: number, w: number}>()([1], ['z', 'w']);
it('instance comparison should ignore pointer and fieldname differences', () => {
expect(C1(9,9)).is(C2(9,9));
expect(C1(9,9)).not.is(C2(9,8));
@ -67,9 +70,9 @@ describe('RecordConstructorInfo', () => {
describe('records', () => {
it('should have correct getConstructorInfo', () => {
expect(Discard().getConstructorInfo()).toEqual(Discard.constructorInfo);
expect(Capture(Discard()).getConstructorInfo()).toEqual(Capture.constructorInfo);
expect(Observe(Capture(Discard())).getConstructorInfo()).toEqual(Observe.constructorInfo);
expect(Record.constructorInfo(Discard())).toEqual(Discard.constructorInfo);
expect(Record.constructorInfo(Capture(Discard()))).toEqual(Capture.constructorInfo);
expect(Record.constructorInfo(Observe(Capture(Discard())))).toEqual(Observe.constructorInfo);
});
});
@ -167,7 +170,8 @@ describe('common test suite', () => {
const samples_bin = fs.readFileSync(__dirname + '/../../../tests/samples.bin');
const samples = decodeWithAnnotations(samples_bin, { decodePointer });
const TestCases = Record.makeConstructor('TestCases', ['cases']);
const TestCases = Record.makeConstructor<{cases: Dictionary<Value<Pointer>, Pointer>}>()(Symbol.for('TestCases'), ['cases']);
type TestCases = ReturnType<typeof TestCases>;
function DS(bs: Bytes) {
return decode(bs, { decodePointer });
@ -192,24 +196,35 @@ describe('common test suite', () => {
annotate<Pointer>(2, 1),
annotate<Pointer>(4, 3)),
back: 5 },
annotation5: { forward: annotate(new Record<Pointer>(Symbol.for('R'),
[annotate(Symbol.for('f'),
Symbol.for('af'))]),
Symbol.for('ar')),
back: new Record<Pointer>(Symbol.for('R'), [Symbol.for('f')]) },
annotation6: { forward: new Record<Pointer>(annotate<Pointer>(Symbol.for('R'),
Symbol.for('ar')),
[annotate(Symbol.for('f'),
Symbol.for('af'))]),
back: new Record<Pointer>(Symbol.for('R'), [Symbol.for('f')]) },
annotation7: { forward: annotate<Pointer>([], Symbol.for('a'), Symbol.for('b'), Symbol.for('c')),
back: [] },
list1: { forward: [1, 2, 3, 4],
back: [1, 2, 3, 4] },
record2: { value: Observe(new Record(Symbol.for("speak"), [
Discard(),
Capture(Discard())
])) },
annotation5: {
forward: annotate<Pointer>(
Record<symbol, Pointer>(Symbol.for('R'),
[annotate<Pointer>(Symbol.for('f'),
Symbol.for('af'))]),
Symbol.for('ar')),
back: Record<Value<Pointer>, Pointer>(Symbol.for('R'), [Symbol.for('f')])
},
annotation6: {
forward: Record<Value<Pointer>, Pointer>(annotate<Pointer>(Symbol.for('R'),
Symbol.for('ar')),
[annotate<Pointer>(Symbol.for('f'),
Symbol.for('af'))]),
back: Record<symbol, Pointer>(Symbol.for('R'), [Symbol.for('f')])
},
annotation7: {
forward: annotate<Pointer>([], Symbol.for('a'), Symbol.for('b'), Symbol.for('c')),
back: []
},
list1: {
forward: [1, 2, 3, 4],
back: [1, 2, 3, 4]
},
record2: {
value: Observe(Record(Symbol.for("speak"), [
Discard(),
Capture(Discard())
]))
},
};
type Variety = 'normal' | 'nondeterministic' | 'decode';
@ -241,10 +256,10 @@ describe('common test suite', () => {
});
}
const tests = peel(TestCases._.cases(peel(samples))!) as Dictionary<Value<Pointer>, Pointer>;
const tests = peel(TestCases._.cases(peel(samples) as TestCases)) as Dictionary<Value<Pointer>, Pointer>;
tests.forEach((t0: Value<Pointer>, tName0: Value<Pointer>) => {
const tName = Symbol.keyFor(strip(tName0) as symbol)!;
const t = peel(t0) as Record<Pointer>;
const t = peel(t0) as Record<symbol, Pointer>;
switch (t.label) {
case Symbol.for('Test'):
runTestCase('normal', tName, strip(t[0]) as Bytes, t[1]);