Update JavaScript implementation and port to TypeScript

This commit is contained in:
Tony Garnock-Jones 2021-01-07 17:41:46 +01:00
parent 2c5ed693f5
commit 44f142d86b
25 changed files with 1571 additions and 1359 deletions

View File

@ -1,4 +1,3 @@
.nyc_output/
coverage/
lib/
node_modules/
package-lock.json

View File

@ -0,0 +1,10 @@
import './src/values'; // required here because it monkey-patches
// various globals like Array, Symbol etc., and
// we need the patching to be done here so the
// patching is consistently visible in the
// per-test sub-VMs.
export default {
preset: 'ts-jest',
testEnvironment: 'node',
};

View File

@ -1,6 +1,6 @@
{
"name": "preserves",
"version": "0.3.1",
"version": "0.4.0",
"description": "Experimental data serialization format",
"homepage": "https://gitlab.com/preserves/preserves",
"license": "Apache-2.0",
@ -8,17 +8,22 @@
"access": "public"
},
"repository": "gitlab:preserves/preserves",
"scripts": {
"test": "mocha",
"cover": "nyc --reporter=html mocha"
},
"main": "src/index.js",
"author": "Tony Garnock-Jones <tonyg@leastfixedpoint.com>",
"devDependencies": {
"mocha": "^8.2.0",
"nyc": "^14.1.1"
"@types/jest": "^26.0.19",
"jest": "^26.6.3",
"ts-jest": "^26.4.4",
"ts-node-dev": "^1.1.1",
"typescript": "^4.1.3"
},
"dependencies": {
"immutable": "^4.0.0-rc.12"
}
"scripts": {
"prepare": "npx tsc",
"watch": "npx tsc -w",
"clean": "rm -rf lib",
"veryclean": "npm run clean && rm -rf node_modules",
"test": "npx jest",
"testwatch": "npx jest -w"
},
"dependencies": {}
}

View File

@ -1,89 +0,0 @@
"use strict";
// Preserves Annotations.
if (require('./singletonmodule.js')('leastfixedpoint.com/preserves',
require('../package.json').version,
'annotations.js',
module)) return;
const { Record, List, Map, Set, is, hash } = require('./values.js');
const { PreserveOn, AsPreserve } = require('./symbols.js');
function Annotated(item) {
this.annotations = [];
this.item = item;
}
Annotated.prototype[AsPreserve] = function () {
return this;
};
Annotated.prototype[PreserveOn] = function (encoder) {
for (const a of this.annotations) {
encoder.header(0, 0, 5);
encoder.push(a);
}
encoder.push(this.item);
};
Annotated.prototype.strip = function (depth) {
return stripAnnotations(this, depth);
};
Annotated.prototype.peel = function () {
return stripAnnotations(this, 1);
};
Annotated.prototype.equals = function (other) {
return isAnnotated(other) && is(this.item, other.item);
};
Annotated.prototype.hashCode = function () {
return hash(this.item);
};
function isAnnotated(v) {
return (v instanceof Annotated);
}
function stripAnnotations(v, depth) {
function step(v, depth) {
if (depth === 0) return v;
if (!isAnnotated(v)) return v;
const nextDepth = depth - 1;
function walk(v) { return step(v, nextDepth); }
if (v.item instanceof Record) {
return new Record(step(v.item.label, depth), v.item.fields.map(walk));
} else if (List.isList(v.item)) {
return v.item.map(walk);
} else if (Set.isSet(v.item)) {
return v.item.map(walk);
} else if (Map.isMap(v.item)) {
return v.item.mapEntries((e) => [walk(e[0]), walk(e[1])]);
} else if (isAnnotated(v.item)) {
const e = new Error("Improper annotation structure");
e.irritant = v;
throw e;
} else {
return v.item;
}
}
return step(v, (depth === void 0) ? Infinity : depth);
}
function annotate(v, ...anns) {
if (!isAnnotated(v)) {
v = new Annotated(v);
}
anns.forEach((a) => v.annotations.push(a));
return v;
}
Object.assign(module.exports, {
Annotated,
isAnnotated,
stripAnnotations,
annotate,
});

View File

