Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(patterns): M.has(el,n) to support want patterns #2710

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/pass-style/src/makeTagged.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import { Fail } from '@endo/errors';
import { PASS_STYLE } from './passStyle-helpers.js';
import { assertPassable } from './passStyleOf.js';

/**
* @import {Passable,CopyTagged} from './types.js'
*/

const { create, prototype: objectPrototype } = Object;

/**
* @template {string} T
* @template {import('./types.js').Passable} P
* @template {Passable} P
* @param {T} tag
* @param {P} payload
* @returns {import('./types.js').CopyTagged<T,P>}
* @returns {CopyTagged<T,P>}
*/
export const makeTagged = (tag, payload) => {
typeof tag === 'string' ||
Expand Down
62 changes: 62 additions & 0 deletions packages/patterns/src/patterns/patternMatchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,65 @@ const makePatternKit = () => {
getRankCover: () => getPassStyleCover('tagged'),
});

/** @type {MatchHelper} */
const matchHasHelper = Far('match:has helper', {
checkMatches: (
specimen,
[elementPatt, countPatt, limits = undefined],
check,
) => {
let count = 0n;
const kind = kindOf(specimen, check);
switch (kind) {
case 'copyArray': {
for (const element of specimen) {
if (matches(element, elementPatt)) {
count += 1n;
}
}
break;
}
case 'copySet': {
for (const element of specimen.payload) {
if (matches(element, elementPatt)) {
count += 1n;
}
}
break;
}
case 'copyBag': {
for (const [element, num] of specimen.payload) {
if (matches(element, elementPatt)) {
count += num;
}
}
break;
}
default: {
return check(false, X`unexpected ${q(kind)}`);
}
}
const { decimalDigitsLimit } = limit(limits);
return (
applyLabelingError(
checkDecimalDigitsLimit,
[count, decimalDigitsLimit, check],
`${kind} matches`,
) && checkMatches(count, countPatt, check, `${kind} matches`)
);
},

checkIsWellFormed: (payload, check) =>
checkIsWellFormedWithLimit(
payload,
harden([MM.pattern(), MM.pattern()]),
check,
'match:has payload',
),

getRankCover: () => getPassStyleCover('tagged'),
});

/** @type {MatchHelper} */
const matchMapOfHelper = Far('match:mapOf helper', {
checkMatches: (
Expand Down Expand Up @@ -1548,6 +1607,7 @@ const makePatternKit = () => {
'match:recordOf': matchRecordOfHelper,
'match:setOf': matchSetOfHelper,
'match:bagOf': matchBagOfHelper,
'match:has': matchHasHelper,
'match:mapOf': matchMapOfHelper,
'match:splitArray': matchSplitArrayHelper,
'match:splitRecord': matchSplitRecordHelper,
Expand Down Expand Up @@ -1702,6 +1762,8 @@ const makePatternKit = () => {
makeLimitsMatcher('match:setOf', [keyPatt, limits]),
bagOf: (keyPatt = M.any(), countPatt = M.any(), limits = undefined) =>
makeLimitsMatcher('match:bagOf', [keyPatt, countPatt, limits]),
has: (elementPatt = M.any(), countPatt = MM.gte(1n), limits = undefined) =>
makeLimitsMatcher('match:has', [elementPatt, countPatt, limits]),
mapOf: (keyPatt = M.any(), valuePatt = M.any(), limits = undefined) =>
makeLimitsMatcher('match:mapOf', [keyPatt, valuePatt, limits]),
splitArray: (base, optional = undefined, rest = undefined) =>
Expand Down
7 changes: 7 additions & 0 deletions packages/patterns/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,13 @@ export {};
* `countPatt` is expected to rarely be useful,
* but is provided to minimize surprise.
*
* @property {(elementPatt?: Pattern,
* countPatt?: Pattern,
* limits?: Limits
* ) => Matcher} has
* Matches any array, CopySet, or CopyBag in which the bigint number of
* elements that match `elementPatt` is a number that matches `countPatt`.
*
* @property {(keyPatt?: Pattern,
* valuePatt?: Pattern,
* limits?: Limits
Expand Down
49 changes: 49 additions & 0 deletions packages/patterns/test/patterns.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,12 @@ const runTests = (t, successCase, failCase) => {

successCase(specimen, M.arrayOf(M.number()));

successCase(specimen, M.has(3));
successCase(specimen, M.has(3, 1n));
successCase(specimen, M.has(M.number(), 2n));
successCase(specimen, M.has('c', 0n));
successCase(specimen, M.has('c', M.nat()));

failCase(specimen, [4, 3], '[3,4] - Must be: [4,3]');
failCase(specimen, [3], '[3,4] - Must be: [3]');
failCase(
Expand Down Expand Up @@ -227,6 +233,13 @@ const runTests = (t, successCase, failCase) => {
M.arrayOf(M.string()),
'[0]: number 3 - Must be a string',
);

failCase(specimen, M.has(3, 1), 'copyArray matches: "[1n]" - Must be: 1');
failCase(
specimen,
M.has('c'),
'copyArray matches: "[0n]" - Must be >= "[1n]"',
);
}
{
const specimen = { foo: 3, bar: 4 };
Expand Down Expand Up @@ -419,6 +432,12 @@ const runTests = (t, successCase, failCase) => {
successCase(specimen, M.lte(makeCopySet([3, 4, 5])));
successCase(specimen, M.setOf(M.number()));

successCase(specimen, M.has(3));
successCase(specimen, M.has(3, 1n));
successCase(specimen, M.has(M.number(), 2n));
successCase(specimen, M.has('c', 0n));
successCase(specimen, M.has('c', M.nat()));

failCase(specimen, makeCopySet([]), '"[copySet]" - Must be: "[copySet]"');
failCase(
specimen,
Expand All @@ -440,6 +459,13 @@ const runTests = (t, successCase, failCase) => {
M.setOf(M.string()),
'set elements[0]: number 4 - Must be a string',
);

failCase(specimen, M.has(3, 1), 'copySet matches: "[1n]" - Must be: 1');
failCase(
specimen,
M.has('c'),
'copySet matches: "[0n]" - Must be >= "[1n]"',
);
}
{
const specimen = makeCopyBag([
Expand Down Expand Up @@ -472,6 +498,13 @@ const runTests = (t, successCase, failCase) => {
);
successCase(specimen, M.bagOf(M.string()));
successCase(specimen, M.bagOf(M.string(), M.lt(5n)));
successCase(specimen, M.bagOf(M.string(), M.gte(2n)));

successCase(specimen, M.has('a'));
successCase(specimen, M.has('a', 2n));
successCase(specimen, M.has(M.string(), 5n));
successCase(specimen, M.has('c', 0n));
successCase(specimen, M.has('c', M.nat()));

failCase(
specimen,
Expand Down Expand Up @@ -509,6 +542,22 @@ const runTests = (t, successCase, failCase) => {
M.bagOf(M.any(), M.gt(2n)),
'bag counts[1]: "[2n]" - Must be > "[2n]"',
);

failCase(
specimen,
M.has('a', 1n),
'copyBag matches: "[2n]" - Must be: "[1n]"',
);
failCase(
specimen,
M.has(M.string(), M.lte(4n)),
'copyBag matches: "[5n]" - Must be <= "[4n]"',
);
failCase(
specimen,
M.has('c'),
'copyBag matches: "[0n]" - Must be >= "[1n]"',
);
}
{
const specimen = makeCopyMap([
Expand Down
Loading