Skip to content

Commit

Permalink
feat(patterns): Support CopyMap Patterns
Browse files Browse the repository at this point in the history
Merge pull request #1737 from endojs/gibson-1727-copymap-patterns
  • Loading branch information
gibson042 authored Aug 30, 2023
2 parents da2243e + 1ffb631 commit a3529f7
Show file tree
Hide file tree
Showing 17 changed files with 1,066 additions and 126 deletions.
1 change: 1 addition & 0 deletions packages/marshal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export {
} from './src/encodePassable.js';

export {
trivialComparator,
assertRankSorted,
compareRank,
isRankSorted,
Expand Down
2 changes: 1 addition & 1 deletion packages/marshal/src/rankOrder.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const { entries, fromEntries, setPrototypeOf, is } = Object;
*/
const sameValueZero = (x, y) => x === y || is(x, y);

const trivialComparator = (left, right) =>
export const trivialComparator = (left, right) =>
// eslint-disable-next-line no-nested-ternary, @endo/restrict-comparison-operands
left < right ? -1 : left === right ? 0 : 1;

Expand Down
5 changes: 5 additions & 0 deletions packages/patterns/NEWS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
User-visible changes in `@endo/patterns`:

# Next

- Adds support for CopyMap patterns (e.g., `matches(specimen, makeCopyMap([]))`).
4 changes: 2 additions & 2 deletions packages/patterns/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export {
export { coerceToElements } from './src/keys/copySet.js';
export { coerceToBagEntries } from './src/keys/copyBag.js';
export {
bagCompare,
setCompare,
compareKeys,
keyLT,
keyLTE,
Expand All @@ -36,7 +38,6 @@ export {
elementsDisjointSubtract,
setIsSuperset,
setIsDisjoint,
setCompare,
setUnion,
setDisjointUnion,
setIntersection,
Expand All @@ -45,7 +46,6 @@ export {

export {
bagIsSuperbag,
bagCompare,
bagUnion,
bagIntersection,
bagDisjointSubtract,
Expand Down
17 changes: 17 additions & 0 deletions packages/patterns/src/keys/checkKey.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,23 @@ export const getCopyMapValues = m => {
};
harden(getCopyMapValues);

/**
* Returns an array of a CopyMap's entries in storage order.
*
* @template {Key} K
* @template {Passable} V
* @param {CopyMap<K,V>} m
* @returns {Array<[K,V]>}
*/
export const getCopyMapEntryArray = m => {
assertCopyMap(m);
const {
payload: { keys, values },
} = m;
return harden(keys.map((key, i) => [key, values[i]]));
};
harden(getCopyMapEntryArray);

/**
* @template {Key} K
* @template {Passable} V
Expand Down
102 changes: 79 additions & 23 deletions packages/patterns/src/keys/compareKeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,87 @@ import {
compareRank,
recordNames,
recordValues,
trivialComparator,
} from '@endo/marshal';
import { assertKey } from './checkKey.js';
import { bagCompare } from './merge-bag-operators.js';
import { setCompare } from './merge-set-operators.js';
import {
assertKey,
getCopyBagEntries,
getCopyMapEntryArray,
getCopySetKeys,
} from './checkKey.js';
import { makeCompareCollection } from './keycollection-operators.js';

/** @template {Key} [K=Key] @typedef {import('../types').CopySet<K>} CopySet */

const { quote: q, Fail } = assert;

/**
* CopySet X is smaller than CopySet Y iff all of these conditions hold:
* 1. For every x in X, x is also in Y.
* 2. There is a y in Y that is not in X.
*
* X is equivalent to Y iff the condition 1 holds but condition 2 does not.
*/
export const setCompare = makeCompareCollection(
/** @type {<K extends unknown>(s: CopySet<K>) => Array<[K, 1]>} */ (
s => harden(getCopySetKeys(s).map(key => [key, 1]))
),
0,
trivialComparator,
);
harden(setCompare);

/**
* CopyBag X is smaller than CopyBag Y iff all of these conditions hold
* (where `count(A, a)` is shorthand for the count associated with `a` in `A`):
* 1. For every x in X, x is also in Y and count(X, x) <= count(Y, x).
* 2. There is a y in Y such that y is not in X or count(X, y) < count(Y, y).
*
* X is equivalent to Y iff the condition 1 holds but condition 2 does not.
*/
export const bagCompare = makeCompareCollection(
getCopyBagEntries,
0n,
trivialComparator,
);
harden(bagCompare);

// TODO The desired semantics for CopyMap comparison have not yet been decided.
// See https://github.com/endojs/endo/pull/1737#pullrequestreview-1596595411
// The below is a currently-unused extension of CopyBag semantics (i.e., absent
// entries treated as present with a value that is smaller than everything).
/**
* A unique local value that is guaranteed to not exist in any inbound data
* structure (which would not be the case if we used `Symbol.for`).
*/
const ABSENT = Symbol('absent');
/**
* CopyMap X is smaller than CopyMap Y iff all of these conditions hold:
* 1. X and Y are both Keys (i.e., neither contains non-comparable data).
* 2. For every x in X, x is also in Y and X[x] is smaller than or equivalent to Y[x].
* 3. There is a y in Y such that y is not in X or X[y] is smaller than Y[y].
*
* X is equivalent to Y iff conditions 1 and 2 hold but condition 3 does not.
*/
// eslint-disable-next-line no-underscore-dangle
const _mapCompare = makeCompareCollection(
getCopyMapEntryArray,
ABSENT,
(leftValue, rightValue) => {
if (leftValue === ABSENT && rightValue === ABSENT) {
throw Fail`Internal: Unexpected absent entry pair`;
} else if (leftValue === ABSENT) {
return -1;
} else if (rightValue === ABSENT) {
return 1;
} else {
// eslint-disable-next-line no-use-before-define
return compareKeys(leftValue, rightValue);
}
},
);
harden(_mapCompare);

/** @type {import('../types').KeyCompare} */
export const compareKeys = (left, right) => {
assertKey(left);
Expand Down Expand Up @@ -124,32 +198,14 @@ export const compareKeys = (left, right) => {
}
switch (leftTag) {
case 'copySet': {
// copySet X is smaller than copySet Y when every element of X
// is keyEQ to some element of Y and some element of Y is
// not keyEQ to any element of X.
return setCompare(left, right);
}
case 'copyBag': {
// copyBag X is smaller than copyBag Y when every element of X
// occurs no more than the keyEQ element of Y, and some element
// of Y occurs more than some element of X, where being absent
// from X counts as occurring zero times.
return bagCompare(left, right);
}
case 'copyMap': {
// Two copyMaps that have different keys (according to keyEQ) are
// incommensurate. The representation of copyMaps includes the keys
// first, in the same reverse rank order used by sets. Thus, all
// copyMaps with keys of the same rank (which is
// less precise!) will be grouped together when copyMaps are sorted
// by rank, minimizing the number of misses when range searching.
//
// Among copyMaps with the same keys (according to keyEQ), they
// compare by a corresponding comparison of their values. Thus, as
// with records, for two copyMaps X and Y, if `compareKeys(X,Y) <
// 0` then, because these values obey the above invariants, none of
// the values in X have a later rank than the corresponding value
// of Y. Thus, `compareRank(X,Y) <= 0`. TODO implement
// TODO The desired semantics for CopyMap comparison have not yet been decided.
// See https://github.com/endojs/endo/pull/1737#pullrequestreview-1596595411
throw Fail`Map comparison not yet implemented: ${left} vs ${right}`;
}
default: {
Expand Down
2 changes: 1 addition & 1 deletion packages/patterns/src/keys/copySet.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const checkNoDuplicates = (elements, fullCompare, check) => {
const k0 = elements[i - 1];
const k1 = elements[i];
if (fullCompare(k0, k1) === 0) {
return check(false, X`value has duplicates: ${k0}`);
return check(false, X`value has duplicate keys: ${k0}`);
}
}
return true;
Expand Down
Loading

0 comments on commit a3529f7

Please sign in to comment.