@ -1,394 +0,0 @@
"use strict";
// Preserves Binary codec.
if (require('./singletonmodule.js')('leastfixedpoint.com/preserves',
require('../package.json').version,
'codec.js',
module)) return;
const Values = require('./values.js');
const Annotations = require('./annotations.js');
const { fromJS, List, Map, Set, Bytes, Record, Single, Double } = Values;
const { PreserveOn } = require('./symbols.js');
class DecodeError extends Error {}
class EncodeError extends Error {
constructor(message, irritant) {
super(message);
this.irritant = irritant;
}
}
class ShortPacket extends DecodeError {}
class Decoder {
constructor(packet, options) {
options = options || {};
this.packet = packet
? (packet._view || packet) // strip off Bytes wrapper, if any
: new Uint8Array(0);
this.index = 0;
this.includeAnnotations = options.includeAnnotations || false;
}
write(data) {
this.packet = Bytes.concat([this.packet.slice(this.index), data])._view;
this.index = 0;
}
nextbyte() {
if (this.index >= this.packet.length) throw new ShortPacket("Short packet");
// ^ NOTE: greater-than-or-equal-to, not greater-than.
return this.packet[this.index++];
}
nextbytes(n) {
const start = this.index;
this.index += n;
if (this.index > this.packet.length) throw new ShortPacket("Short packet");
// ^ NOTE: greater-than, not greater-than-or-equal-to.
return new DataView(this.packet.buffer, this.packet.byteOffset + start, n);
}
wirelength(arg) {
if (arg < 15) return arg;
return this.varint();
}
varint() {
// TODO: Bignums :-/
const v = this.nextbyte();
if (v < 128) return v;
return (this.varint() << 7) + (v - 128);
}
nextvalues(n) {
const result = [];
for (let i = 0; i < n; i++) result.push(this.next());
return result;
}
nextop() {
const b = this.nextbyte();
const major = b >> 6;
const minor = (b >> 4) & 3;
const arg = b & 15;
return [major, minor, arg];
}
peekend() {
const result = this.nextbyte() === 4;
if (!result) this.index--;
return result;
}
binarystream(minor) {
const result = [];
while (!this.peekend()) {
const chunk = Annotations.stripAnnotations(this.next());
if (ArrayBuffer.isView(chunk)) {
if (chunk.byteLength == 0) {
throw new DecodeError("Empty binary chunks are forbidden");
}
result.push(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
} else if (chunk instanceof Bytes) {
if (chunk._view.length == 0) {
throw new DecodeError("Empty binary chunks are forbidden");
}
result.push(chunk._view);
} else {
const e = new DecodeError("Unexpected non-binary chunk");
e.irritant = chunk;
throw e;
}
}
return this.decodebinary(minor, Bytes.concat(result));
}
valuestream(minor) {
const result = [];
while (!this.peekend()) result.push(this.next());
return this.decodecompound(minor, result);
}
decodeint(bs) {
// TODO: Bignums :-/
if (bs.length === 0) return 0;
let acc = bs[0];
if (acc & 0x80) acc -= 256;
for (let i = 1; i < bs.length; i++) acc = (acc << 8) | bs[i];
return acc;
}
decodebinary(minor, bs) {
switch (minor) {
case 0: return this.decodeint(bs._view);
case 1: return bs.fromUtf8();
case 2: return bs;
case 3: return Symbol.for(bs.fromUtf8());
}
}
decodecompound(minor, vs) {
switch (minor) {
case 0: {
if (vs.length === 0) throw new DecodeError("Too few elements in encoded record");
return new Record(vs[0], vs.slice(1));
}
case 1: return List(vs);
case 2: return Set(vs);
case 3: return this.mapFromArray(vs);
}
}
mapFromArray(vs) {
return Map().withMutations((m) => {
if (vs.length % 2) throw new DecodeError("Missing dictionary value");
for (let i = 0; i < vs.length; i += 2) {
m.set(vs[i], vs[i+1]);
}
});
}
wrap(v) {
return this.includeAnnotations ? new Annotations.Annotated(v) : v;
}
unshiftAnnotation(a, v) {
if (this.includeAnnotations) {
v.annotations.unshift(a);
}
return v;
}
next() {
while (true) { // we loop because we may need to consume an arbitrary number of no-ops
const [major, minor, arg] = this.nextop();
switch (major) {
case 0:
switch (minor) {
case 0:
switch (arg) {
case 0: return this.wrap(false);
case 1: return this.wrap(true);
case 2: return this.wrap(Single(this.nextbytes(4).getFloat32(0, false)));
case 3: return this.wrap(Double(this.nextbytes(8).getFloat64(0, false)));
case 4: throw new DecodeError("Unexpected end-of-stream marker");
case 5: {
const a = this.next();
const v = this.next();
return this.unshiftAnnotation(a, v);
}
default: throw new DecodeError("Illegal format A lead byte");
}
case 1: throw new DecodeError("Illegal format A lead byte");
case 2: {
const t = arg >> 2;
const n = arg & 3;
switch (t) {
case 1: return this.wrap(this.binarystream(n));
case 2: return this.wrap(this.valuestream(n));
default: throw new DecodeError("Invalid format C start byte");
}
}
case 3:
return this.wrap((arg > 12) ? arg - 16 : arg);
}
case 1:
return this.wrap(this.decodebinary(minor, Bytes.from(this.nextbytes(this.wirelength(arg)))));
case 2:
return this.wrap(this.decodecompound(minor, this.nextvalues(this.wirelength(arg))));
case 3:
if (minor === 3 && arg === 15) {
// no-op.
continue;
} else {
throw new DecodeError("Invalid lead byte (major 3)");
}
}
}
}
try_next() {
const start = this.index;
try {
return this.next();
} catch (e) {
if (e instanceof ShortPacket) {
this.index = start;
return void 0;
}
throw e;
}
}
}
function decode(bs, options) {
return new Decoder(bs, options).next();
}
function decodeWithAnnotations(bs, options) {
options = options || {};
options.includeAnnotations = true;
return decode(bs, options);
}
class Encoder {
constructor(options) {
options = options || {}
this.chunks = [];
this.view = new DataView(new ArrayBuffer(256));
this.index = 0;
}
contents() {
this.rotatebuffer(4096);
return Bytes.concat(this.chunks);
}
rotatebuffer(size) {
this.chunks.push(new Uint8Array(this.view.buffer, 0, this.index));
this.view = new DataView(new ArrayBuffer(size));
this.index = 0;
}
makeroom(amount) {
if (this.index + amount > this.view.byteLength) {
this.rotatebuffer(amount + 4096);
}
}
emitbyte(b) {
this.makeroom(1);
this.view.setUint8(this.index++, b);
}
emitbytes(bs) {
this.makeroom(bs.length);
(new Uint8Array(this.view.buffer)).set(bs, this.index);
this.index += bs.length;
}
varint(v) {
while (v >= 128) {
this.emitbyte((v % 128) + 128);
v = Math.floor(v / 128);
}
this.emitbyte(v);
}
leadbyte(major, minor, arg) {
this.emitbyte(((major & 3) << 6) | ((minor & 3) << 4) | (arg & 15));
}
header(major, minor, wirelength) {
if (wirelength < 15) {
this.leadbyte(major, minor, wirelength);
} else {
this.leadbyte(major, minor, 15);
this.varint(wirelength);
}
}
encodeint(v) {
// TODO: Bignums :-/
const plain_bitcount = Math.floor(Math.log2(v > 0 ? v : ~v)) + 1;
const signed_bitcount = plain_bitcount + 1;
const bytecount = (signed_bitcount + 7) >> 3;
this.header(1, 0, bytecount);
const enc = (n, x) => {
if (n > 0) {
enc(n - 1, x >> 8);
this.emitbyte(x & 255);
}
};
enc(bytecount, v);
}
encodecollection(minor, items) {
this.header(2, minor, items.size);
for (const item of items) { this.push(item); }
}
encodestream(t, n, items) {
const tn = ((t & 3) << 2) | (n & 3);
this.header(0, 2, tn);
for (const item of items) { this.push(item); }
this.header(0, 0, 4);
}
encodenoop() {
this.leadbyte(3, 3, 15);
}
push(v) {
if (typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function') {
v[PreserveOn](this);
}
else if (typeof v === 'boolean') {
this.leadbyte(0, 0, v ? 1 : 0);
}
else if (typeof v === 'number') {
if (v >= -3 && v <= 12) {
this.leadbyte(0, 3, v >= 0 ? v : v + 16);
} else {
this.encodeint(v);
}
}
else if (typeof v === 'string') {
const bs = Bytes(v)._view;
this.header(1, 1, bs.length);
this.emitbytes(bs);
}
else if (typeof v === 'symbol') {
const key = Symbol.keyFor(v);
if (key === void 0) throw new EncodeError("Cannot preserve non-global Symbol", v);
const bs = Bytes(key)._view;
this.header(1, 3, bs.length);
this.emitbytes(bs);
}
else if (ArrayBuffer.isView(v)) {
if (v instanceof Uint8Array) {
this.header(1, 2, v.length);
this.emitbytes(v);
} else {
const bs = new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
this.header(1, 2, bs.length);
this.emitbytes(bs);
}
}
else if (List.isList(v)) {
this.encodecollection(1, v);
}
else if (Set.isSet(v)) {
this.encodecollection(2, v);
}
else if (Map.isMap(v)) {
this.encodecollection(3, List().withMutations((l) => {
v.forEach((val, key) => { l.push(key).push(val); });
}));
}
else if (typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function') {
this.encodestream(2, 1, v);
}
else {
throw new EncodeError("Cannot encode", v);
}
return this; // for chaining
}
}
function encode(v, options) {
return new Encoder(options).push(v).contents();
}
Object.assign(module.exports, {
DecodeError,
EncodeError,
ShortPacket,
Decoder,
decode,
decodeWithAnnotations,
Encoder,
encode,
});

View File

@ -0,0 +1,370 @@
// Preserves Binary codec.
import {
underlying,
Annotated,
Dictionary, Set, Bytes, Record, Single, Double,
isSet, isDictionary,
BytesLike,
Value,
} from './values';
import { Tag } from './constants';
import { PreserveOn } from './symbols';
export type ErrorType = 'DecodeError' | 'EncodeError' | 'ShortPacket';
export function isCodecError(e: any, t?: ErrorType): e is PreservesCodecError {
return typeof e === 'object' && e !== null &&
'_codecErrorType' in e &&
(!t || e._codecErrorType() === t);
}
export const isDecodeError = (e: any): e is DecodeError => isCodecError(e, 'DecodeError');
export const isEncodeError = (e: any): e is EncodeError => isCodecError(e, 'EncodeError');
export const isShortPacket = (e: any): e is ShortPacket => isCodecError(e, 'ShortPacket');
export abstract class PreservesCodecError {
abstract _codecErrorType(): ErrorType;
}
export class DecodeError extends Error {
_codecErrorType(): ErrorType { return 'DecodeError' }
}
export class EncodeError extends Error {
_codecErrorType(): ErrorType { return 'EncodeError' }
readonly irritant: any;
constructor(message: string, irritant: any) {
super(message);
this.irritant = irritant;
}
}
export class ShortPacket extends DecodeError {
_codecErrorType(): ErrorType { return 'ShortPacket' }
}
export interface DecoderOptions {
includeAnnotations?: boolean;
}
export class Decoder {
packet: Uint8Array;
index: number;
options: DecoderOptions;
constructor(packet: BytesLike = new Uint8Array(0), options: DecoderOptions = {}) {
this.packet = underlying(packet);
this.index = 0;
this.options = options;
}
get includeAnnotations(): boolean {
return this.options.includeAnnotations ?? false;
}
write(data: BytesLike) {
this.packet = Bytes.concat([this.packet.slice(this.index), data])._view;
this.index = 0;
}
nextbyte(): number {
if (this.index >= this.packet.length) throw new ShortPacket("Short packet");
// ^ NOTE: greater-than-or-equal-to, not greater-than.
return this.packet[this.index++];
}
nextbytes(n: number): DataView {
const start = this.index;
this.index += n;
if (this.index > this.packet.length) throw new ShortPacket("Short packet");
// ^ NOTE: greater-than, not greater-than-or-equal-to.
return new DataView(this.packet.buffer, this.packet.byteOffset + start, n);
}
varint(): number {
// TODO: Bignums :-/
const v = this.nextbyte();
if (v < 128) return v;
return (this.varint() << 7) + (v - 128);
}
peekend(): boolean {
const matched = this.nextbyte() === Tag.End;
if (!matched) this.index--;
return matched;
}
nextvalues(): Value[] {
const result = [];
while (!this.peekend()) result.push(this.next());
return result;
}
nextint(n: number): number {
// TODO: Bignums :-/
if (n === 0) return 0;
let acc = this.nextbyte();
if (acc & 0x80) acc -= 256;
for (let i = 1; i < n; i++) acc = (acc << 8) | this.nextbyte();
return acc;
}
wrap(v: Value): Value {
return this.includeAnnotations ? new Annotated(v) : v;
}
static dictionaryFromArray(vs: Value[]): Dictionary {
const d = new Dictionary();
if (vs.length % 2) throw new DecodeError("Missing dictionary value");
for (let i = 0; i < vs.length; i += 2) {
d.set(vs[i], vs[i+1]);
}
return d;
}
unshiftAnnotation(a: Value, v: Annotated) {
if (this.includeAnnotations) {
v.annotations.unshift(a);
}
return v;
}
next(): Value {
const tag = this.nextbyte();
switch (tag) {
case Tag.False: return this.wrap(false);
case Tag.True: return this.wrap(true);
case Tag.Float: return this.wrap(new Single(this.nextbytes(4).getFloat32(0, false)));
case Tag.Double: return this.wrap(new Double(this.nextbytes(8).getFloat64(0, false)));
case Tag.End: throw new DecodeError("Unexpected Compound end marker");
case Tag.Annotation: {
const a = this.next();
const v = this.next() as Annotated;
return this.unshiftAnnotation(a, v);
}
case Tag.SignedInteger: return this.wrap(this.nextint(this.varint()));
case Tag.String: return this.wrap(Bytes.from(this.nextbytes(this.varint())).fromUtf8());
case Tag.ByteString: return this.wrap(Bytes.from(this.nextbytes(this.varint())));
case Tag.Symbol: return this.wrap(Symbol.for(Bytes.from(this.nextbytes(this.varint())).fromUtf8()));
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)));
}
case Tag.Sequence: return this.wrap(this.nextvalues());
case Tag.Set: return this.wrap(new Set(this.nextvalues()));
case Tag.Dictionary: return this.wrap(Decoder.dictionaryFromArray(this.nextvalues()));
default: {
if (tag >= Tag.SmallInteger_lo && tag <= Tag.SmallInteger_lo + 15) {
const v = tag - Tag.SmallInteger_lo;
return this.wrap(v > 12 ? v - 16 : v);
}
if (tag >= Tag.MediumInteger_lo && tag <= Tag.MediumInteger_lo + 15) {
const n = tag - Tag.MediumInteger_lo;
return this.wrap(this.nextint(n + 1));
}
throw new DecodeError("Unsupported Preserves tag: " + tag);
}
}
}
try_next() {
const start = this.index;
try {
return this.next();
} catch (e) {
if (e instanceof ShortPacket) {
this.index = start;
return void 0;
}
throw e;
}
}
}
export function decode(bs: BytesLike, options?: DecoderOptions) {
return new Decoder(bs, options).next();
}
export function decodeWithAnnotations(bs: BytesLike, options: DecoderOptions = {}): Annotated {
return decode(bs, { ... options, includeAnnotations: true }) as Annotated;
}
export interface EncoderOptions {
canonical?: boolean;
includeAnnotations?: boolean;
}
export class Encoder {
chunks: Array<Uint8Array>;
view: DataView;
index: number;
options: EncoderOptions;
constructor(options: EncoderOptions = {}) {
this.chunks = [];
this.view = new DataView(new ArrayBuffer(256));
this.index = 0;
this.options = options;
}
get canonical(): boolean {
return this.options.canonical ?? true;
}
get includeAnnotations(): boolean {
return this.options.includeAnnotations ?? !this.canonical;
}
contents(): Bytes {
this.rotatebuffer(4096);
return Bytes.concat(this.chunks);
}
rotatebuffer(size: number) {
this.chunks.push(new Uint8Array(this.view.buffer, 0, this.index));
this.view = new DataView(new ArrayBuffer(size));
this.index = 0;
}
makeroom(amount: number) {
if (this.index + amount > this.view.byteLength) {
this.rotatebuffer(amount + 4096);
}
}
emitbyte(b: number) {
this.makeroom(1);
this.view.setUint8(this.index++, b);
}
emitbytes(bs: Uint8Array) {
this.makeroom(bs.length);
(new Uint8Array(this.view.buffer)).set(bs, this.index);
this.index += bs.length;
}
varint(v: number) {
while (v >= 128) {
this.emitbyte((v % 128) + 128);
v = Math.floor(v / 128);
}
this.emitbyte(v);
}
encodeint(v: number) {
// TODO: Bignums :-/
const plain_bitcount = Math.floor(Math.log2(v > 0 ? v : ~v)) + 1;
const signed_bitcount = plain_bitcount + 1;
const bytecount = (signed_bitcount + 7) >> 3;
if (bytecount <= 16) {
this.emitbyte(Tag.MediumInteger_lo + bytecount - 1);
} else {
this.emitbyte(Tag.SignedInteger);
this.varint(bytecount);
}
const enc = (n, x) => {
if (n > 0) {
enc(n - 1, x >> 8);
this.emitbyte(x & 255);
}
};
enc(bytecount, v);
}
encodebytes(tag: Tag, bs: Uint8Array) {
this.emitbyte(tag);
this.varint(bs.length);
this.emitbytes(bs);
}
encodevalues(tag: Tag, items: Iterable<Value>) {
this.emitbyte(tag);
for (let i of items) { this.push(i); }
this.emitbyte(Tag.End);
}
encoderawvalues(tag: Tag, items: BytesLike[]) {
this.emitbyte(tag);
items.forEach((i) => this.emitbytes(underlying(i)));
this.emitbyte(Tag.End);
}
push(v: Value) {
if (typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function') {
v[PreserveOn](this);
}
else if (typeof v === 'boolean') {
this.emitbyte(v ? Tag.True : Tag.False);
}
else if (typeof v === 'number') {
if (v >= -3 && v <= 12) {
this.emitbyte(Tag.SmallInteger_lo + ((v + 16) & 0xf));
} else {
this.encodeint(v);
}
}
else if (typeof v === 'string') {
this.encodebytes(Tag.String, new Bytes(v)._view);
}
else if (typeof v === 'symbol') {
const key = Symbol.keyFor(v);
if (key === void 0) throw new EncodeError("Cannot preserve non-global Symbol", v);
this.encodebytes(Tag.Symbol, new Bytes(key)._view);
}
else if (ArrayBuffer.isView(v)) {
if (v instanceof Uint8Array) {
this.encodebytes(Tag.ByteString, v);
} else {
const bs = new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
this.encodebytes(Tag.ByteString, bs);
}
}
else if (Array.isArray(v)) {
this.encodevalues(Tag.Sequence, v);
}
else if (isSet(v)) {
if (this.canonical) {
const pieces = v._map((_v, k) => encode(k, { canonical: true }));
pieces.sort(Bytes.compare);
this.encoderawvalues(Tag.Set, pieces);
} else {
this.encodevalues(Tag.Set, v);
}
}
else if (isDictionary(v)) {
if (this.canonical) {
const pieces = v._map((v, k) => Bytes.concat([encode(k, { canonical: true }),
encode(v, { canonical: true })]));
pieces.sort(Bytes.compare);
this.encoderawvalues(Tag.Dictionary, pieces);
} else {
this.emitbyte(Tag.Dictionary);
v._forEach((v, k) => {
this.push(k);
this.push(v);
});
this.emitbyte(Tag.End);
}
}
else if (typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function') {
this.encodevalues(Tag.Sequence, v as Iterable<Value>);
}
else {
throw new EncodeError("Cannot encode", v);
}
return this; // for chaining
}
}
export function encode(v: Value, options?: EncoderOptions): Bytes {
return new Encoder(options).push(v).contents();
}
export function encodeWithAnnotations(v: Value, options: EncoderOptions = {}): Bytes {
return encode(v, { ... options, includeAnnotations: true });
}

