Skip to content

Commit 27b3006

Browse files
committed
feat(ertp,zoe): minimal want patterns using M.has(el,n)
1 parent bc8b038 commit 27b3006

38 files changed

+520
-218
lines changed

packages/ERTP/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"@endo/far": "^1.1.10",
4949
"@endo/marshal": "^1.6.3",
5050
"@endo/nat": "^5.0.14",
51+
"@endo/pass-style": "^1.4.8",
5152
"@endo/patterns": "^1.4.8",
5253
"@endo/promise-kit": "^1.1.9"
5354
},

packages/ERTP/src/amountMath.js

Lines changed: 140 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { q, Fail } from '@endo/errors';
2-
import { passStyleOf, assertRemotable, assertRecord } from '@endo/marshal';
2+
import { assertRemotable, assertRecord, assertChecker } from '@endo/pass-style';
3+
import { containerHasSplit, kindOf, mustMatch } from '@endo/patterns';
34

4-
import { M, matches } from '@agoric/store';
55
import { natMathHelpers } from './mathHelpers/natMathHelpers.js';
66
import { setMathHelpers } from './mathHelpers/setMathHelpers.js';
77
import { copySetMathHelpers } from './mathHelpers/copySetMathHelpers.js';
88
import { copyBagMathHelpers } from './mathHelpers/copyBagMathHelpers.js';
9+
import { AmountShape } from './typeGuards.js';
910

1011
/**
11-
* @import {CopyBag, CopySet} from '@endo/patterns';
12-
* @import {Amount, AmountValue, AssetValueForKind, Brand, CopyBagAmount, CopySetAmount, MathHelpers, NatAmount, NatValue, SetAmount, SetValue} from './types.js';
12+
* @import {Key, CopyBag, CopySet} from '@endo/patterns';
13+
* @import {Amount, AmountBound, AssetValueForKind, Brand, CopyBagAmount, CopySetAmount, MathHelpers, NatAmount, NatValue, SetAmount, SetValue, HasBound} from './types.js';
1314
*/
1415

1516
// NB: AssetKind is both a constant for enumerated values and a type for those values.
@@ -75,39 +76,42 @@ const helpers = {
7576
copyBag: copyBagMathHelpers,
7677
};
7778

