diff --git a/implementations/javascript/src/annotated.ts b/implementations/javascript/src/annotated.ts index b6479be..f924c4e 100644 --- a/implementations/javascript/src/annotated.ts +++ b/implementations/javascript/src/annotated.ts @@ -72,10 +72,10 @@ export function strip(v: Value, depth: num const nextDepth = depth - 1; function walk(v: Value): Value { return step(v, nextDepth); } - if (Record.isRecord(v.item)) { - return new Record(step(v.item.label, depth), v.item.map(walk)); + if (Record.isRecord, 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[]).map(walk); } else if (Set.isSet(v.item)) { return v.item.map(walk); } else if (Dictionary.isDictionary, T>(v.item)) { diff --git a/implementations/javascript/src/codec.ts b/implementations/javascript/src/codec.ts index ff8f076..fdbd9a5 100644 --- a/implementations/javascript/src/codec.ts +++ b/implementations/javascript/src/codec.ts @@ -178,7 +178,7 @@ export class Decoder { 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 { } } - try_next() { + try_next(): Value | undefined { const start = this.index; try { return this.next(); @@ -379,6 +379,12 @@ export class Encoder { this.encodebytes(Tag.ByteString, bs); } } + else if (Record.isRecord, 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); } diff --git a/implementations/javascript/src/fold.ts b/implementations/javascript/src/fold.ts index a5b455e..b8424a3 100644 --- a/implementations/javascript/src/fold.ts +++ b/implementations/javascript/src/fold.ts @@ -11,7 +11,7 @@ export interface FoldMethods { bytes(b: Bytes): R; symbol(s: symbol): R; - record(r: Record, k: Fold): R; + record(r: Record, T>, k: Fold): R; array(a: Array>, k: Fold): R; set(s: Set, k: Fold): R; dictionary(d: Dictionary, T>, k: Fold): R; @@ -43,8 +43,8 @@ export abstract class ValueFold implemen symbol(s: symbol): Value { return s; } - record(r: Record, k: Fold>): Value { - return new Record(k(r.label), r.map(k)); + record(r: Record, T>, k: Fold>): Value { + return Record(k(r.label), r.map(k)); } array(a: Value[], k: Fold>): Value { return a.map(k); @@ -99,7 +99,7 @@ export function fold(v: Value, o: FoldMethods): R case 'symbol': return o.symbol(v); case 'object': - if (Record.isRecord(v)) { + if (Record.isRecord, T>(v)) { return o.record(v, walk); } else if (Array.isArray(v)) { return o.array(v, walk); diff --git a/implementations/javascript/src/node_support.ts b/implementations/javascript/src/node_support.ts index 53b5de7..7734048 100644 --- a/implementations/javascript/src/node_support.ts +++ b/implementations/javascript/src/node_support.ts @@ -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(); diff --git a/implementations/javascript/src/record.ts b/implementations/javascript/src/record.ts index 72c0aa5..1628fd3 100644 --- a/implementations/javascript/src/record.ts +++ b/implementations/javascript/src/record.ts @@ -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 = Array | [T]; -export class Record extends Array> { - readonly label: Value; +export type Record, T extends object = DefaultPointer> + = Array> & { label: LabelType }; - constructor(label: Value, fields: Value[]) { - 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, T extends object, Fs> = { + [K in string & keyof Fs]: (r: Record) => Fs[K]; +}; - super(fields.length); - fields.forEach((f, i) => this[i] = f); - this.label = label; - Object.freeze(this); - } +export type CtorTypes, T extends object> = + { [K in keyof Names]: Fs[keyof Fs & Names[K]] } & any[]; - get(index: number, defaultValue?: Value): Value | undefined { - return (index < this.length) ? this[index] : defaultValue; - } +export interface RecordConstructor, Fs, Names extends Tuple, T extends object = DefaultPointer> { + (...fields: CtorTypes): Record; + constructorInfo: RecordConstructorInfo; + isClassOf(v: any): v is Record; + _: RecordGetters; +}; - 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(label: Value, fieldNames: string[]): RecordConstructor { - const arity = fieldNames.length; - const ctor: RecordConstructor = (...fields: Value[]): 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: Value): 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: Value[]): Record; - constructorInfo: RecordConstructorInfo; - isClassOf(v: any): v is Record; - _: { [getter: string]: (r: Value) => Value | undefined }; -} - -export interface RecordConstructorInfo { - label: Value; +export interface RecordConstructorInfo, T extends object = DefaultPointer> { + label: L; arity: number; } + +export function Record, T extends object = DefaultPointer>( + label: L, fields: Array>): Record +{ + (fields as any).label = label; + return fields as Record; +} + +export namespace Record { + export function isRecord, T extends object = DefaultPointer>(x: any): x is Record { + return Array.isArray(x) && 'label' in x; + } + + export function fallbackToString (_f: Value): string { + return ''; + } + + export function constructorInfo, T extends object = DefaultPointer>( + r: Record): RecordConstructorInfo + { + return { label: r.label, arity: r.length }; + } + + export function isClassOf, T extends object = DefaultPointer>( + ci: RecordConstructorInfo, v: any): v is Record + { + return (Record.isRecord(v)) && is(ci.label, v.label) && (ci.arity === v.length); + } + + export function makeConstructor() + : (, Names extends Tuple>(label: L, fieldNames: Names) => + RecordConstructor) + { + return , Names extends Tuple>(label: L, fieldNames: Names) => { + const ctor: RecordConstructor = + ((...fields: CtorTypes) => + Record(label, fields)) as RecordConstructor; + const constructorInfo = { label, arity: fieldNames.length }; + ctor.constructorInfo = constructorInfo; + ctor.isClassOf = (v: any): v is Record => Record.isClassOf(constructorInfo, v); + (ctor as any)._ = {}; + fieldNames.forEach((name, i) => (ctor._ as any)[name] = (r: Record) => r[i]); + return ctor; + }; + } +} diff --git a/implementations/javascript/src/values.ts b/implementations/javascript/src/values.ts index 87ee80b..333903e 100644 --- a/implementations/javascript/src/values.ts +++ b/implementations/javascript/src/values.ts @@ -13,11 +13,11 @@ export * from './record'; export * from './annotated'; export * from './dictionary'; -export type DefaultPointer = object +export type DefaultPointer = object; export type Value = Atom | Compound | T | Annotated; export type Atom = boolean | SingleFloat | DoubleFloat | number | string | Bytes | symbol; -export type Compound = Record | Array> | Set | Dictionary, T>; +export type Compound = Record | Array> | Set | Dictionary, T>; export function fromJS(x: any): Value { switch (typeof x) { @@ -44,11 +44,11 @@ export function fromJS(x: any): Value { if (typeof x[AsPreserve] === 'function') { return x[AsPreserve](); } - if (Record.isRecord(x)) { + if (Record.isRecord, T>(x)) { return x; } if (Array.isArray(x)) { - return (x as Array>).map>(fromJS); + return x.map>(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) => i.asPreservesText()).join(', ') + ']'; + if ('label' in (this as any)) { + const r = this as Record; + 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(', ') + ']'; + } }; diff --git a/implementations/javascript/test/codec.test.ts b/implementations/javascript/test/codec.test.ts index 10eeff3..2c0db4f 100644 --- a/implementations/javascript/test/codec.test.ts +++ b/implementations/javascript/test/codec.test.ts @@ -35,9 +35,12 @@ function encodePointer(w: Pointer): Value { return w.v; } -const Discard = Record.makeConstructor('discard', []); -const Capture = Record.makeConstructor('capture', ['pattern']); -const Observe = Record.makeConstructor('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>()(_capture, ['pattern']); +const Observe = Record.makeConstructor<{pattern: Value}, 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, Pointer>}>()(Symbol.for('TestCases'), ['cases']); + type TestCases = ReturnType; function DS(bs: Bytes) { return decode(bs, { decodePointer }); @@ -192,24 +196,35 @@ describe('common test suite', () => { annotate(2, 1), annotate(4, 3)), back: 5 }, - annotation5: { forward: annotate(new Record(Symbol.for('R'), - [annotate(Symbol.for('f'), - Symbol.for('af'))]), - Symbol.for('ar')), - back: new Record(Symbol.for('R'), [Symbol.for('f')]) }, - annotation6: { forward: new Record(annotate(Symbol.for('R'), - Symbol.for('ar')), - [annotate(Symbol.for('f'), - Symbol.for('af'))]), - back: new Record(Symbol.for('R'), [Symbol.for('f')]) }, - annotation7: { forward: annotate([], 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( + Record(Symbol.for('R'), + [annotate(Symbol.for('f'), + Symbol.for('af'))]), + Symbol.for('ar')), + back: Record, Pointer>(Symbol.for('R'), [Symbol.for('f')]) + }, + annotation6: { + forward: Record, Pointer>(annotate(Symbol.for('R'), + Symbol.for('ar')), + [annotate(Symbol.for('f'), + Symbol.for('af'))]), + back: Record(Symbol.for('R'), [Symbol.for('f')]) + }, + annotation7: { + forward: annotate([], 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, Pointer>; + const tests = peel(TestCases._.cases(peel(samples) as TestCases)) as Dictionary, Pointer>; tests.forEach((t0: Value, tName0: Value) => { const tName = Symbol.keyFor(strip(tName0) as symbol)!; - const t = peel(t0) as Record; + const t = peel(t0) as Record; switch (t.label) { case Symbol.for('Test'): runTestCase('normal', tName, strip(t[0]) as Bytes, t[1]);