Skip to content

Commit 4426a4a

Browse files
committed
feat(ertp,zoe): minimal want patterns using M.has(el,n)
1 parent 09ccd43 commit 4426a4a

File tree

17 files changed

+363
-137
lines changed

17 files changed

+363
-137
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, AmountValue, 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 {AmountValue} 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
@@ -127,35 +136,38 @@ const optionalBrandCheck = (allegedBrand, brand) => {
127136
};
128137

129138
/**
130-
* @template {AssetKind} K
139+
* @template {AssetKind} K=AssetKind
140+
* @template {Key} M=Key
141+
* @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
131142
* @param {Amount<K>} leftAmount
132143
* @param {Amount<K>} rightAmount
133144
* @param {Brand<K> | undefined} brand
134-
* @returns {MathHelpers<any>}
145+
* @returns {MathHelpers<K, M, V>}
135146
*/
136147
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');
148+
mustMatch(leftAmount, AmountShape, 'left amount');
149+
mustMatch(rightAmount, AmountShape, 'right amount');
150+
const { brand: leftBrand, value: leftValue } = leftAmount;
151+
const { brand: rightBrand, value: rightValue } = rightAmount;
143152
optionalBrandCheck(leftBrand, brand);
144153
optionalBrandCheck(rightBrand, brand);
145154
leftBrand === rightBrand ||
146155
Fail`Brands in left ${q(leftBrand)} and right ${q(
147156
rightBrand,
148157
)} 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;
158+
const leftKind = assertValueGetAssetKind(leftValue);
159+
const rightKind = assertValueGetAssetKind(rightValue);
160+
leftKind === rightKind ||
161+
Fail`The left ${leftAmount} and right amounts ${rightAmount} had different assetKinds: ${q(leftKind)} vs ${q(rightKind)}`;
162+
// @ts-expect-error cast
163+
return helpers[leftKind];
154164
};
155165

156166
/**
157-
* @template {AssetKind} K
158-
* @param {MathHelpers<AssetValueForKind<K>>} h
167+
* @template {AssetKind} K=AssetKind
168+
* @template {Key} M=Key
169+
* @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
170+
* @param {MathHelpers<K, M, V>} h
159171
* @param {Amount<K>} leftAmount
160172
* @param {Amount<K>} rightAmount
161173
* @returns {[K, K]}
@@ -166,21 +178,53 @@ const coerceLR = (h, leftAmount, rightAmount) => {
166178
};
167179

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

186230
/**
@@ -309,6 +353,7 @@ export const AmountMath = {
309353
*/
310354
isEqual: (leftAmount, rightAmount, brand = undefined) => {
311355
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
356+
// @ts-expect-error cast?
312357
return h.doIsEqual(...coerceLR(h, leftAmount, rightAmount));
313358
},
314359
/**
@@ -326,29 +371,58 @@ export const AmountMath = {
326371
*/
327372
add: (leftAmount, rightAmount, brand = undefined) => {
328373
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
374+
// @ts-expect-error cast?
329375
const value = h.doAdd(...coerceLR(h, leftAmount, rightAmount));
330376
// @ts-expect-error different subtype
331377
return harden({ brand: leftAmount.brand, value });
332378
},
333379
/**
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.
380+
* Returns a new amount that is the leftAmount minus the rightAmountBound
381+
* (i.e. everything in the leftAmount that is not in the rightAmountBound). If
382+
* leftAmount doesn't include rightAmountBound (subtraction results in a
383+
* negative), throw an error. Because the left amount must include the right
384+
* amount bound, this is NOT equivalent to set subtraction.
339385
*
340-
* @template {Amount} L
341-
* @template {Amount} R
386+
* @template {AssetKind} K
387+
* @template {Amount<K>} L
388+
* @template {AmountBound<K>} R
342389
* @param {L} leftAmount
343-
* @param {R} rightAmount
390+
* @param {R} rightAmountBound
344391
* @param {Brand} [brand]
345-
* @returns {L extends R ? L : never}
392+
* @returns {L}
346393
*/
347-
subtract: (leftAmount, rightAmount, brand = undefined) => {
348-
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
349-
const value = h.doSubtract(...coerceLR(h, leftAmount, rightAmount));
350-
// @ts-expect-error different subtype
351-
return harden({ brand: leftAmount.brand, value });
394+
subtract: (leftAmount, rightAmountBound, brand = undefined) => {
395+
if (isKey(rightAmountBound)) {
396+
const rightAmount = /** @type {Amount<K>} */ (rightAmountBound);
397+
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
398+
// @ts-expect-error cast?
399+
const value = h.doSubtract(...coerceLR(h, leftAmount, rightAmount));
400+
// @ts-expect-error different subtype
401+
return harden({ brand: leftAmount.brand, value });
402+
}
403+
mustMatch(leftAmount, AmountShape, 'left amount');
404+
mustMatch(rightAmountBound, AmountBoundShape, 'right amount bound');
405+
const { brand: leftBrand, value: leftValue } = leftAmount;
406+
const { brand: rightBrand, value: rightValueHasBound } = rightAmountBound;
407+
optionalBrandCheck(leftBrand, brand);
408+
optionalBrandCheck(rightBrand, brand);
409+
leftBrand === rightBrand ||
410+
Fail`Brands in left ${q(leftBrand)} and right ${q(
411+
rightBrand,
412+
)} should match but do not`;
413+
const leftKind = assertValueGetAssetKind(leftValue);
414+
// If it were anything else, it would have been a Key and so taken care of
415+
// in the first case above.
416+
mustMatch(
417+
rightValueHasBound,
418+
AmountValueHasBoundShape,
419+
'right value bound',
420+
);
421+
const h = helpers[leftKind];
422+
// @ts-expect-error cast?
423+
const value = h.doSubtract(h.doCoerce(leftValue), rightValueHasBound);
424+
// @ts-expect-error cast?
425+
return harden({ brand: leftBrand, value });
352426
},
353427
/**
354428
* 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)