11import { 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' ;
55import { natMathHelpers } from './mathHelpers/natMathHelpers.js' ;
66import { setMathHelpers } from './mathHelpers/setMathHelpers.js' ;
77import { copySetMathHelpers } from './mathHelpers/copySetMathHelpers.js' ;
88import { 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 */
112116export 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 */
136142const 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 } ) ;
0 commit comments