Skip to content

Commit 01529a5

Browse files
authored
feat(patterns): M.containerHas(el,n) to support want patterns (#2710)
Closes: #XXXX Refs: #2002 #2008 #2113 #1739 Agoric/agoric-sdk#10952 ## Description This PR adds a new `M.containerHas(elementPatt, positiveBigint)` matcher, and exported `containerHasSplit` function. This is motivated to support Agoric/agoric-sdk#10952 , which introduces a minimal form of want pattern in terms of `M.containerHas`. - [x] Actually merging this must happen only after we've decided either to move forward with #2008 or to give up on it. Once a decision is made, and even before it is acted on, then this PR can move forward. (Any decision to move forward or not with #2008 should also consider changing the default of the feature flag introduced by #2002 .) ### Security Considerations none ### Scaling Considerations Might help due to early termination of the split operations, which Agoric/agoric-sdk#10952 uses for `AmountMath.isGTE`. ### Documentation Considerations Already doc-documents `M.containerHas` in the types.js file for `M`. That's probably good enough for this PR. The interesting documentation will be explaining want patterns in Agoric/agoric-sdk#10952 ### Testing Considerations Added tests for `M.containerHas` ### Compatibility Considerations The reason to postpone merging this PR until decisions are made on #2008 is that this PR will further expose `rankOrder` in the API, amplifying the danger that changing the string order will cause surprising observable changes. ### Upgrade Considerations This PR itself does not introduce any BREAKING changes or Upgrade issues. - [x] Update `NEWS.md` for user-facing changes.
1 parent d9c11e1 commit 01529a5

File tree

9 files changed

+306
-18
lines changed

9 files changed

+306
-18
lines changed

packages/nat/src/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ function isNat(allegedNum) {
5555
*/
5656
function Nat(allegedNum) {
5757
if (typeof allegedNum === 'bigint') {
58-
if (allegedNum < 0) {
58+
if (allegedNum < 0n) {
5959
throw RangeError(`${allegedNum} is negative`);
6060
}
6161
return allegedNum;

packages/pass-style/src/makeTagged.js

+6-2
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/NEWS.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
User-visible changes in `@endo/patterns`:
22

3+
# Next release
4+
5+
- New pattern: `M.containerHas(elementPatt, bound = 1n)` motivated to support want patterns in Zoe, to pull out only `bound` number of elements that match `elementPatt`. `bound` must be a positive bigint.
6+
- Closely related, `@endo/patterns` now exports `containerHasSplit` to support ERTP's use of `M.containerHas` on non-fungible (`set`, `copySet`) and semifungible (`copyBag`) assets, respectively. See https://github.com/Agoric/agoric-sdk/pull/10952 .
7+
38
# v1.4.0 (2024-05-06)
49

510
- `Passable` is now an accurate type instead of `any`. Downstream type checking may require changes ([example](https://github.com/Agoric/agoric-sdk/pull/8774))

packages/patterns/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export {
6565
assertMethodGuard,
6666
assertInterfaceGuard,
6767
kindOf,
68+
containerHasSplit,
6869
} from './src/patterns/patternMatchers.js';
6970

7071
export {

packages/patterns/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@endo/errors": "workspace:^",
3737
"@endo/eventual-send": "workspace:^",
3838
"@endo/marshal": "workspace:^",
39+
"@endo/pass-style": "workspace:^",
3940
"@endo/promise-kit": "workspace:^"
4041
},
4142
"devDependencies": {

packages/patterns/src/keys/merge-bag-operators.js

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { q, Fail } from '@endo/errors';
88
import { assertNoDuplicateKeys, makeBagOfEntries } from './copyBag.js';
99

1010
/**
11-
* @import {Passable} from '@endo/pass-style';
1211
* @import {FullCompare, RankCompare} from '@endo/marshal'
1312
* @import {Key} from '../types.js'
1413
*/

packages/patterns/src/patterns/patternMatchers.js

+241-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
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
4+
import { q, b, X, Fail, makeError, annotateError } from '@endo/errors';
5+
import { identChecker } from '@endo/common/ident-checker.js';
6+
import { applyLabelingError } from '@endo/common/apply-labeling-error.js';
7+
import { fromUniqueEntries } from '@endo/common/from-unique-entries.js';
8+
import { listDifference } from '@endo/common/list-difference.js';
39
import {
410
assertChecker,
511
Far,
@@ -8,19 +14,16 @@ import {
814
passStyleOf,
915
hasOwnPropertyOf,
1016
nameForPassableSymbol,
17+
} from '@endo/pass-style';
18+
import {
1119
compareRank,
1220
getPassStyleCover,
1321
intersectRankCovers,
1422
unionRankCovers,
1523
recordNames,
1624
recordValues,
1725
} from '@endo/marshal';
18-
import { identChecker } from '@endo/common/ident-checker.js';
19-
import { applyLabelingError } from '@endo/common/apply-labeling-error.js';
20-
import { fromUniqueEntries } from '@endo/common/from-unique-entries.js';
21-
import { listDifference } from '@endo/common/list-difference.js';
2226

23-
import { q, b, X, Fail, makeError, annotateError } from '@endo/errors';
2427
import { keyEQ, keyGT, keyGTE, keyLT, keyLTE } from '../keys/compareKeys.js';
2528
import {
2629
assertKey,
@@ -33,12 +36,14 @@ import {
3336
checkCopyBag,
3437
getCopyMapEntryArray,
3538
makeCopyMap,
39+
makeCopySet,
40+
makeCopyBag,
3641
} from '../keys/checkKey.js';
3742
import { generateCollectionPairEntries } from '../keys/keycollection-operators.js';
3843

3944
/**
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'
45+
* @import {Checker, CopyArray, CopyRecord, CopyTagged, Passable} from '@endo/pass-style'
46+
* @import {CopySet, CopyBag, ArgGuard, AwaitArgGuard, CheckPattern, GetRankCover, InterfaceGuard, MatcherNamespace, MethodGuard, MethodGuardMaker, Pattern, RawGuard, SyncValueGuard, Kind, Limits, AllLimits, Key, DefaultGuardType} from '../types.js'
4247
* @import {MatchHelper, PatternKit} from './types.js'
4348
*/
4449

@@ -1258,6 +1263,230 @@ const makePatternKit = () => {
12581263
getRankCover: () => getPassStyleCover('tagged'),
12591264
});
12601265

1266+
/**
1267+
* @param {CopyArray} elements
1268+
* @param {Pattern} elementPatt
1269+
* @param {bigint} bound Must be >= 1n
1270+
* @param {CopyArray} [inResults]
1271+
* @param {CopyArray} [outResults]
1272+
* @param {Checker} [check]
1273+
* @returns {boolean}
1274+
*/
1275+
const elementsHasSplit = (
1276+
elements,
1277+
elementPatt,
1278+
bound,
1279+
inResults = undefined,
1280+
outResults = undefined,
1281+
check = identChecker,
1282+
) => {
1283+
let count = 0n;
1284+
// Since this feature is motivated by ERTP's use on
1285+
// non-fungible (`set`, `copySet`) amounts,
1286+
// their arrays store their elements in decending lexicographic order.
1287+
// But this function has to make some choice amoung equally good minimal
1288+
// results. It is more intuitive for the choice to be the first `bound`
1289+
// matching elements in ascending lexicigraphic order, rather than
1290+
// decending. Thus we iterate `elements` in reverse order.
1291+
for (let i = elements.length - 1; i >= 0; i -= 1) {
1292+
const element = elements[i];
1293+
if (count < bound) {
1294+
if (matches(element, elementPatt)) {
1295+
count += 1n;
1296+
inResults?.push(element);
1297+
} else {
1298+
outResults?.push(element);
1299+
}
1300+
} else if (outResults === undefined) {
1301+
break;
1302+
} else {
1303+
outResults.push(element);
1304+
}
1305+
}
1306+
return check(
1307+
count >= bound,
1308+
X`Has only ${q(count)} matches, but needs ${q(bound)}`,
1309+
);
1310+
};
1311+
1312+
/**
1313+
* @param {CopyArray<[Key, bigint]>} pairs
1314+
* @param {Pattern} elementPatt
1315+
* @param {bigint} bound Must be >= 1n
1316+
* @param {CopyArray<[Key, bigint]>} [inResults]
1317+
* @param {CopyArray<[Key, bigint]>} [outResults]
1318+
* @param {Checker} [check]
1319+
* @returns {boolean}
1320+
*/
1321+
const pairsHasSplit = (
1322+
pairs,
1323+
elementPatt,
1324+
bound,
1325+
inResults = undefined,
1326+
outResults = undefined,
1327+
check = identChecker,
1328+
) => {
1329+
let count = 0n;
1330+
// Since this feature is motivated by ERTP's use on
1331+
// semi-fungible (`copyBag`) amounts,
1332+
// their arrays store their elements in decending lexicographic order.
1333+
// But this function has to make some choice amoung equally good minimal
1334+
// results. It is more intuitive for the choice to be the first `bound`
1335+
// matching elements in ascending lexicigraphic order, rather than
1336+
// decending. Thus we iterate `pairs` in reverse order.
1337+
for (let i = pairs.length - 1; i >= 0; i -= 1) {
1338+
const [element, num] = pairs[i];
1339+
const numRest = bound - count;
1340+
if (numRest >= 1n) {
1341+
if (matches(element, elementPatt)) {
1342+
if (num <= numRest) {
1343+
count += num;
1344+
inResults?.push([element, num]);
1345+
} else {
1346+
const numIn = numRest;
1347+
count += numIn;
1348+
inResults?.push([element, numRest]);
1349+
outResults?.push([element, num - numRest]);
1350+
}
1351+
} else {
1352+
outResults?.push([element, num]);
1353+
}
1354+
} else if (outResults === undefined) {
1355+
break;
1356+
} else {
1357+
outResults.push([element, num]);
1358+
}
1359+
}
1360+
return check(
1361+
count >= bound,
1362+
X`Has only ${q(count)} matches, but needs ${q(bound)}`,
1363+
);
1364+
};
1365+
1366+
/**
1367+
* @typedef {CopyArray | CopySet | CopyBag} Container
1368+
* @param {Container} specimen
1369+
* @param {Pattern} elementPatt
1370+
* @param {bigint} bound Must be >= 1n
1371+
* @param {boolean} [needInResults]
1372+
* @param {boolean} [needOutResults]
1373+
* @param {Checker} [check]
1374+
* @returns {[Container | undefined, Container | undefined] | false}
1375+
*/
1376+
const containerHasSplit = (
1377+
specimen,
1378+
elementPatt,
1379+
bound,
1380+
needInResults = false,
1381+
needOutResults = false,
1382+
check = identChecker,
1383+
) => {
1384+
const inResults = needInResults ? [] : undefined;
1385+
const outResults = needOutResults ? [] : undefined;
1386+
const kind = kindOf(specimen);
1387+
switch (kind) {
1388+
case 'copyArray': {
1389+
if (
1390+
!elementsHasSplit(
1391+
specimen,
1392+
elementPatt,
1393+
bound,
1394+
inResults,
1395+
outResults,
1396+
check,
1397+
)
1398+
) {
1399+
// check logic already performed by elementsHasSplit
1400+
return false;
1401+
}
1402+
return [inResults, outResults];
1403+
}
1404+
case 'copySet': {
1405+
if (
1406+
!elementsHasSplit(
1407+
specimen.payload,
1408+
elementPatt,
1409+
bound,
1410+
inResults,
1411+
outResults,
1412+
check,
1413+
)
1414+
) {
1415+
return false;
1416+
}
1417+
return [
1418+
inResults && makeCopySet(inResults),
1419+
outResults && makeCopySet(outResults),
1420+
];
1421+
}
1422+
case 'copyBag': {
1423+
if (
1424+
!pairsHasSplit(
1425+
specimen.payload,
1426+
elementPatt,
1427+
bound,
1428+
inResults,
1429+
outResults,
1430+
check,
1431+
)
1432+
) {
1433+
return false;
1434+
}
1435+
return [
1436+
inResults && makeCopyBag(inResults),
1437+
outResults && makeCopyBag(outResults),
1438+
];
1439+
}
1440+
default: {
1441+
return check(false, X`unexpected ${q(kind)}`);
1442+
}
1443+
}
1444+
};
1445+
1446+
/** @type {MatchHelper} */
1447+
const matchContainerHasHelper = Far('M.containerHas helper', {
1448+
/**
1449+
* @param {CopyArray | CopySet | CopyBag} specimen
1450+
* @param {[Pattern, bigint, Limits?]} payload
1451+
* @param {Checker} check
1452+
*/
1453+
checkMatches: (
1454+
specimen,
1455+
[elementPatt, bound, limits = undefined],
1456+
check,
1457+
) => {
1458+
const kind = kindOf(specimen, check);
1459+
const { decimalDigitsLimit } = limit(limits);
1460+
if (
1461+
!applyLabelingError(
1462+
checkDecimalDigitsLimit,
1463+
[bound, decimalDigitsLimit, check],
1464+
`${kind} matches`,
1465+
)
1466+
) {
1467+
return false;
1468+
}
1469+
return !!containerHasSplit(
1470+
specimen,
1471+
elementPatt,
1472+
bound,
1473+
false,
1474+
false,
1475+
check,
1476+
);
1477+
},
1478+
1479+
checkIsWellFormed: (payload, check) =>
1480+
checkIsWellFormedWithLimit(
1481+
payload,
1482+
harden([MM.pattern(), MM.gte(1n)]),
1483+
check,
1484+
'M.containerHas payload',
1485+
),
1486+
1487+
getRankCover: () => getPassStyleCover('tagged'),
1488+
});
1489+
12611490
/** @type {MatchHelper} */
12621491
const matchMapOfHelper = Far('match:mapOf helper', {
12631492
checkMatches: (
@@ -1548,6 +1777,7 @@ const makePatternKit = () => {
15481777
'match:recordOf': matchRecordOfHelper,
15491778
'match:setOf': matchSetOfHelper,
15501779
'match:bagOf': matchBagOfHelper,
1780+
'match:containerHas': matchContainerHasHelper,
15511781
'match:mapOf': matchMapOfHelper,
15521782
'match:splitArray': matchSplitArrayHelper,
15531783
'match:splitRecord': matchSplitRecordHelper,
@@ -1702,6 +1932,8 @@ const makePatternKit = () => {
17021932
makeLimitsMatcher('match:setOf', [keyPatt, limits]),
17031933
bagOf: (keyPatt = M.any(), countPatt = M.any(), limits = undefined) =>
17041934
makeLimitsMatcher('match:bagOf', [keyPatt, countPatt, limits]),
1935+
containerHas: (elementPatt = M.any(), countPatt = 1n, limits = undefined) =>
1936+
makeLimitsMatcher('match:containerHas', [elementPatt, countPatt, limits]),
17051937
mapOf: (keyPatt = M.any(), valuePatt = M.any(), limits = undefined) =>
17061938
makeLimitsMatcher('match:mapOf', [keyPatt, valuePatt, limits]),
17071939
splitArray: (base, optional = undefined, rest = undefined) =>
@@ -1763,6 +1995,7 @@ const makePatternKit = () => {
17631995
getRankCover,
17641996
M,
17651997
kindOf,
1998+
containerHasSplit,
17661999
});
17672000
};
17682001

@@ -1781,6 +2014,7 @@ export const {
17812014
getRankCover,
17822015
M,
17832016
kindOf,
2017+
containerHasSplit,
17842018
} = makePatternKit();
17852019

17862020
MM = M;

packages/patterns/src/types.js

+7
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} containerHas
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

0 commit comments

Comments
 (0)