View File

@ -0,0 +1,20 @@
export enum Tag {
False = 0x80,
True,
Float,
Double,
End,
Annotation,
SmallInteger_lo = 0x90,
MediumInteger_lo = 0xa0,
SignedInteger = 0xb0,
String,
ByteString,
Symbol,
Record,
Sequence,
Set,
Dictionary,
}

View File

@ -1,11 +0,0 @@
"use strict";
if (require('./singletonmodule.js')('leastfixedpoint.com/preserves',
require('../package.json').version,
'index.js',
module)) return;
Object.assign(module.exports, require('./symbols.js'));
Object.assign(module.exports, require('./codec.js'));
Object.assign(module.exports, require('./values.js'));
Object.assign(module.exports, require('./annotations.js'));

View File

@ -0,0 +1,5 @@
export * from './symbols';
export * from './codec';
export * from './values';
export * from './text';
export * as Constants from './constants';

View File

@ -0,0 +1,12 @@
// Patching to support node.js extensions.
import * as util from 'util';
import { Record, Bytes, Annotated } from './values';
[Bytes, Annotated, Record].forEach((C) => {
C.prototype[util.inspect.custom] = function (_depth: any, _options: any) {
return this.asPreservesText();
};
});
Record.fallbackToString = util.inspect;

View File

@ -1,26 +0,0 @@
"use strict";
function initialize_singleton(namespace_key_str, package_version, module_key, module_object) {
const namespace_key = Symbol.for(namespace_key_str);
if (!(namespace_key in global)) {
global[namespace_key] = {
version: package_version,
modules: {}
};
}
let cache = global[namespace_key];
if (cache.version !== package_version) {
console.warn('Potentially incompatible versions of ' + namespace_key_str + ' loaded:',
cache.version,
package_version);
}
if (module_key in cache.modules) {
module_object.exports = cache.modules[module_key];
return true;
} else {
cache.modules[module_key] = module_object.exports;
return false;
}
}
module.exports = initialize_singleton;

View File

@ -1,15 +0,0 @@
"use strict";
// Symbols for various Preserves protocols.
if (require('./singletonmodule.js')('leastfixedpoint.com/preserves',
require('../package.json').version,
'symbols.js',
module)) return;
const PreserveOn = Symbol.for('PreserveOn');
const AsPreserve = Symbol.for('AsPreserve');
Object.assign(module.exports, {
PreserveOn,
AsPreserve,
});

View File

@ -0,0 +1,5 @@
// Symbols for various Preserves protocols.
export const PreserveOn = Symbol.for('PreserveOn');
export const AsPreserve = Symbol.for('AsPreserve');

View File

@ -0,0 +1,18 @@
import { Value } from './values';
export function stringify(x: Value): string {
if (typeof x === 'object' && x !== null && 'asPreservesText' in x) {
return x.asPreservesText();
} else {
return JSON.stringify(x);
}
}
export function preserves(pieces: TemplateStringsArray, ...values: Value[]): string {
const result = [pieces[0]];
values.forEach((v, i) => {
result.push(stringify(v));
result.push(pieces[i + 1]);
});
return result.join('');
}

View File

