Skip to content

Commit efb4c47

Browse files
committed
feat(patterns): M.has(el,n) to support want patterns
1 parent 85483c0 commit efb4c47

File tree

5 files changed

+230
-14
lines changed

5 files changed

+230
-14
lines changed

packages/pass-style/src/makeTagged.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ import { Fail } from '@endo/errors';
44
import { PASS_STYLE } from './passStyle-helpers.js';
55
import { assertPassable } from './passStyleOf.js';
66

7+
/**
8+
* @import {Passable,CopyTagged} from './types.js'
9+
*/
10+
711
const { create, prototype: objectPrototype } = Object;
812

913
/**
1014
* @template {string} T
11-
* @template {import('./types.js').Passable} P
15+
* @template {Passable} P
1216
* @param {T} tag
1317
* @param {P} payload
14-
* @returns {import('./types.js').CopyTagged<T,P>}
18+
* @returns {CopyTagged<T,P>}
1519
*/
1620
export const makeTagged = (tag, payload) => {
1721
typeof tag === 'string' ||

packages/patterns/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export {
6565
assertMethodGuard,
6666
assertInterfaceGuard,
6767
kindOf,
68+
elementsHasSplit,
69+
pairsHasSplit,
6870
} from './src/patterns/patternMatchers.js';
6971

7072
export {

packages/patterns/src/patterns/patternMatchers.js

Lines changed: 181 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @endo/no-optional-chaining */
12
// @ts-nocheck So many errors that the suppressions hamper readability.
23
// TODO parameterize MatchHelper which will solve most of them
34
import {
@@ -37,8 +38,8 @@ import {
3738
import { generateCollectionPairEntries } from '../keys/keycollection-operators.js';
3839

3940
/**
40-
* @import {Checker, CopyRecord, CopyTagged, Passable} from '@endo/pass-style'
41-
* @import {ArgGuard, AwaitArgGuard, CheckPattern, GetRankCover, InterfaceGuard, MatcherNamespace, MethodGuard, MethodGuardMaker, Pattern, RawGuard, SyncValueGuard, Kind, Limits, AllLimits, Key, DefaultGuardType} from '../types.js'
41+
* @import {Checker, CopyArray, CopyRecord, CopyTagged, Passable} from '@endo/pass-style'
42+
* @import {CopySet, CopyBag, ArgGuard, AwaitArgGuard, CheckPattern, GetRankCover, InterfaceGuard, MatcherNamespace, MethodGuard, MethodGuardMaker, Pattern, RawGuard, SyncValueGuard, Kind, Limits, AllLimits, Key, DefaultGuardType} from '../types.js'
4243
* @import {MatchHelper, PatternKit} from './types.js'
4344
*/
4445

@@ -1258,6 +1259,177 @@ const makePatternKit = () => {
12581259
getRankCover: () => getPassStyleCover('tagged'),
12591260
});
12601261

1262+
/**
1263+
* @param {CopyArray} elements
1264+
* @param {Pattern} elementPatt
1265+
* @param {bigint} bound Must be >= 1n
1266+
* @param {CopyArray} [inResults]
1267+
* @param {CopyArray} [outResults]
1268+
* @param {Checker} [check]
1269+
* @returns {boolean}
1270+
*/
1271+
const elementsHasSplit = (
1272+
elements,
1273+
elementPatt,
1274+
bound,
1275+
inResults = undefined,
1276+
outResults = undefined,
1277+
check = identChecker,
1278+
) => {
1279+
let count = 0n;
1280+
// Since this feature is motivated by ERTP's use on
1281+
// non-fungible (`set`, `copySet`) amounts,
1282+
// their arrays store their elements in decending lexicographic order.
1283+
// But this function has to make some choice amoung equally good minimal
1284+
// results. It is more intuitive for the choice to be the first `bound`
1285+
// matching elements in ascending lexicigraphic order, rather than
1286+
// decending. Thus we iterate `elements` in reverse order.
1287+
for (let i = elements.length - 1; i >= 0; i -= 1) {
1288+
const element = elements[i];
1289+
if (count < bound) {
1290+
if (matches(element, elementPatt)) {
1291+
count += 1n;
1292+
inResults?.push(element);
1293+
} else {
1294+
outResults?.push(element);
1295+
}
1296+
} else if (outResults === undefined) {
1297+
break;
1298+
} else {
1299+
outResults.push(element);
1300+
}
1301+
}
1302+
return check(
1303+
count >= bound,
1304+
X`Has only ${q(count)} matches, but needs ${q(bound)}`,
1305+
);
1306+
};
1307+
1308+
/**
1309+
* @param {CopyArray<[Key, bigint]>} pairs
1310+
* @param {Pattern} elementPatt
1311+
* @param {bigint} bound Must be >= 1n
1312+
* @param {CopyArray<[Key, bigint]>} [inResults]
1313+
* @param {CopyArray<[Key, bigint]>} [outResults]
1314+
* @returns {boolean}
1315+
* @param {Checker} [check]
1316+
*/
1317+
const pairsHasSplit = (
1318+
pairs,
1319+
elementPatt,
1320+
bound,
1321+
inResults = undefined,
1322+
outResults = undefined,
1323+
check = identChecker,
1324+
) => {
1325+
let count = 0n;
1326+
// Since this feature is motivated by ERTP's use on
1327+
// semi-fungible (`copyBag`) amounts,
1328+
// their arrays store their elements in decending lexicographic order.
1329+
// But this function has to make some choice amoung equally good minimal
1330+
// results. It is more intuitive for the choice to be the first `bound`
1331+
// matching elements in ascending lexicigraphic order, rather than
1332+
// decending. Thus we iterate `pairs` in reverse order.
1333+
for (let i = pairs.length - 1; i >= 0n; i -= 1) {
1334+
const [element, num] = pairs[i];
1335+
const numRest = bound - count;
1336+
if (numRest >= 1n) {
1337+
if (matches(element, elementPatt)) {
1338+
if (num <= numRest) {
1339+
count += num;
1340+
inResults?.push([element, num]);
1341+
} else {
1342+
const numIn = num - numRest;
1343+
count += numIn;
1344+
inResults?.push([element, numIn]);
1345+
outResults?.push([element, numRest]);
1346+
}
1347+
} else {
1348+
outResults?.push([element, num]);
1349+
}
1350+
} else if (outResults === undefined) {
1351+
break;
1352+
} else {
1353+
outResults.push([element, num]);
1354+
}
1355+
}
1356+
return check(
1357+
count >= bound,
1358+
X`Has only ${q(count)} matches, but needs ${q(bound)}`,
1359+
);
1360+
};
1361+
1362+
/** @type {MatchHelper} */
1363+
const matchHasHelper = Far('match:has helper', {
1364+
/**
1365+
* @param {CopyArray | CopySet | CopyBag} specimen
1366+
* @param {[Pattern, bigint, Limits?]} payload
1367+
* @param {Checker} check
1368+
*/
1369+
checkMatches: (
1370+
specimen,
1371+
[elementPatt, bound, limits = undefined],
1372+
check,
1373+
) => {
1374+
const kind = kindOf(specimen, check);
1375+
const { decimalDigitsLimit } = limit(limits);
1376+
if (
1377+
!applyLabelingError(
1378+
checkDecimalDigitsLimit,
1379+
[bound, decimalDigitsLimit, check],
1380+
`${kind} matches`,
1381+
)
1382+
) {
1383+
return false;
1384+
}
1385+
switch (kind) {
1386+
case 'copyArray': {
1387+
return elementsHasSplit(
1388+
specimen,
1389+
elementPatt,
1390+
bound,
1391+
undefined,
1392+
undefined,
1393+
check,
1394+
);
1395+
}
1396+
case 'copySet': {
1397+
return elementsHasSplit(
1398+
specimen.payload,
1399+
elementPatt,
1400+
bound,
1401+
undefined,
1402+
undefined,
1403+
check,
1404+
);
1405+
}
1406+
case 'copyBag': {
1407+
return pairsHasSplit(
1408+
specimen.payload,
1409+
elementPatt,
1410+
bound,
1411+
undefined,
1412+
undefined,
1413+
check,
1414+
);
1415+
}
1416+
default: {
1417+
return check(false, X`unexpected ${q(kind)}`);
1418+
}
1419+
}
1420+
},
1421+
1422+
checkIsWellFormed: (payload, check) =>
1423+
checkIsWellFormedWithLimit(
1424+
payload,
1425+
harden([MM.pattern(), MM.gte(1n)]),
1426+
check,
1427+
'match:has payload',
1428+
),
1429+
1430+
getRankCover: () => getPassStyleCover('tagged'),
1431+
});
1432+
12611433
/** @type {MatchHelper} */
12621434
const matchMapOfHelper = Far('match:mapOf helper', {
12631435
checkMatches: (
@@ -1548,6 +1720,7 @@ const makePatternKit = () => {
15481720
'match:recordOf': matchRecordOfHelper,
15491721
'match:setOf': matchSetOfHelper,
15501722
'match:bagOf': matchBagOfHelper,
1723+
'match:has': matchHasHelper,
15511724
'match:mapOf': matchMapOfHelper,
15521725
'match:splitArray': matchSplitArrayHelper,
15531726
'match:splitRecord': matchSplitRecordHelper,
@@ -1702,6 +1875,8 @@ const makePatternKit = () => {
17021875
makeLimitsMatcher('match:setOf', [keyPatt, limits]),
17031876
bagOf: (keyPatt = M.any(), countPatt = M.any(), limits = undefined) =>
17041877
makeLimitsMatcher('match:bagOf', [keyPatt, countPatt, limits]),
1878+
has: (elementPatt = M.any(), countPatt = 1n, limits = undefined) =>
1879+
makeLimitsMatcher('match:has', [elementPatt, countPatt, limits]),
17051880
mapOf: (keyPatt = M.any(), valuePatt = M.any(), limits = undefined) =>
17061881
makeLimitsMatcher('match:mapOf', [keyPatt, valuePatt, limits]),
17071882
splitArray: (base, optional = undefined, rest = undefined) =>
@@ -1763,6 +1938,8 @@ const makePatternKit = () => {
17631938
getRankCover,
17641939
M,
17651940
kindOf,
1941+
elementsHasSplit,
1942+
pairsHasSplit,
17661943
});
17671944
};
17681945

@@ -1781,6 +1958,8 @@ export const {
17811958
getRankCover,
17821959
M,
17831960
kindOf,
1961+
elementsHasSplit,
1962+
pairsHasSplit,
17841963
} = makePatternKit();
17851964

17861965
MM = M;

packages/patterns/src/types.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,13 @@ export {};
380380
* `countPatt` is expected to rarely be useful,
381381
* but is provided to minimize surprise.
382382
*
383+
* @property {(elementPatt?: Pattern,
384+
* bound?: bigint,
385+
* limits?: Limits
386+
* ) => Matcher} has
387+
* Matches any array, CopySet, or CopyBag in which the bigint number of
388+
* elements that match `elementPatt` is >= `bound` (which defaults to `1n`).
389+
*
383390
* @property {(keyPatt?: Pattern,
384391
* valuePatt?: Pattern,
385392
* limits?: Limits

packages/patterns/test/patterns.test.js

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ const runTests = (t, successCase, failCase) => {
199199

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

202+
successCase(specimen, M.has(3));
203+
successCase(specimen, M.has(3, 1n));
204+
successCase(specimen, M.has(M.number(), 2n));
205+
202206
failCase(specimen, [4, 3], '[3,4] - Must be: [4,3]');
203207
failCase(specimen, [3], '[3,4] - Must be: [3]');
204208
failCase(
@@ -227,6 +231,8 @@ const runTests = (t, successCase, failCase) => {
227231
M.arrayOf(M.string()),
228232
'[0]: number 3 - Must be a string',
229233
);
234+
235+
failCase(specimen, M.has('c'), 'Has only "[0n]" matches, but needs "[1n]"');
230236
}
231237
{
232238
const specimen = { foo: 3, bar: 4 };
@@ -419,6 +425,10 @@ const runTests = (t, successCase, failCase) => {
419425
successCase(specimen, M.lte(makeCopySet([3, 4, 5])));
420426
successCase(specimen, M.setOf(M.number()));
421427

428+
successCase(specimen, M.has(3));
429+
successCase(specimen, M.has(3, 1n));
430+
successCase(specimen, M.has(M.number(), 2n));
431+
422432
failCase(specimen, makeCopySet([]), '"[copySet]" - Must be: "[copySet]"');
423433
failCase(
424434
specimen,
@@ -440,6 +450,8 @@ const runTests = (t, successCase, failCase) => {
440450
M.setOf(M.string()),
441451
'set elements[0]: number 4 - Must be a string',
442452
);
453+
454+
failCase(specimen, M.has('c'), 'Has only "[0n]" matches, but needs "[1n]"');
443455
}
444456
{
445457
const specimen = makeCopyBag([
@@ -472,6 +484,13 @@ const runTests = (t, successCase, failCase) => {
472484
);
473485
successCase(specimen, M.bagOf(M.string()));
474486
successCase(specimen, M.bagOf(M.string(), M.lt(5n)));
487+
successCase(specimen, M.bagOf(M.string(), M.gte(2n)));
488+
489+
successCase(specimen, M.has('a'));
490+
successCase(specimen, M.has('a', 2n));
491+
successCase(specimen, M.has(M.string(), 5n));
492+
successCase(specimen, M.has('a', 1n));
493+
successCase(specimen, M.has('a'));
475494

476495
failCase(
477496
specimen,
@@ -499,16 +518,8 @@ const runTests = (t, successCase, failCase) => {
499518
'bag keys[0]: string "b" - Must be a boolean',
500519
);
501520
failCase(specimen, M.bagOf('b'), 'bag keys[1]: "a" - Must be: "b"');
502-
failCase(
503-
specimen,
504-
M.bagOf(M.any(), M.gt(5n)),
505-
'bag counts[0]: "[3n]" - Must be > "[5n]"',
506-
);
507-
failCase(
508-
specimen,
509-
M.bagOf(M.any(), M.gt(2n)),
510-
'bag counts[1]: "[2n]" - Must be > "[2n]"',
511-
);
521+
522+
failCase(specimen, M.has('c'), 'Has only "[0n]" matches, but needs "[1n]"');
512523
}
513524
{
514525
const specimen = makeCopyMap([
@@ -867,4 +878,17 @@ test('well formed patterns', t => {
867878
t.throws(() => M.remotable(88), {
868879
message: 'match:remotable payload: label: number 88 - Must be a string',
869880
});
881+
882+
t.throws(() => M.has('c', 0n), {
883+
message: 'match:has payload: [1]: "[0n]" - Must be >= "[1n]"',
884+
});
885+
// @ts-expect-error purposeful type violation for testing
886+
t.throws(() => M.has('c', M.nat()), {
887+
message:
888+
'match:has payload: [1]: A passable tagged "match:nat" is not a key: "[match:nat]"',
889+
});
890+
// @ts-expect-error purposeful type violation for testing
891+
t.throws(() => M.has(3, 1), {
892+
message: 'match:has payload: [1]: 1 - Must be >= "[1n]"',
893+
});
870894
});

0 commit comments

Comments
 (0)