From ffd5cdda20ffceb5ef624994e9cda00cd1c32fbc Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Mon, 10 Feb 2025 17:36:52 -0800 Subject: [PATCH] fixup! first helpers --- packages/ERTP/src/amountMath.js | 66 +++++++++++++++---- .../ERTP/src/mathHelpers/natMathHelpers.js | 9 ++- .../ERTP/src/mathHelpers/setMathHelpers.js | 52 +++++++++++++-- packages/ERTP/src/typeGuards.js | 2 +- packages/ERTP/src/types.ts | 7 +- 5 files changed, 112 insertions(+), 24 deletions(-) diff --git a/packages/ERTP/src/amountMath.js b/packages/ERTP/src/amountMath.js index ba76ef18df8..477393bbfc2 100644 --- a/packages/ERTP/src/amountMath.js +++ b/packages/ERTP/src/amountMath.js @@ -182,6 +182,17 @@ const coerceLR = (h, leftAmount, rightAmount) => { * A is greater than rectangle B depends on whether rectangle A includes * rectangle B as defined by the logic in MathHelpers. * + * For non-fungible or sem-fungible amounts, the right operand can also be an + * `AmountBound` which can a normal concrete `Amount` or a specialized pattern: + * A `RecordPattern` of a normal concrete `brand: Brand` and a `value: + * AmountValueHasBound`, as made by `M.has(elementPattern)` or + * `M.has(elementPattern, bigint)`. This represents those elements of the value + * collection that match the elementPattern, if that number is exactly the same + * as the bigint argument. If the second argument of `M.has` is omitted, it + * defaults to `1n`. IOW, the left operand is `>=` such a bound if the total + * number of elements in the left operand that match the element pattern is `>=` + * the bigint argument in the `M.has` pattern. + * * @template {AssetKind} K * @param {Amount} leftAmount * @param {AmountBound} rightAmountBound @@ -364,25 +375,52 @@ export const AmountMath = { return harden({ brand: leftAmount.brand, value }); }, /** - * Returns a new amount that is the leftAmount minus the rightAmount (i.e. - * everything in the leftAmount that is not in the rightAmount). If leftAmount - * doesn't include rightAmount (subtraction results in a negative), throw an - * error. Because the left amount must include the right amount, this is NOT - * equivalent to set subtraction. + * Returns a new amount that is the leftAmount minus the rightAmountBound + * (i.e. everything in the leftAmount that is not in the rightAmountBound). If + * leftAmount doesn't include rightAmountBound (subtraction results in a + * negative), throw an error. Because the left amount must include the right + * amount bound, this is NOT equivalent to set subtraction. * - * @template {Amount} L - * @template {Amount} R + * @template {AssetKind} K + * @template {Amount} L + * @template {AmountBound} R * @param {L} leftAmount - * @param {R} rightAmount + * @param {R} rightAmountBound * @param {Brand} [brand] - * @returns {L extends R ? L : never} + * @returns {L} */ - subtract: (leftAmount, rightAmount, brand = undefined) => { - const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand); + subtract: (leftAmount, rightAmountBound, brand = undefined) => { + if (isKey(rightAmountBound)) { + const rightAmount = /** @type {Amount} */ (rightAmountBound); + const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand); + // @ts-expect-error cast? + const value = h.doSubtract(...coerceLR(h, leftAmount, rightAmount)); + // @ts-expect-error different subtype + return harden({ brand: leftAmount.brand, value }); + } + mustMatch(leftAmount, AmountShape, 'left amount'); + mustMatch(rightAmountBound, AmountBoundShape, 'right amount bound'); + const { brand: leftBrand, value: leftValue } = leftAmount; + const { brand: rightBrand, value: rightValueHasBound } = rightAmountBound; + optionalBrandCheck(leftBrand, brand); + optionalBrandCheck(rightBrand, brand); + leftBrand === rightBrand || + Fail`Brands in left ${q(leftBrand)} and right ${q( + rightBrand, + )} should match but do not`; + const leftKind = assertValueGetAssetKind(leftValue); + // If it were anything else, it would have been a Key and so taken care of + // in the first case above. + mustMatch( + rightValueHasBound, + AmountValueHasBoundShape, + 'right value bound', + ); + const h = helpers[leftKind]; // @ts-expect-error cast? - const value = h.doSubtract(...coerceLR(h, leftAmount, rightAmount)); - // @ts-expect-error different subtype - return harden({ brand: leftAmount.brand, value }); + const value = h.doSubtract(h.doCoerce(leftValue), rightValueHasBound); + // @ts-expect-error cast? + return harden({ brand: leftBrand, value }); }, /** * Returns the min value between x and y using isGTE diff --git a/packages/ERTP/src/mathHelpers/natMathHelpers.js b/packages/ERTP/src/mathHelpers/natMathHelpers.js index 0ef4e9c9193..4e2b6ee7b73 100644 --- a/packages/ERTP/src/mathHelpers/natMathHelpers.js +++ b/packages/ERTP/src/mathHelpers/natMathHelpers.js @@ -19,6 +19,9 @@ const empty = 0n; * smallest whole unit such that the `natMathHelpers` never deals with * fractional parts. * + * For this 'nat' asset kind, the rightBound is always a bigint, since a a + * fungible number has no "elements" to match against an elementPattern. + * * @type {MathHelpers<'nat', Key, NatValue>} */ export const natMathHelpers = harden({ @@ -30,9 +33,11 @@ export const natMathHelpers = harden({ }, doMakeEmpty: () => empty, doIsEmpty: nat => nat === empty, - doIsGTE: (left, right) => left >= right, + doIsGTE: (left, rightBound) => + left >= /** @type {bigint} */ (/** @type {unknown} */ (rightBound)), doIsEqual: (left, right) => left === right, // BigInts don't observably overflow doAdd: (left, right) => left + right, - doSubtract: (left, right) => Nat(left - right), + doSubtract: (left, rightBound) => + Nat(left - /** @type {bigint} */ (/** @type {unknown} */ (rightBound))), }); diff --git a/packages/ERTP/src/mathHelpers/setMathHelpers.js b/packages/ERTP/src/mathHelpers/setMathHelpers.js index 3eeae4c50a9..f1b17b77174 100644 --- a/packages/ERTP/src/mathHelpers/setMathHelpers.js +++ b/packages/ERTP/src/mathHelpers/setMathHelpers.js @@ -1,14 +1,15 @@ // @jessie-check -import { passStyleOf } from '@endo/marshal'; +import { passStyleOf } from '@endo/pass-style'; +import { isKey, assertKey, mustMatch, matches } from '@endo/patterns'; import { - assertKey, elementsIsSuperset, elementsDisjointUnion, elementsDisjointSubtract, coerceToElements, elementsCompare, } from '@agoric/store'; +import { AmountValueHasBoundShape } from '../typeGuards.js'; /** * @import {Key} from '@endo/patterns' @@ -36,8 +37,51 @@ export const setMathHelpers = harden({ }, doMakeEmpty: () => empty, doIsEmpty: list => passStyleOf(list) === 'copyArray' && list.length === 0, - doIsGTE: elementsIsSuperset, + doIsGTE: (left, rightBound) => { + if (isKey(rightBound)) { + return elementsIsSuperset(left, rightBound); + } + mustMatch(rightBound, AmountValueHasBoundShape, 'right value bound'); + const { + payload: [elementPatt, bound], + } = rightBound; + if (bound === 0n) { + return true; + } + let count = 0n; + for (const element of left) { + if (matches(element, elementPatt)) { + count += 1n; + } + if (count >= bound) { + return true; + } + } + }, doIsEqual: (x, y) => elementsCompare(x, y) === 0, doAdd: elementsDisjointUnion, - doSubtract: elementsDisjointSubtract, + doSubtract: (left, rightBound) => { + if (isKey(rightBound)) { + return elementsDisjointSubtract(left, rightBound); + } + mustMatch(rightBound, AmountValueHasBoundShape, 'right value bound'); + const { + payload: [elementPatt, bound], + } = rightBound; + if (bound === 0n) { + return []; + } + let count = 0n; + const result = []; + for (const element of left) { + if (matches(element, elementPatt)) { + count += 1n; + } else { + result.push(element); + } + if (count >= bound) { + return result; + } + } + }, }); diff --git a/packages/ERTP/src/typeGuards.js b/packages/ERTP/src/typeGuards.js index a9fc69641f9..3af5448ca73 100644 --- a/packages/ERTP/src/typeGuards.js +++ b/packages/ERTP/src/typeGuards.js @@ -99,7 +99,7 @@ export const AmountPatternShape = M.pattern(); /** @see {AmountValueHasBound} */ export const AmountValueHasBoundShape = M.tagged( 'match:has', - M.splitArray([M.pattern(), M.bigint()], [M.record()]), + M.splitArray([M.pattern(), M.nat()], [M.record()]), ); /** @see {AmountValueBound} */ diff --git a/packages/ERTP/src/types.ts b/packages/ERTP/src/types.ts index fc29da7d7b9..a3566fd5412 100644 --- a/packages/ERTP/src/types.ts +++ b/packages/ERTP/src/types.ts @@ -453,6 +453,7 @@ export type MathHelpers< K extends AssetKind = AssetKind, M extends Key = Key, V extends AssetValueForKind = AssetValueForKind, + VBound extends AmountValueHasBound = AmountValueHasBound, > = { /** * Check the kind of this value and @@ -471,9 +472,9 @@ export type MathHelpers< doIsEmpty: (value: V) => boolean; /** * Is the left greater than - * or equal to the right? + * or equal to the right bound? */ - doIsGTE: (left: V, right: V) => boolean; + doIsGTE: (left: V, rightBound: VBound) => boolean; /** * Does left equal right? */ @@ -488,7 +489,7 @@ export type MathHelpers< * removing the right from the left. If something in the right was not in the * left, we throw an error. */ - doSubtract: (left: V, right: V) => V; + doSubtract: (left: V, rightBound: VBound) => V; }; export type NatValue = bigint; export type SetValue = K[];