@ -1,422 +0,0 @@
"use strict";
// Preserves Values.
// Uses Immutable.js for many things; adds immutable values of its own for the rest.
if (require('./singletonmodule.js')('leastfixedpoint.com/preserves',
require('../package.json').version,
'values.js',
module)) return;
const util = require('util');
const Immutable = require('immutable');
const { List, Map, Set, is, hash } = Immutable;
const { PreserveOn, AsPreserve } = require('./symbols.js');
function _decode(bs) {
return Buffer.from(bs.buffer, bs.byteOffset, bs.byteLength).toString();
}
const encoder = (typeof TextEncoder === 'undefined') ? { encode: Buffer.from } : new TextEncoder();
const decoder = (typeof TextDecoder === 'undefined') ? { decode: _decode } : new TextDecoder();
function fromJS(x) {
switch (typeof x) {
case 'number':
if (!Number.isInteger(x)) {
// We require that clients be explicit about integer vs. non-integer types.
throw new TypeError("Refusing to autoconvert non-integer number to Single or Double");
}
// FALL THROUGH
case 'string':
case 'symbol':
case 'boolean':
return x;
case 'undefined':
throw new TypeError("Cannot represent JavaScript undefined value as Preserves");
case 'function':
// We are relaxed about these, for now, even though Preserves can't serialize them.
return x;
case 'object':
if (x === null) {
// We are relaxed about these, for now, even though null is
// strictly speaking not a Preserves Value.
return x;
}
if (typeof x[AsPreserve] === 'function') {
return x[AsPreserve]();
}
if (Array.isArray(x)) {
return List().withMutations((l) => {
for (let i = 0; i < x.length; i++) {
l.push(fromJS(x[i]));
}
});
}
if (ArrayBuffer.isView(x) || x instanceof ArrayBuffer) {
return Bytes(x);
}
return Immutable.fromJS(x);
}
}
function _Float(value) {
Object.defineProperty(this, 'value', {
value: value,
writable: false,
enumerable: true
});
}
function Float(value) {
if (!(this instanceof Float)) return new Float(value);
_Float.call(this, value);
}
Float.prototype.toString = function () {
return '' + this.value;
};
Float.prototype.hashCode = function () {
return this.value | 0; // TODO: something better?
};
Float.unwrap = function (v) {
if (typeof v === 'number') return v;
if (v instanceof Float) return v.value;
{
const e = new TypeError("Float.unwrap failed");
e.irritant = v;
throw e;
}
};
function Single(value) {
if (!(this instanceof Single)) return new Single(value);
_Float.call(this, value);
}
Single.prototype = Float(NaN);
Single.prototype.equals = function (other) {
return (other instanceof Single) && (other.value === this.value);
};
Single.prototype[PreserveOn] = function (encoder) {
encoder.leadbyte(0, 0, 2);
encoder.makeroom(4);
encoder.view.setFloat32(encoder.index, this.value, false);
encoder.index += 4;
};
function Double(value) {
if (!(this instanceof Double)) return new Double(value);
_Float.call(this, value);
}
Double.prototype = Float(NaN);
Double.prototype.equals = function (other) {
return (other instanceof Double) && (other.value === this.value);
};
Double.prototype[PreserveOn] = function (encoder) {
encoder.leadbyte(0, 0, 3);
encoder.makeroom(8);
encoder.view.setFloat64(encoder.index, this.value, false);
encoder.index += 8;
};
function Bytes(maybeByteIterable) {
if (!(this instanceof Bytes)) return new Bytes(maybeByteIterable);
if (maybeByteIterable === void 0) {
_installView.call(this, new Uint8Array());
} else if (ArrayBuffer.isView(maybeByteIterable)) {
_installView.call(this, new Uint8Array(maybeByteIterable.buffer,
maybeByteIterable.byteOffset,
maybeByteIterable.byteLength));
} else if (maybeByteIterable instanceof ArrayBuffer) {
_installView.call(this, new Uint8Array(maybeByteIterable.slice()));
} else if (typeof maybeByteIterable === 'string') {
_installView.call(this, encoder.encode(maybeByteIterable));
} else if (typeof maybeByteIterable === 'number') {
_installView.call(this, new Uint8Array(maybeByteIterable));
} else if (typeof maybeByteIterable.length === 'number') {
_installView.call(this, Uint8Array.from(maybeByteIterable));
} else if (maybeByteIterable instanceof Bytes) {
_installView.call(this, maybeByteIterable._view);
} else if (typeof maybeByteIterable.size === 'number') {
_installView.call(this, new Uint8Array(maybeByteIterable.size));
for (let i = 0; i < this.size; i++) { this._view[i] = maybeByteIterable.get(i); }
} else {
const e = new TypeError("Attempt to initialize Bytes from unsupported value");
e.irritant = maybeByteIterable;
throw e;
}
}
function _installView(view) {
Object.defineProperty(this, '_view', { value: view, writable: false });
Object.defineProperty(this, 'size', { value: view.length, writable: false, enumerable: true });
}
Bytes.from = Bytes;
Bytes.of = function (...args) { return Bytes(Uint8Array.of(...args)); };
function unhexDigit(asciiCode) {
if (asciiCode >= 48 && asciiCode <= 57) return asciiCode - 48;
if (asciiCode >= 97 && asciiCode <= 102) return asciiCode - 97 + 10;
if (asciiCode >= 65 && asciiCode <= 70) return asciiCode - 65 + 10;
throw new Error("Invalid hex digit: " + String.fromCharCode(asciiCode));
}
Bytes.fromHex = function (s) {
if (s.length & 1) throw new Error("Cannot decode odd-length hexadecimal string");
const len = s.length >> 1;
const result = Bytes(len);
for (let i = 0; i < len; i++) {
result._view[i] =
(unhexDigit(s.charCodeAt(i << 1)) << 4) | unhexDigit(s.charCodeAt((i << 1) + 1));
}
return result;
};
Bytes.fromIO = function (io) {
if (typeof io === 'string') return io;
if (io instanceof Bytes) return io;
if (io instanceof Uint8Array) return Bytes.from(io);
{
const e = new TypeError("Bytes.fromIO: unsupported value");
e.irritant = io;
throw e;
}
};
Bytes.toIO = function (b) {
if (typeof b === 'string') return b;
if (b instanceof Bytes) return b._view;
if (b instanceof Uint8Array) return b;
{
const e = new TypeError("Bytes.toIO: unsupported value");
e.irritant = b;
throw e;
}
};
function underlying(b) {
return b._view || b;
}
Bytes.concat = function (bss) {
let len = 0;
for (let i = 0; i < bss.length; i++) { len += underlying(bss[i]).length; }
const result = Bytes(len);
let index = 0;
for (let i = 0; i < bss.length; i++) {
const bs = underlying(bss[i]);
result._view.set(bs, index);
index += bs.length;
}
return result;
};
Bytes.prototype.get = function (index) {
return this._view[index];
};
Bytes.prototype.equals = function (other) {
if (!(other instanceof Bytes)) return false;
if (other.size !== this.size) return false;
const va = this._view;
const vb = other._view;
for (let i = 0; i < va.length; i++) {
if (va[i] !== vb[i]) return false;
}
return true;
};
Bytes.prototype.hashCode = function () {
// Immutable.js uses this function for strings.
const v = this._view;
let hash = 0;
for (let i = 0; i < v.length; i++) {
hash = ((31 * hash) + v[i]) | 0;
}
return hash;
};
Bytes.decodeUtf8 = function (bs) {
return decoder.decode(underlying(bs));
};
Bytes.prototype.fromUtf8 = function () {
return decoder.decode(this._view);
};
Bytes.prototype.toString = function () {
return '#"' + this.__asciify() + '"';
};
if (util.inspect) {
Bytes.prototype[util.inspect.custom] = function (depth, options) {
return this.toString();
};
}
Bytes.prototype.__asciify = function () {
const pieces = [];
const v = this._view;
for (let i = 0; i < v.length; i++) {
const b = v[i];
if (b === 92 || b === 34) {
pieces.push('\\' + String.fromCharCode(b));
} else if (b >= 32 && b <= 126) {
pieces.push(String.fromCharCode(b));
} else {
pieces.push('\\x' + hexDigit(b >> 4) + hexDigit(b & 15));
}
}
return pieces.join('');
};
function hexDigit(n) { return '0123456789abcdef'[n]; }
Bytes.prototype.toHex = function () {
var nibbles = [];
for (let i = 0; i < this.size; i++) {
nibbles.push(hexDigit(this._view[i] >> 4));
nibbles.push(hexDigit(this._view[i] & 15));
}
return nibbles.join('');
};
Bytes.prototype[PreserveOn] = function (encoder) {
encoder.header(1, 2, this.size);
encoder.emitbytes(this._view);
};
// Uint8Array / TypedArray methods
(function () {
for (const k of `entries every find findIndex forEach includes indexOf join
keys lastIndexOf reduce reduceRight some toLocaleString values`.split(/\s+/))
{
Bytes.prototype[k] = function (...args) { return this._view[k](...args); };
}
for (const k of `filter map slice subarray`.split(/\s+/))
{
Bytes.prototype[k] = function (...args) { return Bytes(this._view[k](...args)); };
}
for (const k of `reverse sort`.split(/\s+/))
{
Bytes.prototype[k] = function (...args) { return Bytes(this._view.slice()[k](...args)); };
}
Bytes.prototype[Symbol.iterator] = function () { return this._view[Symbol.iterator](); };
})();
function Record(label, fields) {
if (!(this instanceof Record)) {
throw new TypeError("Class constructor Record cannot be invoked without 'new'");
}
Object.defineProperty(this, 'label', { value: fromJS(label), writable: false, enumerable: true });
Object.defineProperty(this, 'fields', { value: fromJS(fields), writable: false, enumerable: true });
}
Record.prototype.equals = function (other) {
return (other instanceof Record) &&
is(this.label, other.label) &&
is(this.fields, other.fields);
};
Record.prototype.hashCode = function () {
return Immutable.List([this.label, this.fields]).hashCode();
};
Record.prototype.get = function (index, defaultValue) {
return this.fields.get(index, defaultValue);
};
Record.prototype.set = function (index, newValue) {
return new Record(this.label, this.fields.set(index, newValue));
};
Record.prototype[PreserveOn] = function (encoder) {
encoder.header(2, 0, this.fields.size + 1);
encoder.push(this.label);
for (const field of this.fields) { encoder.push(field); }
};
Record.prototype.getConstructorInfo = function () {
return new RecordConstructorInfo(this.label, this.fields.size);
};
Record.prototype.toString = function () {
return this.label.toString().replace(/^Symbol\((.*)\)$/, '$1') +
'(' + this.fields.map((f) => {
try {
return "" + f;
} catch (e) {
return (util.inspect || ((f) => '<unprintable_preserves_field_value>'))(f);
}
}).join(', ') + ')';
};
Record.makeConstructor = function (labelSymbolText, fieldNames) {
return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames);
};
Record.makeBasicConstructor = function (label, fieldNames) {
const arity = fieldNames.length;
const ctor = (...fields) => {
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);
};
ctor.constructorInfo = new RecordConstructorInfo(label, arity);
ctor.isClassOf = (v) => ((v instanceof Record) &&
is(label, v.label) &&
v.fields.size === arity);
fieldNames.forEach((name, i) => {
ctor['_'+name] = function (r) {
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;
};
function RecordConstructorInfo(label, arity) {
this.label = label;
this.arity = arity;
}
RecordConstructorInfo.prototype.equals = function (other) {
return (other instanceof RecordConstructorInfo) &&
is(this.label, other.label) &&
(this.arity === other.arity);
};
RecordConstructorInfo.prototype.hashCode = function () {
return Immutable.List([this.label, this.arity]).hashCode();
};
RecordConstructorInfo.prototype.isClassOf = function (v) {
return (v instanceof Record) &&
is(this.label, v.label) &&
(this.arity === v.fields.size);
};
Object.assign(module.exports, {
fromJS,
List, Map, Set, is, hash,
Float, Single, Double,
Bytes,
Record, RecordConstructorInfo,
});

View File

@ -0,0 +1,793 @@
// Preserves Values.
import { PreserveOn, AsPreserve } from './symbols';
import { Tag } from './constants';
import { Encoder, encode } from './codec';
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
export type Value = Atom | Compound | Annotated;
export type Atom = boolean | Single | Double | number | string | Bytes | symbol;
export type Compound = Record | Array<Value> | Set | Dictionary;
export function isRecord(x: any): x is Record {
return Array.isArray(x) && 'label' in x;
}
export function fromJS(x: any): Value {
switch (typeof x) {
case 'number':
if (!Number.isInteger(x)) {
// We require that clients be explicit about integer vs. non-integer types.
throw new TypeError("Refusing to autoconvert non-integer number to Single or Double");
}
// FALL THROUGH
case 'string':
case 'symbol':
case 'boolean':
return x;
case 'undefined':
case 'function':
break;
case 'object':
if (x === null) {
break;
}
if (typeof x[AsPreserve] === 'function') {
return x[AsPreserve]();
}
if (isRecord(x)) {
return x;
}
if (Array.isArray(x)) {
return x.map(fromJS);
}
if (ArrayBuffer.isView(x) || x instanceof ArrayBuffer) {
return Bytes.from(x);
}
return Dictionary.fromJS(x);
}
throw new TypeError("Cannot represent JavaScript value as Preserves: " + x);
}
export type FloatType = 'Single' | 'Double';
export abstract class Float {
readonly value: number;
constructor(value: number | Float) {
this.value = typeof value === 'number' ? value : value.value;
}
toString() {
return this.asPreservesText();
}
equals(other: any): boolean {
return Object.is(other.constructor, this.constructor) && (other.value === this.value);
}
hashCode(): number {
return (this.value | 0); // TODO: something better?
}
abstract asPreservesText(): string;
}
export class Single extends Float {
[AsPreserve](): Value {
return this;
}
[PreserveOn](encoder: Encoder) {
encoder.emitbyte(Tag.Float);
encoder.makeroom(4);
encoder.view.setFloat32(encoder.index, this.value, false);
encoder.index += 4;
}
_floatType(): FloatType {
return 'Single';
}
asPreservesText(): string {
return '' + this.value + 'f';
}
}
export class Double extends Float {
[AsPreserve](): Value {
return this;
}
[PreserveOn](encoder: Encoder) {
encoder.emitbyte(Tag.Double);
encoder.makeroom(8);
encoder.view.setFloat64(encoder.index, this.value, false);
encoder.index += 8;
}
_floatType(): FloatType {
return 'Double';
}
asPreservesText(): string {
return '' + this.value;
}
}
function isFloat(x: any, t: FloatType): x is Float {
return typeof x === 'object' && x !== null &&
'value' in x && typeof x.value === 'number' &&
'_floatType' in x && x._floatType() === t;
}
export const isSingle = (x: any): x is Single => isFloat(x, 'Single');
export const isDouble = (x: any): x is Double => isFloat(x, 'Double');
export type BytesLike = Bytes | Uint8Array;
export class Bytes {
readonly _view: Uint8Array;
constructor(maybeByteIterable: any = new Uint8Array()) {
if (isBytes(maybeByteIterable)) {
this._view = maybeByteIterable._view;
} else if (ArrayBuffer.isView(maybeByteIterable)) {
this._view = new Uint8Array(maybeByteIterable.buffer,
maybeByteIterable.byteOffset,
maybeByteIterable.byteLength);
} else if (maybeByteIterable instanceof ArrayBuffer) {
this._view = new Uint8Array(maybeByteIterable.slice(0));
} else if (typeof maybeByteIterable === 'string') {
this._view = textEncoder.encode(maybeByteIterable);
} else if (typeof maybeByteIterable === 'number') {
this._view = new Uint8Array(maybeByteIterable);
} else if (typeof maybeByteIterable.length === 'number') {
this._view = Uint8Array.from(maybeByteIterable);
} else {
throw new TypeError("Attempt to initialize Bytes from unsupported value: " +
maybeByteIterable);
}
}
get length(): number {
return this._view.length;
}
static from(x: any): Bytes {
return new Bytes(x);
}
static of(...bytes: number[]): Bytes {
return new Bytes(Uint8Array.of(...bytes));
}
static fromHex(s: string): Bytes {
if (s.length & 1) throw new Error("Cannot decode odd-length hexadecimal string");
const len = s.length >> 1;
const result = new Bytes(len);
for (let i = 0; i < len; i++) {
result._view[i] =
(unhexDigit(s.charCodeAt(i << 1)) << 4) | unhexDigit(s.charCodeAt((i << 1) + 1));
}
return result;
}
static fromIO(io: string | BytesLike): string | Bytes {
if (typeof io === 'string') return io;
if (isBytes(io)) return io;
if (io instanceof Uint8Array) return new Bytes(io);
}
static toIO(b : string | BytesLike): string | Uint8Array {
if (typeof b === 'string') return b;
if (isBytes(b)) return b._view;
if (b instanceof Uint8Array) return b;
}
static concat = function (bss: BytesLike[]): Bytes {
let len = 0;
for (let i = 0; i < bss.length; i++) { len += underlying(bss[i]).length; }
const result = new Bytes(len);
let index = 0;
for (let i = 0; i < bss.length; i++) {
const bs = underlying(bss[i]);
result._view.set(bs, index);
index += bs.length;
}
return result;
}
get(index: number): number {
return this._view[index];
}
equals(other: any): boolean {
if (!isBytes(other)) return false;
if (other.length !== this.length) return false;
const va = this._view;
const vb = other._view;
for (let i = 0; i < va.length; i++) {
if (va[i] !== vb[i]) return false;
}
return true;
}
hashCode(): number {
// Immutable.js uses this function for strings.
const v = this._view;
let hash = 0;
for (let i = 0; i < v.length; i++) {
hash = ((31 * hash) + v[i]) | 0;
}
return hash;
}
static compare(a: Bytes, b: Bytes): number {
if (a < b) return -1;
if (b < a) return 1;
return 0;
}
static decodeUtf8(bs: Bytes | Uint8Array): string {
return textDecoder.decode(underlying(bs));
}
fromUtf8(): string {
return textDecoder.decode(this._view);
}
toString(): string {
return this.asPreservesText();
}
[AsPreserve](): Value {
return this;
}
asPreservesText(): string {
return '#"' + this.__asciify() + '"';
}
__asciify(): string {
const pieces = [];
const v = this._view;
for (let i = 0; i < v.length; i++) {
const b = v[i];
if (b === 92 || b === 34) {
pieces.push('\\' + String.fromCharCode(b));
} else if (b >= 32 && b <= 126) {
pieces.push(String.fromCharCode(b));
} else {
pieces.push('\\x' + hexDigit(b >> 4) + hexDigit(b & 15));
}
}
return pieces.join('');
}
toHex(): string {
var nibbles = [];
for (let i = 0; i < this.length; i++) {
nibbles.push(hexDigit(this._view[i] >> 4));
nibbles.push(hexDigit(this._view[i] & 15));
}
return nibbles.join('');
}
[PreserveOn](encoder: Encoder) {
encoder.emitbyte(Tag.ByteString);
encoder.varint(this.length);
encoder.emitbytes(this._view);
}
}
export function hexDigit(n: number): string {
return '0123456789abcdef'[n];
}
export function unhexDigit(asciiCode: number) {
if (asciiCode >= 48 && asciiCode <= 57) return asciiCode - 48;
if (asciiCode >= 97 && asciiCode <= 102) return asciiCode - 97 + 10;
if (asciiCode >= 65 && asciiCode <= 70) return asciiCode - 65 + 10;
throw new Error("Invalid hex digit: " + String.fromCharCode(asciiCode));
}
export function isBytes(x: any): x is Bytes {
return typeof x === 'object' && x !== null &&
'_view' in x &&
x._view instanceof Uint8Array;
}
export function underlying(b: Bytes | Uint8Array): Uint8Array {
return (b instanceof Uint8Array) ? b : b._view;
}
declare global {
interface Boolean { asPreservesText(): string; }
interface Number { asPreservesText(): string; }
interface String { asPreservesText(): string; }
interface Symbol { asPreservesText(): string; }
interface Array<T> { asPreservesText(): string; }
}
Boolean.prototype.asPreservesText = function (): string {
return this ? '#t' : '#f';
};
Number.prototype.asPreservesText = function (): string {
return '' + this;
};
String.prototype.asPreservesText = function (): string {
return JSON.stringify(this);
};
Symbol.prototype.asPreservesText = function (): string {
// TODO: escaping
return this.description;
};
Array.prototype.asPreservesText = function (): string {
return '[' + this.map((i: Value) => i.asPreservesText()).join(', ') + ']';
};
// Uint8Array / TypedArray methods
export interface Bytes {
entries(): IterableIterator<[number, number]>;
every(predicate: (value: number, index: number, array: Uint8Array) => unknown,
thisArg?: any): boolean;
find(predicate: (value: number, index: number, obj: Uint8Array) => boolean,
thisArg?: any): number;
findIndex(predicate: (value: number, index: number, obj: Uint8Array) => boolean,
thisArg?: any): number;
forEach(callbackfn: (value: number, index: number, array: Uint8Array) => void,
thisArg?: any): void;
includes(searchElement: number, fromIndex?: number): boolean;
indexOf(searchElement: number, fromIndex?: number): number;
join(separator?: string): string;
keys(): IterableIterator<number>;
lastIndexOf(searchElement: number, fromIndex?: number): number;
reduce(callbackfn: (previousValue: number,
currentValue: number,
currentIndex: number,
array: Uint8Array) => number,
initialValue?: number): number;
reduceRight(callbackfn: (previousValue: number,
currentValue: number,
currentIndex: number,
array: Uint8Array) => number,
initialValue?: number): number;
some(predicate: (value: number, index: number, array: Uint8Array) => unknown,
thisArg?: any): boolean;
toLocaleString(): string;
values(): IterableIterator<number>;
filter(predicate: (value: number, index: number, array: Uint8Array) => any,
thisArg?: any): Bytes;
map(callbackfn: (value: number, index: number, array: Uint8Array) => number,
thisArg?: any): Bytes;
slice(start?: number, end?: number): Bytes;
subarray(begin?: number, end?: number): Bytes;
reverse(): Bytes;
sort(compareFn?: (a: number, b: number) => number): Bytes;
[Symbol.iterator](): IterableIterator<number>;
}
(function () {
for (const k of `entries every find findIndex forEach includes indexOf join
keys lastIndexOf reduce reduceRight some toLocaleString values`.split(/\s+/))
{
Bytes.prototype[k] = function (...args: any[]) { return this._view[k](...args); };
}
for (const k of `filter map slice subarray`.split(/\s+/))
{
Bytes.prototype[k] = function (...args: any[]) { return new Bytes(this._view[k](...args)); };
}
for (const k of `reverse sort`.split(/\s+/))
{
Bytes.prototype[k] = function (...args: any[]) { return new Bytes(this._view.slice()[k](...args)); };
}
Bytes.prototype[Symbol.iterator] = function () { return this._view[Symbol.iterator](); };
})();
export class Record extends Array<Value> {
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);
return;
}
super(fieldsJS.length);
fieldsJS.forEach((f, i) => this[i] = fromJS(f));
this.label = label;
Object.freeze(this);
}
get(index: number, defaultValue?: Value): Value {
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 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) => '<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(labelSymbolText: string, fieldNames: string[]) {
return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames);
}
static makeBasicConstructor(label0: any, fieldNames: string[]): RecordConstructor {
const label = fromJS(label0);
const arity = fieldNames.length;
const ctor = (...fields: any[]) => {
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);
};
ctor.constructorInfo = { label, arity };
ctor.isClassOf =
(v: any): v is Record => (isRecord(v) && is(label, v.label) && v.length === arity);
ctor._ = {};
fieldNames.forEach((name, i) => {
ctor._[name] = function (r: any): Value {
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);
}
}
export interface RecordConstructor {
(...fields: any[]): Record;
constructorInfo: RecordConstructorInfo;
isClassOf(v: any): v is Record;
_: { [getter: string]: (r: any) => Value };
}
export interface RecordConstructorInfo {
label: Value;
arity: number;
}
export function is(a: Value, b: Value): boolean {
if (Object.is(a, b)) return true;
if (typeof a !== typeof b) return false;
if (typeof a === 'object') {
if (a === null || b === null) return false;
if ('equals' in a) return a.equals(b);
if (Array.isArray(a) && Array.isArray(b)) {
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;
}
}
return false;
}
export function hash(a: Value): number {
throw new Error("shouldBeImplemented"); // TODO
}
export function isClassOf(ci: RecordConstructorInfo, v: any): v is Record {
return (isRecord(v)) && is(ci.label, v.label) && (ci.arity === v.length);
}
export type DictionaryType = 'Dictionary' | 'Set';
export function is_Dictionary(x: any, t: DictionaryType): x is _Dictionary {
return typeof x === 'object' && x !== null &&
'_items' in x &&
'_dictionaryType' in x &&
x._dictionaryType() === t;
}
export const isDictionary = (x: any): x is Dictionary => is_Dictionary(x, 'Dictionary');
export const isSet = (x: any): x is Set => is_Dictionary(x, 'Set');
export type DictionaryEntry = [Value, Value];
export abstract class _Dictionary {
_items: { [key: string]: DictionaryEntry } = {};
_key(key: Value): string {
const bs = encode(key, { canonical: true })._view;
const s = String.fromCharCode.apply(null, bs);
return s;
}
_lookup(key: Value): DictionaryEntry | null {
const k = this._key(key);
return k in this._items ? this._items[k] : null;
}
_set(key: Value, value: Value) {
this._items[this._key(key)] = [key, value];
}
_get(key: Value, defaultValue?: Value): Value {
const k = this._key(key);
return k in this._items ? this._items[k][1] : defaultValue;
}
_delete(key: Value) {
delete this._items[this._key(key)];
}
_forEach(f: (value: Value, key: Value) => void) {
for (let ks in this._items) {
const [k, v] = this._items[ks];
f(v, k);
}
}
_map<T>(f: (value: Value, key: Value) => T): Array<T> {
const result = [];
for (let ks in this._items) {
const [k, v] = this._items[ks];
result.push(f(v, k));
}
return result;
}
equals(other: any): boolean {
if (!Object.is(other.constructor, this.constructor)) return false;
const es1 = Object.entries(this._items);
if (es1.length !== Object.entries(other._items).length) return false;
for (let [ks1, e1] of es1) {
const e2 = other._items[ks1];
if (!is(e1[1], e2[1])) return false;
}
return true;
}
toString(): string {
return this.asPreservesText();
}
abstract asPreservesText(): string;
abstract _dictionaryType(): DictionaryType;
}
export class Dictionary extends _Dictionary {
static fromJS(x: object): Dictionary {
if (isDictionary(x)) return x;
const d = new Dictionary();
for (let key in x) {
const value = x[key];
d._set(key, fromJS(value));
}
return d;
}
_dictionaryType(): DictionaryType {
return 'Dictionary';
}
set(key: Value, value: Value) {
this._set(key, value);
}
get(key: Value, defaultValue?: Value): Value {
return this._get(key, defaultValue);
}
delete(key: Value) {
this._delete(key);
}
mapEntries(f: (entry: DictionaryEntry) => DictionaryEntry): Dictionary {
const result = new Dictionary();
for (let ks in this._items) {
const oldEntry = this._items[ks];
const newEntry = f(oldEntry);
result._set(newEntry[0], newEntry[1])
}
return result;
}
forEach(f: (value: Value, key: Value) => void) {
this._forEach(f);
}
asPreservesText(): string {
return '{' +
this._map((v, k) => k.asPreservesText() + ': ' + v.asPreservesText()).join(', ') +
'}';
}
}
export class Set extends _Dictionary implements Iterable<Value> {
constructor(items: Iterable<any> = []) {
super();
for (let item of items) this.add(fromJS(item));
}
_dictionaryType(): DictionaryType {
return 'Set';
}
add(v: Value) {
this._set(v, true);
}
delete(v: Value) {
this._delete(v);
}
includes(key: Value) {
return this._lookup(key) !== null;
}
forEach(f: (value: Value) => void) {
this._forEach((_v, k) => f(k));
}
map(f: (value: Value) => Value): Set {
const result = new Set();
for (let ks in this._items) {
const k = this._items[ks][0];
result._set(f(k), true);
}
return result;
}
[Symbol.iterator](): Iterator<Value> {
return this._map((_v, k) => k)[Symbol.iterator]();
}
asPreservesText(): string {
return '#{' +
this._map((_v, k) => k.asPreservesText()).join(', ') +
'}';
}
}
export function isAnnotated(x: any): x is Annotated {
return typeof x === 'object' && x !== null &&
'annotations' in x &&
'item' in x;
}
export class Annotated {
readonly annotations: Array<Value>;
readonly item: Value;
constructor(item: Value) {
this.annotations = [];
this.item = item;
}
[AsPreserve](): Value {
return this;
}
[PreserveOn](encoder: Encoder) {
if (encoder.includeAnnotations) {
for (const a of this.annotations) {
encoder.emitbyte(Tag.Annotation);
encoder.push(a);
}
}
encoder.push(this.item);
}
equals(other: any): boolean {
return isAnnotated(other) && is(this.item, other.item);
}
hashCode(): number {
return hash(this.item);
}
toString(): string {
return this.asPreservesText();
}
asPreservesText(): string {
const anns = this.annotations.map((a) => '@' + a.asPreservesText()).join(' ');
return (anns ? anns + ' ' : anns) + this.item.asPreservesText();
}
}
export function peel(v: Value): Value {
return strip(v, 1);
}
export function strip(v: Value, depth: number = Infinity) {
function step(v: Value, depth: number): Value {
if (depth === 0) return v;
if (!isAnnotated(v)) return v;
const nextDepth = depth - 1;
function walk(v: Value) { return step(v, nextDepth); }
if (isRecord(v.item)) {
return new Record(step(v.item.label, depth), v.item.map(walk));
} else if (Array.isArray(v.item)) {
return v.item.map(walk);
} else if (isSet(v.item)) {
return v.item.map(walk);
} else if (isDictionary(v.item)) {
return v.item.mapEntries((e) => [walk(e[0]), walk(e[1])]);
} else if (isAnnotated(v.item)) {
throw new Error("Improper annotation structure");
} else {
return v.item;
}
}
return step(v, depth);
}
export function annotate(v0: Value, ...anns: Value[]) {
const v = isAnnotated(v0) ? v0 : new Annotated(v0);
anns.forEach((a) => v.annotations.push(a));
return v;
}

