fix: tools arb passable
MODIFY
packages/marshal/package.json
packages/marshal/package.json
"@endo/eventual-send": "^0.16.9",
"@endo/nat": "^4.1.24",
"@endo/pass-style": "^0.1.0",
- "@endo/promise-kit": "^0.2.53"
+ "@endo/promise-kit": "^0.2.53",
+ "@fast-check/ava": "^1.0.1"
},
"devDependencies": {
"@endo/init": "^0.5.53",
"@endo/lockdown": "^0.1.25",
"@endo/ses-ava": "^0.2.37",
- "@fast-check/ava": "^1.0.1",
"ava": "^5.1.0",
"c8": "^7.7.3"
},

MODIFY
packages/marshal/src/encodePassable.js
packages/marshal/src/encodePassable.js
@@ -293,17 +293,16 @@ const decodeTagged = (encoded, decodePassable) => {
*/
/**
- * @param {EncodeOptions} encodeOptions
- * encodeOptions is actually optional, but not marked as such to work around
- * https://github.com/microsoft/TypeScript/issues/50286
- *
+ * @param {EncodeOptions} [encodeOptions]
* @returns {(passable: Passable) => string}
*/
-export const makeEncodePassable = ({
+export const makeEncodePassable = (encodeOptions = {}) => {
+ const {
encodeRemotable = (rem, _) => Fail`remotable unexpected: ${rem}`,
encodePromise = (prom, _) => Fail`promise unexpected: ${prom}`,
encodeError = (err, _) => Fail`error unexpected: ${err}`,
-} = {}) => {
+ } = encodeOptions;
+
const encodePassable = passable => {
if (isErrorLike(passable)) {
return encodeError(passable, encodePassable);
@@ -387,18 +386,13 @@ harden(makeEncodePassable);
* @param {DecodeOptions} [decodeOptions]
* @returns {(encoded: string) => Passable}
*/
-// `yarn lint` complains here but not for equivalent code in agoric-sdk.
-// Also, vscode does not complain. Hence we're using at-ts-ignore rather than
-// at-ts-expect-error. Using at-ts-ignore should also generate a complaint
-// that we should be using at-expect-error, where we would normally need
-// to suppress that error as well. However, perhaps that second error currently
-// happens only in agoric-sdk, but not yet in endo. TODO figure out and fix.
-// @ts-ignore
-export const makeDecodePassable = ({
+export const makeDecodePassable = (decodeOptions = {}) => {
+ const {
decodeRemotable = (rem, _) => Fail`remotable unexpected: ${rem}`,
decodePromise = (prom, _) => Fail`promise unexpected: ${prom}`,
decodeError = (err, _) => Fail`error unexpected: ${err}`,
-} = {}) => {
+ } = decodeOptions;
+
const decodePassable = encoded => {
switch (encoded.charAt(0)) {
case 'v': {

MODIFY
packages/marshal/src/encodeToCapData.js
packages/marshal/src/encodeToCapData.js
@@ -82,17 +82,16 @@ const dontEncodePromiseToCapData = prom => Fail`promise unexpected: ${prom}`;
const dontEncodeErrorToCapData = err => Fail`error object unexpected: ${err}`;
/**
- * @param {EncodeToCapDataOptions} encodeOptions
- * encodeOptions is actually optional, but not marked as such to work around
- * https://github.com/microsoft/TypeScript/issues/50286
- *
+ * @param {EncodeToCapDataOptions} [encodeOptions]
* @returns {(passable: Passable) => Encoding}
*/
-export const makeEncodeToCapData = ({
+export const makeEncodeToCapData = (encodeOptions = {}) => {
+ const {
encodeRemotableToCapData = dontEncodeRemotableToCapData,
encodePromiseToCapData = dontEncodePromiseToCapData,
encodeErrorToCapData = dontEncodeErrorToCapData,
-} = {}) => {
+ } = encodeOptions;
+
/**
* Must encode `val` into plain JSON data *canonically*, such that
* `JSON.stringify(encode(v1)) === JSON.stringify(encode(v1))`. For most
@@ -296,14 +295,16 @@ const dontDecodeErrorFromCapData = errorEncoding =>
* API where these can reliably differ.
* See https://github.com/Agoric/agoric-sdk/issues/4334
*
- * @param {DecodeOptions} decodeOptions
+ * @param {DecodeOptions} [decodeOptions]
* @returns {(encoded: Encoding) => Passable}
*/
-export const makeDecodeFromCapData = ({
+export const makeDecodeFromCapData = (decodeOptions = {}) => {
+ const {
decodeRemotableFromCapData = dontDecodeRemotableOrPromiseFromCapData,
decodePromiseFromCapData = dontDecodeRemotableOrPromiseFromCapData,
decodeErrorFromCapData = dontDecodeErrorFromCapData,
-} = {}) => {
+ } = decodeOptions;
+
decodeRemotableFromCapData === decodePromiseFromCapData ||
Fail`An implementation restriction for now: If either decodeRemotableFromCapData or decodePromiseFromCapData is provided, both must be provided and they must be the same: ${q(
decodeRemotableFromCapData,

MODIFY
packages/marshal/src/encodeToSmallcaps.js
packages/marshal/src/encodeToSmallcaps.js
@@ -111,17 +111,19 @@ const dontEncodeErrorToSmallcaps = err =>
Fail`error object unexpected: ${q(err)}`;
/**
- * @param {EncodeToSmallcapsOptions} encodeOptions
+ * @param {EncodeToSmallcapsOptions} [encodeOptions]
* encodeOptions is actually optional, but not marked as such to work around
* https://github.com/microsoft/TypeScript/issues/50286
*
* @returns {(passable: Passable) => SmallcapsEncoding}
*/
-export const makeEncodeToSmallcaps = ({
+export const makeEncodeToSmallcaps = (encodeOptions = {}) => {
+ const {
encodeRemotableToSmallcaps = dontEncodeRemotableToSmallcaps,
encodePromiseToSmallcaps = dontEncodePromiseToSmallcaps,
encodeErrorToSmallcaps = dontEncodeErrorToSmallcaps,
-} = {}) => {
+ } = encodeOptions;
+
const assertEncodedError = encoding => {
(typeof encoding === 'object' && hasOwnPropertyOf(encoding, '#error')) ||
Fail`internal: Error encoding must have "#error" property: ${q(
@@ -316,14 +318,16 @@ const dontDecodeErrorFromSmallcaps = encoding =>
Fail`error unexpected: ${q(encoding)}`;
/**
- * @param {DecodeFromSmallcapsOptions} decodeOptions
+ * @param {DecodeFromSmallcapsOptions} [decodeOptions]
* @returns {(encoded: SmallcapsEncoding) => Passable}
*/
-export const makeDecodeFromSmallcaps = ({
+export const makeDecodeFromSmallcaps = (decodeOptions = {}) => {
+ const {
decodeRemotableFromSmallcaps = dontDecodeRemotableFromSmallcaps,
decodePromiseFromSmallcaps = dontDecodePromiseFromSmallcaps,
decodeErrorFromSmallcaps = dontDecodeErrorFromSmallcaps,
-} = {}) => {
+ } = decodeOptions;
+
/**
* `decodeFromSmallcaps` may rely on `encoding` being the result of a
* plain call to JSON.parse. However, it *cannot* rely on `encoding`

MODIFY
packages/marshal/test/test-encodePassable.js
packages/marshal/test/test-encodePassable.js
@@ -11,35 +11,75 @@ import {
} from '../src/encodePassable.js';
import { compareRank, makeComparatorKit } from '../src/rankOrder.js';
import { sample } from './test-rankOrder.js';
+import { arbPassable } from '../tools/arb-passable.js';
-const { Fail } = assert;
+const { Fail, quote: q } = assert;
-const r2e = new Map();
-const e2r = [];
+const buffers = {
+ __proto__: null,
+ r: [],
+ '?': [],
+ '!': [],
+};
+const resetBuffers = () => {
+ buffers.r = [];
+ buffers['?'] = [];
+ buffers['!'] = [];
+};
+const cursors = {
+ __proto__: null,
+ r: 0,
+ '?': 0,
+ '!': 0,
+};
+const resetCursors = () => {
+ cursors.r = 0;
+ cursors['?'] = 0;
+ cursors['!'] = 0;
+};
-const encodeRemotable = r => {
- if (r2e.has(r)) {
- return r2e.get(r);
- }
- const result = `r${e2r.length}`;
- r2e.set(r, result);
- e2r.push(r);
- return result;
+const encodeThing = (prefix, r) => {
+ buffers[prefix].push(r);
+ // With this encoding, all things with the same prefix have the same rank
+ return prefix;
};
-const decodeRemotable = e => {
- e.startsWith('r') || Fail`unexpected encoding ${e}`;
- const i = Number(BigInt(e.substring(1)));
- assert(i >= 0 && i < e2r.length);
- return e2r[i];
+const decodeThing = (prefix, e) => {
+ prefix === e ||
+ Fail`expected encoding ${q(e)} to simply be the prefix ${q(prefix)}`;
+ (cursors[prefix] >= 0 && cursors[prefix] < buffers[prefix].length) ||
+ Fail`while decoding ${q(e)}, expected cursors[${q(prefix)}], i.e., ${q(
+ cursors[prefix],
+ )} <= ${q(buffers[prefix].length)}`;
+ const thing = buffers[prefix][cursors[prefix]];
+ cursors[prefix] += 1;
+ return thing;
};
const compareRemotables = (x, y) =>
- compareRank(encodeRemotable(x), encodeRemotable(y));
+ compareRank(encodeThing('r', x), encodeThing('r', y));
-const encodeKey = makeEncodePassable({ encodeRemotable });
+const encodePassableInternal = makeEncodePassable({
+ encodeRemotable: r => encodeThing('r', r),
+ encodePromise: p => encodeThing('?', p),
+ encodeError: er => encodeThing('!', er),
+});
-const decodeKey = makeDecodePassable({ decodeRemotable });
+const encodePassable = passable => {
+ resetBuffers();
+ return encodePassableInternal(passable);
+};
+
+const decodePassableInternal = makeDecodePassable({
+ decodeRemotable: e => decodeThing('r', e),
+ decodePromise: e => decodeThing('?', e),
+ decodeError: e => decodeThing('!', e),
+});
+
+const decodePassable = encoded => {
+ resetCursors();
+ return decodePassableInternal(encoded);
+};
const { comparator: compareFull } = makeComparatorKit(compareRemotables);
@@ -80,40 +120,46 @@ const goldenPairs = harden([
test('golden round trips', t => {
for (const [k, e] of goldenPairs) {
- t.is(encodeKey(k), e, 'does k encode as expected');
- t.is(decodeKey(e), k, 'does the key round trip through the encoding');
+ t.is(encodePassable(k), e, 'does k encode as expected');
+ t.is(decodePassable(e), k, 'does the key round trip through the encoding');
}
// Not round trips
- t.is(encodeKey(-0), 'f8000000000000000');
- t.is(decodeKey('f0000000000000000'), NaN);
+ t.is(encodePassable(-0), 'f8000000000000000');
+ t.is(decodePassable('f0000000000000000'), NaN);
});
-const orderInvariants = (t, x, y) => {
+const orderInvariants = (x, y) => {
const rankComp = compareRank(x, y);
const fullComp = compareFull(x, y);
if (rankComp !== 0) {
- t.is(rankComp, fullComp);
+ Object.is(rankComp, fullComp) ||
+ Fail`with rankComp ${rankComp}, expected matching fullComp: ${fullComp} for ${x} ${y}`;
}
if (fullComp === 0) {
- t.is(rankComp, 0);
+ Object.is(rankComp, 0) ||
+ Fail`with fullComp 0, expected matching rankComp: ${rankComp} for ${x} ${y}`;
} else {
- t.assert(rankComp === 0 || rankComp === fullComp);
+ rankComp === 0 ||
+ rankComp === fullComp ||
+ Fail`with fullComp ${fullComp}, expected 0 or matching rankComp: ${rankComp} for ${x} ${y}`;
}
-};
-
-test('order invariants', t => {
- for (let i = 0; i < sample.length; i += 1) {
- for (let j = i; j < sample.length; j += 1) {
- orderInvariants(t, sample[i], sample[j]);
+ const ex = encodePassable(x);
+ const ey = encodePassable(y);
+ const encComp = compareRank(ex, ey);
+ if (fullComp !== 0) {
+ Object.is(encComp, fullComp) ||
+ Fail`with fullComp ${fullComp}, expected matching encComp: ${encComp} for ${ex} ${ey}`;
}
- }
-});
+};
-test('BigInt values round-trip', async t => {
+test('Passables round-trip', async t => {
await fc.assert(
- fc.property(fc.bigInt(), n => {
- const rt = decodeKey(encodeKey(n));
- return t.is(rt, n);
+ fc.property(arbPassable, n => {
+ const en = encodePassable(n);
+ const rt = decodePassable(en);
+ const er = encodePassable(rt);
+ t.is(en, er);
+ t.is(compareFull(n, rt), 0);
}),
);
});
@@ -121,9 +167,33 @@ test('BigInt values round-trip', async t => {
test('BigInt encoding comparison corresponds with numeric comparison', async t => {
await fc.assert(
fc.property(fc.bigInt(), fc.bigInt(), (a, b) => {
- const ea = encodeKey(a);
- const eb = encodeKey(b);
- return t.is(a < b, ea < eb) && t.is(a > b, ea > eb);
+ const ea = encodePassable(a);
+ const eb = encodePassable(b);
+ t.is(a < b, ea < eb);
+ t.is(a > b, ea > eb);
+ }),
+ );
+});
+
+test('Passable encoding corresponds to rankOrder', async t => {
+ await fc.assert(
+ fc.property(arbPassable, arbPassable, (a, b) => {
+ return orderInvariants(a, b);
}),
);
+ // Ensure at least one ava assertion.
+ t.pass();
+});
+
+// The following is logically like the test above, but rather than relying on
+// the heuristic generation of fuzzing test cases, it always checks everything
+// in `sample`.
+test('Also test against all enumerated in sample', t => {
+ for (let i = 0; i < sample.length; i += 1) {
+ for (let j = i; j < sample.length; j += 1) {
+ orderInvariants(sample[i], sample[j]);
+ }
+ }
+ // Ensure at least one ava assertion.
+ t.pass();
});

MODIFY
packages/marshal/test/test-rankOrder.js
packages/marshal/test/test-rankOrder.js
@@ -4,7 +4,7 @@ import { test } from './prepare-test-env-ava.js';
// eslint-disable-next-line import/no-extraneous-dependencies
import { fc } from '@fast-check/ava';
-import { makeTagged, Far } from '@endo/pass-style';
+import { makeTagged } from '@endo/pass-style';
import {
FullRankCover,
@@ -16,73 +16,18 @@ import {
assertRankSorted,
} from '../src/rankOrder.js';
-const { quote: q } = assert;
-
-/**
- * The only elements with identity. Everything else should be equal
- * by contents.
- */
-const alice = Far('alice', {});
-const bob = Far('bob', {});
-const carol = Far('carol', {});
+import {
+ arbPassable,
+ exampleAlice,
+ exampleBob,
+ exampleCarol,
+} from '../tools/arb-passable.js';
-/**
- * A factory for arbitrary passables
- */
-const { passable } = fc.letrec(tie => {
- return {
- passable: tie('dag').map(x => harden(x)),
- dag: fc.oneof(
- { depthFactor: 0.5, withCrossShrink: true },
- // a tagged value whose payload is an array of [key, leaf] pairs
- // where each key is unique within the payload
- // XXX can the payload be generalized further?
- fc
- .record({
- type: fc.constantFrom('copyMap', 'copySet', 'nonsense'),
- payload: fc
- .uniqueArray(fc.fullUnicodeString(), { maxLength: 3 })
- .chain(k => {
- return fc.tuple(fc.constant(k), tie('leaf'));
- }),
- })
- .map(({ type, payload }) => makeTagged(type, payload)),
- fc.array(tie('dag'), { maxLength: 3 }),
- fc.dictionary(
- fc.fullUnicodeString().filter(s => s !== 'then'),
- tie('dag'),
- { maxKeys: 3 },
- ),
- tie('dag').map(v => Promise.resolve(v)),
- tie('leaf'),
- ),
- leaf: fc.oneof(
- fc.record({}),
- fc.fullUnicodeString(),
- fc.fullUnicodeString().map(s => Symbol.for(s)),
- fc.fullUnicodeString().map(s => new Error(s)),
- // primordial symbols and registered lookalikes
- fc.constantFrom(
- ...Object.getOwnPropertyNames(Symbol).flatMap(k => {
- const v = Symbol[k];
- if (typeof v !== 'symbol') return [];
- return [v, Symbol.for(k), Symbol.for(`@@${k}`)];
- }),
- ),
- fc.bigInt(),
- fc.integer(),
- fc.constantFrom(-0, NaN, Infinity, -Infinity),
- fc.constantFrom(null, undefined, false, true),
- fc.constantFrom(alice, bob, carol),
- // unresolved promise
- fc.constant(new Promise(() => {})),
- ),
- };
-});
+const { quote: q } = assert;
test('compareRank is reflexive', async t => {
await fc.assert(
- fc.property(passable, x => {
+ fc.property(arbPassable, x => {
return t.is(compareRank(x, x), 0);
}),
);
@@ -90,7 +35,7 @@ test('compareRank is reflexive', async t => {
test('compareRank totally orders ranks', async t => {
await fc.assert(
- fc.property(passable, passable, (a, b) => {
+ fc.property(arbPassable, arbPassable, (a, b) => {
const ab = compareRank(a, b);
const ba = compareRank(b, a);
if (ab === 0) {
@@ -105,15 +50,12 @@ test('compareRank totally orders ranks', async t => {
);
});
-// TODO Had to remove key-level cases from the test-encodePassable.js as
-// migrated to endo. As a result, some of the tests here are broken.
-// Fix.
-test.skip('compareRank is transitive', async t => {
+test('compareRank is transitive', async t => {
await fc.assert(
fc.property(
// operate on a set of three passables covering at least two ranks
fc
- .uniqueArray(passable, { minLength: 3, maxLength: 3 })
+ .uniqueArray(arbPassable, { minLength: 3, maxLength: 3 })
.filter(
([a, b, c]) => compareRank(a, b) !== 0 || compareRank(a, c) !== 0,
),
@@ -122,39 +64,44 @@ test.skip('compareRank is transitive', async t => {
assertRankSorted(sorted, compareRank);
const [a, b, c] = sorted;
const failures = [];
- let result;
- let resultOk;
- result = compareRank(a, b);
- resultOk = t.true(result <= 0, 'a <= b');
- if (!resultOk) {
- failures.push(`Expected <= 0: ${result} from ${q(a)} vs. ${q(b)}`);
- }
- result = compareRank(a, c);
- resultOk = t.true(result <= 0, 'a <= c');
- if (!resultOk) {
- failures.push(`Expected <= 0: ${result} from ${q(a)} vs. ${q(c)}`);
- }
- result = compareRank(b, c);
- resultOk = t.true(result <= 0, 'b <= c');
- if (!resultOk) {
- failures.push(`Expected <= 0: ${result} from ${q(b)} vs. ${q(c)}`);
- }
- result = compareRank(c, b);
- resultOk = t.true(result >= 0, 'c >= b');
- if (!resultOk) {
- failures.push(`Expected >= 0: ${result} from ${q(c)} vs. ${q(b)}`);
- }
- result = compareRank(c, a);
- resultOk = t.true(result >= 0, 'c >= a');
- if (!resultOk) {
- failures.push(`Expected >= 0: ${result} from ${q(c)} vs. ${q(a)}`);
- }
- result = compareRank(b, a);
- resultOk = t.true(result >= 0, 'b >= a');
- if (!resultOk) {
- failures.push(`Expected >= 0: ${result} from ${q(b)} vs. ${q(a)}`);
+ const testCompare = (outcome, message, failure) => {
+ t.true(outcome, message);
+ if (!outcome) {
+ failures.push(failure);
}
+ };
+
+ testCompare(
+ compareRank(a, b) <= 0,
+ 'a <= b',
+ `Expected <= 0: ${q(a)} vs. ${q(b)}`,
+ );
+ testCompare(
+ compareRank(a, c) <= 0,
+ 'a <= c',
+ `Expected <= 0: ${q(a)} vs. ${q(c)}`,
+ );
+ testCompare(
+ compareRank(b, c) <= 0,
+ 'b <= c',
+ `Expected <= 0: ${q(b)} vs. ${q(c)}`,
+ );
+ testCompare(
+ compareRank(c, b) >= 0,
+ 'c >= b',
+ `Expected >= 0: ${q(c)} vs. ${q(b)}`,
+ );
+ testCompare(
+ compareRank(c, a) >= 0,
+ 'c >= a',
+ `Expected >= 0: ${q(c)} vs. ${q(a)}`,
+ );
+ testCompare(
+ compareRank(b, a) >= 0,
+ 'b >= a',
+ `Expected >= 0: ${q(b)} vs. ${q(a)}`,
+ );
return t.deepEqual(failures, []);
},
@@ -178,7 +125,7 @@ export const sample = harden([
2,
null,
[5, { foo: 4, bar: null }],
- bob,
+ exampleBob,
0,
makeTagged('copySet', [
['a', 4],
@@ -189,7 +136,7 @@ export const sample = harden([
undefined,
-Infinity,
[5],
- alice,
+ exampleAlice,
[],
Symbol.for('foo'),
new Error('not erroneous'),
@@ -197,7 +144,7 @@ export const sample = harden([
[5, { bar: 5 }],
Symbol.for(''),
false,
- carol,
+ exampleCarol,
-0,
{},
[5, undefined],
@@ -219,6 +166,12 @@ export const sample = harden([
[5, { foo: 4, bar: undefined }],
Promise.resolve('fulfillment'),
[5, { foo: 4 }],
+ // The promises should be of the same rank, in which case
+ // the singleton array should be earlier. But if the encoded
+ // gives the earlier promise an earlier encoding (as it used to),
+ // then the encoded forms will not be order preserving.
+ [Promise.resolve(null), 'x'],
+ [Promise.resolve(null)],
]);
const rejectedP = Promise.reject(new Error('broken'));
@@ -261,6 +214,8 @@ const sortedSample = harden([
// Lexicographic records by reverse sorted property name, then by values
// in that order.
[],
+ [Promise.resolve(null)],
+ [Promise.resolve(null), 'x'],
[5],
[5, { bar: 5 }],
[5, { foo: 4 }],
@@ -287,9 +242,9 @@ const sortedSample = harden([
// All remotables are tied for the same rank and the sort is stable,
// so their relative order is preserved
- bob,
- alice,
- carol,
+ exampleBob,
+ exampleAlice,
+ exampleCarol,
// Lexicographic strings. Shorter beats longer.
// TODO Probe UTF-16 vs Unicode vs UTF-8 (Moddable) ordering.
@@ -318,7 +273,8 @@ test('compare and sort by rank', t => {
);
});
-const rangeSample = harden([
+// Unused in that it is used only in a skipped test
+const unusedRangeSample = harden([
{}, // 0 -- prefix are earlier, so empty is earliest
{ bar: null }, // 1
{ bar: undefined }, // 2 -- records with same names grouped together
@@ -338,7 +294,9 @@ const rangeSample = harden([
]);
/** @type {[RankCover, IndexCover][]} */
-const queries = harden([
+// @ts-expect-error Stale from when RankCover was a pair of extreme values
+// rather than a pair of strings to be compared to passable encodings.
+const brokenQueries = harden([
[
[['c'], ['c']],
// first > last implies absent.
@@ -368,9 +326,9 @@ const queries = harden([
// adding composite key handling to the durable store implementation) will need
// to re-enable and (likely) update this test.
test.skip('range queries', t => {
- t.assert(isRankSorted(rangeSample, compareRank));
- for (const [rankCover, indexRange] of queries) {
- const range = getIndexCover(rangeSample, compareRank, rankCover);
+ t.assert(isRankSorted(unusedRangeSample, compareRank));
+ for (const [rankCover, indexRange] of brokenQueries) {
+ const range = getIndexCover(unusedRangeSample, compareRank, rankCover);
t.is(range[0], indexRange[0]);
t.is(range[1], indexRange[1]);
}

ADD
packages/marshal/tools/arb-passable.js
+// @ts-check
+import '../src/types.js';
+import { fc } from '@fast-check/ava';
+import { Far, makeTagged } from '@endo/pass-style';
+
+/**
+ * The only elements with identity. Everything else should be equal
+ * by contents.
+ */
+export const exampleAlice = Far('alice', {});
+export const exampleBob = Far('bob', {});
+export const exampleCarol = Far('carol', {});
+
+export const arbString = fc.oneof(fc.string(), fc.fullUnicodeString());
+
+export const arbLeaf = fc.oneof(
+ fc.constantFrom(null, undefined, false, true),
+ arbString,
+ arbString.map(s => Symbol.for(s)),
+ // primordial symbols and registered lookalikes
+ fc.constantFrom(
+ ...Object.getOwnPropertyNames(Symbol).flatMap(k => {
+ const v = Symbol[k];
+ if (typeof v !== 'symbol') return [];
+ return [v, Symbol.for(k), Symbol.for(`@@${k}`)];
+ }),
+ ),
+ fc.bigInt(),
+ fc.integer(),
+ fc.constantFrom(-0, NaN, Infinity, -Infinity),
+ fc.record({}),
+ fc.constantFrom(exampleAlice, exampleBob, exampleCarol),
+ arbString.map(s => new Error(s)),
+ // unresolved promise
+ fc.constant(new Promise(() => {})),
+);
+
+const { arbDag } = fc.letrec(tie => {
+ return {
+ arbDag: fc.oneof(
+ { withCrossShrink: true },
+ arbLeaf,
+ tie('arbDag').map(v => Promise.resolve(v)),
+ fc.array(tie('arbDag')),
+ fc.dictionary(
+ arbString.filter(s => s !== 'then'),
+ tie('arbDag'),
+ ),
+ // A tagged value, either of arbitrary type with arbitrary payload
+ // or of known type with arbitrary or explicitly valid payload.
+ // Ordered by increasing complexity.
+ fc
+ .oneof(
+ fc.record({ type: arbString, payload: tie('arbDag') }),
+ fc.record({
+ type: fc.constantFrom('copySet'),
+ payload: fc.oneof(
+ tie('arbDag'),
+ // copySet valid payload is an array of unique passables.
+ // TODO: A valid copySet payload must be a reverse sorted array,
+ // so we should generate some of those as well.
+ fc.uniqueArray(tie('arbDag')),
+ ),
+ }),
+ fc.record({
+ type: fc.constantFrom('copyBag'),
+ payload: fc.oneof(
+ tie('arbDag'),
+ // copyBag valid payload is an array of [passable, count] tuples
+ // in which each passable is unique.
+ // TODO: A valid copyBag payload must be a reverse sorted array,
+ // so we should generate some of those as well.
+ fc.uniqueArray(fc.tuple(tie('arbDag'), fc.bigInt()), {
+ selector: entry => entry[0],
+ }),
+ ),
+ }),
+ fc.record({
+ type: fc.constantFrom('copyMap'),
+ payload: fc.oneof(
+ tie('arbDag'),
+ // copyMap valid payload is a
+ // `{ keys: Passable[], values: Passable[]}`
+ // record in which keys are unique and both arrays have the
+ // same length.
+ // TODO: In a valid copyMap payload, the keys must be a
+ // reverse sorted array, so we should generate some of
+ // those as well.
+ fc
+ .uniqueArray(
+ fc.record({ key: tie('arbDag'), value: tie('arbDag') }),
+ { selector: entry => entry.key },
+ )
+ .map(entries => ({
+ keys: entries.map(({ key }) => key),
+ values: entries.map(({ value }) => value),
+ })),
+ ),
+ }),
+ )
+ .map(({ type, payload }) => makeTagged(type, payload)),
+ ),
+ };
+});
+
+/**
+ * A factory for arbitrary passables
+ */
+export const arbPassable = arbDag.map(x => harden(x));

fix: tools arb passable (#1291)