Skip to content

Commit b2b30dc

Browse files
committed
feat(ertp,zoe): minimal want patterns using M.has(el,n)
1 parent 6504dc6 commit b2b30dc

File tree

17 files changed

+298
-138
lines changed

17 files changed

+298
-138
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: 139 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import { q, Fail } from '@endo/errors';
2-
import { passStyleOf, assertRemotable, assertRecord } from '@endo/marshal';
2+
import { assertRemotable, assertRecord } from '@endo/pass-style';
3+
import { isKey, 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 {
10+
AmountBoundShape,
11+
AmountShape,
12+
AmountValueHasBoundShape,
13+
} from './typeGuards.js';
914

1015
/**
11-
* @import {CopyBag, CopySet} from '@endo/patterns';
12-
* @import {Amount, AssetValueForKind, Brand, CopyBagAmount, CopySetAmount, MathHelpers, NatAmount, NatValue, SetAmount, SetValue} from './types.js';
16+
* @import {Passable} from '@endo/pass-style'
17+
* @import {Key, CopyBag, CopySet} from '@endo/patterns';
18+
* @import {Amount, AmountBound, AmountValue, AssetValueForKind, Brand, CopyBagAmount, CopySetAmount, MathHelpers, NatAmount, NatValue, SetAmount, SetValue} from './types.js';
1319
*/
1420

1521
// NB: AssetKind is both a constant for enumerated values and a type for those values.
@@ -75,39 +81,42 @@ const helpers = {
7581
copyBag: copyBagMathHelpers,
7682
};
7783

78-
/** @type {(value: unknown) => 'nat' | 'set' | 'copySet' | 'copyBag'} } */
84+
/**
85+
* @template {AssetKind} K=AssetKind
86+
* @template {Key} M=Key
87+
* @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
88+
* @param {V} value
89+
* @returns {AssetKind}
90+
*/
7991
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';
92+
const kind = kindOf(value);
93+
switch (kind) {
94+
case 'bigint': {
95+
return 'nat';
96+
}
97+
case 'copyArray': {
98+
return 'set';
99+
}
100+
case 'copySet':
101+
case 'copyBag': {
102+
return kind;
103+
}
104+
default: {
105+
throw Fail`value ${value} must be an AmountValue, not ${q(kind)}`;
106+
}
92107
}
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-
)}`;
101108
};
102109

103110
/**
104111
* Asserts that value is a valid AmountMath and returns the appropriate helpers.
105112
*
106113
* Made available only for testing, but it is harmless for other uses.
107114
*
108-
* @template V
115+
* @template {AssetKind} K=AssetKind
116+
* @template {Key} M=Key
117+
* @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
109118
* @param {V} value
110-
* @returns {MathHelpers<V>}
119+
* @returns {MathHelpers<K, M, V>}
111120
*/
112121
export const assertValueGetHelpers = value =>
113122
// @ts-expect-error cast
@@ -125,35 +134,38 @@ const optionalBrandCheck = (allegedBrand, brand) => {
125134
};
126135

127136
/**
128-
* @template {AssetKind} K
137+
* @template {AssetKind} K=AssetKind
138+
* @template {Key} M=Key
139+
* @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
129140
* @param {Amount<K>} leftAmount
130141
* @param {Amount<K>} rightAmount
131142
* @param {Brand<K> | undefined} brand
132-
* @returns {MathHelpers<any>}
143+
* @returns {MathHelpers<K, M, V>}
133144
*/
134145
const checkLRAndGetHelpers = (leftAmount, rightAmount, brand = undefined) => {
135-
assertRecord(leftAmount, 'leftAmount');
136-
assertRecord(rightAmount, 'rightAmount');
137-
const { value: leftValue, brand: leftBrand } = leftAmount;
138-
const { value: rightValue, brand: rightBrand } = rightAmount;
139-
assertRemotable(leftBrand, 'leftBrand');
140-
assertRemotable(rightBrand, 'rightBrand');
146+
mustMatch(leftAmount, AmountShape, 'left amount');
147+
mustMatch(rightAmount, AmountShape, 'right amount');
148+
const { brand: leftBrand, value: leftValue } = leftAmount;
149+
const { brand: rightBrand, value: rightValue } = rightAmount;
141150
optionalBrandCheck(leftBrand, brand);
142151
optionalBrandCheck(rightBrand, brand);
143152
leftBrand === rightBrand ||
144153
Fail`Brands in left ${q(leftBrand)} and right ${q(
145154
rightBrand,
146155
)} should match but do not`;
147-
const leftHelpers = assertValueGetHelpers(leftValue);
148-
const rightHelpers = assertValueGetHelpers(rightValue);
149-
leftHelpers === rightHelpers ||
150-
Fail`The left ${leftAmount} and right amount ${rightAmount} had different assetKinds`;
151-
return leftHelpers;
156+
const leftKind = assertValueGetAssetKind(leftValue);
157+
const rightKind = assertValueGetAssetKind(rightValue);
158+
leftKind === rightKind ||
159+
Fail`The left ${leftAmount} and right amounts ${rightAmount} had different assetKinds: ${q(leftKind)} vs ${q(rightKind)}`;
160+
// @ts-expect-error cast
161+
return helpers[leftKind];
152162
};
153163

154164
/**
155-
* @template {AssetKind} K
156-
* @param {MathHelpers<AssetValueForKind<K>>} h
165+
* @template {AssetKind} K=AssetKind
166+
* @template {Key} M=Key
167+
* @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
168+
* @param {MathHelpers<K, M, V>} h
157169
* @param {Amount<K>} leftAmount
158170
* @param {Amount<K>} rightAmount
159171
* @returns {[K, K]}
@@ -164,21 +176,53 @@ const coerceLR = (h, leftAmount, rightAmount) => {
164176
};
165177

166178
/**
167-
* Returns true if the leftAmount is greater than or equal to the rightAmount.
168-
* The notion of "greater than or equal to" depends on the kind of amount, as
169-
* defined by the MathHelpers. For example, whether rectangle A is greater than
170-
* rectangle B depends on whether rectangle A includes rectangle B as defined by
171-
* the logic in MathHelpers.
179+
* Returns true if the leftAmount is greater than or equal to the
180+
* rightAmountBound. The notion of "greater than or equal to" depends on the
181+
* kind of amount, as defined by the MathHelpers. For example, whether rectangle
182+
* A is greater than rectangle B depends on whether rectangle A includes
183+
* rectangle B as defined by the logic in MathHelpers.
184+
*
185+
* For non-fungible or sem-fungible amounts, the right operand can also be an
186+
* `AmountBound` which can a normal concrete `Amount` or a specialized pattern:
187+
* A `RecordPattern` of a normal concrete `brand: Brand` and a `value:
188+
* AmountValueHasBound`, as made by `M.has(elementPattern)` or
189+
* `M.has(elementPattern, bigint)`. This represents those elements of the value
190+
* collection that match the elementPattern, if that number is exactly the same
191+
* as the bigint argument. If the second argument of `M.has` is omitted, it
192+
* defaults to `1n`. IOW, the left operand is `>=` such a bound if the total
193+
* number of elements in the left operand that match the element pattern is `>=`
194+
* the bigint argument in the `M.has` pattern.
172195
*
173196
* @template {AssetKind} K
174197
* @param {Amount<K>} leftAmount
175-
* @param {Amount<K>} rightAmount
198+
* @param {AmountBound<K>} rightAmountBound
176199
* @param {Brand<K>} [brand]
177200
* @returns {boolean}
178201
*/
179-
const isGTE = (leftAmount, rightAmount, brand = undefined) => {
180-
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
181-
return h.doIsGTE(...coerceLR(h, leftAmount, rightAmount));
202+
const isGTE = (leftAmount, rightAmountBound, brand = undefined) => {
203+
if (isKey(rightAmountBound)) {
204+
const rightAmount = /** @type {Amount<K>} */ (rightAmountBound);
205+
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
206+
// @ts-expect-error cast?
207+
return h.doIsGTE(...coerceLR(h, leftAmount, rightAmount));
208+
}
209+
mustMatch(leftAmount, AmountShape, 'left amount');
210+
mustMatch(rightAmountBound, AmountBoundShape, 'right amount bound');
211+
const { brand: leftBrand, value: leftValue } = leftAmount;
212+
const { brand: rightBrand, value: rightValueHasBound } = rightAmountBound;
213+
optionalBrandCheck(leftBrand, brand);
214+
optionalBrandCheck(rightBrand, brand);
215+
leftBrand === rightBrand ||
216+
Fail`Brands in left ${q(leftBrand)} and right ${q(
217+
rightBrand,
218+
)} should match but do not`;
219+
const leftKind = assertValueGetAssetKind(leftValue);
220+
// If it were anything else, it would have been a Key and so taken care of
221+
// in the first case above.
222+
mustMatch(rightValueHasBound, AmountValueHasBoundShape, 'right value bound');
223+
const h = helpers[leftKind];
224+
// @ts-expect-error cast?
225+
return h.doIsGTE(h.doCoerce(leftValue), rightValueHasBound);
182226
};
183227

184228
/**
@@ -307,6 +351,7 @@ export const AmountMath = {
307351
*/
308352
isEqual: (leftAmount, rightAmount, brand = undefined) => {
309353
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
354+
// @ts-expect-error cast?
310355
return h.doIsEqual(...coerceLR(h, leftAmount, rightAmount));
311356
},
312357
/**
@@ -324,29 +369,58 @@ export const AmountMath = {
324369
*/
325370
add: (leftAmount, rightAmount, brand = undefined) => {
326371
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
372+
// @ts-expect-error cast?
327373
const value = h.doAdd(...coerceLR(h, leftAmount, rightAmount));
328374
// @ts-expect-error different subtype
329375
return harden({ brand: leftAmount.brand, value });
330376
},
331377
/**
332-
* Returns a new amount that is the leftAmount minus the rightAmount (i.e.
333-
* everything in the leftAmount that is not in the rightAmount). If leftAmount
334-
* doesn't include rightAmount (subtraction results in a negative), throw an
335-
* error. Because the left amount must include the right amount, this is NOT
336-
* equivalent to set subtraction.
378+
* Returns a new amount that is the leftAmount minus the rightAmountBound
379+
* (i.e. everything in the leftAmount that is not in the rightAmountBound). If
380+
* leftAmount doesn't include rightAmountBound (subtraction results in a
381+
* negative), throw an error. Because the left amount must include the right
382+
* amount bound, this is NOT equivalent to set subtraction.
337383
*
338-
* @template {Amount} L
339-
* @template {Amount} R
384+
* @template {AssetKind} K
385+
* @template {Amount<K>} L
386+
* @template {AmountBound<K>} R
340387
* @param {L} leftAmount
341-
* @param {R} rightAmount
388+
* @param {R} rightAmountBound
342389
* @param {Brand} [brand]
343-
* @returns {L extends R ? L : never}
390+
* @returns {L}
344391
*/
345-
subtract: (leftAmount, rightAmount, brand = undefined) => {
346-
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
347-
const value = h.doSubtract(...coerceLR(h, leftAmount, rightAmount));
348-
// @ts-expect-error different subtype
349-
return harden({ brand: leftAmount.brand, value });
392+
subtract: (leftAmount, rightAmountBound, brand = undefined) => {
393+
if (isKey(rightAmountBound)) {
394+
const rightAmount = /** @type {Amount<K>} */ (rightAmountBound);
395+
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
396+
// @ts-expect-error cast?
397+
const value = h.doSubtract(...coerceLR(h, leftAmount, rightAmount));
398+
// @ts-expect-error different subtype
399+
return harden({ brand: leftAmount.brand, value });
400+
}
401+
mustMatch(leftAmount, AmountShape, 'left amount');
402+
mustMatch(rightAmountBound, AmountBoundShape, 'right amount bound');
403+
const { brand: leftBrand, value: leftValue } = leftAmount;
404+
const { brand: rightBrand, value: rightValueHasBound } = rightAmountBound;
405+
optionalBrandCheck(leftBrand, brand);
406+
optionalBrandCheck(rightBrand, brand);
407+
leftBrand === rightBrand ||
408+
Fail`Brands in left ${q(leftBrand)} and right ${q(
409+
rightBrand,
410+
)} should match but do not`;
411+
const leftKind = assertValueGetAssetKind(leftValue);
412+
// If it were anything else, it would have been a Key and so taken care of
413+
// in the first case above.
414+
mustMatch(
415+
rightValueHasBound,
416+
AmountValueHasBoundShape,
417+
'right value bound',
418+
);
419+
const h = helpers[leftKind];
420+
// @ts-expect-error cast?
421+
const value = h.doSubtract(h.doCoerce(leftValue), rightValueHasBound);
422+
// @ts-expect-error cast?
423+
return harden({ brand: leftBrand, value });
350424
},
351425
/**
352426
* Returns the min value between x and y using isGTE

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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ import {
1212
} from '@agoric/store';
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: 12 additions & 4 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,10 @@ const empty = 0n;
1619
* smallest whole unit such that the `natMathHelpers` never deals with
1720
* fractional parts.
1821
*
19-
* @type {MathHelpers<NatValue>}
22+
* For this 'nat' asset kind, the rightBound is always a bigint, since a a
23+
* fungible number has no "elements" to match against an elementPattern.
24+
*
25+
* @type {MathHelpers<'nat', Key, NatValue>}
2026
*/
2127
export const natMathHelpers = harden({
2228
doCoerce: nat => {
@@ -27,9 +33,11 @@ export const natMathHelpers = harden({
2733
},
2834
doMakeEmpty: () => empty,
2935
doIsEmpty: nat => nat === empty,
30-
doIsGTE: (left, right) => left >= right,
36+
doIsGTE: (left, rightBound) =>
37+
left >= /** @type {bigint} */ (/** @type {unknown} */ (rightBound)),
3138
doIsEqual: (left, right) => left === right,
3239
// BigInts don't observably overflow
3340
doAdd: (left, right) => left + right,
34-
doSubtract: (left, right) => Nat(left - right),
41+
doSubtract: (left, rightBound) =>
42+
Nat(left - /** @type {bigint} */ (/** @type {unknown} */ (rightBound))),
3543
});

0 commit comments

Comments
 (0)