View File

@ -0,0 +1,80 @@
import { Bytes, fromJS } from '../src/values';
import './test-utils';
describe('immutable byte arrays', () => {
describe('Uint8Array methods', () => {
const bs = Bytes.of(10, 20, 30, 40);
it('should yield entries', () => {
expect(fromJS(Array.from(bs.entries())))
.is(fromJS([[0,10],[1,20],[2,30],[3,40]]));
});
it('should implement every', () => {
expect(bs.every((b) => !(b & 1))).toBe(true);
expect(bs.every((b) => b !== 50)).toBe(true);
expect(!(bs.every((b) => b !== 20))).toBe(true);
});
// it('should implement find', () => {
// assert.strictEqual(bs.find((b) => b > 20), 30);
// assert.strictEqual(bs.find((b) => b > 50), void 0);
// });
// it('should implement findIndex', () => {
// assert.strictEqual(bs.findIndex((b) => b > 20), 2);
// assert.strictEqual(bs.findIndex((b) => b > 50), -1);
// });
// it('should implement forEach', () => {
// const vs = [];
// bs.forEach((b) => vs.push(b));
// assert(is(fromJS(vs), fromJS([10, 20, 30, 40])));
// });
// it('should implement includes', () => {
// assert(bs.includes(20));
// assert(!bs.includes(50));
// });
// it('should implement indexOf', () => {
// assert.strictEqual(bs.indexOf(20), 1);
// assert.strictEqual(bs.indexOf(50), -1);
// });
// it('should implement join', () => assert.strictEqual(bs.join('-'), '10-20-30-40'));
// it('should implement keys', () => {
// assert(is(fromJS(Array.from(bs.keys())), fromJS([0,1,2,3])));
// });
// it('should implement values', () => {
// assert(is(fromJS(Array.from(bs.values())), fromJS([10,20,30,40])));
// });
// it('should implement filter', () => {
// assert(is(bs.filter((b) => b !== 30), Bytes.of(10,20,40)));
// });
// it('should implement slice', () => {
// const vs = bs.slice(2);
// assert(!Object.is(vs._view.buffer, bs._view.buffer));
// assert.strictEqual(vs._view.buffer.byteLength, 2);
// assert.strictEqual(vs.get(0), 30);
// assert.strictEqual(vs.get(1), 40);
// assert.strictEqual(vs.size, 2);
// });
// it('should implement subarray', () => {
// const vs = bs.subarray(2);
// assert(Object.is(vs._view.buffer, bs._view.buffer));
// assert.strictEqual(vs._view.buffer.byteLength, 4);
// assert.strictEqual(vs.get(0), 30);
// assert.strictEqual(vs.get(1), 40);
// assert.strictEqual(vs.size, 2);
// });
// it('should implement reverse', () => {
// const vs = bs.reverse();
// assert(!Object.is(vs._view.buffer, bs._view.buffer));
// assert.strictEqual(bs.get(0), 10);
// assert.strictEqual(bs.get(3), 40);
// assert.strictEqual(vs.get(0), 40);
// assert.strictEqual(vs.get(3), 10);
// });
// it('should implement sort', () => {
// const vs = bs.reverse().sort();
// assert(!Object.is(vs._view.buffer, bs._view.buffer));
// assert.strictEqual(bs.get(0), 10);
// assert.strictEqual(bs.get(3), 40);
// assert.strictEqual(vs.get(0), 10);
// assert.strictEqual(vs.get(3), 40);
// });
});
});

