Skip to content

Commit

Permalink
fix: amountMath does amount patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Jan 11, 2021
1 parent f7ab514 commit 9f67079
Show file tree
Hide file tree
Showing 29 changed files with 684 additions and 70 deletions.
116 changes: 110 additions & 6 deletions packages/ERTP/src/amountMath.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
// @ts-check

import { assert, details } from '@agoric/assert';
import { assert, details as d } from '@agoric/assert';

import { mustBeComparable } from '@agoric/same-structure';
import {
mustBeComparable,
patternKindOf,
STAR_PATTERN,
isGround,
matches,
} from '@agoric/same-structure';

import './types';
import natMathHelpers from './mathHelpers/natMathHelpers';
Expand Down Expand Up @@ -87,7 +93,7 @@ function makeAmountMath(brand, amountMathKind) {
const helpers = mathHelpers[amountMathKind];
assert(
helpers !== undefined,
details`unrecognized amountMathKind: ${amountMathKind}`,
d`unrecognized amountMathKind: ${amountMathKind}`,
);

// Cache the amount if we can.
Expand All @@ -105,6 +111,10 @@ function makeAmountMath(brand, amountMathKind) {
* @returns {Amount}
*/
make: allegedValue => {
assert(
isGround(allegedValue),
d`allegedValue ${allegedValue} must not be a non-ground pattern`,
);
const value = helpers.doCoerce(allegedValue);
const amount = harden({ brand, value });
cache.add(amount);
Expand All @@ -126,11 +136,11 @@ function makeAmountMath(brand, amountMathKind) {
const { brand: allegedBrand, value } = allegedAmount;
assert(
allegedBrand !== undefined,
details`alleged brand is undefined. Did you pass a value rather than an amount?`,
d`alleged brand is undefined. Did you pass a value rather than an amount?`,
);
assert(
brand === allegedBrand,
details`the brand in the allegedAmount in 'coerce' didn't match the amountMath brand`,
d`the brand in the allegedAmount in 'coerce' didn't match the amountMath brand`,
);
// Will throw on inappropriate value
return amountMath.make(value);
Expand Down Expand Up @@ -181,11 +191,105 @@ function makeAmountMath(brand, amountMathKind) {
amountMath.getValue(rightAmount),
),
),

/**
* TODO explain.
*
* @param {ValuePattern} valuePattern
* @returns {AmountPattern}
*/
makePattern: valuePattern => {
mustBeComparable(valuePattern);
if (isGround(valuePattern)) {
return amountMath.make(valuePattern);
}
return harden({ brand, value: valuePattern });
},

/**
* TODO explain.
*
* @returns {AmountPattern}
*/
makeStarPattern: () => {
return amountMath.makePattern(STAR_PATTERN);
},

/**
* TODO explain.
*
* @param {AmountPattern} allegedAmountPattern
* @returns {AmountPattern} or throws if invalid
*/
coercePattern: allegedAmountPattern => {
const { brand: allegedBrand, value: valuePattern } = allegedAmountPattern;
assert(
allegedBrand !== undefined,
d`alleged brand is undefined. Did you pass a value pattern rather than an amount pattern?`,
);
assert(
brand === allegedBrand,
d`the brand in the allegedAmountPattern in 'coerce' didn't match the amountMath brand`,
);
// Will throw on inappropriate value
return amountMath.makePattern(valuePattern);
},

// TODO explain.
getValuePattern: amountPattern =>
amountMath.coercePattern(amountPattern).value,

frugalSplit: (pattern, specimen) => {
if (isGround(pattern)) {
if (amountMath.isGTE(specimen, pattern)) {
return harden({
matched: pattern,
change: amountMath.subtract(specimen, pattern),
});
}
return undefined;
}
const patternKind = patternKindOf(pattern);
switch (patternKind) {
case '*': {
return harden({
// eslint-disable-next-line no-use-before-define
matched: empty,
change: specimen,
});
}
default: {
const split = helpers.doFrugalSplit(
amountMath.getValuePattern(pattern),
amountMath.getValue(specimen),
);
if (split === undefined) {
return undefined;
}
const { matched: valueMatched, change: valueChange } = split;
const matched = amountMath.make(valueMatched);
const change = amountMath.make(valueChange);
assert(
matches(pattern, matched),
d`Internal: doFrugalSplit algorithm violated matching invariant`,
);
assert(
amountMath.isEqual(amountMath.add(matched, change), specimen),
d`Internal: doFrugalSplit algorithm violated conservation invariant`,
);
return harden({ matched, change });
}
}
},
satisfies: (pattern, specimen) => {
// TODO should somehow enable individual MathKinds to override for
// a more efficient test that gives the same answer.
return undefined !== amountMath.frugalSplit(pattern, specimen);
},
});
const empty = amountMath.make(helpers.doGetEmpty());
return amountMath;
}

harden(makeAmountMath);

export { makeAmountMath };
5 changes: 5 additions & 0 deletions packages/ERTP/src/mathHelpers/natMathHelpers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @ts-check

import Nat from '@agoric/nat';
import { assert, details as d } from '@agoric/assert';

