diff --git a/packages/marshal/index.js b/packages/marshal/index.js index f0fb439e5d6..db046deaa32 100644 --- a/packages/marshal/index.js +++ b/packages/marshal/index.js @@ -4,6 +4,7 @@ export { isObject, assertChecker, getTag, + hasOwnPropertyOf, } from './src/helpers/passStyle-helpers.js'; export { getErrorConstructor, toPassableError } from './src/helpers/error.js'; @@ -17,12 +18,7 @@ export { passableSymbolForName, } from './src/helpers/symbol.js'; -export { - passStyleOf, - assertPassable, - everyPassableChild, - somePassableChild, -} from './src/passStyleOf.js'; +export { passStyleOf, assertPassable } from './src/passStyleOf.js'; export { pureCopy, sameValueZero } from './src/pureCopy.js'; export { deeplyFulfilled } from './src/deeplyFulfilled.js'; diff --git a/packages/marshal/src/assertPassStyleOf.js b/packages/marshal/src/assertPassStyleOf.js index e10978d9388..df2503d8882 100644 --- a/packages/marshal/src/assertPassStyleOf.js +++ b/packages/marshal/src/assertPassStyleOf.js @@ -4,19 +4,11 @@ import { passStyleOf } from './passStyleOf.js'; const { details: X, quote: q } = assert; -/** - * @typedef {Array} CopyArray - */ - -/** - * @typedef {Record} CopyRecord - */ - /** * Check whether the argument is a pass-by-copy array, AKA a "copyArray" * in @agoric/marshal terms * - * @param {CopyArray} array + * @param {CopyArray} array * @returns {boolean} */ const isCopyArray = array => passStyleOf(array) === 'copyArray'; @@ -26,7 +18,7 @@ harden(isCopyArray); * Check whether the argument is a pass-by-copy record, AKA a * "copyRecord" in @agoric/marshal terms * - * @param {CopyRecord} record + * @param {CopyRecord} record * @returns {boolean} */ const isRecord = record => passStyleOf(record) === 'copyRecord'; @@ -45,7 +37,7 @@ harden(isRemotable); * Assert that the argument is a pass-by-copy array, AKA a "copyArray" * in @agoric/marshal terms * - * @param {CopyArray} array + * @param {CopyArray} array * @param {string=} optNameOfArray * @returns {void} */ @@ -64,7 +56,7 @@ harden(assertCopyArray); * Assert that the argument is a pass-by-copy record, or a * "copyRecord" in @agoric/marshal terms * - * @param {CopyRecord} record + * @param {CopyRecord} record * @param {string=} optNameOfRecord * @returns {void} */ diff --git a/packages/marshal/src/helpers/copyArray.js b/packages/marshal/src/helpers/copyArray.js index e171eb3053e..6ba6c9b8175 100644 --- a/packages/marshal/src/helpers/copyArray.js +++ b/packages/marshal/src/helpers/copyArray.js @@ -44,11 +44,6 @@ export const CopyArrayHelper = harden({ TypeError, ); // Recursively validate that each member is passable. - CopyArrayHelper.every(candidate, v => !!passStyleOfRecur(v)); + candidate.every(v => !!passStyleOfRecur(v)); }, - - every: (passable, fn) => - // Note that we explicitly call `fn` with only the arguments we want - // to provide. - passable.every((v, i) => fn(v, i)), }); diff --git a/packages/marshal/src/helpers/copyRecord.js b/packages/marshal/src/helpers/copyRecord.js index 0ec733cbc72..36cef4d363b 100644 --- a/packages/marshal/src/helpers/copyRecord.js +++ b/packages/marshal/src/helpers/copyRecord.js @@ -18,7 +18,6 @@ const { ownKeys } = Reflect; const { getPrototypeOf, getOwnPropertyDescriptors, - entries, prototype: objectPrototype, } = Object; @@ -62,11 +61,6 @@ export const CopyRecordHelper = harden({ checkNormalProperty(candidate, name, 'string', true, assertChecker); } // Recursively validate that each member is passable. - CopyRecordHelper.every(candidate, v => !!passStyleOfRecur(v)); + Object.values(candidate).every(v => !!passStyleOfRecur(v)); }, - - every: (passable, fn) => - // Note that we explicitly call `fn` with only the arguments we want - // to provide. - entries(passable).every(([k, v]) => fn(v, k)), }); diff --git a/packages/marshal/src/helpers/error.js b/packages/marshal/src/helpers/error.js index 74c5fda88f8..2c122680958 100644 --- a/packages/marshal/src/helpers/error.js +++ b/packages/marshal/src/helpers/error.js @@ -95,8 +95,6 @@ export const ErrorHelper = harden({ assertValid: candidate => { ErrorHelper.canBeValid(candidate, assertChecker); }, - - every: (_passable, _fn) => true, }); /** diff --git a/packages/marshal/src/helpers/internal-types.js b/packages/marshal/src/helpers/internal-types.js index f39ae2cb2c4..dc95bf05a93 100644 --- a/packages/marshal/src/helpers/internal-types.js +++ b/packages/marshal/src/helpers/internal-types.js @@ -27,10 +27,4 @@ * @property {(candidate: any, * passStyleOfRecur: PassStyleOf * ) => void} assertValid - * - * @property {(passable: Passable, - * fn: (passable: Passable, index: any) => boolean - * ) => boolean} every - * For recuring through the nested passable structure. Like - * `Array.prototype.every`, return `false` to stop early. */ diff --git a/packages/marshal/src/helpers/tagged.js b/packages/marshal/src/helpers/tagged.js index 8b4ffd9f304..d589e3adb9f 100644 --- a/packages/marshal/src/helpers/tagged.js +++ b/packages/marshal/src/helpers/tagged.js @@ -51,8 +51,6 @@ export const TaggedHelper = harden({ checkNormalProperty(candidate, 'payload', 'string', true, assertChecker); // Recursively validate that each member is passable. - TaggedHelper.every(candidate, v => !!passStyleOfRecur(v)); + !!passStyleOfRecur(candidate.payload); }, - - every: (passable, fn) => fn(passable.payload, 'payload'), }); diff --git a/packages/marshal/src/passStyleOf.js b/packages/marshal/src/passStyleOf.js index 17e7c80940c..bd56b0e6e54 100644 --- a/packages/marshal/src/passStyleOf.js +++ b/packages/marshal/src/passStyleOf.js @@ -28,9 +28,9 @@ const { isFrozen } = Object; * does what it is supposed to do. `makePassStyleOf` is not trying to defend * itself against malicious helpers, though it does defend against some * accidents. - * @returns {{passStyleOf: PassStyleOf, HelperTable: any}} + * @returns {PassStyleOf} */ -const makePassStyleOfKit = passStyleHelpers => { +const makePassStyleOf = passStyleHelpers => { const HelperTable = { __proto__: null, copyArray: undefined, @@ -179,35 +179,18 @@ const makePassStyleOfKit = passStyleHelpers => { return passStyleOfRecur(passable); }; - return harden({ passStyleOf, HelperTable }); + return harden(passStyleOf); }; -const { passStyleOf, HelperTable } = makePassStyleOfKit([ +export const passStyleOf = makePassStyleOf([ CopyArrayHelper, CopyRecordHelper, TaggedHelper, RemotableHelper, ErrorHelper, ]); -export { passStyleOf }; export const assertPassable = val => { passStyleOf(val); // throws if val is not a passable }; harden(assertPassable); - -export const everyPassableChild = (passable, fn) => { - const passStyle = passStyleOf(passable); - const helper = HelperTable[passStyle]; - if (helper) { - // everyPassable guards .every so that each helper only gets a - // genuine passable of its own flavor. - return helper.every(passable, fn); - } - return true; -}; -harden(everyPassableChild); - -export const somePassableChild = (passable, fn) => - !everyPassableChild(passable, (v, i) => !fn(v, i)); -harden(somePassableChild); diff --git a/packages/marshal/src/types.js b/packages/marshal/src/types.js index bce223ab605..38ec3aaa1fe 100644 --- a/packages/marshal/src/types.js +++ b/packages/marshal/src/types.js @@ -97,6 +97,16 @@ * The leaves of a Passable's pass-by-copy superstructure. */ +/** + * @template T + * @typedef {T[]} CopyArray + */ + +/** + * @template T + * @typedef {Record} CopyRecord + */ + /** * @typedef {{ * [PASS_STYLE]: 'tagged', diff --git a/packages/store/src/index.js b/packages/store/src/index.js index d4816994ade..07bf1165a24 100644 --- a/packages/store/src/index.js +++ b/packages/store/src/index.js @@ -3,8 +3,14 @@ export { isKey, assertKey } from './keys/checkKey.js'; export { keyLT, keyLTE, keyEQ, keyGTE, keyGT } from './keys/compareKeys.js'; -export { makePatternKit } from './patterns/patternMatchers.js'; -export { compareRank } from './patterns/rankOrder.js'; +export { + M, + isPattern, + assertPattern, + matches, + fit, +} from './patterns/patternMatchers.js'; +// export { compareRank } from './patterns/rankOrder.js'; export { makeScalarWeakSetStore } from './stores/scalarWeakSetStore.js'; export { makeScalarSetStore } from './stores/scalarSetStore.js'; diff --git a/packages/store/src/keys/checkKey.js b/packages/store/src/keys/checkKey.js index 7c0444c007f..00bad0b57a8 100644 --- a/packages/store/src/keys/checkKey.js +++ b/packages/store/src/keys/checkKey.js @@ -6,7 +6,6 @@ import { assertChecker, assertPassable, - everyPassableChild, getTag, isObject, passStyleOf, @@ -94,10 +93,13 @@ const checkKeyInternal = (val, check = x => x) => { const passStyle = passStyleOf(val); switch (passStyle) { - case 'copyRecord': + case 'copyRecord': { + // A copyRecord is a key iff all its children are keys + return Object.values(val).every(checkIt); + } case 'copyArray': { - // A copyRecord or copyArray is a key iff all its children are keys - return everyPassableChild(val, checkIt); + // A copyArray is a key iff all its children are keys + return val.every(checkIt); } case 'tagged': { const tag = getTag(val); diff --git a/packages/store/src/keys/copyMap.js b/packages/store/src/keys/copyMap.js index 9a2078b6621..14eb39d9b13 100644 --- a/packages/store/src/keys/copyMap.js +++ b/packages/store/src/keys/copyMap.js @@ -6,7 +6,7 @@ import { makeTagged, passStyleOf, } from '@agoric/marshal'; -import { compareRank, sortByRank } from '../patterns/rankOrder.js'; +import { compareAntiRank, sortByRank } from '../patterns/rankOrder.js'; import { checkCopySetKeys } from './copySet.js'; // eslint-disable-next-line spaced-comment @@ -15,7 +15,7 @@ import { checkCopySetKeys } from './copySet.js'; const { details: X } = assert; const { ownKeys } = Reflect; -/** @type WeakSet */ +/** @type WeakSet> */ const copyMapMemo = new WeakSet(); /** @@ -63,30 +63,84 @@ export const assertCopyMap = m => checkCopyMap(m, assertChecker); harden(assertCopyMap); /** - * @param {CopyMap} m - * @param {(v: Passable, i: number) => boolean} fn - * @returns {boolean} + * @template K,V + * @param {CopyMap} m + * @returns {K[]} */ -export const everyCopyMapKey = (m, fn) => { +export const getCopyMapKeys = m => { assertCopyMap(m); - return m.payload.keys.every((v, i) => fn(v, i)); + return m.payload.keys; }; -harden(everyCopyMapKey); +harden(getCopyMapKeys); /** - * @param {CopyMap} m - * @param {(v: Passable, i: number) => boolean} fn - * @returns {boolean} + * @template K,V + * @param {CopyMap} m + * @returns {V[]} */ -export const everyCopyMapValue = (m, fn) => { +export const getCopyMapValues = m => { assertCopyMap(m); - return m.payload.values.every((v, i) => fn(v, i)); + return m.payload.values; }; +harden(getCopyMapValues); + +/** + * @template K,V + * @param {CopyMap} m + * @returns {Iterable<[K,V]>} + */ +export const getCopyMapEntries = m => { + assertCopyMap(m); + const { + payload: { keys, values }, + } = m; + const { length } = keys; + return harden({ + [Symbol.iterator]: () => { + let i = 0; + return harden({ + next: () => { + /** @type {IteratorResult<[K,V],void>} */ + let result; + if (i < length) { + result = harden({ done: false, value: [keys[i], values[i]] }); + i += 1; + return result; + } else { + result = harden({ done: true, value: undefined }); + } + return result; + }, + }); + }, + }); +}; +harden(getCopyMapEntries); + +/** + * @template K,V + * @param {CopyMap} m + * @param {(key: K, index: number) => boolean} fn + * @returns {boolean} + */ +export const everyCopyMapKey = (m, fn) => + getCopyMapKeys(m).every((key, index) => fn(key, index)); +harden(everyCopyMapKey); + +/** + * @template K,V + * @param {CopyMap} m + * @param {(value: V, index: number) => boolean} fn + * @returns {boolean} + */ +export const everyCopyMapValue = (m, fn) => + getCopyMapValues(m).every((value, index) => fn(value, index)); harden(everyCopyMapValue); /** - * @param {CopyMap} m - * @returns {CopySet} + * @template K,V + * @param {CopyMap} m + * @returns {CopySet} */ export const copyMapKeySet = m => // A copyMap's keys are already in the internal form used by copySets. @@ -94,18 +148,20 @@ export const copyMapKeySet = m => harden(copyMapKeySet); /** - * @param {Iterable<[Passable, Passable]>} entries - * @returns {CopyMap} + * @template K,V + * @param {Iterable<[K, V]>} entries + * @returns {CopyMap} */ export const makeCopyMap = entries => { // This is weird, but reverse rank sorting the entries is a good first step // for getting the rank sorted keys together with the values // organized by those keys. Also, among values associated with - // keys in the same equivalence class, those are rank sorted. This + // keys in the same equivalence class, those are rank sorted. + // TODO This // could solve the copyMap cover issue explained in patternMatchers.js. // But only if we include this criteria in our validation of copyMaps, // which we currently do not. - const sortedEntries = [...sortByRank(entries, compareRank)].reverse(); + const sortedEntries = sortByRank(entries, compareAntiRank); const keys = sortedEntries.map(([k, _v]) => k); const values = sortedEntries.map(([_k, v]) => v); return makeTagged('copyMap', { keys, values }); diff --git a/packages/store/src/keys/copySet.js b/packages/store/src/keys/copySet.js index 26fed92aeaf..05d8000f954 100644 --- a/packages/store/src/keys/copySet.js +++ b/packages/store/src/keys/copySet.js @@ -7,7 +7,7 @@ import { passStyleOf, } from '@agoric/marshal'; import { - compareRank, + compareAntiRank, isRankSorted, sortByRank, } from '../patterns/rankOrder.js'; @@ -29,8 +29,7 @@ export const checkCopySetKeys = (keys, check = x => x) => { X`The keys of a copySet or copyMap must be a copyArray: ${keys}`, ); } - const reverseKeys = harden([...keys].reverse()); - if (!isRankSorted(reverseKeys, compareRank)) { + if (!isRankSorted(keys, compareAntiRank)) { return check( false, X`The keys of a copySet or copyMap must be sorted in reverse rank order: ${keys}`, @@ -40,7 +39,7 @@ export const checkCopySetKeys = (keys, check = x => x) => { }; harden(checkCopySetKeys); -/** @type WeakSet */ +/** @type WeakSet> */ const copySetMemo = new WeakSet(); /** @@ -71,20 +70,31 @@ export const assertCopySet = s => checkCopySet(s, assertChecker); harden(assertCopySet); /** - * @param {CopySet} s - * @param {(v: Passable, i: number) => boolean} fn - * @returns {boolean} + * @template K + * @param {CopySet} s + * @returns {K[]} */ -export const everyCopySetKey = (s, fn) => { +export const getCopySetKeys = s => { assertCopySet(s); - return s.payload.every((v, i) => fn(v, i)); + return s.payload; }; +harden(getCopySetKeys); + +/** + * @template K + * @param {CopySet} s + * @param {(key: K, index: number) => boolean} fn + * @returns {boolean} + */ +export const everyCopySetKey = (s, fn) => + getCopySetKeys(s).every((key, index) => fn(key, index)); harden(everyCopySetKey); /** - * @param {Iterable} elements - * @returns {CopySet} + * @template K + * @param {Iterable} elements + * @returns {CopySet} */ export const makeCopySet = elements => - makeTagged('copySet', [...sortByRank(elements, compareRank)].reverse()); + makeTagged('copySet', sortByRank(elements, compareAntiRank)); harden(makeCopySet); diff --git a/packages/store/src/patterns/patternMatchers.js b/packages/store/src/patterns/patternMatchers.js index 58623f14477..001dcbde80e 100644 --- a/packages/store/src/patterns/patternMatchers.js +++ b/packages/store/src/patterns/patternMatchers.js @@ -2,13 +2,14 @@ import { assertChecker, - everyPassableChild, Far, getTag, makeTagged, passStyleOf, + hasOwnPropertyOf, } from '@agoric/marshal'; import { + compareAntiRank, compareRank, getPassStyleCover, intersectRankCovers, @@ -36,9 +37,9 @@ const { quote: q, details: X } = assert; const patternMemo = new WeakSet(); /** - * @returns {Object} + * @returns {PatternKit} */ -export const makePatternKit = () => { +const makePatternKit = () => { /** * If this is a recognized match tag, return the MatchHelper. * Otherwise result undefined. @@ -82,11 +83,15 @@ export const makePatternKit = () => { const passStyle = passStyleOf(patt); switch (passStyle) { - case 'copyRecord': + case 'copyRecord': { + // A copyRecord is a pattern iff all its children are + // patterns + return Object.values(patt).every(checkIt); + } case 'copyArray': { - // A copyRecord or copyArray is a pattern iff all its children are + // A copyArray is a pattern iff all its children are // patterns - return everyPassableChild(patt, checkIt); + return patt.every(checkIt); } case 'tagged': { const tag = getTag(patt); @@ -243,9 +248,10 @@ export const makePatternKit = () => { // logic can assume patterns that are not key. return check( keyEQ(specimen, patt), - X`${specimen} - Must be equivalent to the literal pattern: ${patt}`, + X`${specimen} - Must be equivalent to: ${patt}`, ); } + assertPattern(patt); const specStyle = passStyleOf(specimen); const pattStyle = passStyleOf(patt); switch (pattStyle) { @@ -260,12 +266,10 @@ export const makePatternKit = () => { if (specimen.length !== length) { return check( false, - X`Array ${specimen} - must be as long as copyArray pattern: ${patt}`, + X`Array ${specimen} - Must be as long as copyArray pattern: ${patt}`, ); } - return everyPassableChild(patt, (p, i) => - checkMatches(specimen[i], p, check), - ); + return patt.every((p, i) => checkMatches(specimen[i], p, check)); } case 'copyRecord': { if (specStyle !== 'copyRecord') { @@ -274,16 +278,8 @@ export const makePatternKit = () => { X`${specimen} - Must be a copyRecord to match a copyRecord pattern: ${patt}`, ); } - const specNames = harden( - ownKeys(specimen) - .sort() - .reverse(), - ); - const pattNames = harden( - ownKeys(patt) - .sort() - .reverse(), - ); + const specNames = harden(ownKeys(specimen).sort(compareAntiRank)); + const pattNames = harden(ownKeys(patt).sort(compareAntiRank)); if (!keyEQ(specNames, pattNames)) { return check( false, @@ -369,7 +365,7 @@ export const makePatternKit = () => { * @param {Passable} specimen * @param {Pattern} patt */ - const assertMatches = (specimen, patt) => { + const fit = (specimen, patt) => { checkMatches(specimen, patt, assertChecker); }; @@ -379,20 +375,29 @@ export const makePatternKit = () => { const getRankCover = (patt, encodeKey) => { if (isKey(patt)) { const encoded = encodeKey(patt); - return [encoded, `${encoded}~`]; + if (encoded !== undefined) { + return [encoded, `${encoded}~`]; + } } const passStyle = passStyleOf(patt); switch (passStyle) { case 'copyArray': { - const rankCovers = patt.map(p => getRankCover(p, encodeKey)); - return harden([ - rankCovers.map(([left, _right]) => left), - rankCovers.map(([_left, right]) => right), - ]); + // XXX this doesn't get along with the world of cover === pair of + // strings. In the meantime, fall through to the default which + // returns a cover that covers all copyArrays. + // + // const rankCovers = patt.map(p => getRankCover(p, encodeKey)); + // return harden([ + // rankCovers.map(([left, _right]) => left), + // rankCovers.map(([_left, right]) => right), + // ]); + break; } case 'copyRecord': { // XXX this doesn't get along with the world of cover === pair of - // strings + // strings. In the meantime, fall through to the default which + // returns a cover that covers all copyRecords. + // // const pattKeys = ownKeys(patt); // const pattEntries = harden(pattKeys.map(key => [key, patt[key]])); // const [leftEntriesLimit, rightEntriesLimit] = @@ -401,18 +406,22 @@ export const makePatternKit = () => { // fromEntries(leftEntriesLimit), // fromEntries(rightEntriesLimit), // ]); - assert.fail('not supporting copyRecord patterns yet'); // XXX TEMP + break; } case 'tagged': { const tag = getTag(patt); const matchHelper = maybeMatchHelper(tag); if (matchHelper) { + // Buried here is the important case, where we process + // the various patternNodes return matchHelper.getRankCover(patt.payload, encodeKey); } switch (tag) { case 'copySet': { // XXX this doesn't get along with the world of cover === pair of - // strings + // strings. In the meantime, fall through to the default which + // returns a cover that covers all copySets. + // // // Should already be validated by checkPattern. But because this // // is a check that may loosen over time, we also assert // // everywhere we still rely on the restriction. @@ -428,31 +437,36 @@ export const makePatternKit = () => { // makeCopySet([leftElementLimit]), // makeCopySet([rightElementLimit]), // ]); - assert.fail('not supporting copySet patterns yet'); // XXX TEMP + break; } case 'copyMap': { - // A matching copyMap must have the same keys, or at most one - // non-key key pattern. Thus we can assume that value positions - // match 1-to-1. - // - // TODO I may be overlooking that the less precise rankOrder - // equivalence class may cause values to be out of order, - // making this rankCover not actually cover. In that case, for - // all the values for keys at the same rank, we should union their - // rank covers. TODO POSSIBLE SILENT CORRECTNESS BUG + // XXX this doesn't get along with the world of cover === pair of + // strings. In the meantime, fall through to the default which + // returns a cover that covers all copyMaps. // - // If this is a bug, it probably affects the getRankCover - // cases if matchLTEHelper and matchGTEHelper on copyMap as - // well. See makeCopyMap for an idea on fixing - // this bug. - const [leftPayloadLimit, rightPayloadLimit] = getRankCover( - patt.payload, - encodeKey, - ); - return harden([ - makeTagged('copyMap', leftPayloadLimit), - makeTagged('copyMap', rightPayloadLimit), - ]); + // // A matching copyMap must have the same keys, or at most one + // // non-key key pattern. Thus we can assume that value positions + // // match 1-to-1. + // // + // // TODO I may be overlooking that the less precise rankOrder + // // equivalence class may cause values to be out of order, + // // making this rankCover not actually cover. In that case, for + // // all the values for keys at the same rank, we should union their + // // rank covers. TODO POSSIBLE SILENT CORRECTNESS BUG + // // + // // If this is a bug, it probably affects the getRankCover + // // cases of matchLTEHelper and matchGTEHelper on copyMap as + // // well. See makeCopyMap for an idea on fixing + // // this bug. + // const [leftPayloadLimit, rightPayloadLimit] = getRankCover( + // patt.payload, + // encodeKey, + // ); + // return harden([ + // makeTagged('copyMap', leftPayloadLimit), + // makeTagged('copyMap', rightPayloadLimit), + // ]); + break; } default: { break; // fall through to default @@ -471,114 +485,35 @@ export const makePatternKit = () => { /** @type {MatchHelper} */ const matchAnyHelper = Far('M.any helper', { - checkIsMatcherPayload: (matcherPayload, check = x => x) => - check( - matcherPayload === undefined, - X`An M.any matcher's .payload must be undefined: ${matcherPayload}`, - ), - checkMatches: (_specimen, _matcherPayload, _check = x => x) => true, - getRankCover: (_matchPayload, _encodeKey) => ['', '{'], - - checkKeyPattern: (_matcherPayload, _check = x => x) => true, - }); - - /** @type {MatchHelper} */ - const matchScalarHelper = Far('M.scalar helper', { checkIsMatcherPayload: (matcherPayload, check = x => x) => check( matcherPayload === undefined, - X`An M.scalar matcher's .payload must be undefined: ${matcherPayload}`, + X`Payload must be undefined: ${matcherPayload}`, ), - checkMatches: (specimen, _matcherPayload, check = x => x) => - checkScalarKey(specimen, check), - - getRankCover: (_matchPayload, _encodeKey) => ['a', 'z~'], + getRankCover: (_matchPayload, _encodeKey) => ['', '{'], checkKeyPattern: (_matcherPayload, _check = x => x) => true, }); /** @type {MatchHelper} */ - const matchKindHelper = Far('M.kind helper', { - checkIsMatcherPayload: (allegedKeyKind, check = x => x) => - check( - // We cannot further restrict this to only possible passStyles - // or tags, because we wish to allow matching of tags that we - // don't know ahead of time. Do we need to separate the namespaces? - // TODO are we asking for trouble by lumping passStyles and tags - // together into kinds? - typeof allegedKeyKind === 'string', - X`A kind name must be a string: ${allegedKeyKind}`, - ), - - checkMatches: (specimen, kind, check = x => x) => - check( - passStyleOf(specimen) === kind || - (passStyleOf(specimen) === 'tagged' && getTag(specimen) === kind), - X`${specimen} - Must have passStyle or tag ${q(kind)}`, - ), - - getRankCover: (kind, _encodeKey) => { - switch (kind) { - case 'copySet': { - // The bounds in the cover are not valid copySets, which is fine. - // They only need to be valid copyTagged that bound all possible - // copySets. Thus, we need to call makeTagged directly, rather - // than using makeCopySet. - return [ - makeTagged('copySet', null), - makeTagged('copySet', undefined), - ]; - } - case 'copyMap': { - // The bounds in the cover are not valid copyMaps, which is fine. - // They only need to be valid copyTagged that bound all possible - // copyMaps. - return [ - makeTagged('copyMap', null), - makeTagged('copyMap', undefined), - ]; - } - default: { - return getPassStyleCover(/** @type {PassStyle} */ (kind)); - } - } - }, - - checkKeyPattern: (kind, check = x => x) => { - switch (kind) { - case 'boolean': - case 'number': - case 'bigint': - case 'string': - case 'symbol': - case 'remotable': - case 'undefined': - return true; - default: - return check(false, X`${kind} keys are not supported`); - } + const matchAndHelper = Far('match:and helper', { + checkMatches: (specimen, patts, check = x => x) => { + return patts.every(patt => checkMatches(specimen, patt, check)); }, - }); - /** @type {MatchHelper} */ - const matchAndHelper = Far('match:and helper', { checkIsMatcherPayload: (allegedPatts, check = x => x) => { const checkIt = patt => checkPattern(patt, check); return ( (check( passStyleOf(allegedPatts) === 'copyArray', X`Needs array of sub-patterns: ${allegedPatts}`, - ) && everyPassableChild(allegedPatts, checkIt)) + ) && allegedPatts.every(checkIt)) ); }, - checkMatches: (specimen, patts, check = x => x) => { - return patts.every(patt => checkMatches(specimen, patt, check)); - }, - getRankCover: (patts, encodeKey) => intersectRankCovers( compareRank, @@ -592,17 +527,22 @@ export const makePatternKit = () => { /** @type {MatchHelper} */ const matchOrHelper = Far('match:or helper', { - checkIsMatcherPayload: matchAndHelper.checkIsMatcherPayload, - checkMatches: (specimen, patts, check = x => x) => { - return ( - (check( - patts.length >= 1, + const { length } = patts; + if (length === 0) { + return check( + false, X`${specimen} - no pattern disjuncts to match: ${patts}`, - ) && !patts.every(patt => !checkMatches(specimen, patt, check))) - ); + ); + } + if (patts.some(patt => matches(specimen, patt))) { + return true; + } + return check(false, X`${specimen} - Must match one of ${patts}`); }, + checkIsMatcherPayload: matchAndHelper.checkIsMatcherPayload, + getRankCover: (patts, encodeKey) => unionRankCovers( compareRank, @@ -616,8 +556,6 @@ export const makePatternKit = () => { /** @type {MatchHelper} */ const matchNotHelper = Far('match:not helper', { - checkIsMatcherPayload: checkPattern, - checkMatches: (specimen, patt, check = x => x) => { if (matches(specimen, patt)) { return check( @@ -629,74 +567,122 @@ export const makePatternKit = () => { } }, + checkIsMatcherPayload: checkPattern, + getRankCover: (_patt, _encodeKey) => ['', '{'], checkKeyPattern: (patt, check = x => x) => checkKeyPattern(patt, check), }); /** @type {MatchHelper} */ - const matchLTEHelper = Far('match:lte helper', { - checkIsMatcherPayload: checkKey, + const matchScalarHelper = Far('M.scalar helper', { + checkMatches: (specimen, _matcherPayload, check = x => x) => + checkScalarKey(specimen, check), + + checkIsMatcherPayload: matchAnyHelper.checkIsMatcherPayload, + + getRankCover: (_matchPayload, _encodeKey) => ['a', 'z~'], + + checkKeyPattern: (_matcherPayload, _check = x => x) => true, + }); + + /** @type {MatchHelper} */ + const matchKeyHelper = Far('M.key helper', { + checkMatches: (specimen, _matcherPayload, check = x => x) => + checkKey(specimen, check), + + checkIsMatcherPayload: matchAnyHelper.checkIsMatcherPayload, + + getRankCover: (_matchPayload, _encodeKey) => ['a', 'z~'], + + checkKeyPattern: (_matcherPayload, _check = x => x) => true, + }); + + /** @type {MatchHelper} */ + const matchPatternHelper = Far('M.pattern helper', { + checkMatches: (specimen, _matcherPayload, check = x => x) => + checkPattern(specimen, check), + + checkIsMatcherPayload: matchAnyHelper.checkIsMatcherPayload, + + getRankCover: (_matchPayload, _encodeKey) => ['a', 'z~'], + + checkKeyPattern: (_matcherPayload, _check = x => x) => true, + }); + + /** @type {MatchHelper} */ + const matchKindHelper = Far('M.kind helper', { + checkMatches: (specimen, kind, check = x => x) => + check( + passStyleOf(specimen) === kind || + (passStyleOf(specimen) === 'tagged' && getTag(specimen) === kind), + X`${specimen} - Must have passStyle or tag ${q(kind)}`, + ), + + checkIsMatcherPayload: (allegedKeyKind, check = x => x) => + check( + // We cannot further restrict this to only possible passStyles + // or tags, because we wish to allow matching of tags that we + // don't know ahead of time. Do we need to separate the namespaces? + // TODO are we asking for trouble by lumping passStyles and tags + // together into kinds? + typeof allegedKeyKind === 'string', + X`A kind name must be a string: ${allegedKeyKind}`, + ), + + getRankCover: (kind, _encodeKey) => { + let style; + switch (kind) { + case 'copySet': + case 'copyMap': { + style = 'tagged'; + break; + } + default: { + style = kind; + break; + } + } + return getPassStyleCover(style); + }, + checkKeyPattern: (kind, check = x => x) => { + switch (kind) { + case 'boolean': + case 'number': + case 'bigint': + case 'string': + case 'symbol': + case 'remotable': + case 'undefined': + return true; + default: + return check(false, X`${kind} keys are not supported`); + } + }, + }); + + /** @type {MatchHelper} */ + const matchLTEHelper = Far('match:lte helper', { checkMatches: (specimen, rightOperand, check = x => x) => check( keyLTE(specimen, rightOperand), X`${specimen} - Must be <= ${rightOperand}`, ), + checkIsMatcherPayload: checkKey, + getRankCover: (rightOperand, encodeKey) => { const passStyle = passStyleOf(rightOperand); // The prefer-const makes no sense when some of the variables need // to be `let` // eslint-disable-next-line prefer-const - let [leftBound, _rightBound] = getPassStyleCover(passStyle); - switch (passStyle) { - case 'number': { - if (Number.isNaN(rightOperand)) { - // leftBound = NaN; - leftBound = 'f'; // XXX BOGUS - } - break; - } - case 'copyRecord': { - // XXX this doesn't get along with the world of cover === pair of - // strings - // leftBound = harden( - // fromEntries(entries(rightOperand).map(([k, _v]) => [k, null])), - // ); - break; - } - case 'tagged': { - leftBound = makeTagged(getTag(rightOperand), null); - switch (getTag(rightOperand)) { - case 'copyMap': { - const { keys } = rightOperand.payload; - const values = keys.map(_ => null); - // See note in getRankCover for copyMap about why we - // may need to take variable values orders into account - // to be correct. - leftBound = makeTagged('copyMap', harden({ keys, values })); - break; - } - default: { - break; - } - } - break; - } - case 'remotable': { - // This does not make for a tighter rankCover, but if an - // underlying table internally further optimizes, for example with - // an identityHash of a virtual object, then this might - // help it take advantage of that. - leftBound = encodeKey(rightOperand); - break; - } - default: { - break; - } + let [leftBound, rightBound] = getPassStyleCover(passStyle); + const newRightBound = `${encodeKey(rightOperand)}~`; + if (newRightBound !== undefined) { + rightBound = newRightBound; } - return [leftBound, `${encodeKey(rightOperand)}~`]; + return [leftBound, rightBound]; }, checkKeyPattern: (rightOperand, check = x => x) => @@ -705,14 +691,14 @@ export const makePatternKit = () => { /** @type {MatchHelper} */ const matchLTHelper = Far('match:lt helper', { - checkIsMatcherPayload: checkKey, - checkMatches: (specimen, rightOperand, check = x => x) => check( keyLT(specimen, rightOperand), X`${specimen} - Must be < ${rightOperand}`, ), + checkIsMatcherPayload: checkKey, + getRankCover: matchLTEHelper.getRankCover, checkKeyPattern: (rightOperand, check = x => x) => @@ -721,72 +707,25 @@ export const makePatternKit = () => { /** @type {MatchHelper} */ const matchGTEHelper = Far('match:gte helper', { - checkIsMatcherPayload: checkKey, - checkMatches: (specimen, rightOperand, check = x => x) => check( keyGTE(specimen, rightOperand), X`${specimen} - Must be >= ${rightOperand}`, ), + checkIsMatcherPayload: checkKey, + getRankCover: (rightOperand, encodeKey) => { const passStyle = passStyleOf(rightOperand); // The prefer-const makes no sense when some of the variables need // to be `let` // eslint-disable-next-line prefer-const - let [_leftBound, rightBound] = getPassStyleCover(passStyle); - switch (passStyle) { - case 'number': { - if (Number.isNaN(rightOperand)) { - // rightBound = NaN; - rightBound = 'f'; - } else { - // rightBound = Infinity; - rightBound = 'f~'; - } - break; - } - case 'copyRecord': { - // XXX this doesn't get along with the world of cover === pair of - // strings - // rightBound = harden( - // fromEntries( - // entries(rightOperand).map(([k, _v]) => [k, undefined]), - // ), - // ); - break; - } - case 'tagged': { - rightBound = makeTagged(getTag(rightOperand), undefined); - switch (getTag(rightOperand)) { - case 'copyMap': { - const { keys } = rightOperand.payload; - const values = keys.map(_ => undefined); - // See note in getRankCover for copyMap about why we - // may need to take variable values orders into account - // to be correct. - rightBound = makeTagged('copyMap', harden({ keys, values })); - break; - } - default: { - break; - } - } - break; - } - case 'remotable': { - // This does not make for a tighter rankCover, but if an - // underlying table internally further optimizes, for example with - // an identityHash of a virtual object, then this might - // help it take advantage of that. - rightBound = encodeKey(rightOperand); - break; - } - default: { - break; - } + let [leftBound, rightBound] = getPassStyleCover(passStyle); + const newLeftBound = encodeKey(rightOperand); + if (newLeftBound !== undefined) { + leftBound = newLeftBound; } - return [encodeKey(rightOperand), rightBound]; + return [leftBound, rightBound]; }, checkKeyPattern: (rightOperand, check = x => x) => @@ -795,8 +734,6 @@ export const makePatternKit = () => { /** @type {MatchHelper} */ const matchGTHelper = Far('match:gt helper', { - getMatchTag: () => 'gt', - checkMatches: (specimen, rightOperand, check = x => x) => check( keyGT(specimen, rightOperand), @@ -811,78 +748,308 @@ export const makePatternKit = () => { checkKeyPattern(rightOperand, check), }); + /** @type {MatchHelper} */ + const matchArrayOfHelper = Far('match:arrayOf helper', { + checkMatches: (specimen, subPatt, check = x => x) => + check( + passStyleOf(specimen) === 'copyArray', + X`${specimen} - Must be an array`, + ) && specimen.every(el => checkMatches(el, subPatt, check)), + + checkIsMatcherPayload: checkPattern, + + getRankCover: () => getPassStyleCover('copyArray'), + + checkKeyPattern: (_, check = x => x) => + check(false, X`Arrays not yet supported as keys`), + }); + + /** @type {MatchHelper} */ + const matchRecordOfHelper = Far('match:recordOf helper', { + checkMatches: (specimen, entryPatt, check = x => x) => + check( + passStyleOf(specimen) === 'copyRecord', + X`${specimen} - Must be a record`, + ) && + Object.entries(specimen).every(el => + checkMatches(harden(el), entryPatt, check), + ), + + checkIsMatcherPayload: (entryPatt, check = x => x) => + check( + passStyleOf(entryPatt) === 'copyArray' && entryPatt.length === 2, + X`${entryPatt} - Must be an pair of patterns`, + ) && checkPattern(entryPatt, check), + + getRankCover: _entryPatt => getPassStyleCover('copyRecord'), + + checkKeyPattern: (_entryPatt, check = x => x) => + check(false, X`Records not yet supported as keys`), + }); + + /** @type {MatchHelper} */ + const matchSetOfHelper = Far('match:setOf helper', { + checkMatches: (specimen, keyPatt, check = x => x) => + check( + passStyleOf(specimen) === 'tagged' && getTag(specimen) === 'copySet', + X`${specimen} - Must be a a CopySet`, + ) && specimen.payload.every(el => checkMatches(el, keyPatt)), + + checkIsMatcherPayload: checkPattern, + + getRankCover: () => getPassStyleCover('tagged'), + + checkKeyPattern: (_, check = x => x) => + check(false, X`CopySets not yet supported as keys`), + }); + + /** @type {MatchHelper} */ + const matchMapOfHelper = Far('match:mapOf helper', { + checkMatches: (specimen, [keyPatt, valuePatt], check = x => x) => + check( + passStyleOf(specimen) === 'tagged' && getTag(specimen) === 'copyMap', + X`${specimen} - Must be a CopyMap`, + ) && + specimen.payload.keys.every(k => checkMatches(k, keyPatt, check)) && + specimen.payload.values.every(v => checkMatches(v, valuePatt, check)), + + checkIsMatcherPayload: (entryPatt, check = x => x) => + check( + passStyleOf(entryPatt) === 'copyArray' && entryPatt.length === 2, + X`${entryPatt} - Must be an pair of patterns`, + ) && checkPattern(entryPatt, check), + + getRankCover: _entryPatt => getPassStyleCover('tagged'), + + checkKeyPattern: (_entryPatt, check = x => x) => + check(false, X`CopyMap not yet supported as keys`), + }); + + /** @type {MatchHelper} */ + const matchSplitHelper = Far('match:split helper', { + checkMatches: (specimen, [base, rest = undefined], check = x => x) => { + const specimenStyle = passStyleOf(specimen); + const baseStyle = passStyleOf(base); + if (specimenStyle !== baseStyle) { + return check( + false, + X`${specimen} - Must have shape of base: ${q(baseStyle)}`, + ); + } + let specB; + let specR; + if (baseStyle === 'copyArray') { + const { length: baseLen } = base; + // Frozen below + specB = specimen.slice(0, baseLen); + specR = specimen.slice(baseLen); + } else { + assert(baseStyle === 'copyRecord'); + // Not yet frozen! Mutated in place + specB = {}; + specR = {}; + for (const [name, value] of Object.entries(specimen)) { + if (hasOwnPropertyOf(base, name)) { + specB[name] = value; + } else { + specR[name] = value; + } + } + } + harden(specB); + harden(specR); + return ( + (checkMatches(specB, base, check) && + (rest === undefined || checkMatches(specR, rest, check))) + ); + }, + + checkIsMatcherPayload: (splitArgs, check = x => x) => { + if ( + passStyleOf(splitArgs) === 'copyArray' && + (splitArgs.length === 1 || splitArgs.length === 2) + ) { + const [base, rest = undefined] = splitArgs; + const baseStyle = passStyleOf(base); + if ( + isPattern(base) && + (baseStyle === 'copyArray' || baseStyle === 'copyRecord') && + (rest === undefined || isPattern(rest)) + ) { + return true; + } + } + return check( + false, + X`Must be an array of a base structure and an optional rest pattern: ${splitArgs}`, + ); + }, + + getRankCover: ([base, _rest = undefined]) => + getPassStyleCover(passStyleOf(base)), + + checkKeyPattern: ([base, _rest = undefined], check = x => x) => + check(false, X`${q(passStyleOf(base))} not yet supported as keys`), + }); + + /** @type {MatchHelper} */ + const matchPartialHelper = Far('match:partial helper', { + checkMatches: (specimen, [base, rest = undefined], check = x => x) => { + const specimenStyle = passStyleOf(specimen); + const baseStyle = passStyleOf(base); + if (specimenStyle !== baseStyle) { + return check( + false, + X`${specimen} - Must have shape of base: ${q(baseStyle)}`, + ); + } + let specB; + let specR; + let newBase = base; + if (baseStyle === 'copyArray') { + const { length: specLen } = specimen; + const { length: baseLen } = base; + if (specLen < baseLen) { + newBase = harden(base.slice(0, specLen)); + } + // Frozen below + specB = specimen.slice(0, baseLen); + specR = specimen.slice(baseLen); + } else { + assert(baseStyle === 'copyRecord'); + // Not yet frozen! Mutated in place + specB = {}; + specR = {}; + newBase = {}; + for (const [name, value] of Object.entries(specimen)) { + if (hasOwnPropertyOf(base, name)) { + specB[name] = value; + newBase[name] = base[name]; + } else { + specR[name] = value; + } + } + } + harden(specB); + harden(specR); + harden(newBase); + return ( + (checkMatches(specB, newBase, check) && + (rest === undefined || checkMatches(specR, rest, check))) + ); + }, + + checkIsMatcherPayload: matchSplitHelper.checkIsMatcherPayload, + + getRankCover: matchSplitHelper.getRankCover, + + checkKeyPattern: matchSplitHelper.checkKeyPattern, + }); + /** @type {Record} */ const HelpersByMatchTag = harden({ 'match:any': matchAnyHelper, - 'match:scalar': matchScalarHelper, - 'match:kind': matchKindHelper, 'match:and': matchAndHelper, 'match:or': matchOrHelper, 'match:not': matchNotHelper, + + 'match:scalar': matchScalarHelper, + 'match:key': matchKeyHelper, + 'match:pattern': matchPatternHelper, + 'match:kind': matchKindHelper, + 'match:lt': matchLTHelper, 'match:lte': matchLTEHelper, 'match:gte': matchGTEHelper, 'match:gt': matchGTHelper, + + 'match:arrayOf': matchArrayOfHelper, + 'match:recordOf': matchRecordOfHelper, + 'match:setOf': matchSetOfHelper, + 'match:mapOf': matchMapOfHelper, + 'match:split': matchSplitHelper, + 'match:partial': matchPartialHelper, }); - const patt = p => { - assertPattern(p); - return p; + const makeMatcher = (tag, payload) => { + const matcher = makeTagged(tag, payload); + assertPattern(matcher); + return matcher; }; + const makeKindMatcher = kind => makeMatcher('match:kind', kind); + + const theAnyPattern = makeMatcher('match:any', undefined); + const theScalarPattern = makeMatcher('match:scalar', undefined); + const theKeyPattern = makeMatcher('match:key', undefined); + const thePatternPattern = makeMatcher('match:pattern', undefined); + const theBooleanPattern = makeKindMatcher('boolean'); + const theNumberPattern = makeKindMatcher('number'); + const theBigintPattern = makeKindMatcher('bigint'); + const theNatPattern = makeMatcher('match:gte', 0n); + const theStringPattern = makeKindMatcher('string'); + const theSymbolPattern = makeKindMatcher('symbol'); + const theRecordPattern = makeKindMatcher('copyRecord'); + const theArrayPattern = makeKindMatcher('copyArray'); + const theSetPattern = makeKindMatcher('copySet'); + const theMapPattern = makeKindMatcher('copyMap'); + const theRemotablePattern = makeKindMatcher('remotable'); + const theErrorPattern = makeKindMatcher('error'); + const thePromisePattern = makeKindMatcher('promise'); + const theUndefinedPattern = makeKindMatcher('undefined'); + + /** @type {MatcherNamespace} */ const M = harden({ - any: () => patt(makeTagged('match:any', undefined)), - scalar: () => patt(makeTagged('match:scalar', undefined)), - and: (...patts) => patt(makeTagged('match:and', patts)), - or: (...patts) => patt(makeTagged('match:or', patts)), - not: subPatt => patt(makeTagged('match:not', subPatt)), - - kind: kind => patt(makeTagged('match:kind', kind)), - boolean: () => M.kind('boolean'), - number: () => M.kind('number'), - bigint: () => M.kind('bigint'), - string: () => M.kind('string'), - symbol: () => M.kind('symbol'), - record: () => M.kind('copyRecord'), - array: () => M.kind('copyArray'), - set: () => M.kind('copySet'), - map: () => M.kind('copyMap'), - remotable: () => M.kind('remotable'), - error: () => M.kind('error'), - promise: () => M.kind('promise'), - - /** - * All keys including `undefined` are already valid patterns and - * so can validly represent themselves. But optional pattern arguments - * `(pattern = undefined) => ...` - * cannot distinguish between `undefined` passed as a pattern vs - * omission of the argument. It will interpret the first as the - * second. Thus, when a passed pattern does not also need to be a key, - * we recommend passing `M.undefined()` instead of `undefined`. - */ - undefined: () => M.kind('undefined'), + any: () => theAnyPattern, + and: (...patts) => makeMatcher('match:and', patts), + or: (...patts) => makeMatcher('match:or', patts), + not: subPatt => makeMatcher('match:not', subPatt), + + scalar: () => theScalarPattern, + key: () => theKeyPattern, + pattern: () => thePatternPattern, + kind: makeKindMatcher, + boolean: () => theBooleanPattern, + number: () => theNumberPattern, + bigint: () => theBigintPattern, + nat: () => theNatPattern, + string: () => theStringPattern, + symbol: () => theSymbolPattern, + record: () => theRecordPattern, + array: () => theArrayPattern, + set: () => theSetPattern, + map: () => theMapPattern, + remotable: () => theRemotablePattern, + error: () => theErrorPattern, + promise: () => thePromisePattern, + undefined: () => theUndefinedPattern, null: () => null, - lt: rightSide => patt(makeTagged('match:lt', rightSide)), - lte: rightSide => patt(makeTagged('match:lte', rightSide)), + lt: rightOperand => makeMatcher('match:lt', rightOperand), + lte: rightOperand => makeMatcher('match:lte', rightOperand), eq: key => { assertKey(key); return key === undefined ? M.undefined() : key; }, neq: key => M.not(M.eq(key)), - gte: rightSide => patt(makeTagged('match:gte', rightSide)), - gt: rightSide => patt(makeTagged('match:gt', rightSide)), - - // TODO make more precise - arrayOf: _elementPatt => M.array(), - recordOf: _entryPatt => M.record(), - setOf: _elementPatt => M.set(), - mapOf: _entryPatt => M.map(), + gte: rightOperand => makeMatcher('match:gte', rightOperand), + gt: rightOperand => makeMatcher('match:gt', rightOperand), + + arrayOf: (subPatt = M.any()) => makeMatcher('match:arrayOf', subPatt), + recordOf: (keyPatt = M.any(), valuePatt = M.any()) => + makeMatcher('match:recordOf', [keyPatt, valuePatt]), + setOf: (keyPatt = M.any()) => makeMatcher('match:setOf', keyPatt), + mapOf: (keyPatt = M.any(), valuePatt = M.any()) => + makeMatcher('match:mapOf', [keyPatt, valuePatt]), + split: (base, rest = undefined) => + makeMatcher('match:split', rest === undefined ? [base] : [base, rest]), + partial: (base, rest = undefined) => + makeMatcher('match:partial', rest === undefined ? [base] : [base, rest]), }); return harden({ matches, - assertMatches, + fit, assertPattern, isPattern, assertKeyPattern, @@ -891,3 +1058,19 @@ export const makePatternKit = () => { M, }); }; + +// Only include those whose meaning is independent of an imputed sort order +// of remotables, or of encoding of passable as sortable strings. Thus, +// getRankCover is omitted. To get one, you'd need to instantiate +// `makePatternKit()` yourself. Since there are currently no external +// uses of `getRankCover`, for clarity during development, `makePatternKit` +// is not currently exported. +export const { + matches, + fit, + assertPattern, + isPattern, + assertKeyPattern, + isKeyPattern, + M, +} = makePatternKit(); diff --git a/packages/store/src/patterns/rankOrder.js b/packages/store/src/patterns/rankOrder.js index 274e5d41c15..5e046aec5d4 100644 --- a/packages/store/src/patterns/rankOrder.js +++ b/packages/store/src/patterns/rankOrder.js @@ -46,132 +46,158 @@ export const getPassStyleCover = passStyle => PassStyleRankAndCover[PassStyleRank[passStyle]][1]; harden(getPassStyleCover); -/** @type {CompareRank} */ -export const compareRank = (left, right) => { - if (sameValueZero(left, right)) { - return 0; - } - const leftStyle = passStyleOf(left); - const rightStyle = passStyleOf(right); - if (leftStyle !== rightStyle) { - return compareRank(PassStyleRank[leftStyle], PassStyleRank[rightStyle]); - } - switch (leftStyle) { - case 'undefined': - case 'null': - case 'remotable': - case 'error': - case 'promise': { - // For each of these passStyles, all members of that passStyle are tied - // for the same rank. +/** + * @type {WeakMap>} + */ +const memoOfSorted = new WeakMap(); + +/** + * @type {WeakMap} + */ +const comparatorMirrorImages = new WeakMap(); + +/** + * @param {CompareRank=} compareRemotables + * An option to create a comparator in which an internal order is + * assigned to remotables. This defaults to a comparator that + * always returns `0`, meaning that all remotables are tied + * for the same rank. + * @returns {ComparatorKit} + */ +const makeComparatorKit = (compareRemotables = (_x, _y) => 0) => { + /** @type {CompareRank} */ + const comparator = (left, right) => { + if (sameValueZero(left, right)) { return 0; } - case 'boolean': - case 'bigint': - case 'string': { - // Within each of these passStyles, the rank ordering agrees with - // JavaScript's relational operators `<` and `>`. - if (left < right) { - return -1; - } else { - assert(left > right); - return 1; - } - } - case 'symbol': { - return compareRank( - nameForPassableSymbol(left), - nameForPassableSymbol(right), - ); + const leftStyle = passStyleOf(left); + const rightStyle = passStyleOf(right); + if (leftStyle !== rightStyle) { + return comparator(PassStyleRank[leftStyle], PassStyleRank[rightStyle]); } - case 'number': { - // `NaN`'s rank is after all other numbers. - if (Number.isNaN(left)) { - assert(!Number.isNaN(right)); - return 1; - } else if (Number.isNaN(right)) { - return -1; + switch (leftStyle) { + case 'remotable': { + return compareRemotables(left, right); } - // The rank ordering of non-NaN numbers agrees with JavaScript's - // relational operators '<' and '>'. - if (left < right) { - return -1; - } else { - assert(left > right); - return 1; + case 'undefined': + case 'null': + case 'error': + case 'promise': { + // For each of these passStyles, all members of that passStyle are tied + // for the same rank. + return 0; } - } - case 'copyRecord': { - // Lexicographic by inverse sorted order of property names, then - // lexicographic by corresponding values in that same inverse - // order of their property names. Comparing names by themselves first, - // all records with the exact same set of property names sort next to - // each other in a rank-sort of copyRecords. - - // The copyRecord invariants enforced by passStyleOf ensure that - // all the property names are strings. We need the reverse sorted order - // of these names, which we then compare lexicographically. This ensures - // that if the names of record X are a subset of the names of record Y, - // then record X will have an earlier rank and sort to the left of Y. - const leftNames = harden( - ownKeys(left) - .sort() - // TODO Measure which is faster: a reverse sort by sorting and - // reversing, or by sorting with an inverse comparison function. - // If it makes a significant difference, use the faster one. - .reverse(), - ); - const rightNames = harden( - ownKeys(right) - .sort() - .reverse(), - ); - const result = compareRank(leftNames, rightNames); - if (result !== 0) { - return result; + case 'boolean': + case 'bigint': + case 'string': { + // Within each of these passStyles, the rank ordering agrees with + // JavaScript's relational operators `<` and `>`. + if (left < right) { + return -1; + } else { + assert(left > right); + return 1; + } } - const leftValues = harden(leftNames.map(name => left[name])); - const rightValues = harden(rightNames.map(name => right[name])); - return compareRank(leftValues, rightValues); - } - case 'copyArray': { - // Lexicographic - const len = Math.min(left.length, right.length); - for (let i = 0; i < len; i += 1) { - const result = compareRank(left[i], right[i]); + case 'symbol': { + return comparator( + nameForPassableSymbol(left), + nameForPassableSymbol(right), + ); + } + case 'number': { + // `NaN`'s rank is after all other numbers. + if (Number.isNaN(left)) { + assert(!Number.isNaN(right)); + return 1; + } else if (Number.isNaN(right)) { + return -1; + } + // The rank ordering of non-NaN numbers agrees with JavaScript's + // relational operators '<' and '>'. + if (left < right) { + return -1; + } else { + assert(left > right); + return 1; + } + } + case 'copyRecord': { + // Lexicographic by inverse sorted order of property names, then + // lexicographic by corresponding values in that same inverse + // order of their property names. Comparing names by themselves first, + // all records with the exact same set of property names sort next to + // each other in a rank-sort of copyRecords. + + // The copyRecord invariants enforced by passStyleOf ensure that + // all the property names are strings. We need the reverse sorted order + // of these names, which we then compare lexicographically. This ensures + // that if the names of record X are a subset of the names of record Y, + // then record X will have an earlier rank and sort to the left of Y. + const leftNames = harden( + ownKeys(left) + .sort() + // TODO Measure which is faster: a reverse sort by sorting and + // reversing, or by sorting with an inverse comparison function. + // If it makes a significant difference, use the faster one. + .reverse(), + ); + const rightNames = harden( + ownKeys(right) + .sort() + .reverse(), + ); + const result = comparator(leftNames, rightNames); if (result !== 0) { return result; } + const leftValues = harden(leftNames.map(name => left[name])); + const rightValues = harden(rightNames.map(name => right[name])); + return comparator(leftValues, rightValues); } - // If all matching elements were tied, then according to their lengths. - // If array X is a prefix of array Y, then X has an earlier rank than Y. - return compareRank(left.length, right.length); - } - case 'tagged': { - // Lexicographic by `[Symbol.toStringTag]` then `.payload`. - const labelComp = compareRank(getTag(left), getTag(right)); - if (labelComp !== 0) { - return labelComp; + case 'copyArray': { + // Lexicographic + const len = Math.min(left.length, right.length); + for (let i = 0; i < len; i += 1) { + const result = comparator(left[i], right[i]); + if (result !== 0) { + return result; + } + } + // If all matching elements were tied, then according to their lengths. + // If array X is a prefix of array Y, then X has an earlier rank than Y. + return comparator(left.length, right.length); + } + case 'tagged': { + // Lexicographic by `[Symbol.toStringTag]` then `.payload`. + const labelComp = comparator(getTag(left), getTag(right)); + if (labelComp !== 0) { + return labelComp; + } + return comparator(left.payload, right.payload); + } + default: { + assert.fail(X`Unrecognized passStyle: ${q(leftStyle)}`); } - return compareRank(left.payload, right.payload); - } - default: { - assert.fail(X`Unrecognized passStyle: ${q(leftStyle)}`); } - } -}; -harden(compareRank); + }; + + /** @type {CompareRank} */ + const antiComparator = (x, y) => comparator(y, x); -/** @type {CompareRank} */ -export const compareAntiRank = (x, y) => compareRank(y, x); + memoOfSorted.set(comparator, new WeakSet()); + memoOfSorted.set(antiComparator, new WeakSet()); + comparatorMirrorImages.set(comparator, antiComparator); + comparatorMirrorImages.set(antiComparator, comparator); + return harden({ comparator, antiComparator }); +}; /** - * @type {Map>} + * @param {CompareRank} comparator + * @returns {CompareRank=} */ -const memoOfSorted = new Map([ - [compareRank, new WeakSet()], - [compareAntiRank, new WeakSet()], -]); +export const comparatorMirrorImage = comparator => + comparatorMirrorImages.get(comparator); /** * @param {Passable[]} passables @@ -347,3 +373,51 @@ export const intersectRankCovers = (compare, covers) => { ]; return covers.reduce(intersectRankCoverPair, ['', '{']); }; + +export const { + comparator: compareRank, + antiComparator: compareAntiRank, +} = makeComparatorKit(); + +/** + * Create a comparator kit in which remotables are fully ordered + * by the order in which they are first seen by *this* comparator kit. + * BEWARE: This is observable mutable state, so such a comparator kit + * should never be shared among subsystems that should not be able + * to communicate. + * + * Note that this order does not meet the requirements for store + * ordering, since it has no memory of deleted keys. + * + * These full order comparator kit is strictly more precise that the + * rank order comparator kits above. As a result, any array which is + * sorted by such a full order will pass the isRankSorted test with + * a corresponding rank order. + * + * An array which is sorted by a *fresh* full order comparator, i.e., + * one that has not yet seen any remotables, will of course remain + * sorted by according to *that* full order comparator. An array *of + * scalars* sorted by a fresh full order will remain sorted even + * according to a new fresh full order comparator, since it will see + * the remotables in the same order again. Unfortunately, this is + * not true of arrays of passables in general. + * + * @returns {ComparatorKit} + */ +export const makeFullOrderComparatorKit = () => { + let numSeen = 0; + // Could be a WeakMap but would perform poorly. There are a dynamic + // number of these created, and each typically has a short lifetime. + const seen = new Map(); + const tag = r => { + if (seen.has(r)) { + return seen.get(r); + } + numSeen += 1; + seen.set(r, numSeen); + return numSeen; + }; + const compareRemotables = (x, y) => compareRank(tag(x), tag(y)); + return makeComparatorKit(compareRemotables); +}; +harden(makeFullOrderComparatorKit); diff --git a/packages/store/src/stores/scalarMapStore.js b/packages/store/src/stores/scalarMapStore.js index fbc04823d64..db19555985b 100644 --- a/packages/store/src/stores/scalarMapStore.js +++ b/packages/store/src/stores/scalarMapStore.js @@ -4,12 +4,11 @@ import { Far, assertPassable } from '@agoric/marshal'; import { compareRank } from '../patterns/rankOrder.js'; import { assertScalarKey } from '../keys/checkKey.js'; import { makeCopyMap } from '../keys/copyMap.js'; -import { makePatternKit } from '../patterns/patternMatchers.js'; +import { fit, assertPattern } from '../patterns/patternMatchers.js'; import { makeWeakMapStoreMethods } from './scalarWeakMapStore.js'; import { makeCursorKit } from './store-utils.js'; const { details: X, quote: q } = assert; -const { assertMatches, assertPattern } = makePatternKit(); /** * @template K,V @@ -110,7 +109,7 @@ export const makeScalarMapStore = ( assertScalarKey(key); assertPassable(value); if (schema) { - assertMatches(harden([key, value]), schema); + fit(harden([key, value]), schema); } }; const mapStore = Far(`scalar MapStore of ${q(keyName)}`, { diff --git a/packages/store/src/stores/scalarSetStore.js b/packages/store/src/stores/scalarSetStore.js index ee71ba03d69..9c5ade51368 100644 --- a/packages/store/src/stores/scalarSetStore.js +++ b/packages/store/src/stores/scalarSetStore.js @@ -4,12 +4,11 @@ import { Far } from '@agoric/marshal'; import { compareRank } from '../patterns/rankOrder.js'; import { assertScalarKey } from '../keys/checkKey.js'; import { makeCopySet } from '../keys/copySet.js'; -import { makePatternKit } from '../patterns/patternMatchers.js'; +import { fit, assertPattern } from '../patterns/patternMatchers.js'; import { makeWeakSetStoreMethods } from './scalarWeakSetStore.js'; import { makeCursorKit } from './store-utils.js'; const { details: X, quote: q } = assert; -const { assertMatches, assertPattern } = makePatternKit(); /** * @template K @@ -103,7 +102,7 @@ export const makeScalarSetStore = ( assertScalarKey(key); if (schema) { - assertMatches(key, schema); + fit(key, schema); } }; const setStore = Far(`scalar SetStore of ${q(keyName)}`, { diff --git a/packages/store/src/stores/scalarWeakMapStore.js b/packages/store/src/stores/scalarWeakMapStore.js index 522b7e4e761..6be0bbc00bf 100644 --- a/packages/store/src/stores/scalarWeakMapStore.js +++ b/packages/store/src/stores/scalarWeakMapStore.js @@ -1,10 +1,9 @@ // @ts-check import { Far, assertPassable, passStyleOf } from '@agoric/marshal'; -import { makePatternKit } from '../patterns/patternMatchers.js'; +import { fit, assertPattern } from '../patterns/patternMatchers.js'; const { details: X, quote: q } = assert; -const { assertMatches, assertPattern } = makePatternKit(); /** * @template K,V @@ -94,7 +93,7 @@ export const makeScalarWeakMapStore = ( ); assertPassable(value); if (schema) { - assertMatches(harden([key, value]), schema); + fit(harden([key, value]), schema); } }; const weakMapStore = Far(`scalar WeakMapStore of ${q(keyName)}`, { diff --git a/packages/store/src/stores/scalarWeakSetStore.js b/packages/store/src/stores/scalarWeakSetStore.js index 09a89720f33..95264695ba6 100644 --- a/packages/store/src/stores/scalarWeakSetStore.js +++ b/packages/store/src/stores/scalarWeakSetStore.js @@ -1,10 +1,9 @@ // @ts-check import { Far, passStyleOf } from '@agoric/marshal'; -import { makePatternKit } from '../patterns/patternMatchers.js'; +import { fit, assertPattern } from '../patterns/patternMatchers.js'; const { details: X, quote: q } = assert; -const { assertMatches, assertPattern } = makePatternKit(); /** * @template K @@ -78,7 +77,7 @@ export const makeScalarWeakSetStore = ( X`Only remotables can be keys of scalar WeakStores: ${key}`, ); if (schema) { - assertMatches(key, schema); + fit(key, schema); } }; const weakSetStore = Far(`scalar WeakSetStore of ${q(keyName)}`, { diff --git a/packages/store/src/stores/store-utils.js b/packages/store/src/stores/store-utils.js index 8c55a796899..4a2f9cda7cc 100644 --- a/packages/store/src/stores/store-utils.js +++ b/packages/store/src/stores/store-utils.js @@ -1,11 +1,10 @@ // @ts-check import { filterIterable } from '@agoric/marshal'; -import { makePatternKit } from '../patterns/patternMatchers.js'; +import { matches } from '../patterns/patternMatchers.js'; import { assertRankSorted } from '../patterns/rankOrder.js'; const { details: X, quote: q } = assert; -const { matches } = makePatternKit(); export const makeCursorKit = ( compare, diff --git a/packages/store/src/types.js b/packages/store/src/types.js index 0d7eb054eed..cce8501519c 100644 --- a/packages/store/src/types.js +++ b/packages/store/src/types.js @@ -13,10 +13,12 @@ */ /** + * @template K * @typedef {CopyTagged} CopySet */ /** + * @template K,V * @typedef {CopyTagged} CopyMap */ @@ -74,8 +76,8 @@ * @property {(key: K) => void} delete * Remove the key. Throws if not found. * @property {(keyPattern?: Pattern) => K[]} keys - * @property {(keyPattern?: Pattern) => CopySet} snapshot - * @property {(copySet: CopySet) => void} addAll + * @property {(keyPattern?: Pattern) => CopySet} snapshot + * @property {(copySet: CopySet) => void} addAll * @property {(keyPattern?: Pattern, * direction?: Direction * ) => Iterable} cursor @@ -118,8 +120,8 @@ * @property {(keyPattern?: Pattern) => K[]} keys * @property {(valuePattern?: Pattern) => V[]} values * @property {(entryPattern?: Pattern) => [K,V][]} entries - * @property {(entryPattern?: Pattern) => CopyMap} snapshot - * @property {(copyMap: CopyMap) => void} addAll + * @property {(entryPattern?: Pattern) => CopyMap} snapshot + * @property {(copyMap: CopyMap) => void} addAll * @property {(entryPattern?: Pattern, * direction?: Direction * ) => Iterable<[K,V]>} cursor @@ -213,6 +215,12 @@ * @returns {-1 | 0 | 1} */ +/** + * @typedef {Object} ComparatorKit + * @property {CompareRank} comparator + * @property {CompareRank} antiComparator + */ + // ///////////////////// Should be internal only types ///////////////////////// /** @@ -273,8 +281,11 @@ /** * @callback KeyToDBKey + * If this key can be encoded as a DBKey string which sorts correctly, + * return that string. Else return `undefined`. For example, a scalar-only + * encodeKey would return `undefined` for all non-scalar keys. * @param {Passable} key - * @returns {string} + * @returns {string=} */ /** @@ -289,6 +300,102 @@ * @property {GetRankCover} getRankCover */ +/** + * @typedef {Object} MatcherNamespace + * @property {() => Matcher} any + * Matches any passable + * @property {(...patts: Pattern[]) => Matcher} and + * Only if it matches all the sub-patterns + * @property {(...patts: Pattern[]) => Matcher} or + * Only if it matches at least one subPattern + * @property {(subPatt: Pattern) => Matcher} not + * Only if it does not match the sub-pattern + * + * @property {() => Matcher} scalar + * The scalars are the primitive values and Remotables. + * All scalars are keys. + * @property {() => Matcher} key + * Can be in a copySet or the key in a CopyMap. + * (Will eventually be able to a key is a MapStore.) + * All keys are patterns that match only themselves. + * @property {() => Matcher} pattern + * If it matches M.pattern(), the it is itself a pattern used + * to match other passables. A pattern cannot contain errors + * or promises, as these are not stable enough to usefully match. + * @property {(kind: string) => Matcher} kind + * @property {() => Matcher} boolean + * @property {() => Matcher} number Only floating point numbers + * @property {() => Matcher} bigint + * @property {() => Matcher} nat + * @property {() => Matcher} string + * @property {() => Matcher} symbol + * Only registered and well-known symbols are passable + * @property {() => Matcher} record A CopyRecord + * @property {() => Matcher} array A CopyArray + * @property {() => Matcher} set A CopySet + * @property {() => Matcher} map A CopyMap + * @property {() => Matcher} remotable A far object or its remote presence + * @property {() => Matcher} error + * Error objects are passable, but are neither keys nor symbols. + * They do not have a useful identity. + * @property {() => Matcher} promise + * Promises are passable, but are neither keys nor symbols. + * They do not have a useful identity. + * @property {() => Matcher} undefined + * All keys including `undefined` are already valid patterns and + * so can validly represent themselves. But optional pattern arguments + * `(pattern = undefined) => ...` + * cannot distinguish between `undefined` passed as a pattern vs + * omission of the argument. It will interpret the first as the + * second. Thus, when a passed pattern does not also need to be a key, + * we recommend passing `M.undefined()` instead of `undefined`. + * + * @property {() => null} null + * + * @property {(rightOperand :Key) => Matcher} lt + * Matches if < the right operand by compareKeys + * @property {(rightOperand :Key) => Matcher} lte + * Matches if <= the right operand by compareKeys + * @property {(key :Key) => Matcher} eq + * @property {(key :Key) => Matcher} neq + * @property {(rightOperand :Key) => Matcher} gte + * Matches if >= the right operand by compareKeys + * @property {(rightOperand :Key) => Matcher} gt + * Matches if > the right operand by compareKeys + * + * @property {(subPatt?: Pattern) => Matcher} arrayOf + * @property {(keyPatt?: Pattern, valuePatt?: Pattern) => Matcher} recordOf + * @property {(keyPatt?: Pattern) => Matcher} setOf + * @property {(keyPatt?: Pattern, valuePatt?: Pattern) => Matcher} mapOf + * @property {( + * base: CopyRecord<*> | CopyArray<*>, + * rest?: Pattern + * ) => Matcher} split + * An array or record is split into the first part that matches the + * base pattern, and the remainder, which matches against the optional + * rest pattern if present. + * @property {( + * base: CopyRecord<*> | CopyArray<*>, + * rest?: Pattern + * ) => Matcher} partial + * An array or record is split into the first part that matches the + * base pattern, and the remainder, which matches against the optional + * rest pattern if present. + * Unlike `M.split`, `M.partial` ignores properties on the base + * pattern that are not present on the specimen. + */ + +/** + * @typedef {Object} PatternKit + * @property {(specimen: Passable, patt: Pattern) => boolean} matches + * @property {(specimen: Passable, patt: Pattern) => void} fit + * @property {(patt: Pattern) => void} assertPattern + * @property {(patt: Passable) => boolean} isPattern + * @property {(patt: Pattern) => void} assertKeyPattern + * @property {(patt: Passable) => boolean} isKeyPattern + * @property {MatcherNamespace} M + */ + // ///////////////////////////////////////////////////////////////////////////// // TODO diff --git a/packages/store/test/test-patterns.js b/packages/store/test/test-patterns.js index 3def7f5d304..5f7c97a5cc6 100644 --- a/packages/store/test/test-patterns.js +++ b/packages/store/test/test-patterns.js @@ -3,9 +3,8 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; import { makeCopySet } from '../src/keys/copySet.js'; -import { makePatternKit } from '../src/patterns/patternMatchers.js'; - -const { assertMatches, matches, M } = makePatternKit(x => x); +import { fit, matches, M } from '../src/patterns/patternMatchers.js'; +import '../src/types.js'; /** * @typedef MatchTest @@ -29,20 +28,24 @@ const matchTests = harden([ M.and(3, 3), M.or(3, 4), M.and(), + + M.scalar(), + M.key(), + M.pattern(), ], noPatterns: [ - [4, /3 - Must be equivalent to the literal pattern: 4/], + [4, /3 - Must be equivalent to: 4/], [M.not(3), /3 - must fail negated pattern: 3/], [M.not(M.any()), /3 - must fail negated pattern: "\[match:any\]"/], [M.string(), /3 - Must have passStyle or tag "string"/], - [[3, 4], /3 - Must be equivalent to the literal pattern: \[3,4\]/], + [[3, 4], /3 - Must be equivalent to: \[3,4\]/], [M.gte(7), /3 - Must be >= 7/], [M.lte(2), /3 - Must be <= 2/], // incommensurate comparisons are neither <= nor >= [M.lte('x'), /3 - Must be <= "x"/], [M.gte('x'), /3 - Must be >= "x"/], - [M.and(3, 4), /3 - Must be equivalent to the literal pattern: 4/], - [M.or(4, 4), /3 - Must be equivalent to the literal pattern: 4/], + [M.and(3, 4), /3 - Must be equivalent to: 4/], + [M.or(4, 4), /3 - Must match one of \[4,4\]/], [M.or(), /3 - no pattern disjuncts to match: \[\]/], ], }, @@ -57,15 +60,38 @@ const matchTests = harden([ M.lte([4, 4]), M.gte([3]), M.lte([3, 4, 1]), + + M.split([3], [4]), + M.split([3]), + M.split([3], M.array()), + M.split([3, 4], []), + M.split([], [3, 4]), + + M.partial([3], [4]), + M.partial([3, 4, 5, 6]), + M.partial([3, 4, 5, 6], []), + + M.array(), + M.key(), + M.pattern(), ], noPatterns: [ - [[4, 3], /\[3,4\] - Must be equivalent to the literal pattern: \[4,3\]/], - [[3], /\[3,4\] - Must be equivalent to the literal pattern: \[3\]/], + [[4, 3], /\[3,4\] - Must be equivalent to: \[4,3\]/], + [[3], /\[3,4\] - Must be equivalent to: \[3\]/], [[M.string(), M.any()], /3 - Must have passStyle or tag "string"/], [M.lte([3, 3]), /\[3,4\] - Must be <= \[3,3\]/], [M.gte([4, 4]), /\[3,4\] - Must be >= \[4,4\]/], [M.lte([3]), /\[3,4\] - Must be <= \[3\]/], [M.gte([3, 4, 1]), /\[3,4\] - Must be >= \[3,4,1\]/], + + [M.split([3, 4, 5, 6]), /\[3,4\] - Must be equivalent to: \[3,4,5,6\]/], + [M.split([5]), /\[3\] - Must be equivalent to: \[5\]/], + [M.split({}), /\[3,4\] - Must have shape of base: "copyRecord"/], + [M.split([3], 'x'), /\[4\] - Must be equivalent to: "x"/], + + [M.partial([5]), /\[3\] - Must be equivalent to: \[5\]/], + + [M.scalar(), /A "copyArray" cannot be a scalar key: \[3,4\]/], ], }, { @@ -77,11 +103,26 @@ const matchTests = harden([ // Records compare pareto M.gte({ foo: 3, bar: 3 }), M.lte({ foo: 4, bar: 4 }), + + M.split({ foo: 3 }, { bar: 4 }), + M.split({ bar: 4 }, { foo: 3 }), + M.split({ foo: 3 }), + M.split({ foo: 3 }, M.record()), + M.split({}, { foo: 3, bar: 4 }), + M.split({ foo: 3, bar: 4 }, {}), + + M.partial({ zip: 5, zap: 6 }), + M.partial({ zip: 5, zap: 6 }, { foo: 3, bar: 4 }), + M.partial({ foo: 3, zip: 5 }, { bar: 4 }), + + M.record(), + M.key(), + M.pattern(), ], noPatterns: [ [ { foo: 4, bar: 3 }, - /{"foo":3,"bar":4} - Must be equivalent to the literal pattern: {"foo":4,"bar":3}/, + /{"foo":3,"bar":4} - Must be equivalent to: {"foo":4,"bar":3}/, ], [ { foo: M.string(), bar: M.any() }, @@ -109,6 +150,22 @@ const matchTests = harden([ ], [M.lte({ baz: 3 }), /{"foo":3,"bar":4} - Must be <= {"baz":3}/], [M.gte({ baz: 3 }), /{"foo":3,"bar":4} - Must be >= {"baz":3}/], + + [M.split([]), /{"foo":3,"bar":4} - Must have shape of base: "copyArray"/], + [ + M.split({ foo: 3, z: 4 }), + /{"foo":3} - Must be equivalent to: {"foo":3,"z":4}/, + ], + [ + M.split({ foo: 3 }, { foo: 3, bar: 4 }), + /{"bar":4} - Must be equivalent to: {"foo":3,"bar":4}/, + ], + [ + M.partial({ foo: 7, zip: 5 }, { bar: 4 }), + /{"foo":3} - Must be equivalent to: {"foo":7}/, + ], + + [M.scalar(), /A "copyRecord" cannot be a scalar key: {"foo":3,"bar":4}/], ], }, { @@ -119,13 +176,10 @@ const matchTests = harden([ M.lte(makeCopySet([3, 4, 5])), ], noPatterns: [ - [ - makeCopySet([]), - /"\[copySet\]" - Must be equivalent to the literal pattern: "\[copySet\]"/, - ], + [makeCopySet([]), /"\[copySet\]" - Must be equivalent to: "\[copySet\]"/], [ makeCopySet([3, 4, 5]), - /"\[copySet\]" - Must be equivalent to the literal pattern: "\[copySet\]"/, + /"\[copySet\]" - Must be equivalent to: "\[copySet\]"/, ], [M.lte(makeCopySet([])), /"\[copySet\]" - Must be <= "\[copySet\]"/], [ @@ -139,12 +193,12 @@ const matchTests = harden([ test('test simple matches', t => { for (const { specimen, yesPatterns, noPatterns } of matchTests) { for (const yesPattern of yesPatterns) { - t.notThrows(() => assertMatches(specimen, yesPattern), `${yesPattern}`); + t.notThrows(() => fit(specimen, yesPattern), `${yesPattern}`); t.assert(matches(specimen, yesPattern), `${yesPattern}`); } for (const [noPattern, msg] of noPatterns) { t.throws( - () => assertMatches(specimen, noPattern), + () => fit(specimen, noPattern), { message: msg }, `${noPattern}`, );