View File

@ -0,0 +1,178 @@
import {
Value,
Dictionary,
decode, decodeWithAnnotations, encodeWithAnnotations,
isDecodeError, isShortPacket,
Bytes, Record,
annotate,
strip, peel,
preserves,
} from '../src/index';
import './test-utils';
import * as fs from 'fs';
const Discard = Record.makeConstructor('discard', []);
const Capture = Record.makeConstructor('capture', ['pattern']);
const Observe = Record.makeConstructor('observe', ['pattern']);
describe('record constructors', () => {
it('should have constructorInfo', () => {
expect(Discard.constructorInfo.label).toEqual(Symbol.for('discard'));
expect(Capture.constructorInfo.label).toEqual(Symbol.for('capture'));
expect(Observe.constructorInfo.label).toEqual(Symbol.for('observe'));
expect(Discard.constructorInfo.arity).toEqual(0);
expect(Capture.constructorInfo.arity).toEqual(1);
expect(Observe.constructorInfo.arity).toEqual(1);
});
})
describe('RecordConstructorInfo', () => {
const C1 = Record.makeBasicConstructor([1], ['x', 'y']);
const C2 = Record.makeBasicConstructor([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));
});
it('comparison based on pointer equality should not work', () => {
expect(C1.constructorInfo).not.toBe(C2.constructorInfo);
});
it('comparison based on .equals should work', () => {
expect(C1.constructorInfo).toEqual(C2.constructorInfo);
});
});
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);
});
});
describe('parsing from subarray', () => {
it('should maintain alignment of nextbytes', () => {
const u = Uint8Array.of(1, 1, 1, 1, 0xb1, 0x03, 0x33, 0x33, 0x33);
const bs = Bytes.from(u.subarray(4));
expect(decode(bs)).is("333");
});
});
describe('common test suite', () => {
const samples_bin = fs.readFileSync(__dirname + '/../../../tests/samples.bin');
const samples = decodeWithAnnotations(samples_bin);
const TestCases = Record.makeConstructor('TestCases', ['cases']);
function DS(bs: Bytes) {
return decode(bs);
}
function D(bs: Bytes) {
return decodeWithAnnotations(bs);
}
function E(v: Value) {
return encodeWithAnnotations(v);
}
const expectedValues = {
annotation1: { forward: annotate(9, "abc"),
back: 9 },
annotation2: { forward: annotate([[], annotate([], "x")], "abc", "def"),
back: [[], []] },
annotation3: { forward: annotate(5,
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())
])) },
};
type Variety = 'normal' | 'nondeterministic' | 'decode';
function runTestCase(variety: Variety, tName: string, binaryForm: Bytes, annotatedTextForm: Value) {
describe(tName, () => {
const textForm = strip(annotatedTextForm);
const {forward, back} = (function () {
const entry = expectedValues[tName] || {value: textForm};
if ('value' in entry) {
return {forward: entry.value, back: entry.value};
} else if ('forward' in entry && 'back' in entry) {
return entry;
} else {
throw new Error('Invalid expectedValues entry for ' + tName);
}
})();
it('should match the expected value', () => expect(textForm).is(back));
it('should round-trip', () => expect(DS(E(textForm))).is(back));
it('should go forward', () => expect(DS(E(forward))).is(back));
it('should go back', () => expect(DS(binaryForm)).is(back));
it('should go back with annotations',
() => expect(D(E(annotatedTextForm))).is(annotatedTextForm));
if (variety !== 'decode' && variety !== 'nondeterministic') {
it('should encode correctly', () => expect(E(forward)).is(binaryForm));
it('should encode correctly with annotations',
() => expect(E(annotatedTextForm)).is(binaryForm));
}
});
}
const tests = peel(TestCases._.cases(peel(samples))) as Dictionary;
tests.forEach((t0: Value, tName0: Value) => {
const tName = Symbol.keyFor(strip(tName0) as symbol);
const t = peel(t0) as Record;
switch (t.label) {
case Symbol.for('Test'):
runTestCase('normal', tName, strip(t[0]) as Bytes, t[1]);
break;
case Symbol.for('NondeterministicTest'):
runTestCase('nondeterministic', tName, strip(t[0]) as Bytes, t[1]);
break;
case Symbol.for('DecodeTest'):
runTestCase('decode', tName, strip(t[0]) as Bytes, t[1]);
break;
case Symbol.for('DecodeError'):
describe(tName, () => {
it('should fail with DecodeError', () => {
expect(() => D(strip(t[0]) as Bytes))
.toThrowFilter(e => isDecodeError(e) && !isShortPacket(e));
});
});
break;
case Symbol.for('DecodeEOF'): // fall through
case Symbol.for('DecodeShort'):
describe(tName, () => {
it('should fail with ShortPacket', () => {
expect(() => D(strip(t[0]) as Bytes))
.toThrowFilter(e => isShortPacket(e));
});
});
break;
case Symbol.for('ParseError'):
case Symbol.for('ParseEOF'):
case Symbol.for('ParseShort'):
/* Skipped for now, until we have an implementation of text syntax */
break;
default:{
const e = new Error(preserves`Unsupported test kind ${t}`);
console.error(e);
throw e;
}
}
});
});

