From 4394e6e3d1a953c22934d5f327ce173f32b4f3a1 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Wed, 21 Feb 2024 06:23:07 -0800 Subject: [PATCH] feat(patterns): New `M.tagged` pattern maker (#2091) --- packages/patterns/NEWS.md | 5 +++ .../patterns/src/patterns/patternMatchers.js | 31 ++++++++++++++++++ packages/patterns/src/types.js | 6 ++++ packages/patterns/test/test-patterns.js | 32 +++++++++++++++++++ 4 files changed, 74 insertions(+) diff --git a/packages/patterns/NEWS.md b/packages/patterns/NEWS.md index 6dc9ca380e..f2a706836c 100644 --- a/packages/patterns/NEWS.md +++ b/packages/patterns/NEWS.md @@ -1,5 +1,10 @@ User-visible changes in `@endo/patterns`: +# next release + +- Add `M.tagged(tagPattern, payloadPattern)` for making patterns that match + Passable Tagged objects. + # v0.2.6 (2023-09-11) - Adds support for CopyMap patterns (e.g., `matches(specimen, makeCopyMap([]))`). diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index dae54dc48d..7f3f1271a0 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -869,6 +869,34 @@ const makePatternKit = () => { }, }); + /** @type {import('./types.js').MatchHelper} */ + const matchTaggedHelper = Far('match:tagged helper', { + checkMatches: (specimen, [tagPatt, payloadPatt], check) => { + if (passStyleOf(specimen) !== 'tagged') { + return check( + false, + X`Expected tagged object, not ${q( + passStyleOf(specimen), + )}: ${specimen}`, + ); + } + return ( + checkMatches(getTag(specimen), tagPatt, check, 'tag') && + checkMatches(specimen.payload, payloadPatt, check, 'payload') + ); + }, + + checkIsWellFormed: (payload, check) => + checkMatches( + payload, + harden([MM.pattern(), MM.pattern()]), + check, + 'match:tagged payload', + ), + + getRankCover: (_kind, _encodePassable) => getPassStyleCover('tagged'), + }); + /** @type {import('./types.js').MatchHelper} */ const matchBigintHelper = Far('match:bigint helper', { checkMatches: (specimen, [limits = undefined], check) => { @@ -1500,6 +1528,7 @@ const makePatternKit = () => { 'match:key': matchKeyHelper, 'match:pattern': matchPatternHelper, 'match:kind': matchKindHelper, + 'match:tagged': matchTaggedHelper, 'match:bigint': matchBigintHelper, 'match:nat': matchNatHelper, 'match:string': matchStringHelper, @@ -1604,6 +1633,8 @@ const makePatternKit = () => { key: () => KeyShape, pattern: () => PatternShape, kind: makeKindMatcher, + tagged: (tagPatt = M.string(), payloadPatt = M.any()) => + makeMatcher('match:tagged', harden([tagPatt, payloadPatt])), boolean: () => BooleanShape, number: () => NumberShape, bigint: (limits = undefined) => diff --git a/packages/patterns/src/types.js b/packages/patterns/src/types.js index 5b7ff5577e..0e06dc4912 100644 --- a/packages/patterns/src/types.js +++ b/packages/patterns/src/types.js @@ -272,6 +272,12 @@ export {}; * Otherwise, does not match any value. * TODO: Reject attempts to create a kind matcher with unknown `kind`? * + * @property {(tagPatt?: Pattern, payloadPatt?: Pattern) => Matcher} tagged + * For matching an arbitrary Passable Tagged object, whether it has a + * recognized kind or not. If `tagPatt` is omitted, it defaults to + * `M.string()`. If `payloadPatt` is omitted, it defaults to + * `M.any()`. + * * @property {() => Matcher} boolean * Matches `true` or `false`. * diff --git a/packages/patterns/test/test-patterns.js b/packages/patterns/test/test-patterns.js index 65f21dd7d7..6d032ed736 100644 --- a/packages/patterns/test/test-patterns.js +++ b/packages/patterns/test/test-patterns.js @@ -104,6 +104,7 @@ const runTests = (t, successCase, failCase) => { 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.tagged(), 'Expected tagged object, not "number": 3'); } { const specimen = 0n; @@ -672,6 +673,37 @@ const runTests = (t, successCase, failCase) => { 'match:recordOf payload: [1]: A "promise" cannot be a pattern', ); } + const specimen = makeTagged('Vowish', { + vowVX: Far('VowVX', {}), + }); + successCase(specimen, M.any()); + successCase(specimen, M.tagged()); + successCase(specimen, M.tagged('Vowish')); + successCase( + specimen, + M.tagged( + 'Vowish', + harden({ + vowVX: M.remotable('VowVX'), + }), + ), + ); + failCase( + specimen, + M.record(), + 'cannot check unrecognized tag "Vowish": "[Vowish]"', + ); + failCase( + specimen, + M.kind('tagged'), + 'cannot check unrecognized tag "Vowish": "[Vowish]"', + ); + failCase(specimen, M.tagged('Vowoid'), 'tag: "Vowish" - Must be: "Vowoid"'); + failCase( + specimen, + M.tagged(undefined, harden({})), + 'payload: {"vowVX":"[Alleged: VowVX]"} - Must be: {}', + ); }; /**