diff --git a/packages/patterns/src/keys/checkKey.js b/packages/patterns/src/keys/checkKey.js index 5a2bad79c4..26e8cace61 100644 --- a/packages/patterns/src/keys/checkKey.js +++ b/packages/patterns/src/keys/checkKey.js @@ -583,7 +583,7 @@ const checkKeyInternal = (val, check) => { } case 'error': case 'promise': { - return check(false, X`A ${q(passStyle)} cannot be a key`); + return check(false, X`A ${q(passStyle)} cannot be a key: ${val}`); } default: { // Unexpected tags are just non-keys, but an unexpected passStyle diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index 735ceccb2c..3911635419 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -2,7 +2,6 @@ // TODO parameterize MatchHelper which will solve most of them import { assertChecker, - Far, getTag, makeTagged, passStyleOf, @@ -19,6 +18,7 @@ import { identChecker } from '@endo/common/ident-checker.js'; import { applyLabelingError } from '@endo/common/apply-labeling-error.js'; import { fromUniqueEntries } from '@endo/common/from-unique-entries.js'; import { listDifference } from '@endo/common/list-difference.js'; +import { objectMap } from '@endo/common/object-map.js'; import { q, b, X, Fail, makeError, annotateError } from '@endo/errors'; import { keyEQ, keyGT, keyGTE, keyLT, keyLTE } from '../keys/compareKeys.js'; @@ -86,50 +86,6 @@ export const defaultLimits = harden({ const limit = (limits = {}) => /** @type {AllLimits} */ (harden({ __proto__: defaultLimits, ...limits })); -const checkIsWellFormedWithLimit = ( - payload, - mainPayloadShape, - check, - label, -) => { - assert(Array.isArray(mainPayloadShape)); - if (!Array.isArray(payload)) { - return check(false, X`${q(label)} payload must be an array: ${payload}`); - } - - // Was the following, but its overuse of patterns caused an infinite regress - // const payloadLimitShape = harden( - // M.split( - // mainPayloadShape, - // M.partial(harden([M.recordOf(M.string(), M.number())]), harden([])), - // ), - // ); - // return checkMatches(payload, payloadLimitShape, check, label); - - const mainLength = mainPayloadShape.length; - if (!(payload.length === mainLength || payload.length === mainLength + 1)) { - return check(false, X`${q(label)} payload unexpected size: ${payload}`); - } - const limits = payload[mainLength]; - payload = harden(payload.slice(0, mainLength)); - // eslint-disable-next-line no-use-before-define - if (!checkMatches(payload, mainPayloadShape, check, label)) { - return false; - } - if (limits === undefined) { - return true; - } - return ( - (passStyleOf(limits) === 'copyRecord' || - check(false, X`Limits must be a record: ${q(limits)}`)) && - entries(limits).every( - ([key, value]) => - passStyleOf(value) === 'number' || - check(false, X`Value of limit ${q(key)} but be a number: ${q(value)}`), - ) - ); -}; - /** * @param {unknown} specimen * @param {number} decimalDigitsLimit @@ -152,6 +108,57 @@ const checkDecimalDigitsLimit = (specimen, decimalDigitsLimit, check) => { * @returns {PatternKit} */ const makePatternKit = () => { + // Define early to break a circularity is use of checkIsWellFormedWithLimit + const PatternShape = makeTagged('match:pattern', undefined); + + // Define within makePatternKit so can use checkMatches early. + const checkIsWellFormedWithLimit = ( + payload, + mainPayloadShape, + check, + label, + ) => { + assert(Array.isArray(mainPayloadShape)); + if (!Array.isArray(payload)) { + return check(false, X`${q(label)} payload must be an array: ${payload}`); + } + + // Was the following, but its overuse of patterns caused an infinite regress + // const payloadLimitShape = harden( + // M.split( + // mainPayloadShape, + // M.partial(harden([M.recordOf(M.string(), M.number())]), harden([])), + // ), + // ); + // return checkMatches(payload, payloadLimitShape, check, label); + + const mainLength = mainPayloadShape.length; + if (!(payload.length === mainLength || payload.length === mainLength + 1)) { + return check(false, X`${q(label)} payload unexpected size: ${payload}`); + } + const limits = payload[mainLength]; + payload = harden(payload.slice(0, mainLength)); + // eslint-disable-next-line no-use-before-define + if (!checkMatches(payload, mainPayloadShape, check, label)) { + return false; + } + if (limits === undefined) { + return true; + } + return ( + (passStyleOf(limits) === 'copyRecord' || + check(false, X`Limits must be a record: ${q(limits)}`)) && + entries(limits).every( + ([key, value]) => + passStyleOf(value) === 'number' || + check( + false, + X`Value of limit ${q(key)} but be a number: ${q(value)}`, + ), + ) + ); + }; + /** * If this is a recognized match tag, return the MatchHelper. * Otherwise result undefined. @@ -261,6 +268,17 @@ const makePatternKit = () => { }; harden(kindOf); + const matchHelperTagRE = harden(/^match:(\w+)(:\w+)?$/); + + const getMatchSubTag = tag => { + const parts = matchHelperTagRE.exec(tag); + if (parts && parts[2] !== undefined) { + return `match:${parts[1]}`; + } else { + return undefined; + } + }; + /** * Checks only recognized kinds, and only if the specimen * passes the invariants associated with that recognition. @@ -278,8 +296,14 @@ const makePatternKit = () => { } const realKind = kindOf(specimen, check); - if (kind === realKind) { - return true; + if (realKind !== undefined) { + if (kind === realKind) { + return true; + } + const subTag = getMatchSubTag(realKind); + if (subTag !== undefined && kind === subTag) { + return true; + } } if (check !== identChecker) { // `kind` and `realKind` can be embedded without quotes @@ -444,6 +468,15 @@ const makePatternKit = () => { case 'copySet': case 'copyBag': case 'remotable': { + if (!isKey(specimen)) { + assert(specimenKind !== patternKind); + return check( + false, + X`${specimen} - Must be a ${patternKind} to match a ${patternKind} pattern: ${q( + patt, + )}`, + ); + } // These kinds are necessarily keys return checkAsKeyPatt(specimen, patt, check); } @@ -730,7 +763,9 @@ const makePatternKit = () => { // /////////////////////// Match Helpers ///////////////////////////////////// /** @type {MatchHelper} */ - const matchAnyHelper = Far('match:any helper', { + const matchAnyHelper = harden({ + tag: 'match:any', + checkMatches: (_specimen, _matcherPayload, _check) => true, checkIsWellFormed: (matcherPayload, check) => @@ -741,7 +776,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchAndHelper = Far('match:and helper', { + const matchAndHelper = harden({ + tag: 'match:and', + checkMatches: (specimen, patts, check) => { return patts.every(patt => checkMatches(specimen, patt, check)); }, @@ -750,7 +787,10 @@ const makePatternKit = () => { const checkIt = patt => checkPattern(patt, check); return ( (passStyleOf(allegedPatts) === 'copyArray' || - check(false, X`Needs array of sub-patterns: ${q(allegedPatts)}`)) && + check(false, X`Needs array of sub-patterns: ${allegedPatts}`)) && + Array.isArray(allegedPatts) && // redundant. just for type checker + (allegedPatts.length >= 2 || + check(false, X`Must have at least two sub-patterns`)) && allegedPatts.every(checkIt) ); }, @@ -763,7 +803,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchOrHelper = Far('match:or helper', { + const matchOrHelper = harden({ + tag: 'match:or', + checkMatches: (specimen, patts, check) => { const { length } = patts; if (length === 0) { @@ -774,9 +816,8 @@ const makePatternKit = () => { } if ( patts.length === 2 && - !matches(specimen, patts[0]) && - isKind(patts[0], 'match:kind') && - patts[0].payload === 'undefined' + patts[0] === undefined && + !matches(specimen, undefined) ) { // Worth special casing the optional pattern for // better error messages. @@ -798,7 +839,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchNotHelper = Far('match:not helper', { + const matchNotHelper = harden({ + tag: 'match:not', + checkMatches: (specimen, patt, check) => { if (matches(specimen, patt)) { return check( @@ -816,7 +859,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchScalarHelper = Far('match:scalar helper', { + const matchScalarHelper = harden({ + tag: 'match:scalar', + checkMatches: (specimen, _matcherPayload, check) => checkScalarKey(specimen, check), @@ -826,7 +871,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchKeyHelper = Far('match:key helper', { + const matchKeyHelper = harden({ + tag: `match:key`, + checkMatches: (specimen, _matcherPayload, check) => checkKey(specimen, check), @@ -836,7 +883,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchPatternHelper = Far('match:pattern helper', { + const matchPatternHelper = harden({ + tag: `match:pattern`, + checkMatches: (specimen, _matcherPayload, check) => checkPattern(specimen, check), @@ -846,7 +895,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchKindHelper = Far('match:kind helper', { + const matchKindHelper = harden({ + tag: `match:kind`, + checkMatches: checkKind, checkIsWellFormed: (allegedKeyKind, check) => @@ -874,7 +925,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchTaggedHelper = Far('match:tagged helper', { + const matchTaggedHelper = harden({ + tag: `match:tagged`, + checkMatches: (specimen, [tagPatt, payloadPatt], check) => { if (passStyleOf(specimen) !== 'tagged') { return check( @@ -902,7 +955,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchBigintHelper = Far('match:bigint helper', { + const matchBigintHelper = harden({ + tag: `match:bigint`, + checkMatches: (specimen, [limits = undefined], check) => { const { decimalDigitsLimit } = limit(limits); return ( @@ -924,7 +979,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchNatHelper = Far('match:nat helper', { + const matchNatHelper = harden({ + tag: `match:nat`, + checkMatches: (specimen, [limits = undefined], check) => { const { decimalDigitsLimit } = limit(limits); return ( @@ -951,7 +1008,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchStringHelper = Far('match:string helper', { + const matchStringHelper = harden({ + tag: `match:string`, + checkMatches: (specimen, [limits = undefined], check) => { const { stringLengthLimit } = limit(limits); // prettier-ignore @@ -979,7 +1038,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchSymbolHelper = Far('match:symbol helper', { + const matchSymbolHelper = harden({ + tag: `match:symbol`, + checkMatches: (specimen, [limits = undefined], check) => { const { symbolNameLengthLimit } = limit(limits); if (!checkKind(specimen, 'symbol', check)) { @@ -1011,7 +1072,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchRemotableHelper = Far('match:remotable helper', { + const matchRemotableHelper = harden({ + tag: `match:remotable`, + checkMatches: (specimen, remotableDesc, check) => { if (isKind(specimen, 'remotable')) { return true; @@ -1049,7 +1112,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchLTEHelper = Far('match:lte helper', { + const matchLTEHelper = harden({ + tag: `match:lte`, + checkMatches: (specimen, rightOperand, check) => keyLTE(specimen, rightOperand) || check(false, X`${specimen} - Must be <= ${rightOperand}`), @@ -1071,7 +1136,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchLTHelper = Far('match:lt helper', { + const matchLTHelper = harden({ + tag: `match:lt`, + checkMatches: (specimen, rightOperand, check) => keyLT(specimen, rightOperand) || check(false, X`${specimen} - Must be < ${rightOperand}`), @@ -1082,7 +1149,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchGTEHelper = Far('match:gte helper', { + const matchGTEHelper = harden({ + tag: `match:gte`, + checkMatches: (specimen, rightOperand, check) => keyGTE(specimen, rightOperand) || check(false, X`${specimen} - Must be >= ${rightOperand}`), @@ -1104,7 +1173,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchGTHelper = Far('match:gt helper', { + const matchGTHelper = harden({ + tag: `match:gt`, + checkMatches: (specimen, rightOperand, check) => keyGT(specimen, rightOperand) || check(false, X`${specimen} - Must be > ${rightOperand}`), @@ -1115,7 +1186,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchRecordOfHelper = Far('match:recordOf helper', { + const matchRecordOfHelper = harden({ + tag: `match:recordOf`, + checkMatches: ( specimen, [keyPatt, valuePatt, limits = undefined], @@ -1155,7 +1228,7 @@ const makePatternKit = () => { checkIsWellFormed: (payload, check) => checkIsWellFormedWithLimit( payload, - harden([MM.pattern(), MM.pattern()]), + harden([PatternShape, PatternShape]), check, 'match:recordOf payload', ), @@ -1164,7 +1237,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchArrayOfHelper = Far('match:arrayOf helper', { + const matchArrayOfHelper = harden({ + tag: `match:arrayOf`, + checkMatches: (specimen, [subPatt, limits = undefined], check) => { const { arrayLengthLimit } = limit(limits); // prettier-ignore @@ -1182,7 +1257,7 @@ const makePatternKit = () => { checkIsWellFormed: (payload, check) => checkIsWellFormedWithLimit( payload, - harden([MM.pattern()]), + harden([PatternShape]), check, 'match:arrayOf payload', ), @@ -1191,7 +1266,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchSetOfHelper = Far('match:setOf helper', { + const matchSetOfHelper = harden({ + tag: `match:setOf`, + checkMatches: (specimen, [keyPatt, limits = undefined], check) => { const { numSetElementsLimit } = limit(limits); return ( @@ -1209,7 +1286,7 @@ const makePatternKit = () => { checkIsWellFormed: (payload, check) => checkIsWellFormedWithLimit( payload, - harden([MM.pattern()]), + harden([PatternShape]), check, 'match:setOf payload', ), @@ -1218,7 +1295,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchBagOfHelper = Far('match:bagOf helper', { + const matchBagOfHelper = harden({ + tag: `match:bagOf`, + checkMatches: ( specimen, [keyPatt, countPatt, limits = undefined], @@ -1250,7 +1329,7 @@ const makePatternKit = () => { checkIsWellFormed: (payload, check) => checkIsWellFormedWithLimit( payload, - harden([MM.pattern(), MM.pattern()]), + harden([PatternShape, PatternShape]), check, 'match:bagOf payload', ), @@ -1259,7 +1338,9 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchMapOfHelper = Far('match:mapOf helper', { + const matchMapOfHelper = harden({ + tag: `match:mapOf`, + checkMatches: ( specimen, [keyPatt, valuePatt, limits = undefined], @@ -1293,7 +1374,7 @@ const makePatternKit = () => { checkIsWellFormed: (payload, check) => checkIsWellFormedWithLimit( payload, - harden([MM.pattern(), MM.pattern()]), + harden([PatternShape, PatternShape]), check, 'match:mapOf payload', ), @@ -1336,7 +1417,9 @@ const makePatternKit = () => { harden(optionalPatt.slice(0, length).map(patt => MM.opt(patt))); /** @type {MatchHelper} */ - const matchSplitArrayHelper = Far('match:splitArray helper', { + const matchSplitArrayHelper = harden({ + tag: `match:splitArray`, + checkMatches: ( specimen, [requiredPatt, optionalPatt = [], restPatt = MM.any()], @@ -1449,14 +1532,15 @@ const makePatternKit = () => { * compression distinguishing `undefined` from absence. * * @param {CopyRecord} optionalPatt - * @param {string[]} names * @returns {CopyRecord} The partialPatt */ - const adaptRecordPattern = (optionalPatt, names) => - fromUniqueEntries(names.map(name => [name, MM.opt(optionalPatt[name])])); + const adaptRecordPattern = optionalPatt => + objectMap(optionalPatt, p => MM.opt(p)); /** @type {MatchHelper} */ - const matchSplitRecordHelper = Far('match:splitRecord helper', { + const matchSplitRecordHelper = harden({ + tag: `match:splitRecord`, + checkMatches: ( specimen, [requiredPatt, optionalPatt = {}, restPatt = MM.any()], @@ -1468,8 +1552,8 @@ const makePatternKit = () => { const { requiredSpecimen, optionalSpecimen, restSpecimen } = splitRecordParts(specimen, requiredPatt, optionalPatt); - const partialNames = /** @type {string[]} */ (ownKeys(optionalSpecimen)); - const partialPatt = adaptRecordPattern(optionalPatt, partialNames); + const partialNames = recordNames(optionalSpecimen); + const partialPatt = adaptRecordPattern(optionalPatt); return ( checkMatches(requiredSpecimen, requiredPatt, check) && partialNames.every(name => @@ -1521,85 +1605,111 @@ const makePatternKit = () => { ]) => getPassStyleCover(passStyleOf(requiredPatt)), }); + const makeHelpersTable = () => { + const helpers = harden([ + matchAnyHelper, + matchAndHelper, + matchOrHelper, + matchNotHelper, + + matchScalarHelper, + matchKeyHelper, + matchPatternHelper, + matchKindHelper, + matchTaggedHelper, + matchBigintHelper, + matchNatHelper, + matchStringHelper, + matchSymbolHelper, + matchRemotableHelper, + + matchLTHelper, + matchLTEHelper, + matchGTEHelper, + matchGTHelper, + + matchArrayOfHelper, + matchRecordOfHelper, + matchSetOfHelper, + matchBagOfHelper, + matchMapOfHelper, + matchSplitArrayHelper, + matchSplitRecordHelper, + ]); + + /** @type {Record} */ + // don't freeze yet + const helpersByMatchTag = {}; + + for (const helper of helpers) { + const { tag } = helper; + if (!matchHelperTagRE.test(tag)) { + throw Fail`malformed matcher tag ${q(tag)}`; + } + const subTag = getMatchSubTag(tag); + subTag === undefined || + Fail`Should not be any subtags before compression ${q(tag)}`; + helpersByMatchTag[tag] = helper; + } + return harden(helpersByMatchTag); + }; + /** @type {Record} */ - const HelpersByMatchTag = harden({ - 'match:any': matchAnyHelper, - 'match:and': matchAndHelper, - 'match:or': matchOrHelper, - 'match:not': matchNotHelper, - - 'match:scalar': matchScalarHelper, - 'match:key': matchKeyHelper, - 'match:pattern': matchPatternHelper, - 'match:kind': matchKindHelper, - 'match:tagged': matchTaggedHelper, - 'match:bigint': matchBigintHelper, - 'match:nat': matchNatHelper, - 'match:string': matchStringHelper, - 'match:symbol': matchSymbolHelper, - 'match:remotable': matchRemotableHelper, - - 'match:lt': matchLTHelper, - 'match:lte': matchLTEHelper, - 'match:gte': matchGTEHelper, - 'match:gt': matchGTHelper, - - 'match:arrayOf': matchArrayOfHelper, - 'match:recordOf': matchRecordOfHelper, - 'match:setOf': matchSetOfHelper, - 'match:bagOf': matchBagOfHelper, - 'match:mapOf': matchMapOfHelper, - 'match:splitArray': matchSplitArrayHelper, - 'match:splitRecord': matchSplitRecordHelper, - }); + const HelpersByMatchTag = makeHelpersTable(); - const makeMatcher = (tag, payload) => { - const matcher = makeTagged(tag, payload); + /** + * @param {MatchHelper} matchHelper + * @param {Passable} payload + */ + const makeMatcher = (matchHelper, payload) => { + const matcher = makeTagged(matchHelper.tag, payload); assertPattern(matcher); return matcher; }; - const makeKindMatcher = kind => makeMatcher('match:kind', kind); + const makeKindMatcher = kind => makeMatcher(matchKindHelper, kind); - const AnyShape = makeMatcher('match:any', undefined); - const ScalarShape = makeMatcher('match:scalar', undefined); - const KeyShape = makeMatcher('match:key', undefined); - const PatternShape = makeMatcher('match:pattern', undefined); + // Note that PatternShape was defined above to break a circularity. + + const AnyShape = makeMatcher(matchAnyHelper, undefined); + const ScalarShape = makeMatcher(matchScalarHelper, undefined); + const KeyShape = makeMatcher(matchKeyHelper, undefined); const BooleanShape = makeKindMatcher('boolean'); const NumberShape = makeKindMatcher('number'); - const BigIntShape = makeTagged('match:bigint', []); - const NatShape = makeTagged('match:nat', []); - const StringShape = makeTagged('match:string', []); - const SymbolShape = makeTagged('match:symbol', []); - const RecordShape = makeTagged('match:recordOf', [AnyShape, AnyShape]); - const ArrayShape = makeTagged('match:arrayOf', [AnyShape]); - const SetShape = makeTagged('match:setOf', [AnyShape]); - const BagShape = makeTagged('match:bagOf', [AnyShape, AnyShape]); - const MapShape = makeTagged('match:mapOf', [AnyShape, AnyShape]); + const BigIntShape = makeMatcher(matchBigintHelper, []); + const NatShape = makeMatcher(matchNatHelper, []); + const StringShape = makeMatcher(matchStringHelper, []); + const SymbolShape = makeMatcher(matchSymbolHelper, []); + const RecordShape = makeMatcher(matchRecordOfHelper, [AnyShape, AnyShape]); + const ArrayShape = makeMatcher(matchArrayOfHelper, [AnyShape]); + const SetShape = makeMatcher(matchSetOfHelper, [AnyShape]); + const BagShape = makeMatcher(matchBagOfHelper, [AnyShape, AnyShape]); + const MapShape = makeMatcher(matchMapOfHelper, [AnyShape, AnyShape]); const RemotableShape = makeKindMatcher('remotable'); const ErrorShape = makeKindMatcher('error'); const PromiseShape = makeKindMatcher('promise'); const UndefinedShape = makeKindMatcher('undefined'); + const NullShape = makeKindMatcher('null'); /** * For when the last element of the payload is the optional limits, * so that when it is `undefined` it is dropped from the end of the * payloads array. * - * @param {string} tag + * @param {MatchHelper} matchHelper * @param {Passable[]} payload */ - const makeLimitsMatcher = (tag, payload) => { + const makeLimitsMatcher = (matchHelper, payload) => { if (payload[payload.length - 1] === undefined) { payload = harden(payload.slice(0, payload.length - 1)); } - return makeMatcher(tag, payload); + return makeMatcher(matchHelper, payload); }; const makeRemotableMatcher = (label = undefined) => label === undefined ? RemotableShape - : makeMatcher('match:remotable', harden({ label })); + : makeMatcher(matchRemotableHelper, harden({ label })); /** * @template T @@ -1629,26 +1739,38 @@ const makePatternKit = () => { /** @type {MatcherNamespace} */ const M = harden({ any: () => AnyShape, - and: (...patts) => makeMatcher('match:and', patts), - or: (...patts) => makeMatcher('match:or', patts), - not: subPatt => makeMatcher('match:not', subPatt), + and: (...patts) => + // eslint-disable-next-line no-nested-ternary + patts.length === 0 + ? M.any() + : patts.length === 1 + ? patts[0] + : makeMatcher(matchAndHelper, patts), + or: (...patts) => + // eslint-disable-next-line no-nested-ternary + patts.length === 0 + ? M.not(M.any()) + : patts.length === 1 + ? patts[0] + : makeMatcher(matchOrHelper, patts), + not: subPatt => makeMatcher(matchNotHelper, subPatt), scalar: () => ScalarShape, key: () => KeyShape, pattern: () => PatternShape, kind: makeKindMatcher, tagged: (tagPatt = M.string(), payloadPatt = M.any()) => - makeMatcher('match:tagged', harden([tagPatt, payloadPatt])), + makeMatcher(matchTaggedHelper, harden([tagPatt, payloadPatt])), boolean: () => BooleanShape, number: () => NumberShape, bigint: (limits = undefined) => - limits ? makeLimitsMatcher('match:bigint', [limits]) : BigIntShape, + limits ? makeLimitsMatcher(matchBigintHelper, [limits]) : BigIntShape, nat: (limits = undefined) => - limits ? makeLimitsMatcher('match:nat', [limits]) : NatShape, + limits ? makeLimitsMatcher(matchNatHelper, [limits]) : NatShape, string: (limits = undefined) => - limits ? makeLimitsMatcher('match:string', [limits]) : StringShape, + limits ? makeLimitsMatcher(matchStringHelper, [limits]) : StringShape, symbol: (limits = undefined) => - limits ? makeLimitsMatcher('match:symbol', [limits]) : SymbolShape, + limits ? makeLimitsMatcher(matchSymbolHelper, [limits]) : SymbolShape, record: (limits = undefined) => limits ? M.recordOf(M.any(), M.any(), limits) : RecordShape, array: (limits = undefined) => @@ -1662,36 +1784,36 @@ const makePatternKit = () => { error: () => ErrorShape, promise: () => PromiseShape, undefined: () => UndefinedShape, - null: () => null, + null: () => NullShape, - lt: rightOperand => makeMatcher('match:lt', rightOperand), - lte: rightOperand => makeMatcher('match:lte', rightOperand), + lt: rightOperand => makeMatcher(matchLTHelper, rightOperand), + lte: rightOperand => makeMatcher(matchLTEHelper, rightOperand), eq: key => { assertKey(key); return key === undefined ? M.undefined() : key; }, neq: key => M.not(M.eq(key)), - gte: rightOperand => makeMatcher('match:gte', rightOperand), - gt: rightOperand => makeMatcher('match:gt', rightOperand), + gte: rightOperand => makeMatcher(matchGTEHelper, rightOperand), + gt: rightOperand => makeMatcher(matchGTHelper, rightOperand), recordOf: (keyPatt = M.any(), valuePatt = M.any(), limits = undefined) => - makeLimitsMatcher('match:recordOf', [keyPatt, valuePatt, limits]), + makeLimitsMatcher(matchRecordOfHelper, [keyPatt, valuePatt, limits]), arrayOf: (subPatt = M.any(), limits = undefined) => - makeLimitsMatcher('match:arrayOf', [subPatt, limits]), + makeLimitsMatcher(matchArrayOfHelper, [subPatt, limits]), setOf: (keyPatt = M.any(), limits = undefined) => - makeLimitsMatcher('match:setOf', [keyPatt, limits]), + makeLimitsMatcher(matchSetOfHelper, [keyPatt, limits]), bagOf: (keyPatt = M.any(), countPatt = M.any(), limits = undefined) => - makeLimitsMatcher('match:bagOf', [keyPatt, countPatt, limits]), + makeLimitsMatcher(matchBagOfHelper, [keyPatt, countPatt, limits]), mapOf: (keyPatt = M.any(), valuePatt = M.any(), limits = undefined) => - makeLimitsMatcher('match:mapOf', [keyPatt, valuePatt, limits]), + makeLimitsMatcher(matchMapOfHelper, [keyPatt, valuePatt, limits]), splitArray: (base, optional = undefined, rest = undefined) => makeMatcher( - 'match:splitArray', + matchSplitArrayHelper, makeSplitPayload([], base, optional, rest), ), splitRecord: (base, optional = undefined, rest = undefined) => makeMatcher( - 'match:splitRecord', + matchSplitRecordHelper, makeSplitPayload({}, base, optional, rest), ), split: (base, rest = undefined) => { @@ -1714,7 +1836,8 @@ const makePatternKit = () => { }, eref: t => M.or(t, M.promise()), - opt: t => M.or(M.undefined(), t), + // `undefined` compresses better than `M.undefined()` + opt: t => M.or(undefined, t), interface: (interfaceName, methodGuards, options) => // eslint-disable-next-line no-use-before-define @@ -1741,6 +1864,7 @@ const makePatternKit = () => { assertPattern, isPattern, getRankCover, + maybeMatchHelper, M, kindOf, }); @@ -1759,6 +1883,7 @@ export const { assertPattern, isPattern, getRankCover, + maybeMatchHelper, M, kindOf, } = makePatternKit(); diff --git a/packages/patterns/src/patterns/types.js b/packages/patterns/src/patterns/types.js index af50133967..77e835afd5 100644 --- a/packages/patterns/src/patterns/types.js +++ b/packages/patterns/src/patterns/types.js @@ -13,6 +13,8 @@ export {}; * encapsulated, and its methods can make the stated unchecked assumptions * enforced by the common calling logic. * + * @property {string} tag + * * @property {(allegedPayload: Passable, * check: Checker * ) => boolean} checkIsWellFormed @@ -47,6 +49,7 @@ export {}; * @property {(patt: Pattern) => void} assertPattern * @property {(patt: any) => boolean} isPattern * @property {GetRankCover} getRankCover + * @property {(tag: string) => (MatchHelper | undefined)} maybeMatchHelper * @property {MatcherNamespace} M * @property {(specimen: Passable, check?: Checker) => Kind | undefined} kindOf */ diff --git a/packages/patterns/test/test-patterns.js b/packages/patterns/test/test-patterns.js index f5279579df..3071ac3448 100644 --- a/packages/patterns/test/test-patterns.js +++ b/packages/patterns/test/test-patterns.js @@ -102,7 +102,7 @@ const runTests = (t, successCase, failCase) => { failCase(specimen, M.gte(3n), '3 - Must be >= "[3n]"'); failCase(specimen, M.and(3, 4), '3 - Must be: 4'); failCase(specimen, M.or(4, 4), '3 - Must match one of [4,4]'); - failCase(specimen, M.or(), '3 - no pattern disjuncts to match: []'); + failCase(specimen, M.or(), '3 - Must fail negated pattern: "[match:any]"'); failCase(specimen, M.tagged(), 'Expected tagged object, not "number": 3'); } { @@ -146,7 +146,11 @@ const runTests = (t, successCase, failCase) => { M.or(4n, 4n), '"[0n]" - Must match one of ["[4n]","[4n]"]', ); - failCase(specimen, M.or(), '"[0n]" - no pattern disjuncts to match: []'); + failCase( + specimen, + M.or(), + '"[0n]" - Must fail negated pattern: "[match:any]"', + ); } { const specimen = -1n;