View File

@ -1,84 +0,0 @@
"use strict";
const assert = require('assert');
const Immutable = require('immutable');
const { is, Bytes, fromJS } = require('../src/index.js');
describe('immutable byte arrays', () => {
describe('Uint8Array methods', () => {
const bs = Bytes.of(10, 20, 30, 40);
it('should yield entries', () => {
assert(is(fromJS(Array.from(bs.entries())),
fromJS([[0,10],[1,20],[2,30],[3,40]])));
});
it('should implement every', () => {
assert(bs.every((b) => !(b & 1)));
assert(bs.every((b) => b !== 50));
assert(!(bs.every((b) => b !== 20)));
});
it('should implement find', () => {
assert.strictEqual(bs.find((b) => b > 20), 30);
assert.strictEqual(bs.find((b) => b > 50), void 0);
});
it('should implement findIndex', () => {
assert.strictEqual(bs.findIndex((b) => b > 20), 2);
assert.strictEqual(bs.findIndex((b) => b > 50), -1);
});
it('should implement forEach', () => {
const vs = [];
bs.forEach((b) => vs.push(b));
assert(is(fromJS(vs), fromJS([10, 20, 30, 40])));
});
it('should implement includes', () => {
assert(bs.includes(20));
assert(!bs.includes(50));
});
it('should implement indexOf', () => {
assert.strictEqual(bs.indexOf(20), 1);
assert.strictEqual(bs.indexOf(50), -1);
});
it('should implement join', () => assert.strictEqual(bs.join('-'), '10-20-30-40'));
it('should implement keys', () => {
assert(is(fromJS(Array.from(bs.keys())), fromJS([0,1,2,3])));
});
it('should implement values', () => {
assert(is(fromJS(Array.from(bs.values())), fromJS([10,20,30,40])));
});
it('should implement filter', () => {
assert(is(bs.filter((b) => b !== 30), Bytes.of(10,20,40)));
});
it('should implement slice', () => {
const vs = bs.slice(2);
assert(!Object.is(vs._view.buffer, bs._view.buffer));
assert.strictEqual(vs._view.buffer.byteLength, 2);
assert.strictEqual(vs.get(0), 30);
assert.strictEqual(vs.get(1), 40);
assert.strictEqual(vs.size, 2);
});
it('should implement subarray', () => {
const vs = bs.subarray(2);
assert(Object.is(vs._view.buffer, bs._view.buffer));
assert.strictEqual(vs._view.buffer.byteLength, 4);
assert.strictEqual(vs.get(0), 30);
assert.strictEqual(vs.get(1), 40);
assert.strictEqual(vs.size, 2);
});
it('should implement reverse', () => {
const vs = bs.reverse();
assert(!Object.is(vs._view.buffer, bs._view.buffer));
assert.strictEqual(bs.get(0), 10);
assert.strictEqual(bs.get(3), 40);
assert.strictEqual(vs.get(0), 40);
assert.strictEqual(vs.get(3), 10);
});
it('should implement sort', () => {
const vs = bs.reverse().sort();
assert(!Object.is(vs._view.buffer, bs._view.buffer));
assert.strictEqual(bs.get(0), 10);
assert.strictEqual(bs.get(3), 40);
assert.strictEqual(vs.get(0), 10);
assert.strictEqual(vs.get(3), 40);
});
});
});

View File