import '../types';

Expand All @@ -26,6 +27,10 @@ const natMathHelpers = harden({
doIsEqual: (left, right) => left === right,
doAdd: (left, right) => Nat(left + right),
doSubtract: (left, right) => Nat(left - right),

doFrugalSplit: (pattern, _specimen) => {
throw assert.fail(d`Unexpected nat pattern ${pattern}`);
},
});

harden(natMathHelpers);
Expand Down
8 changes: 5 additions & 3 deletions packages/ERTP/src/mathHelpers/setMathHelpers.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// @ts-check

import { passStyleOf } from '@agoric/marshal';
import { assert, details } from '@agoric/assert';
import { assert, details as d } from '@agoric/assert';
import { sameStructure } from '@agoric/same-structure';
import { doFrugalSplit } from './strSetMathHelpers';

import '../types';

Expand Down Expand Up @@ -43,7 +44,7 @@ const checkForDupes = buckets => {
for (let j = i + 1; j < maybeMatches.length; j += 1) {
assert(
!sameStructure(maybeMatches[i], maybeMatches[j]),
details`value has duplicates: ${maybeMatches[i]} and ${maybeMatches[j]}`,
d`value has duplicates: ${maybeMatches[i]} and ${maybeMatches[j]}`,
);
}
}
Expand Down Expand Up @@ -92,12 +93,13 @@ const setMathHelpers = harden({
right.forEach(rightElem => {
assert(
hasElement(leftBuckets, rightElem),
details`right element ${rightElem} was not in left`,
d`right element ${rightElem} was not in left`,
);
});
const leftElemNotInRight = leftElem => !hasElement(rightBuckets, leftElem);
return harden(left.filter(leftElemNotInRight));
},
doFrugalSplit,
});

harden(setMathHelpers);
Expand Down
74 changes: 67 additions & 7 deletions packages/ERTP/src/mathHelpers/strSetMathHelpers.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,77 @@
// @ts-check

import { passStyleOf } from '@agoric/marshal';
import { assert, details } from '@agoric/assert';
import { assert, details as d, q } from '@agoric/assert';
import { patternKindOf, matches } from '@agoric/same-structure';

const { entries } = Object;

const identity = harden([]);

const checkForDupes = list => {
const set = new Set(list);
assert(set.size === list.length, details`value has duplicates: ${list}`);
assert(set.size === list.length, d`value has duplicates: ${list}`);
};

/**
* If the pattern is an array of patterns, then we define frugal according
* to a first-fit rule, where the order of elements in the pattern and the
* specimen matter. The most frugal match happens when the pattern is ordered
* from most specific to most general. However, this is advice to the caller,
* not something we either enforce or normalize to. As a result, a frugalSplit
* can fail even if each of the sub patterns could have been matched with
* distinct elements.
*
* For each element of the specimen, from left to right, we see if
* it matches a sub pattern of pattern, from left to right. If so,
* that sub pattern is satisfied and removed from the sub patterns yet
* to be satisfied. The element is moved into matched or change accordingly.
*
* This algorithm is generic across mathKind values that represent a set
* as an array of elements, so that it can be reused by other mathKinds.
*
* @param {ValuePattern} pattern
* @param {Value} specimen
* @returns {ValueSplit | undefined}
*/
const doFrugalSplit = (pattern, specimen) => {
const patternKind = patternKindOf(pattern);
switch (patternKind) {
case undefined: {
if (!Array.isArray(pattern)) {
return undefined;
}
const hungryPatterns = [...pattern];
const matched = [];
const change = [];
for (const element of specimen) {
let found = false;
for (const [iStr, subPattern] of entries(hungryPatterns)) {
if (matches(subPattern, element)) {
matched.push(element);
hungryPatterns.splice(+iStr, 1);
found = true;
break;
}
}
if (!found) {
change.push(element);
}
}
if (hungryPatterns.length >= 1) {
return undefined;
}
return harden({ matched, change });
}
default: {
throw assert.fail(d`Unexpected patternKind ${q(patternKind)}`);
}
}
};

harden(doFrugalSplit);
export { doFrugalSplit };

/**
* Operations for arrays with unique string elements. More information
* about these assets might be provided by some other mechanism, such as
Expand Down Expand Up @@ -43,10 +105,7 @@ const strSetMathHelpers = harden({
doAdd: (left, right) => {
const union = new Set(left);
const addToUnion = elem => {
assert(
!union.has(elem),
details`left and right have same element ${elem}`,
);
assert(!union.has(elem), d`left and right have same element ${elem}`);
union.add(elem);
};
right.forEach(addToUnion);
Expand All @@ -58,10 +117,11 @@ const strSetMathHelpers = harden({
const allRemovedCorrectly = right.every(remove);
assert(
allRemovedCorrectly,
details`some of the elements in right (${right}) were not present in left (${left})`,
d`some of the elements in right (${right}) were not present in left (${left})`,
);
return harden(Array.from(leftSet));
},
doFrugalSplit,
});

harden(strSetMathHelpers);
Expand Down
Loading

0 comments on commit 9f67079

Please sign in to comment.