Typed Records
This commit is contained in:
parent
074fc5db98
commit
993689356b
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(', ') + ']';
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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]);
|
||||
|
|
Loading…
Reference in New Issue