@ -1,240 +0,0 @@
"use strict";
const assert = require('assert');
const Immutable = require('immutable');
const Preserves = require('../src/index.js');
const {
is, List, Set, Map,
Decoder, Encoder, decode, decodeWithAnnotations, encode,
DecodeError, EncodeError, ShortPacket,
Bytes, Record, Single, Double,
annotate,
stripAnnotations,
PreserveOn,
} = Preserves;
const fs = require('fs');
const util = require('util');
const Discard = Record.makeConstructor('discard', []);
const Capture = Record.makeConstructor('capture', ['pattern']);
const Observe = Record.makeConstructor('observe', ['pattern']);
describe('record constructors', () => {
it('should have constructorInfo', () => {
assert.strictEqual(Discard.constructorInfo.label, Symbol.for('discard'));
assert.strictEqual(Capture.constructorInfo.label, Symbol.for('capture'));
assert.strictEqual(Observe.constructorInfo.label, Symbol.for('observe'));
assert.strictEqual(Discard.constructorInfo.arity, 0);
assert.strictEqual(Capture.constructorInfo.arity, 1);
assert.strictEqual(Observe.constructorInfo.arity, 1);
});
})
describe('RecordConstructorInfo', () => {
const C1 = Record.makeBasicConstructor(Immutable.List([1]), ['x', 'y']);
const C2 = Record.makeBasicConstructor(Immutable.List([1]), ['z', 'w']);
it('instance comparison should ignore pointer and fieldname differences', () => {
assert(is(C1(9,9), C2(9,9)));
assert(!is(C1(9,9), C2(9,8)));
});
it('comparison based on pointer equality should not work', () => {
assert.notStrictEqual(C1.constructorInfo, C2.constructorInfo);
});
it('comparison based on .equals should work', () => {
assert(is(C1.constructorInfo, C2.constructorInfo));
});
});
describe('records', () => {
it('should have correct getConstructorInfo', () => {
assert(Discard().getConstructorInfo().equals(Discard.constructorInfo));
assert(Capture(Discard()).getConstructorInfo().equals(Capture.constructorInfo));
assert(Observe(Capture(Discard())).getConstructorInfo().equals(Observe.constructorInfo));
assert(is(Observe(Capture(Discard())).getConstructorInfo(), Observe.constructorInfo));
});
});
class SimpleStream {
constructor(t, n, items) {
this.t = t;
this.n = n;
this.items = items;
}
[PreserveOn](e) {
e.encodestream(this.t, this.n, this.items);
}
}
class StringStream extends SimpleStream { constructor(items) { super(1, 1, items); }}
class BytesStream extends SimpleStream { constructor(items) { super(1, 2, items); }}
class SymbolStream extends SimpleStream { constructor(items) { super(1, 3, items); }}
class RecordStream extends SimpleStream { constructor(items) { super(2, 0, items); }}
// Not needed -- an ordinary array will do!
// class SequenceStream extends SimpleStream { constructor(items) { super(2, 1, items); }}
class SetStream extends SimpleStream { constructor(items) { super(2, 2, items); }}
class DictionaryStream extends SimpleStream { constructor(items) { super(2, 3, items); }}
describe('common test suite', () => {
const samples_bin = fs.readFileSync(__dirname + '/../../../tests/samples.bin');
const samples = decodeWithAnnotations(samples_bin);
const TestCases = Record.makeConstructor('TestCases', ['cases']);
function DS(bs) {
return decode(bs);
}
function D(bs) {
return decodeWithAnnotations(bs);
}
function E(v) {
return encode(v);
}
const expectedValues = {
annotation1: { forward: annotate(9, "abc"),
back: 9 },
annotation2: { forward: annotate(List([List(), annotate(List(), "x")]), "abc", "def"),
back: List([List(), List()]) },
annotation3: { forward: annotate(5,
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(List(), Symbol.for('a'), Symbol.for('b'), Symbol.for('c')),
back: List() },
bytes1: { forward: new BytesStream([Bytes('he'), Bytes('ll'), Bytes('o')]),
back: Bytes('hello') },
list1: { forward: [1, 2, 3, 4],
back: List([1, 2, 3, 4]) },
list2: {
forward: [ new StringStream([Bytes('abc')]),
new StringStream([Bytes('def')]), ],
back: List(["abc", "def"])
},
list3: {
forward: [List(["a", 1]), List(["b", 2]), List(["c", 3])],
back: List([List(["a", 1]), List(["b", 2]), List(["c", 3])])
},
record2: { value: Observe(new Record(Symbol.for("speak"), [
Discard(),
Capture(Discard())
])) },
string0a: { forward: new StringStream([]), back: '' },
string1: { forward: new StringStream([Bytes('he'), Bytes('ll'), Bytes('o')]),
back: 'hello' },
string2: { forward: new StringStream([Bytes('he'), Bytes('llo')]),
back: 'hello' },
symbol1: { forward: new SymbolStream([Bytes('he'), Bytes('ll'), Bytes('o')]),
back: Symbol.for('hello') },
};
function runTestCase(variety, tName, binaryForm, annotatedTextForm) {
describe(tName, () => {
const textForm = annotatedTextForm.strip();
const {forward, back} = (function () {
const entry = expectedValues[tName] || {value: textForm};
if ('value' in entry) {
return {forward: entry.value, back: entry.value};
} else if ('forward' in entry && 'back' in entry) {
return entry;
} else {
throw new Error('Invalid expectedValues entry for ' + tName);
}
})();
it('should match the expected value', () => assert(is(textForm, back)));
it('should round-trip', () => assert(is(DS(E(textForm)), back)));
it('should go forward', () => assert(is(DS(E(forward)), back)));
it('should go back', () => assert(is(DS(binaryForm), back)));
it('should go back with annotations',
() => assert(is(D(E(annotatedTextForm)), annotatedTextForm)));
if (variety !== 'decode' && variety !== 'nondeterministic') {
it('should encode correctly',
() => assert(is(E(forward), binaryForm),
E(forward) + ' ' + binaryForm));
}
if (variety !== 'decode' && variety !== 'nondeterministic' && variety !== 'streaming') {
it('should encode correctly with annotations',
() => assert(is(E(annotatedTextForm), binaryForm),
E(annotatedTextForm) + ' ' + binaryForm));
}
});
}
const tests = TestCases._cases(samples.peel()).peel();
tests.forEach((t0, tName0) => {
const tName = Symbol.keyFor(tName0.strip());
const t = t0.peel();
switch (t.label) {
case Symbol.for('Test'):
runTestCase('normal', tName, t.get(0).strip(), t.get(1));
break;
case Symbol.for('StreamingTest'):
runTestCase('streaming', tName, t.get(0).strip(), t.get(1));
break;
case Symbol.for('NondeterministicTest'):
runTestCase('nondeterministic', tName, t.get(0).strip(), t.get(1));
break;
case Symbol.for('DecodeTest'):
runTestCase('decode', tName, t.get(0).strip(), t.get(1));
break;
case Symbol.for('DecodeError'):
describe(tName, () => {
it('should fail with DecodeError', () => {
try {
D(t.get(0).strip());
assert.fail("but it didn't");
} catch (e) {
assert(e instanceof DecodeError);
assert(!(e instanceof ShortPacket));
}
});
});
break;
case Symbol.for('DecodeEOF'): // fall through
case Symbol.for('DecodeShort'):
describe(tName, () => {
it('should fail with ShortPacket', () => {
try {
D(t.get(0).strip());
assert.fail("but it didn't");
} catch (e) {
assert(e instanceof ShortPacket);
}
});
});
break;
case Symbol.for('ParseError'):
case Symbol.for('ParseEOF'):
case Symbol.for('ParseShort'):
/* Skipped for now, until we have an implementation of text syntax */
break;
default:{
const e = new Error('Unsupported test kind');
e.irritant = t;
e.testKind = t.label;
console.error(e);
throw e;
}
}
});
});
describe('parsing from subarray', () => {
it('should maintain alignment of nextbytes', () => {
const u = Uint8Array.of(57, 57, 57, 57, 83, 51, 51, 51);
const bs = Bytes.from(u.subarray(4));
assert.strictEqual(new Decoder(bs).next(), "333");
});
});

View File

@ -1,48 +0,0 @@
"use strict";
// We really, really, REALLY want not to load two separate
// implementations of values.js into the same node.js instance, so
// there's a bunch of singleton hackery in values.js. These tests
// check that separate loads don't cause separate instances.
const assert = require('assert');
const Immutable = require('immutable');
describe('reloading values.js', () => {
const V1 = require('../src/values.js');
delete require.cache[require.resolve('../src/values.js')];
const V2 = require('../src/values.js');
const C1 = V1.Record.makeConstructor('c', ['a', 'b']);
const C2 = V2.Record.makeConstructor('c', ['a', 'b']);
it('should reuse RecordConstructorInfo (1)', () => {
assert(C1.constructorInfo instanceof V1.RecordConstructorInfo);
});
it('should reuse RecordConstructorInfo (2)', () => {
assert(C1.constructorInfo instanceof V2.RecordConstructorInfo);
});
it('should identify RecordConstructorInfo', () => {
assert(Object.is(V1.RecordConstructorInfo, V2.RecordConstructorInfo));
});
it('should produce identical module instances', () => { assert.strictEqual(V1, V2); });
it('should produce distinct constructor instances', () => { assert.notStrictEqual(C1, C2); });
it('should produce distinct constructor info', () => {
assert(!Object.is(C1.constructorInfo, C2.constructorInfo));
});
it('should produce compatible constructor info', () => {
assert(Immutable.is(C1.constructorInfo, C2.constructorInfo));
});
it('should produce compatible record instances', () => {
assert(Immutable.is(C1(1,2), C2(1,2)));
});
});
describe('reloading index.js', () => {
it('produces identical module exports objects', () => {
const I1 = require('../src/index.js');
delete require.cache[require.resolve('../src/index.js')];
const I2 = require('../src/index.js');
assert(Object.is(I1, I2));
});
});

View File

@ -0,0 +1,36 @@
import { Value, is, preserves } from '../src/index';
import '../src/node_support';
declare global {
namespace jest {
interface Matchers<R> {
is(expected: Value): R;
toThrowFilter(f: (e: Error) => boolean): R;
}
}
}
expect.extend({
is(actual, expected) {
return is(actual, expected)
? { message: () => preserves`expected ${actual} not to be Preserves.is to ${expected}`,
pass: true }
: { message: () => preserves`expected ${actual} to be Preserves.is to ${expected}`,
pass: false };
},
toThrowFilter(thunk, f) {
try {
thunk();
return { message: () => preserves`expected an exception`, pass: false };
} catch (e) {
if (f(e)) {
return { message: () => preserves`expected an exception not matching the filter`,
pass: true };
} else {
return { message: () => preserves`expected an exception matching the filter: ${e.constructor.name}`,
pass: false };
}
}
}
});

View File

@ -1,18 +0,0 @@
"use strict";
const assert = require('assert');
const Immutable = require('immutable');
const { is, Single, Double, fromJS } = require('../src/index.js');
describe('Single', () => {
it('should print reasonably', () => {
assert.strictEqual(Single(123.45).toString(), "123.45");
});
});
describe('Double', () => {
it('should print reasonably', () => {
assert.strictEqual(Double(123.45).toString(), "123.45");
});
});

View File

@ -0,0 +1,14 @@
import { Single, Double } from '../src/index';
import './test-utils';
describe('Single', () => {
it('should print reasonably', () => {
expect(new Single(123.45).toString()).toEqual("123.45f");
});
});
describe('Double', () => {
it('should print reasonably', () => {
expect(new Double(123.45).toString()).toEqual("123.45");
});
});

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2017",
"declaration": true,
"baseUrl": "./src",
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./lib",
"esModuleInterop": true,
"moduleResolution": "node",
"sourceMap": true
},
"include": ["src/**/*"]
}