78-
/** @type {(value: unknown) => 'nat' | 'set' | 'copySet' | 'copyBag'} } */
79-
const assertValueGetAssetKind = value => {
80-
const passStyle = passStyleOf(value);
81-
if (passStyle === 'bigint') {
82-
return 'nat';
83-
}
84-
if (passStyle === 'copyArray') {
85-
return 'set';
86-
}
87-
if (matches(value, M.set())) {
88-
return 'copySet';
89-
}
90-
if (matches(value, M.bag())) {
91-
return 'copyBag';
79+
/**
80+
* @template {AssetKind} K=AssetKind
81+
* @template {Key} M=Key
82+
* @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
83+
* @param {V} value
84+
* @returns {AssetKind}
85+
*/
86+
export const assertValueGetAssetKind = value => {
87+
const kind = kindOf(value);
88+
switch (kind) {
89+
case 'bigint': {
90+
return 'nat';
91+
}
92+
case 'copyArray': {
93+
return 'set';
94+
}
95+
case 'copySet':
96+
case 'copyBag': {
97+
return kind;
98+
}
99+
default: {
100+
throw Fail`value ${value} must be an AmountValue, not ${q(kind)}`;
101+
}
92102
}
93-
// TODO This isn't quite the right error message, in case valuePassStyle
94-
// is 'tagged'. We would need to distinguish what kind of tagged
95-
// object it is.
96-
// Also, this kind of manual listing is a maintenance hazard we
97-
// (TODO) will encounter when we extend the math helpers further.
98-
throw Fail`value ${value} must be a bigint, copySet, copyBag, or an array, not ${q(
99-
passStyle,
100-
)}`;
101103
};
102104

103105
/**
104106
* Asserts that value is a valid AmountMath and returns the appropriate helpers.
105107
*
106108
* Made available only for testing, but it is harmless for other uses.
107109
*
108-
* @template {AmountValue} V
110+
* @template {AssetKind} K=AssetKind
111+
* @template {Key} M=Key
112+
* @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
109113
* @param {V} value
110-
* @returns {MathHelpers<V>}
114+
* @returns {MathHelpers<K, M, V>}
111115
*/
112116
export const assertValueGetHelpers = value =>
113117
// @ts-expect-error cast
@@ -127,35 +131,38 @@ const optionalBrandCheck = (allegedBrand, brand) => {
127131
};
128132

129133
/**
130-
* @template {AssetKind} K
134+
* @template {AssetKind} K=AssetKind
135+
* @template {Key} M=Key
136+
* @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
131137
* @param {Amount<K>} leftAmount
132138
* @param {Amount<K>} rightAmount
133139
* @param {Brand<K> | undefined} brand
134-
* @returns {MathHelpers<any>}
140+
* @returns {MathHelpers<K, M, V>}
135141
*/
136142
const checkLRAndGetHelpers = (leftAmount, rightAmount, brand = undefined) => {
137-
assertRecord(leftAmount, 'leftAmount');
138-
assertRecord(rightAmount, 'rightAmount');
139-
const { value: leftValue, brand: leftBrand } = leftAmount;
140-
const { value: rightValue, brand: rightBrand } = rightAmount;
141-
assertRemotable(leftBrand, 'leftBrand');
142-
assertRemotable(rightBrand, 'rightBrand');
143+
mustMatch(leftAmount, AmountShape, 'left amount');
144+
mustMatch(rightAmount, AmountShape, 'right amount');
145+
const { brand: leftBrand, value: leftValue } = leftAmount;
146+
const { brand: rightBrand, value: rightValue } = rightAmount;
143147
optionalBrandCheck(leftBrand, brand);
144148
optionalBrandCheck(rightBrand, brand);
145149
leftBrand === rightBrand ||
146150
Fail`Brands in left ${q(leftBrand)} and right ${q(
147151
rightBrand,
148152
)} should match but do not`;
149-
const leftHelpers = assertValueGetHelpers(leftValue);
150-
const rightHelpers = assertValueGetHelpers(rightValue);
151-
leftHelpers === rightHelpers ||
152-
Fail`The left ${leftAmount} and right amount ${rightAmount} had different assetKinds`;
153-
return leftHelpers;
153+
const leftKind = assertValueGetAssetKind(leftValue);
154+
const rightKind = assertValueGetAssetKind(rightValue);
155+
leftKind === rightKind ||
156+
Fail`The left ${leftAmount} and right amounts ${rightAmount} had different assetKinds: ${q(leftKind)} vs ${q(rightKind)}`;
157+
// @ts-expect-error cast
158+
return helpers[leftKind];
154159
};
155160

156161
/**
157-
* @template {AssetKind} K
158-
* @param {MathHelpers<AssetValueForKind<K>>} h
162+
* @template {AssetKind} K=AssetKind
163+
* @template {Key} M=Key
164+
* @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
165+
* @param {MathHelpers<K, M, V>} h
159166
* @param {Amount<K>} leftAmount
160167
* @param {Amount<K>} rightAmount
161168
* @returns {[K, K]}
@@ -166,20 +173,54 @@ const coerceLR = (h, leftAmount, rightAmount) => {
166173
};
167174

168175
/**
169-
* Returns true if the leftAmount is greater than or equal to the rightAmount.
170-
* The notion of "greater than or equal to" depends on the kind of amount, as
171-
* defined by the MathHelpers. For example, whether rectangle A is greater than
172-
* rectangle B depends on whether rectangle A includes rectangle B as defined by
173-
* the logic in MathHelpers.
176+
* Returns true if the leftAmount is greater than or equal to the
177+
* rightAmountBound. The notion of "greater than or equal to" depends on the
178+
* kind of amount, as defined by the MathHelpers. For example, whether rectangle
179+
* A is greater than rectangle B depends on whether rectangle A includes
180+
* rectangle B as defined by the logic in MathHelpers.
181+
*
182+
* For non-fungible or sem-fungible amounts, the right operand can also be an
183+
* `AmountBound` which can a normal concrete `Amount` or a specialized pattern:
184+
* A `RecordPattern` of a normal concrete `brand: Brand` and a `value:
185+
* HasBound`, as made by `M.containerHas(elementPattern)` or
186+
* `M.containerHas(elementPattern, bigint)`. This represents those elements of
187+
* the value collection that match the elementPattern, if that number is exactly
188+
* the same as the bigint argument. If the second argument of `M.containerHas`
189+
* is omitted, it defaults to `1n`. IOW, the left operand is `>=` such a bound
190+
* if the total number of elements in the left operand that match the element
191+
* pattern is `>=` the bigint argument in the `M.containerHas` pattern.
174192
*
175193
* @template {AssetKind} K
176194
* @param {Amount<K>} leftAmount
177-
* @param {Amount<K>} rightAmount
195+
* @param {AmountBound<K>} rightAmountBound
178196
* @param {Brand<K>} [brand]
179197
* @returns {boolean}
180198
*/
181-
const isGTE = (leftAmount, rightAmount, brand = undefined) => {
199+
const isGTE = (leftAmount, rightAmountBound, brand = undefined) => {
200+
const { brand: rightBrand, value: rightValueBound } = rightAmountBound;
201+
if (kindOf(rightValueBound) === 'match:containerHas') {
202+
mustMatch(leftAmount, AmountShape, 'left amount');
203+
const { brand: leftBrand, value: leftValue } = leftAmount;
204+
const {
205+
payload: [elementPatt, bound],
206+
} = /** @type {HasBound} */ (rightValueBound);
207+
optionalBrandCheck(leftBrand, brand);
208+
optionalBrandCheck(rightBrand, brand);
209+
leftBrand === rightBrand ||
210+
Fail`Brands in left ${q(leftBrand)} and right ${q(
211+
rightBrand,
212+
)} should match but do not`;
213+
const leftKind = assertValueGetAssetKind(leftValue);
214+
leftKind !== 'nat' ||
215+
Fail`can only use M.containerHas on container assets, not nat: ${leftValue}`;
216+
const h = helpers[leftKind];
217+
// @ts-expect-error param type of doCoerce should not be never
218+
const lv = h.doCoerce(leftValue);
219+
return !!containerHasSplit(lv, elementPatt, bound);
220+
}
221+
const rightAmount = /** @type {Amount<K>} */ (rightAmountBound);
182222
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
223+
// @ts-expect-error cast?
183224
return h.doIsGTE(...coerceLR(h, leftAmount, rightAmount));
184225
};
185226

@@ -309,6 +350,7 @@ export const AmountMath = {
309350
*/
310351
isEqual: (leftAmount, rightAmount, brand = undefined) => {
311352
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
353+
// @ts-expect-error cast?
312354
return h.doIsEqual(...coerceLR(h, leftAmount, rightAmount));
313355
},
314356
/**
@@ -326,26 +368,64 @@ export const AmountMath = {
326368
*/
327369
add: (leftAmount, rightAmount, brand = undefined) => {
328370
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
371+
// @ts-expect-error cast?
329372
const value = h.doAdd(...coerceLR(h, leftAmount, rightAmount));
330373
// @ts-expect-error different subtype
331374
return harden({ brand: leftAmount.brand, value });
332375
},
333376
/**
334-
* Returns a new amount that is the leftAmount minus the rightAmount (i.e.
335-
* everything in the leftAmount that is not in the rightAmount). If leftAmount
336-
* doesn't include rightAmount (subtraction results in a negative), throw an
337-
* error. Because the left amount must include the right amount, this is NOT
338-
* equivalent to set subtraction.
377+
* Returns a new amount that is the leftAmount minus the rightAmountBound
378+
* (i.e. everything in the leftAmount that is not in the rightAmountBound). If
379+
* leftAmount doesn't include rightAmountBound (subtraction results in a
380+
* negative), throw an error. Because the left amount must include the right
381+
* amount bound, this is NOT equivalent to set subtraction.
339382
*
340-
* @template {Amount} L
341-
* @template {Amount} R
383+
* @template {AssetKind} K
384+
* @template {Amount<K>} L
385+
* @template {AmountBound<K>} R
342386
* @param {L} leftAmount
343-
* @param {R} rightAmount
387+
* @param {R} rightAmountBound
344388
* @param {Brand} [brand]
345-
* @returns {L extends R ? L : never}
389+
* @returns {L}
346390
*/
347-
subtract: (leftAmount, rightAmount, brand = undefined) => {
391+
subtract: (leftAmount, rightAmountBound, brand = undefined) => {
392+
const { brand: rightBrand, value: rightValueBound } = rightAmountBound;
393+
if (kindOf(rightValueBound) === 'match:containerHas') {
394+
mustMatch(leftAmount, AmountShape, 'left amount');
395+
const { brand: leftBrand, value: leftValue } = leftAmount;
396+
const {
397+
payload: [elementPatt, bound],
398+
} = /** @type {HasBound} */ (rightValueBound);
399+
optionalBrandCheck(leftBrand, brand);
400+
optionalBrandCheck(rightBrand, brand);
401+
leftBrand === rightBrand ||
402+
Fail`Brands in left ${q(leftBrand)} and right ${q(
403+
rightBrand,
404+
)} should match but do not`;
405+
const leftKind = assertValueGetAssetKind(leftValue);
406+
leftKind !== 'nat' ||
407+
Fail`can only use M.containerHas on container assets, not nat: ${leftValue}`;
408+
const h = helpers[leftKind];
409+
// @ts-expect-error param type of doCoerce should not be never
410+
const lv = h.doCoerce(leftValue);
411+
// Passing in `assertChecker` as the `check` argument should guarantee
412+
// that `value` is not `undefined`. It would have thrown first.
413+
const [_, value] = containerHasSplit(
414+
lv,
415+
elementPatt,
416+
bound,
417+
false,
418+
true,
419+
assertChecker,
420+
);
421+
// @ts-expect-error cast?
422+
return harden({ brand: leftBrand, value });
423+
}
424+
// @ts-expect-error I don't know why TS complains here but not in
425+
// the identical case in isGTE
426+
const rightAmount = /** @type {Amount<K>} */ (rightAmountBound);
348427
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
428+
// @ts-expect-error cast?
349429
const value = h.doSubtract(...coerceLR(h, leftAmount, rightAmount));
350430
// @ts-expect-error different subtype
351431
return harden({ brand: leftAmount.brand, value });

packages/ERTP/src/legacy-payment-helpers.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export const claim = async (
4040
) => {
4141
const srcPayment = await srcPaymentP;
4242
return E.when(E(recoveryPurse).deposit(srcPayment, optAmountShape), amount =>
43+
// @ts-expect-error Why doesn't TS know that an Amount is a kind
44+
// of AmountBound?
4345
E(recoveryPurse).withdraw(amount),
4446
);
4547
};
@@ -87,6 +89,8 @@ export const combine = async (
8789
if (optTotalAmount !== undefined) {
8890
mustMatch(total, optTotalAmount, 'amount');
8991
}
92+
// @ts-expect-error Why doesn't TS know that an Amount is a kind
93+
// of AmountBound?
9094
return E(recoveryPurse).withdraw(total);
9195
};
9296
harden(combine);
@@ -111,7 +115,11 @@ export const split = async (recoveryPurse, srcPaymentP, paymentAmountA) => {
111115
const srcAmount = await E(recoveryPurse).deposit(srcPayment);
112116
const paymentAmountB = AmountMath.subtract(srcAmount, paymentAmountA);
113117
return Promise.all([
118+
// @ts-expect-error Why doesn't TS know that an Amount is a kind
119+
// of AmountBound?
114120
E(recoveryPurse).withdraw(paymentAmountA),
121+
// @ts-expect-error Why doesn't TS know that an Amount is a kind
122+
// of AmountBound?
115123
E(recoveryPurse).withdraw(paymentAmountB),
116124
]);
117125
};
@@ -147,6 +155,12 @@ export const splitMany = async (recoveryPurse, srcPaymentP, amounts) => {
147155
AmountMath.isEqual(srcAmount, total) ||
148156
Fail`rights were not conserved: ${total} vs ${srcAmount}`;
149157

150-
return Promise.all(amounts.map(amount => E(recoveryPurse).withdraw(amount)));
158+
return Promise.all(
159+
amounts.map(amount =>
160+
// @ts-expect-error Why doesn't TS know that an Amount is a kind
161+
// of AmountBound?
162+
E(recoveryPurse).withdraw(amount),
163+
),
164+
);
151165
};
152166
harden(splitMany);

packages/ERTP/src/mathHelpers/copyBagMathHelpers.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ import {
1111
bagDisjointSubtract,
1212
} from '@agoric/store';
1313

14-
/** @import {MathHelpers} from '../types.js' */
14+
/**
15+
* @import {Key, CopyBag} from '@endo/patterns'
16+
* @import {MathHelpers} from '../types.js'
17+
*/
1518

16-
/** @type {import('@endo/patterns').CopyBag} */
19+
/** @type {CopyBag} */
1720
const empty = makeCopyBag([]);
1821

19-
/** @type {MathHelpers<import('@endo/patterns').CopyBag>} */
22+
/** @type {MathHelpers<'copyBag', Key, CopyBag<Key>>} */
2023
export const copyBagMathHelpers = harden({
2124
doCoerce: bag => {
2225
mustMatch(bag, M.bag(), 'bag of amount');

packages/ERTP/src/mathHelpers/copySetMathHelpers.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ import {
99
setIsSuperset,
1010
setDisjointUnion,
1111
setDisjointSubtract,
12-
} from '@agoric/store';
12+
} from '@endo/patterns';
1313

1414
/**
15+
* @import {Key, CopySet} from '@endo/patterns'
1516
* @import {MathHelpers} from '../types.js'
16-
* @import {CopySet} from '@endo/patterns'
1717
*/
1818

1919
/** @type {CopySet} */
2020
const empty = makeCopySet([]);
2121

22-
/** @type {MathHelpers<CopySet>} */
22+
/** @type {MathHelpers<'copySet', Key, CopySet<Key>>} */
2323
export const copySetMathHelpers = harden({
2424
doCoerce: set => {
2525
mustMatch(set, M.set(), 'set of amount');

packages/ERTP/src/mathHelpers/natMathHelpers.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import { Fail } from '@endo/errors';
44
import { Nat, isNat } from '@endo/nat';
55

6-
/** @import {MathHelpers, NatValue} from '../types.js' */
6+
/**
7+
* @import {Key} from '@endo/patterns'
8+
* @import {MathHelpers, NatValue} from '../types.js'
9+
*/
710

811
const empty = 0n;
912

@@ -16,7 +19,7 @@ const empty = 0n;
1619
* smallest whole unit such that the `natMathHelpers` never deals with
1720
* fractional parts.
1821
*
19-
* @type {MathHelpers<NatValue>}
22+
* @type {MathHelpers<'nat', Key, NatValue>}
2023
*/
2124
export const natMathHelpers = harden({
2225
doCoerce: nat => {

0 commit comments

Comments
 (0)