diff --git a/packages/patterns/index.js b/packages/patterns/index.js
index e3a313a049..a00153950d 100644
--- a/packages/patterns/index.js
+++ b/packages/patterns/index.js
@@ -54,6 +54,8 @@ export {
mustMatch,
} from './src/patterns/patternMatchers.js';
+export { mustCompress, mustDecompress } from './src/patterns/compress.js';
+
// ////////////////// Temporary, until these find their proper home ////////////
export { listDifference, objectMap } from './src/utils.js';
diff --git a/packages/patterns/src/keys/checkKey.js b/packages/patterns/src/keys/checkKey.js
index f46b667a2a..46fbc6a950 100644
--- a/packages/patterns/src/keys/checkKey.js
+++ b/packages/patterns/src/keys/checkKey.js
@@ -575,7 +575,7 @@ const checkKeyInternal = (val, check) => {
}
case 'error':
case 'promise': {
- return check(false, X`A ${q(passStyle)} cannot be a key`);
+ return check(false, X`A ${q(passStyle)} cannot be a key: ${val}`);
}
default: {
// Unexpected tags are just non-keys, but an unexpected passStyle
diff --git a/packages/patterns/src/patterns/compress.js b/packages/patterns/src/patterns/compress.js
new file mode 100644
index 0000000000..746017aee7
--- /dev/null
+++ b/packages/patterns/src/patterns/compress.js
@@ -0,0 +1,292 @@
+// @ts-check
+import { assertChecker, makeTagged, passStyleOf } from '@endo/marshal';
+import { recordNames, recordValues } from '@endo/marshal/src/encodePassable.js';
+
+import {
+ kindOf,
+ assertPattern,
+ maybeMatchHelper,
+ matches,
+ checkMatches,
+ mustMatch,
+} from './patternMatchers.js';
+import { isKey } from '../keys/checkKey.js';
+import { keyEQ } from '../keys/compareKeys.js';
+
+/** @typedef {import('@endo/pass-style').Passable} Passable */
+/** @typedef {import('../types.js').Compress} Compress */
+/** @typedef {import('../types.js').MustCompress} MustCompress */
+/** @typedef {import('../types.js').Decompress} Decompress */
+/** @typedef {import('../types.js').MustDecompress} MustDecompress */
+/** @typedef {import('../types.js').Pattern} Pattern */
+
+const { fromEntries } = Object;
+const { Fail, quote: q } = assert;
+
+const isNonCompressingMatcher = pattern => {
+ const patternKind = kindOf(pattern);
+ if (patternKind === undefined) {
+ return false;
+ }
+ const matchHelper = maybeMatchHelper(patternKind);
+ return matchHelper && matchHelper.compress === undefined;
+};
+
+/**
+ * When, for example, all the specimens in a given store match a
+ * specific pattern, then each of those specimens must contain the same
+ * literal superstructure as their one shared pattern. Therefore, storing
+ * that literal superstructure would be redumdant. If `specimen` does
+ * match `pattern`, then `compress(specimen, pattern)` will return a bindings
+ * array which is hopefully more compact than `specimen` as a whole, but
+ * carries all the information from specimen that cannot be derived just
+ * from knowledge that it matches this `pattern`.
+ *
+ * @type {Compress}
+ */
+const compress = (specimen, pattern) => {
+ if (isNonCompressingMatcher(pattern)) {
+ if (matches(specimen, pattern)) {
+ return harden({ compressed: specimen });
+ }
+ return undefined;
+ }
+
+ // Not yet frozen! Used to accumulate bindings
+ const bindings = [];
+ const emitBinding = binding => {
+ bindings.push(binding);
+ };
+ harden(emitBinding);
+
+ /**
+ * @param {Passable} innerSpecimen
+ * @param {Pattern} innerPattern
+ * @returns {boolean}
+ */
+ const compressRecur = (innerSpecimen, innerPattern) => {
+ assertPattern(innerPattern);
+ if (isKey(innerPattern)) {
+ return keyEQ(innerSpecimen, innerPattern);
+ }
+ const patternKind = kindOf(innerPattern);
+ const specimenKind = kindOf(innerSpecimen);
+ switch (patternKind) {
+ case undefined: {
+ return false;
+ }
+ case 'copyArray': {
+ if (
+ specimenKind !== 'copyArray' ||
+ innerSpecimen.length !== innerPattern.length
+ ) {
+ return false;
+ }
+ return innerPattern.every((p, i) => compressRecur(innerSpecimen[i], p));
+ }
+ case 'copyRecord': {
+ if (specimenKind !== 'copyRecord') {
+ return false;
+ }
+ const specimenNames = recordNames(innerSpecimen);
+ const pattNames = recordNames(innerPattern);
+
+ if (specimenNames.length !== pattNames.length) {
+ return false;
+ }
+ const specimenValues = recordValues(innerSpecimen, specimenNames);
+ const pattValues = recordValues(innerPattern, pattNames);
+
+ return pattNames.every(
+ (name, i) =>
+ specimenNames[i] === name &&
+ compressRecur(specimenValues[i], pattValues[i]),
+ );
+ }
+ case 'copyMap': {
+ if (specimenKind !== 'copyMap') {
+ return false;
+ }
+ const {
+ payload: { keys: pattKeys, values: valuePatts },
+ } = innerPattern;
+ const {
+ payload: { keys: specimenKeys, values: specimenValues },
+ } = innerSpecimen;
+ // TODO BUG: this assumes that the keys appear in the
+ // same order, so we can compare values in that order.
+ // However, we're only guaranteed that they appear in
+ // the same rankOrder. Thus we must search one of these
+ // in the other's rankOrder.
+ if (!keyEQ(specimenKeys, pattKeys)) {
+ return false;
+ }
+ return compressRecur(specimenValues, valuePatts);
+ }
+ default:
+ {
+ const matchHelper = maybeMatchHelper(patternKind);
+ if (matchHelper) {
+ if (matchHelper.compress) {
+ const subCompressedRecord = matchHelper.compress(
+ innerSpecimen,
+ innerPattern.payload,
+ compress,
+ );
+ if (subCompressedRecord === undefined) {
+ return false;
+ } else {
+ emitBinding(subCompressedRecord.compressed);
+ return true;
+ }
+ } else if (matches(innerSpecimen, innerPattern)) {
+ assert(isNonCompressingMatcher(innerPattern));
+ emitBinding(innerSpecimen);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+ throw Fail`unrecognized kind: ${q(patternKind)}`;
+ }
+ };
+
+ if (compressRecur(specimen, pattern)) {
+ return harden({ compressed: bindings });
+ } else {
+ return undefined;
+ }
+};
+harden(compress);
+
+/**
+ * `mustCompress` is to `compress` approximately as `fit` is to `matches`.
+ * Where `compress` indicates pattern match failure by returning `undefined`,
+ * `mustCompress` indicates pattern match failure by throwing an error
+ * with a good pattern-match-failure diagnostic. Thus, like `fit`,
+ * `mustCompress` has an additional optional `label` parameter to be used on
+ * the outside of that diagnostic if needed. If `mustCompress` does return
+ * normally, then the pattern match succeeded and `mustCompress` returns a
+ * valid compressed value.
+ *
+ * @type {MustCompress}
+ */
+export const mustCompress = (specimen, pattern, label = undefined) => {
+ const compressedRecord = compress(specimen, pattern);
+ if (compressedRecord !== undefined) {
+ return compressedRecord.compressed;
+ }
+ // `compress` is validating, so we don't need to redo all of `mustMatch`.
+ // We use it only to generate the error.
+ // Should only throw
+ checkMatches(specimen, pattern, assertChecker, label);
+ throw Fail`internal: ${label}: inconsistent pattern match: ${q(pattern)}`;
+};
+harden(mustCompress);
+
+/**
+ * `decompress` reverses the compression performed by `compress`
+ * or `mustCompress`, in order to recover the equivalent
+ * of the original specimen from the `bindings` array and the `pattern`.
+ *
+ * @type {Decompress}
+ */
+const decompress = (compressed, pattern) => {
+ if (isNonCompressingMatcher(pattern)) {
+ return compressed;
+ }
+
+ assert(Array.isArray(compressed));
+ passStyleOf(compressed) === 'copyArray' ||
+ Fail`Pattern ${pattern} expected bindings array: ${compressed}`;
+ let i = 0;
+ const takeBinding = () => {
+ i < compressed.length ||
+ Fail`Pattern ${q(pattern)} expects more than ${q(
+ compressed.length,
+ )} bindings: ${compressed}`;
+ const binding = compressed[i];
+ i += 1;
+ return binding;
+ };
+ harden(takeBinding);
+
+ const decompressRecur = innerPattern => {
+ assertPattern(innerPattern);
+ if (isKey(innerPattern)) {
+ return innerPattern;
+ }
+ const patternKind = kindOf(innerPattern);
+ switch (patternKind) {
+ case undefined: {
+ throw Fail`decompress expected a pattern: ${q(innerPattern)}`;
+ }
+ case 'copyArray': {
+ return harden(innerPattern.map(p => decompressRecur(p)));
+ }
+ case 'copyRecord': {
+ const pattNames = recordNames(innerPattern);
+ const pattValues = recordValues(innerPattern, pattNames);
+ const entries = pattNames.map((name, j) => [
+ name,
+ decompressRecur(pattValues[j]),
+ ]);
+ // Reverse so printed form looks less surprising,
+ // with ascenting rather than descending property names.
+ return harden(fromEntries(entries.reverse()));
+ }
+ case 'copyMap': {
+ const {
+ payload: { keys: pattKeys, values: valuePatts },
+ } = innerPattern;
+ return makeTagged(
+ 'copyMap',
+ harden({
+ keys: pattKeys,
+ values: valuePatts.map(p => decompressRecur(p)),
+ }),
+ );
+ }
+ default:
+ {
+ const matchHelper = maybeMatchHelper(patternKind);
+ if (matchHelper) {
+ if (matchHelper.decompress) {
+ const subCompressed = takeBinding();
+ return matchHelper.decompress(
+ subCompressed,
+ innerPattern.payload,
+ decompress,
+ );
+ } else {
+ assert(isNonCompressingMatcher(innerPattern));
+ return takeBinding();
+ }
+ }
+ }
+ throw Fail`unrecognized pattern kind: ${q(patternKind)} ${q(
+ innerPattern,
+ )}`;
+ }
+ };
+
+ return decompressRecur(pattern);
+};
+harden(decompress);
+
+/**
+ * `decompress` reverses the compression performed by `compress`
+ * or `mustCompress`, in order to recover the equivalent
+ * of the original specimen from `compressed` and `pattern`.
+ *
+ * @type {MustDecompress}
+ */
+export const mustDecompress = (compressed, pattern, label = undefined) => {
+ const value = decompress(compressed, pattern);
+ // `decompress` does some checking, but is not validating, so we
+ // need to do the full `mustMatch` here to validate as well as to generate
+ // the error if invalid.
+ mustMatch(value, pattern, label);
+ return value;
+};
diff --git a/packages/patterns/src/patterns/internal-types.js b/packages/patterns/src/patterns/internal-types.js
index c354f879b1..3773de187e 100644
--- a/packages/patterns/src/patterns/internal-types.js
+++ b/packages/patterns/src/patterns/internal-types.js
@@ -1,11 +1,11 @@
///
-/** @typedef {import('@endo/marshal').Passable} Passable */
-/** @typedef {import('@endo/marshal').PassStyle} PassStyle */
-/** @typedef {import('@endo/marshal').CopyTagged} CopyTagged */
-/** @template T @typedef {import('@endo/marshal').CopyRecord} CopyRecord */
-/** @template T @typedef {import('@endo/marshal').CopyArray} CopyArray */
-/** @typedef {import('@endo/marshal').Checker} Checker */
+/** @typedef {import('@endo/pass-style').Passable} Passable */
+/** @typedef {import('@endo/pass-style').PassStyle} PassStyle */
+/** @typedef {import('@endo/pass-style').CopyTagged} CopyTagged */
+/** @template T @typedef {import('@endo/pass-style').CopyRecord} CopyRecord */
+/** @template T @typedef {import('@endo/pass-style').CopyArray} CopyArray */
+/** @typedef {import('@endo/pass-style').Checker} Checker */
/** @typedef {import('@endo/marshal').RankCompare} RankCompare */
/** @typedef {import('@endo/marshal').RankCover} RankCover */
@@ -15,6 +15,7 @@
/** @typedef {import('../types.js').InterfaceGuard} InterfaceGuard */
/** @typedef {import('../types.js').MethodGuardMaker0} MethodGuardMaker0 */
+/** @typedef {import('../types.js').Kind} Kind */
/** @typedef {import('../types').MatcherNamespace} MatcherNamespace */
/** @typedef {import('../types').Key} Key */
/** @typedef {import('../types').Pattern} Pattern */
@@ -23,12 +24,20 @@
/** @typedef {import('../types').AllLimits} AllLimits */
/** @typedef {import('../types').GetRankCover} GetRankCover */
+/** @typedef {import('../types.js').CompressedRecord} CompressedRecord */
+/** @typedef {import('../types.js').Compress} Compress */
+/** @typedef {import('../types.js').MustCompress} MustCompress */
+/** @typedef {import('../types.js').Decompress} Decompress */
+/** @typedef {import('../types.js').MustDecompress} MustDecompress */
+
/**
* @typedef {object} MatchHelper
* This factors out only the parts specific to each kind of Matcher. It is
* encapsulated, and its methods can make the stated unchecked assumptions
* enforced by the common calling logic.
*
+ * @property {string} tag
+ *
* @property {(allegedPayload: Passable,
* check: Checker
* ) => boolean} checkIsWellFormed
@@ -42,6 +51,27 @@
* Assuming validity of `matcherPayload` as the payload of a Matcher corresponding
* with this MatchHelper, reports whether `specimen` is matched by that Matcher.
*
+ * @property {(specimen: Passable,
+ * matcherPayload: Passable,
+ * compress: Compress
+ * ) => (CompressedRecord | undefined)} [compress]
+ * Assuming a valid Matcher of this type with `matcherPayload` as its
+ * payload, if this specimen matches this matcher, then return a
+ * CompressedRecord that represents this specimen,
+ * perhaps more compactly, given the knowledge that it matches this matcher.
+ * If the specimen does not match the matcher, return undefined.
+ * If this matcher has a `compress` method, then it must have a matching
+ * `decompress` method.
+ *
+ * @property {(compressed: Passable,
+ * matcherPayload: Passable,
+ * decompress: Decompress
+ * ) => Passable} [decompress]
+ * If `compressed` is the result of a successful `compress` with this matcher,
+ * then `decompress` must return a Passable equivalent to the original specimen.
+ * If this matcher has an `decompress` method, then it must have a matching
+ * `compress` method.
+ *
* @property {import('../types').GetRankCover} getRankCover
* Assumes this is the payload of a CopyTagged with the corresponding
* matchTag. Return a RankCover to bound from below and above,
@@ -63,5 +93,7 @@
* @property {(patt: Pattern) => void} assertPattern
* @property {(patt: Passable) => boolean} isPattern
* @property {GetRankCover} getRankCover
+ * @property {(passable: Passable, check?: Checker) => (Kind | undefined)} kindOf
+ * @property {(tag: string) => (MatchHelper | undefined)} maybeMatchHelper
* @property {MatcherNamespace} M
*/
diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js
index 79173f6e82..21b9bc5de6 100644
--- a/packages/patterns/src/patterns/patternMatchers.js
+++ b/packages/patterns/src/patterns/patternMatchers.js
@@ -1,6 +1,5 @@
import {
assertChecker,
- Far,
getTag,
makeTagged,
passStyleOf,
@@ -18,6 +17,7 @@ import {
applyLabelingError,
fromUniqueEntries,
listDifference,
+ objectMap,
} from '../utils.js';
import { keyEQ, keyGT, keyGTE, keyLT, keyLTE } from '../keys/compareKeys.js';
@@ -30,6 +30,8 @@ import {
checkCopyMap,
copyMapKeySet,
checkCopyBag,
+ makeCopySet,
+ makeCopyBag,
} from '../keys/checkKey.js';
import './internal-types.js';
@@ -38,7 +40,13 @@ import './internal-types.js';
const { quote: q, details: X, Fail } = assert;
const { entries, values } = Object;
-const { ownKeys } = Reflect;
+const { ownKeys, apply } = Reflect;
+
+// TODO simplify once we can assume Object.hasOwn everywhere. This probably
+// means, when we stop supporting Node 14.
+const { hasOwnProperty } = Object.prototype;
+const hasOwn =
+ Object.hasOwn || ((obj, name) => apply(hasOwnProperty, obj, [name]));
/** @type {WeakSet} */
const patternMemo = new WeakSet();
@@ -81,50 +89,6 @@ export const defaultLimits = harden({
const limit = (limits = {}) =>
/** @type {AllLimits} */ (harden({ __proto__: defaultLimits, ...limits }));
-const checkIsWellFormedWithLimit = (
- payload,
- mainPayloadShape,
- check,
- label,
-) => {
- assert(Array.isArray(mainPayloadShape));
- if (!Array.isArray(payload)) {
- return check(false, X`${q(label)} payload must be an array: ${payload}`);
- }
-
- // Was the following, but its overuse of patterns caused an infinite regress
- // const payloadLimitShape = harden(
- // M.split(
- // mainPayloadShape,
- // M.partial(harden([M.recordOf(M.string(), M.number())]), harden([])),
- // ),
- // );
- // return checkMatches(payload, payloadLimitShape, check, label);
-
- const mainLength = mainPayloadShape.length;
- if (!(payload.length === mainLength || payload.length === mainLength + 1)) {
- return check(false, X`${q(label)} payload unexpected size: ${payload}`);
- }
- const limits = payload[mainLength];
- payload = harden(payload.slice(0, mainLength));
- // eslint-disable-next-line no-use-before-define
- if (!checkMatches(payload, mainPayloadShape, check, label)) {
- return false;
- }
- if (limits === undefined) {
- return true;
- }
- return (
- (passStyleOf(limits) === 'copyRecord' ||
- check(false, X`Limits must be a record: ${q(limits)}`)) &&
- entries(limits).every(
- ([key, value]) =>
- passStyleOf(value) === 'number' ||
- check(false, X`Value of limit ${q(key)} but be a number: ${q(value)}`),
- )
- );
-};
-
/**
* @param {unknown} specimen
* @param {number} decimalDigitsLimit
@@ -147,6 +111,57 @@ const checkDecimalDigitsLimit = (specimen, decimalDigitsLimit, check) => {
* @returns {PatternKit}
*/
const makePatternKit = () => {
+ // Define early to break a circularity is use of checkIsWellFormedWithLimit
+ const PatternShape = makeTagged('match:pattern', undefined);
+
+ // Define within makePatternKit so can use checkMatches early.
+ const checkIsWellFormedWithLimit = (
+ payload,
+ mainPayloadShape,
+ check,
+ label,
+ ) => {
+ assert(Array.isArray(mainPayloadShape));
+ if (!Array.isArray(payload)) {
+ return check(false, X`${q(label)} payload must be an array: ${payload}`);
+ }
+
+ // Was the following, but its overuse of patterns caused an infinite regress
+ // const payloadLimitShape = harden(
+ // M.split(
+ // mainPayloadShape,
+ // M.partial(harden([M.recordOf(M.string(), M.number())]), harden([])),
+ // ),
+ // );
+ // return checkMatches(payload, payloadLimitShape, check, label);
+
+ const mainLength = mainPayloadShape.length;
+ if (!(payload.length === mainLength || payload.length === mainLength + 1)) {
+ return check(false, X`${q(label)} payload unexpected size: ${payload}`);
+ }
+ const limits = payload[mainLength];
+ payload = harden(payload.slice(0, mainLength));
+ // eslint-disable-next-line no-use-before-define
+ if (!checkMatches(payload, mainPayloadShape, check, label)) {
+ return false;
+ }
+ if (limits === undefined) {
+ return true;
+ }
+ return (
+ (passStyleOf(limits) === 'copyRecord' ||
+ check(false, X`Limits must be a record: ${q(limits)}`)) &&
+ entries(limits).every(
+ ([key, value]) =>
+ passStyleOf(value) === 'number' ||
+ check(
+ false,
+ X`Value of limit ${q(key)} but be a number: ${q(value)}`,
+ ),
+ )
+ );
+ };
+
/**
* If this is a recognized match tag, return the MatchHelper.
* Otherwise result undefined.
@@ -170,15 +185,6 @@ const makePatternKit = () => {
// eslint-disable-next-line no-use-before-define
GuardPayloadShapes[tag];
- /**
- * @typedef {string} Kind
- * It is either a PassStyle other than 'tagged', or, if the underlying
- * PassStyle is 'tagged', then the `getTag` value for tags that are
- * recognized at the store level of abstraction. For each of those
- * tags, a tagged record only has that kind if it satisfies the invariants
- * that the store level associates with that kind.
- */
-
/**
* @type {WeakMap}
* Only for tagged records of recognized kinds whose store-level invariants
@@ -259,6 +265,17 @@ const makePatternKit = () => {
};
harden(kindOf);
+ const matchHelperTagRE = harden(/^match:(\w+)(:\w+)?$/);
+
+ const getMatchSubTag = tag => {
+ const parts = matchHelperTagRE.exec(tag);
+ if (parts && parts[2] !== undefined) {
+ return `match:${parts[1]}`;
+ } else {
+ return undefined;
+ }
+ };
+
/**
* Checks only recognized kinds, and only if the specimen
* passes the invariants associated with that recognition.
@@ -270,8 +287,14 @@ const makePatternKit = () => {
*/
const checkKind = (specimen, kind, check) => {
const realKind = kindOf(specimen, check);
- if (kind === realKind) {
- return true;
+ if (realKind !== undefined) {
+ if (kind === realKind) {
+ return true;
+ }
+ const subTag = getMatchSubTag(realKind);
+ if (subTag !== undefined && kind === subTag) {
+ return true;
+ }
}
if (check !== identChecker) {
// quoting without quotes
@@ -436,6 +459,15 @@ const makePatternKit = () => {
case 'copySet':
case 'copyBag':
case 'remotable': {
+ if (!isKey(specimen)) {
+ assert(specimenKind !== patternKind);
+ return check(
+ false,
+ X`${specimen} - Must be a ${patternKind} to match a ${patternKind} pattern: ${q(
+ patt,
+ )}`,
+ );
+ }
// These kinds are necessarily keys
return checkAsKeyPatt(specimen, patt, check);
}
@@ -703,10 +735,50 @@ const makePatternKit = () => {
);
};
+ /**
+ * @param { Passable[] } array
+ * @param { Pattern } patt
+ * @param {Compress} compress
+ * @returns {Passable[] | undefined}
+ */
+ const arrayCompressMatchPattern = (array, patt, compress) => {
+ if (isKind(patt, 'match:any')) {
+ return array;
+ }
+ const bindings = [];
+ for (const el of array) {
+ const subCompressedRecord = compress(el, patt);
+ if (subCompressedRecord) {
+ bindings.push(subCompressedRecord.compressed);
+ } else {
+ return undefined;
+ }
+ }
+ return harden(bindings);
+ };
+
+ /**
+ * @param {Passable} compressed
+ * @param {Pattern} patt
+ * @param {Decompress} decompress
+ * @returns {Passable[]}
+ */
+ const arrayDecompressMatchPattern = (compressed, patt, decompress) => {
+ if (!Array.isArray(compressed)) {
+ throw Fail`Compressed array must be an array: ${compressed}`;
+ }
+ if (isKind(patt, 'match:any')) {
+ return compressed;
+ }
+ return harden(compressed.map(subBindings => decompress(subBindings, patt)));
+ };
+
// /////////////////////// Match Helpers /////////////////////////////////////
/** @type {MatchHelper} */
- const matchAnyHelper = Far('match:any helper', {
+ const matchAnyHelper = harden({
+ tag: 'match:any',
+
checkMatches: (_specimen, _matcherPayload, _check) => true,
checkIsWellFormed: (matcherPayload, check) =>
@@ -717,16 +789,40 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchAndHelper = Far('match:and helper', {
+ const matchAndHelper = harden({
+ tag: 'match:and:1',
+
checkMatches: (specimen, patts, check) => {
return patts.every(patt => checkMatches(specimen, patt, check));
},
+ // Compress only according to the last conjunct
+ compress: (specimen, patts, compress) => {
+ const { length } = patts;
+ // We know there are at least two patts
+ const lastPatt = patts[length - 1];
+ const allButLast = patts.slice(0, length - 1);
+ if (
+ !allButLast.every(patt => checkMatches(specimen, patt, identChecker))
+ ) {
+ return undefined;
+ }
+ return compress(specimen, lastPatt);
+ },
+
+ decompress: (compressed, patts, decompress) => {
+ const lastPatt = patts[patts.length - 1];
+ return decompress(compressed, lastPatt);
+ },
+
checkIsWellFormed: (allegedPatts, check) => {
const checkIt = patt => checkPattern(patt, check);
return (
(passStyleOf(allegedPatts) === 'copyArray' ||
- check(false, X`Needs array of sub-patterns: ${q(allegedPatts)}`)) &&
+ check(false, X`Needs array of sub-patterns: ${allegedPatts}`)) &&
+ Array.isArray(allegedPatts) && // redundant. just for type checker
+ (allegedPatts.length >= 2 ||
+ check(false, X`Must have at least two sub-patterns`)) &&
allegedPatts.every(checkIt)
);
},
@@ -739,7 +835,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchOrHelper = Far('match:or helper', {
+ const matchOrHelper = harden({
+ tag: 'match:or:1',
+
checkMatches: (specimen, patts, check) => {
const { length } = patts;
if (length === 0) {
@@ -750,9 +848,8 @@ const makePatternKit = () => {
}
if (
patts.length === 2 &&
- !matches(specimen, patts[0]) &&
- isKind(patts[0], 'match:kind') &&
- patts[0].payload === 'undefined'
+ patts[0] === undefined &&
+ !matches(specimen, undefined)
) {
// Worth special casing the optional pattern for
// better error messages.
@@ -764,6 +861,31 @@ const makePatternKit = () => {
return check(false, X`${specimen} - Must match one of ${q(patts)}`);
},
+ // Compress to an array pair of the index of the
+ // first disjunct that succeeded, and the compressed according to
+ // that disjunct.
+ compress: (specimen, patts, compress) => {
+ assert(Array.isArray(patts)); // redundant. Just for type checker.
+ const { length } = patts;
+ if (length === 0) {
+ return undefined;
+ }
+ for (let i = 0; i < length; i += 1) {
+ const subCompressedRecord = compress(specimen, patts[i]);
+ if (subCompressedRecord !== undefined) {
+ return harden({ compressed: [i, subCompressedRecord.compressed] });
+ }
+ }
+ return undefined;
+ },
+
+ decompress: (compressed, patts, decompress) => {
+ (Array.isArray(compressed) && compressed.length === 2) ||
+ Fail`Or compression must be a case index and a compression by that case: ${compressed}`;
+ const [i, subCompressed] = compressed;
+ return decompress(harden(subCompressed), patts[i]);
+ },
+
checkIsWellFormed: matchAndHelper.checkIsWellFormed,
getRankCover: (patts, encodePassable) =>
@@ -774,7 +896,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchNotHelper = Far('match:not helper', {
+ const matchNotHelper = harden({
+ tag: 'match:not',
+
checkMatches: (specimen, patt, check) => {
if (matches(specimen, patt)) {
return check(
@@ -792,7 +916,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchScalarHelper = Far('match:scalar helper', {
+ const matchScalarHelper = harden({
+ tag: 'match:scalar',
+
checkMatches: (specimen, _matcherPayload, check) =>
checkScalarKey(specimen, check),
@@ -802,7 +928,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchKeyHelper = Far('match:key helper', {
+ const matchKeyHelper = harden({
+ tag: `match:key`,
+
checkMatches: (specimen, _matcherPayload, check) =>
checkKey(specimen, check),
@@ -812,7 +940,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchPatternHelper = Far('match:pattern helper', {
+ const matchPatternHelper = harden({
+ tag: `match:pattern`,
+
checkMatches: (specimen, _matcherPayload, check) =>
checkPattern(specimen, check),
@@ -822,7 +952,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchKindHelper = Far('match:kind helper', {
+ const matchKindHelper = harden({
+ tag: `match:kind`,
+
checkMatches: checkKind,
checkIsWellFormed: (allegedKeyKind, check) =>
@@ -850,7 +982,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchBigintHelper = Far('match:bigint helper', {
+ const matchBigintHelper = harden({
+ tag: `match:bigint`,
+
checkMatches: (specimen, [limits = undefined], check) => {
const { decimalDigitsLimit } = limit(limits);
return (
@@ -872,7 +1006,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchNatHelper = Far('match:nat helper', {
+ const matchNatHelper = harden({
+ tag: `match:nat`,
+
checkMatches: (specimen, [limits = undefined], check) => {
const { decimalDigitsLimit } = limit(limits);
return (
@@ -899,7 +1035,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchStringHelper = Far('match:string helper', {
+ const matchStringHelper = harden({
+ tag: `match:string`,
+
checkMatches: (specimen, [limits = undefined], check) => {
const { stringLengthLimit } = limit(limits);
// prettier-ignore
@@ -927,7 +1065,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchSymbolHelper = Far('match:symbol helper', {
+ const matchSymbolHelper = harden({
+ tag: `match:symbol`,
+
checkMatches: (specimen, [limits = undefined], check) => {
const { symbolNameLengthLimit } = limit(limits);
if (!checkKind(specimen, 'symbol', check)) {
@@ -959,7 +1099,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchRemotableHelper = Far('match:remotable helper', {
+ const matchRemotableHelper = harden({
+ tag: `match:remotable`,
+
checkMatches: (specimen, remotableDesc, check) => {
// Unfortunate duplication of checkKind logic, but no better choices.
if (isKind(specimen, 'remotable')) {
@@ -995,7 +1137,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchLTEHelper = Far('match:lte helper', {
+ const matchLTEHelper = harden({
+ tag: `match:lte`,
+
checkMatches: (specimen, rightOperand, check) =>
keyLTE(specimen, rightOperand) ||
check(false, X`${specimen} - Must be <= ${rightOperand}`),
@@ -1017,7 +1161,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchLTHelper = Far('match:lt helper', {
+ const matchLTHelper = harden({
+ tag: `match:lt`,
+
checkMatches: (specimen, rightOperand, check) =>
keyLT(specimen, rightOperand) ||
check(false, X`${specimen} - Must be < ${rightOperand}`),
@@ -1028,7 +1174,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchGTEHelper = Far('match:gte helper', {
+ const matchGTEHelper = harden({
+ tag: `match:gte`,
+
checkMatches: (specimen, rightOperand, check) =>
keyGTE(specimen, rightOperand) ||
check(false, X`${specimen} - Must be >= ${rightOperand}`),
@@ -1050,7 +1198,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchGTHelper = Far('match:gt helper', {
+ const matchGTHelper = harden({
+ tag: `match:gt`,
+
checkMatches: (specimen, rightOperand, check) =>
keyGT(specimen, rightOperand) ||
check(false, X`${specimen} - Must be > ${rightOperand}`),
@@ -1061,7 +1211,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchRecordOfHelper = Far('match:recordOf helper', {
+ const matchRecordOfHelper = harden({
+ tag: `match:recordOf`,
+
checkMatches: (
specimen,
[keyPatt, valuePatt, limits = undefined],
@@ -1101,7 +1253,7 @@ const makePatternKit = () => {
checkIsWellFormed: (payload, check) =>
checkIsWellFormedWithLimit(
payload,
- harden([MM.pattern(), MM.pattern()]),
+ harden([PatternShape, PatternShape]),
check,
'match:recordOf payload',
),
@@ -1110,7 +1262,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchArrayOfHelper = Far('match:arrayOf helper', {
+ const matchArrayOfHelper = harden({
+ tag: `match:arrayOf:1`,
+
checkMatches: (specimen, [subPatt, limits = undefined], check) => {
const { arrayLengthLimit } = limit(limits);
// prettier-ignore
@@ -1125,10 +1279,33 @@ const makePatternKit = () => {
);
},
+ // Compress to an array of corresponding bindings arrays
+ compress: (specimen, [subPatt, limits = undefined], compress) => {
+ const { arrayLengthLimit } = limit(limits);
+ if (
+ isKind(specimen, 'copyArray') &&
+ Array.isArray(specimen) && // redundant. just for type checker.
+ specimen.length <= arrayLengthLimit
+ ) {
+ const compressed = arrayCompressMatchPattern(
+ specimen,
+ subPatt,
+ compress,
+ );
+ if (compressed) {
+ return harden({ compressed });
+ }
+ }
+ return undefined;
+ },
+
+ decompress: (compressed, [subPatt, _limits = undefined], decompress) =>
+ arrayDecompressMatchPattern(compressed, subPatt, decompress),
+
checkIsWellFormed: (payload, check) =>
checkIsWellFormedWithLimit(
payload,
- harden([MM.pattern()]),
+ harden([PatternShape]),
check,
'match:arrayOf payload',
),
@@ -1137,7 +1314,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchSetOfHelper = Far('match:setOf helper', {
+ const matchSetOfHelper = harden({
+ tag: `match:setOf:1`,
+
checkMatches: (specimen, [keyPatt, limits = undefined], check) => {
const { numSetElementsLimit } = limit(limits);
return (
@@ -1152,10 +1331,32 @@ const makePatternKit = () => {
);
},
+ // Compress to an array of corresponding bindings arrays
+ compress: (specimen, [keyPatt, limits = undefined], compress) => {
+ const { numSetElementsLimit } = limit(limits);
+ if (
+ isKind(specimen, 'copySet') &&
+ /** @type {Array} */ (specimen.payload).length <= numSetElementsLimit
+ ) {
+ const compressed = arrayCompressMatchPattern(
+ specimen.payload,
+ keyPatt,
+ compress,
+ );
+ if (compressed) {
+ return harden({ compressed });
+ }
+ }
+ return undefined;
+ },
+
+ decompress: (compressed, [keyPatt, _limits = undefined], decompress) =>
+ makeCopySet(arrayDecompressMatchPattern(compressed, keyPatt, decompress)),
+
checkIsWellFormed: (payload, check) =>
checkIsWellFormedWithLimit(
payload,
- harden([MM.pattern()]),
+ harden([PatternShape]),
check,
'match:setOf payload',
),
@@ -1164,7 +1365,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchBagOfHelper = Far('match:bagOf helper', {
+ const matchBagOfHelper = harden({
+ tag: `match:bagOf:1`,
+
checkMatches: (
specimen,
[keyPatt, countPatt, limits = undefined],
@@ -1193,10 +1396,50 @@ const makePatternKit = () => {
);
},
+ // Compress to an array of corresponding bindings arrays
+ compress: (
+ specimen,
+ [keyPatt, countPatt, limits = undefined],
+ compress,
+ ) => {
+ const { numUniqueBagElementsLimit, decimalDigitsLimit } = limit(limits);
+ if (
+ isKind(specimen, 'copyBag') &&
+ /** @type {Array} */ (specimen.payload).length <=
+ numUniqueBagElementsLimit &&
+ specimen.payload.every(([_key, count]) =>
+ checkDecimalDigitsLimit(count, decimalDigitsLimit, identChecker),
+ )
+ ) {
+ const compressed = arrayCompressMatchPattern(
+ specimen.payload,
+ harden([keyPatt, countPatt]),
+ compress,
+ );
+ if (compressed) {
+ return harden({ compressed });
+ }
+ }
+ return undefined;
+ },
+
+ decompress: (
+ compressed,
+ [keyPatt, countPatt, _limits = undefined],
+ decompress,
+ ) =>
+ makeCopyBag(
+ arrayDecompressMatchPattern(
+ compressed,
+ harden([keyPatt, countPatt]),
+ decompress,
+ ),
+ ),
+
checkIsWellFormed: (payload, check) =>
checkIsWellFormedWithLimit(
payload,
- harden([MM.pattern(), MM.pattern()]),
+ harden([PatternShape, PatternShape]),
check,
'match:bagOf payload',
),
@@ -1205,7 +1448,9 @@ const makePatternKit = () => {
});
/** @type {MatchHelper} */
- const matchMapOfHelper = Far('match:mapOf helper', {
+ const matchMapOfHelper = harden({
+ tag: `match:mapOf:1`,
+
checkMatches: (
specimen,
[keyPatt, valuePatt, limits = undefined],
@@ -1236,10 +1481,69 @@ const makePatternKit = () => {
);
},
+ // Compress to a pair of bindings arrays, one for the keys
+ // and a matching one for the values.
+ compress: (
+ specimen,
+ [keyPatt, valuePatt, limits = undefined],
+ compress,
+ ) => {
+ const { numMapEntriesLimit } = limit(limits);
+ if (
+ isKind(specimen, 'copyMap') &&
+ /** @type {Array} */ (specimen.payload.keys).length <=
+ numMapEntriesLimit
+ ) {
+ const compressedKeys = arrayCompressMatchPattern(
+ specimen.payload.keys,
+ keyPatt,
+ compress,
+ );
+ if (compressedKeys) {
+ const compressedValues = arrayCompressMatchPattern(
+ specimen.payload.values,
+ valuePatt,
+ compress,
+ );
+ if (compressedValues) {
+ return harden({
+ compressed: [compressedKeys, compressedValues],
+ });
+ }
+ }
+ }
+ return undefined;
+ },
+
+ decompress: (
+ compressed,
+ [keyPatt, valuePatt, _limits = undefined],
+ decompress,
+ ) => {
+ (Array.isArray(compressed) && compressed.length === 2) ||
+ Fail`Compressed map should be a pair of compressed keys and compressed values ${compressed}`;
+ const [compressedKeys, compressedvalues] = compressed;
+ return makeTagged(
+ 'copyMap',
+ harden({
+ keys: arrayDecompressMatchPattern(
+ compressedKeys,
+ keyPatt,
+ decompress,
+ ),
+ values: arrayDecompressMatchPattern(
+ compressedvalues,
+ valuePatt,
+ decompress,
+ ),
+ }),
+ );
+ },
+
checkIsWellFormed: (payload, check) =>
checkIsWellFormedWithLimit(
payload,
- harden([MM.pattern(), MM.pattern()]),
+ harden([PatternShape, PatternShape]),
check,
'match:mapOf payload',
),
@@ -1282,7 +1586,9 @@ const makePatternKit = () => {
harden(optionalPatt.slice(0, length).map(patt => MM.opt(patt)));
/** @type {MatchHelper} */
- const matchSplitArrayHelper = Far('match:splitArray helper', {
+ const matchSplitArrayHelper = harden({
+ tag: `match:splitArray:1`,
+
checkMatches: (
specimen,
[requiredPatt, optionalPatt = [], restPatt = MM.any()],
@@ -1318,6 +1624,70 @@ const makePatternKit = () => {
);
},
+ compress: (
+ specimen,
+ [requiredPatt, optionalPatt = [], restPatt = MM.any()],
+ compress,
+ ) => {
+ if (!checkKind(specimen, 'copyArray', identChecker)) {
+ return undefined;
+ }
+ const { requiredSpecimen, optionalSpecimen, restSpecimen } =
+ splitArrayParts(specimen, requiredPatt, optionalPatt);
+ const partialPatt = adaptArrayPattern(
+ optionalPatt,
+ optionalSpecimen.length,
+ );
+ const compressedRequired = compress(requiredSpecimen, requiredPatt);
+ if (!compressedRequired) {
+ return undefined;
+ }
+ const compressedPartial = [];
+ for (const [i, p] of entries(partialPatt)) {
+ const compressedField = compress(optionalSpecimen[i], p);
+ if (!compressedField) {
+ // imperative loop so can escape early
+ return undefined;
+ }
+ compressedPartial.push(compressedField.compressed[0]);
+ }
+ const compressedRest = compress(restSpecimen, restPatt);
+ if (!compressedRest) {
+ return undefined;
+ }
+ return harden({
+ compressed: [
+ compressedRequired.compressed,
+ compressedPartial,
+ compressedRest.compressed,
+ ],
+ });
+ },
+
+ decompress: (
+ compressed,
+ [requiredPatt, optionalPatt = [], restPatt = MM.any()],
+ decompress,
+ ) => {
+ (Array.isArray(compressed) && compressed.length === 3) ||
+ Fail`splitArray compression must be a triple ${compressed}`;
+ const [compressRequired, compressPartial, compressedRest] = compressed;
+ const partialPatt = adaptArrayPattern(
+ optionalPatt,
+ compressPartial.length,
+ );
+ const requiredParts = decompress(compressRequired, requiredPatt);
+ // const optionalParts = decompress(compressPartial, partialPatt);
+ const optionalParts = [];
+ for (const [i, p] of entries(partialPatt)) {
+ // imperative loop just for similarity to compression code
+ const optionalField = decompress(harden([compressPartial[i]]), p);
+ optionalParts.push(optionalField);
+ }
+ const restParts = decompress(compressedRest, restPatt);
+ return harden([...requiredParts, ...optionalParts, ...restParts]);
+ },
+
/**
* @param {Array} splitArray
* @param {Checker} check
@@ -1395,14 +1765,15 @@ const makePatternKit = () => {
* compression distinguishing `undefined` from absence.
*
* @param {CopyRecord} optionalPatt
- * @param {string[]} names
* @returns {CopyRecord} The partialPatt
*/
- const adaptRecordPattern = (optionalPatt, names) =>
- fromUniqueEntries(names.map(name => [name, MM.opt(optionalPatt[name])]));
+ const adaptRecordPattern = optionalPatt =>
+ objectMap(optionalPatt, p => MM.opt(p));
/** @type {MatchHelper} */
- const matchSplitRecordHelper = Far('match:splitRecord helper', {
+ const matchSplitRecordHelper = harden({
+ tag: `match:splitRecord:1`,
+
checkMatches: (
specimen,
[requiredPatt, optionalPatt = {}, restPatt = MM.any()],
@@ -1414,8 +1785,8 @@ const makePatternKit = () => {
const { requiredSpecimen, optionalSpecimen, restSpecimen } =
splitRecordParts(specimen, requiredPatt, optionalPatt);
- const partialNames = /** @type {string[]} */ (ownKeys(optionalSpecimen));
- const partialPatt = adaptRecordPattern(optionalPatt, partialNames);
+ const partialNames = recordNames(optionalSpecimen);
+ const partialPatt = adaptRecordPattern(optionalPatt);
return (
checkMatches(requiredSpecimen, requiredPatt, check) &&
partialNames.every(name =>
@@ -1430,6 +1801,87 @@ const makePatternKit = () => {
);
},
+ compress: (
+ specimen,
+ [requiredPatt, optionalPatt = {}, restPatt = MM.any()],
+ compress,
+ ) => {
+ if (!checkKind(specimen, 'copyRecord', identChecker)) {
+ return undefined;
+ }
+ const { requiredSpecimen, optionalSpecimen, restSpecimen } =
+ splitRecordParts(specimen, requiredPatt, optionalPatt);
+ const partialPatt = adaptRecordPattern(optionalPatt);
+
+ const compressedRequired = compress(requiredSpecimen, requiredPatt);
+ if (!compressedRequired) {
+ return undefined;
+ }
+ const optionalNames = recordNames(partialPatt);
+ const compressedPartial = [];
+ for (const name of optionalNames) {
+ if (hasOwn(optionalSpecimen, name)) {
+ const compressedField = compress(
+ optionalSpecimen[name],
+ partialPatt[name],
+ );
+ if (!compressedField) {
+ return undefined;
+ }
+ compressedPartial.push(compressedField.compressed[0]);
+ } else {
+ compressedPartial.push(null);
+ }
+ }
+ const compressedRest = compress(restSpecimen, restPatt);
+ if (!compressedRest) {
+ return undefined;
+ }
+ return harden({
+ compressed: [
+ compressedRequired.compressed,
+ compressedPartial,
+ compressedRest.compressed,
+ ],
+ });
+ },
+
+ decompress: (
+ compressed,
+ [requiredPatt, optionalPatt = {}, restPatt = MM.any()],
+ decompress,
+ ) => {
+ (Array.isArray(compressed) && compressed.length === 3) ||
+ Fail`splitRecord compression must be a triple ${compressed}`;
+ const [compressedRequired, compressedPartial, compressedRest] =
+ compressed;
+ const partialPatt = adaptRecordPattern(optionalPatt);
+ const requiredEntries = entries(
+ decompress(compressedRequired, requiredPatt),
+ );
+ const optionalNames = recordNames(partialPatt);
+ compressedPartial.length === optionalNames.length ||
+ Fail`compression or patterns must preserve cardinality: ${compressedPartial}`;
+ /** @type {[string, Passable][]} */
+ const optionalEntries = [];
+ for (const [i, name] of entries(optionalNames)) {
+ const p = partialPatt[name];
+ const c = compressedPartial[i];
+ if (c !== null) {
+ const u = decompress(harden([c]), p);
+ optionalEntries.push([name, u]);
+ }
+ }
+ const restEntries = entries(decompress(compressedRest, restPatt));
+
+ const allEntries = [
+ ...requiredEntries,
+ ...optionalEntries,
+ ...restEntries,
+ ];
+ return fromUniqueEntries(allEntries);
+ },
+
/**
* @param {Array} splitArray
* @param {Checker} check
@@ -1467,60 +1919,93 @@ const makePatternKit = () => {
]) => getPassStyleCover(passStyleOf(requiredPatt)),
});
+ const makeHelpersTable = () => {
+ const helpers = harden([
+ matchAnyHelper,
+ matchAndHelper,
+ matchOrHelper,
+ matchNotHelper,
+
+ matchScalarHelper,
+ matchKeyHelper,
+ matchPatternHelper,
+ matchKindHelper,
+ matchBigintHelper,
+ matchNatHelper,
+ matchStringHelper,
+ matchSymbolHelper,
+ matchRemotableHelper,
+
+ matchLTHelper,
+ matchLTEHelper,
+ matchGTEHelper,
+ matchGTHelper,
+
+ matchArrayOfHelper,
+ matchRecordOfHelper,
+ matchSetOfHelper,
+ matchBagOfHelper,
+ matchMapOfHelper,
+ matchSplitArrayHelper,
+ matchSplitRecordHelper,
+ ]);
+
+ /** @type {Record} */
+ // don't freeze yet
+ const helpersByMatchTag = {};
+
+ for (const helper of helpers) {
+ const { tag, compress, decompress, ...rest } = helper;
+ if (!matchHelperTagRE.test(tag)) {
+ throw Fail`malformed matcher tag ${q(tag)}`;
+ }
+ const subTag = getMatchSubTag(tag);
+ if (subTag === undefined) {
+ (compress === undefined && decompress === undefined) ||
+ Fail`internal: compressing helper must have compression version ${q(
+ tag,
+ )}`;
+ } else {
+ (typeof compress === 'function' && typeof decompress === 'function') ||
+ Fail`internal: expected compression methods ${q(tag)})`;
+ helpersByMatchTag[subTag] = { tag: subTag, ...rest };
+ }
+ helpersByMatchTag[tag] = helper;
+ }
+ return harden(helpersByMatchTag);
+ };
+
/** @type {Record} */
- const HelpersByMatchTag = harden({
- 'match:any': matchAnyHelper,
- 'match:and': matchAndHelper,
- 'match:or': matchOrHelper,
- 'match:not': matchNotHelper,
-
- 'match:scalar': matchScalarHelper,
- 'match:key': matchKeyHelper,
- 'match:pattern': matchPatternHelper,
- 'match:kind': matchKindHelper,
- 'match:bigint': matchBigintHelper,
- 'match:nat': matchNatHelper,
- 'match:string': matchStringHelper,
- 'match:symbol': matchSymbolHelper,
- 'match:remotable': matchRemotableHelper,
-
- 'match:lt': matchLTHelper,
- 'match:lte': matchLTEHelper,
- 'match:gte': matchGTEHelper,
- 'match:gt': matchGTHelper,
-
- 'match:arrayOf': matchArrayOfHelper,
- 'match:recordOf': matchRecordOfHelper,
- 'match:setOf': matchSetOfHelper,
- 'match:bagOf': matchBagOfHelper,
- 'match:mapOf': matchMapOfHelper,
- 'match:splitArray': matchSplitArrayHelper,
- 'match:splitRecord': matchSplitRecordHelper,
- });
+ const HelpersByMatchTag = makeHelpersTable();
- const makeMatcher = (tag, payload) => {
- const matcher = makeTagged(tag, payload);
+ /**
+ * @param {MatchHelper} matchHelper
+ * @param {Passable} payload
+ */
+ const makeMatcher = (matchHelper, payload) => {
+ const matcher = makeTagged(matchHelper.tag, payload);
assertPattern(matcher);
return matcher;
};
- const makeKindMatcher = kind => makeMatcher('match:kind', kind);
+ const makeKindMatcher = kind => makeMatcher(matchKindHelper, kind);
+
+ // Note that PatternShape was defined above to break a circularity.
- const AnyShape = makeMatcher('match:any', undefined);
- const ScalarShape = makeMatcher('match:scalar', undefined);
- const KeyShape = makeMatcher('match:key', undefined);
- const PatternShape = makeMatcher('match:pattern', undefined);
+ const AnyShape = makeMatcher(matchAnyHelper, undefined);
+ const ScalarShape = makeMatcher(matchScalarHelper, undefined);
+ const KeyShape = makeMatcher(matchKeyHelper, undefined);
const BooleanShape = makeKindMatcher('boolean');
const NumberShape = makeKindMatcher('number');
- const BigIntShape = makeTagged('match:bigint', []);
- const NatShape = makeTagged('match:nat', []);
- const StringShape = makeTagged('match:string', []);
- const SymbolShape = makeTagged('match:symbol', []);
- const RecordShape = makeTagged('match:recordOf', [AnyShape, AnyShape]);
- const ArrayShape = makeTagged('match:arrayOf', [AnyShape]);
- const SetShape = makeTagged('match:setOf', [AnyShape]);
- const BagShape = makeTagged('match:bagOf', [AnyShape, AnyShape]);
- const MapShape = makeTagged('match:mapOf', [AnyShape, AnyShape]);
+ const BigIntShape = makeMatcher(matchBigintHelper, []);
+ const NatShape = makeMatcher(matchNatHelper, []);
+ const StringShape = makeMatcher(matchStringHelper, []);
+ const SymbolShape = makeMatcher(matchSymbolHelper, []);
+ const RecordShape = makeMatcher(matchRecordOfHelper, [AnyShape, AnyShape]);
+ const ArrayShape = makeMatcher(matchArrayOfHelper, [AnyShape]);
+ const SetShape = makeMatcher(matchSetOfHelper, [AnyShape]);
+ const BagShape = makeMatcher(matchBagOfHelper, [AnyShape, AnyShape]);
+ const MapShape = makeMatcher(matchMapOfHelper, [AnyShape, AnyShape]);
const RemotableShape = makeKindMatcher('remotable');
const ErrorShape = makeKindMatcher('error');
const PromiseShape = makeKindMatcher('promise');
@@ -1531,20 +2016,20 @@ const makePatternKit = () => {
* so that when it is `undefined` it is dropped from the end of the
* payloads array.
*
- * @param {string} tag
+ * @param {MatchHelper} matchHelper
* @param {Passable[]} payload
*/
- const makeLimitsMatcher = (tag, payload) => {
+ const makeLimitsMatcher = (matchHelper, payload) => {
if (payload[payload.length - 1] === undefined) {
payload = harden(payload.slice(0, payload.length - 1));
}
- return makeMatcher(tag, payload);
+ return makeMatcher(matchHelper, payload);
};
const makeRemotableMatcher = (label = undefined) =>
label === undefined
? RemotableShape
- : makeMatcher('match:remotable', harden({ label }));
+ : makeMatcher(matchRemotableHelper, harden({ label }));
/**
* @template T
@@ -1574,9 +2059,21 @@ const makePatternKit = () => {
/** @type {MatcherNamespace} */
const M = harden({
any: () => AnyShape,
- and: (...patts) => makeMatcher('match:and', patts),
- or: (...patts) => makeMatcher('match:or', patts),
- not: subPatt => makeMatcher('match:not', subPatt),
+ and: (...patts) =>
+ // eslint-disable-next-line no-nested-ternary
+ patts.length === 0
+ ? M.any()
+ : patts.length === 1
+ ? patts[0]
+ : makeMatcher(matchAndHelper, patts),
+ or: (...patts) =>
+ // eslint-disable-next-line no-nested-ternary
+ patts.length === 0
+ ? M.not(M.any())
+ : patts.length === 1
+ ? patts[0]
+ : makeMatcher(matchOrHelper, patts),
+ not: subPatt => makeMatcher(matchNotHelper, subPatt),
scalar: () => ScalarShape,
key: () => KeyShape,
@@ -1585,13 +2082,13 @@ const makePatternKit = () => {
boolean: () => BooleanShape,
number: () => NumberShape,
bigint: (limits = undefined) =>
- limits ? makeLimitsMatcher('match:bigint', [limits]) : BigIntShape,
+ limits ? makeLimitsMatcher(matchBigintHelper, [limits]) : BigIntShape,
nat: (limits = undefined) =>
- limits ? makeLimitsMatcher('match:nat', [limits]) : NatShape,
+ limits ? makeLimitsMatcher(matchNatHelper, [limits]) : NatShape,
string: (limits = undefined) =>
- limits ? makeLimitsMatcher('match:string', [limits]) : StringShape,
+ limits ? makeLimitsMatcher(matchStringHelper, [limits]) : StringShape,
symbol: (limits = undefined) =>
- limits ? makeLimitsMatcher('match:symbol', [limits]) : SymbolShape,
+ limits ? makeLimitsMatcher(matchSymbolHelper, [limits]) : SymbolShape,
record: (limits = undefined) =>
limits ? M.recordOf(M.any(), M.any(), limits) : RecordShape,
array: (limits = undefined) =>
@@ -1607,39 +2104,39 @@ const makePatternKit = () => {
undefined: () => UndefinedShape,
null: () => null,
- lt: rightOperand => makeMatcher('match:lt', rightOperand),
- lte: rightOperand => makeMatcher('match:lte', rightOperand),
+ lt: rightOperand => makeMatcher(matchLTHelper, rightOperand),
+ lte: rightOperand => makeMatcher(matchLTEHelper, rightOperand),
eq: key => {
assertKey(key);
return key === undefined ? M.undefined() : key;
},
neq: key => M.not(M.eq(key)),
- gte: rightOperand => makeMatcher('match:gte', rightOperand),
- gt: rightOperand => makeMatcher('match:gt', rightOperand),
+ gte: rightOperand => makeMatcher(matchGTEHelper, rightOperand),
+ gt: rightOperand => makeMatcher(matchGTHelper, rightOperand),
recordOf: (keyPatt = M.any(), valuePatt = M.any(), limits = undefined) =>
- makeLimitsMatcher('match:recordOf', [keyPatt, valuePatt, limits]),
+ makeLimitsMatcher(matchRecordOfHelper, [keyPatt, valuePatt, limits]),
arrayOf: (subPatt = M.any(), limits = undefined) =>
- makeLimitsMatcher('match:arrayOf', [subPatt, limits]),
+ makeLimitsMatcher(matchArrayOfHelper, [subPatt, limits]),
setOf: (keyPatt = M.any(), limits = undefined) =>
- makeLimitsMatcher('match:setOf', [keyPatt, limits]),
+ makeLimitsMatcher(matchSetOfHelper, [keyPatt, limits]),
bagOf: (keyPatt = M.any(), countPatt = M.any(), limits = undefined) =>
- makeLimitsMatcher('match:bagOf', [keyPatt, countPatt, limits]),
+ makeLimitsMatcher(matchBagOfHelper, [keyPatt, countPatt, limits]),
mapOf: (keyPatt = M.any(), valuePatt = M.any(), limits = undefined) =>
- makeLimitsMatcher('match:mapOf', [keyPatt, valuePatt, limits]),
+ makeLimitsMatcher(matchMapOfHelper, [keyPatt, valuePatt, limits]),
splitArray: (base, optional = undefined, rest = undefined) =>
makeMatcher(
- 'match:splitArray',
+ matchSplitArrayHelper,
makeSplitPayload([], base, optional, rest),
),
splitRecord: (base, optional = undefined, rest = undefined) =>
makeMatcher(
- 'match:splitRecord',
+ matchSplitRecordHelper,
makeSplitPayload({}, base, optional, rest),
),
split: (base, rest = undefined) => {
if (passStyleOf(harden(base)) === 'copyArray') {
- // @ts-expect-error We know it should be an array
+ // @ts-ignore We know `base` should be an array
return M.splitArray(base, rest && [], rest);
} else {
return M.splitRecord(base, rest && {}, rest);
@@ -1647,7 +2144,7 @@ const makePatternKit = () => {
},
partial: (base, rest = undefined) => {
if (passStyleOf(harden(base)) === 'copyArray') {
- // @ts-expect-error We know it should be an array
+ // @ts-ignore We know `base` should be an array
return M.splitArray([], base, rest);
} else {
return M.splitRecord({}, base, rest);
@@ -1655,7 +2152,8 @@ const makePatternKit = () => {
},
eref: t => M.or(t, M.promise()),
- opt: t => M.or(M.undefined(), t),
+ // `undefined` compresses better than `M.undefined()`
+ opt: t => M.or(undefined, t),
interface: (interfaceName, methodGuards, options) =>
// eslint-disable-next-line no-use-before-define
@@ -1678,6 +2176,8 @@ const makePatternKit = () => {
assertPattern,
isPattern,
getRankCover,
+ kindOf,
+ maybeMatchHelper,
M,
});
};
@@ -1695,6 +2195,8 @@ export const {
assertPattern,
isPattern,
getRankCover,
+ kindOf,
+ maybeMatchHelper,
M,
} = makePatternKit();
diff --git a/packages/patterns/src/types.js b/packages/patterns/src/types.js
index e2a0512eb3..92f94ecea3 100644
--- a/packages/patterns/src/types.js
+++ b/packages/patterns/src/types.js
@@ -2,12 +2,12 @@
export {};
-/** @typedef {import('@endo/marshal').Passable} Passable */
-/** @typedef {import('@endo/marshal').PassStyle} PassStyle */
-/** @typedef {import('@endo/marshal').CopyTagged} CopyTagged */
-/** @template T @typedef {import('@endo/marshal').CopyRecord} CopyRecord */
-/** @template T @typedef {import('@endo/marshal').CopyArray} CopyArray */
-/** @typedef {import('@endo/marshal').Checker} Checker */
+/** @typedef {import('@endo/pass-style').Passable} Passable */
+/** @typedef {import('@endo/pass-style').PassStyle} PassStyle */
+/** @typedef {import('@endo/pass-style').CopyTagged} CopyTagged */
+/** @template T @typedef {import('@endo/pass-style').CopyRecord} CopyRecord */
+/** @template T @typedef {import('@endo/pass-style').CopyArray} CopyArray */
+/** @typedef {import('@endo/pass-style').Checker} Checker */
/** @typedef {import('@endo/marshal').RankCompare} RankCompare */
/** @typedef {import('@endo/marshal').RankCover} RankCover */
@@ -253,6 +253,50 @@ export {};
* @typedef {Partial} Limits
*/
+/**
+ * @typedef {string} Kind
+ * It is either a PassStyle other than 'tagged', or, if the underlying
+ * PassStyle is 'tagged', then the `getTag` value for tags that are
+ * recognized at the store level of abstraction. For each of those
+ * tags, a tagged record only has that kind if it satisfies the invariants
+ * that the store level associates with that kind.
+ */
+
+/**
+ * @typedef {object} CompressedRecord
+ * @property {Passable} compressed
+ */
+
+/**
+ * @callback Compress
+ * @param {Passable} specimen
+ * @param {Pattern} pattern
+ * @returns {CompressedRecord | undefined}
+ */
+
+/**
+ * @callback MustCompress
+ * @param {Passable} specimen
+ * @param {Pattern} pattern
+ * @param {string|number} [label]
+ * @returns {Passable}
+ */
+
+/**
+ * @callback Decompress
+ * @param {Passable} compressed
+ * @param {Pattern} pattern
+ * @returns {Passable}
+ */
+
+/**
+ * @callback MustDecompress
+ * @param {Passable} compressed
+ * @param {Pattern} pattern
+ * @param {string|number} [label]
+ * @returns {Passable}
+ */
+
/**
* @typedef {object} PatternMatchers
*
diff --git a/packages/patterns/test/test-compress.js b/packages/patterns/test/test-compress.js
new file mode 100644
index 0000000000..d81e4f5eb9
--- /dev/null
+++ b/packages/patterns/test/test-compress.js
@@ -0,0 +1,269 @@
+// @ts-check
+
+import { test } from './prepare-test-env-ava.js';
+
+// eslint-disable-next-line import/order
+import { Far, makeTagged, makeMarshal } from '@endo/marshal';
+import {
+ makeCopyBagFromElements,
+ makeCopyMap,
+ makeCopySet,
+} from '../src/keys/checkKey.js';
+import { mustCompress, mustDecompress } from '../src/patterns/compress.js';
+import { M } from '../src/patterns/patternMatchers.js';
+
+const runTests = testTriple => {
+ const brand = Far('simoleans', {});
+ const moolaBrand = Far('moola', {});
+ const timer = Far('timer', {});
+
+ testTriple({ brand, value: 37n }, M.any(), { brand, value: 37n });
+ testTriple({ brand, value: 37n }, { brand, value: M.bigint() }, [37n]);
+ testTriple(
+ { brand, value: 37n },
+ { brand: M.remotable(), value: M.bigint() },
+ [37n, brand],
+ );
+ testTriple(
+ { brand, value: 37n },
+ { brand: M.bigint(), value: M.bigint() },
+ undefined,
+ 'test mustCompress: brand: remotable "[Alleged: simoleans]" - Must be a bigint',
+ );
+ testTriple({ brand, value: 37n }, M.recordOf(M.string(), M.scalar()), {
+ brand,
+ value: 37n,
+ });
+ testTriple(
+ [{ foo: 'a' }, { foo: 'b' }, { foo: 'c' }],
+ M.arrayOf(harden({ foo: M.string() })),
+ [[['a'], ['b'], ['c']]],
+ );
+ testTriple(
+ [{ foo: 'a' }, { foo: 'b' }, { foo: 'c' }],
+ // Test that without the compression version tag, there is no
+ // non -default compression or decompression
+ makeTagged('match:arrayOf', harden([{ foo: M.string() }])),
+ [{ foo: 'a' }, { foo: 'b' }, { foo: 'c' }],
+ );
+ testTriple(
+ makeCopySet([{ foo: 'a' }, { foo: 'b' }, { foo: 'c' }]),
+ M.setOf(harden({ foo: M.string() })),
+ [[['c'], ['b'], ['a']]],
+ );
+ testTriple(
+ makeCopyBagFromElements([{ foo: 'a' }, { foo: 'a' }, { foo: 'c' }]),
+ M.bagOf(harden({ foo: M.string() })),
+ [
+ [
+ ['c', 1n],
+ ['a', 2n],
+ ],
+ ],
+ );
+ testTriple(
+ makeCopyBagFromElements([{ foo: 'a' }, { foo: 'a' }, { foo: 'c' }]),
+ M.bagOf(harden({ foo: M.string() }), 1n),
+ undefined,
+ 'test mustCompress: bag counts[1]: "[2n]" - Must be: "[1n]"',
+ );
+ testTriple(
+ makeCopyBagFromElements([{ foo: 'a' }, { foo: 'b' }, { foo: 'c' }]),
+ M.bagOf(harden({ foo: M.string() }), 1n),
+ [[['c'], ['b'], ['a']]],
+ );
+ testTriple(
+ makeCopyMap([
+ [{ foo: 'a' }, { bar: 1 }],
+ [{ foo: 'b' }, { bar: 2 }],
+ [{ foo: 'c' }, { bar: 3 }],
+ ]),
+ M.mapOf(harden({ foo: M.string() }), harden({ bar: M.number() })),
+ [
+ [
+ [['c'], ['b'], ['a']],
+ [[3], [2], [1]],
+ ],
+ ],
+ );
+ testTriple(
+ makeCopyMap([
+ [{ foo: 'c' }, { bar: 3 }],
+ [{ foo: 'b' }, { bar: 2 }],
+ [{ foo: 'a' }, { bar: 1 }],
+ ]),
+ // TODO Add a test case where the keys are in the same rankOrder but not
+ // the same order.
+ makeCopyMap([
+ [{ foo: 'c' }, M.any()],
+ // @ts-expect-error The array need not be generic
+ [{ foo: 'b' }, { bar: M.number() }],
+ [{ foo: 'a' }, { bar: 1 }],
+ ]),
+ [{ bar: 3 }, 2],
+ );
+ testTriple(
+ {
+ want: { Winnings: { brand: moolaBrand, value: ['x', 'y'] } },
+ give: { Bid: { brand, value: 37n } },
+ exit: { afterDeadline: { deadline: 11n, timer } },
+ },
+ {
+ want: { Winnings: { brand: moolaBrand, value: M.array() } },
+ give: { Bid: { brand, value: M.nat() } },
+ exit: { afterDeadline: { deadline: M.gte(10n), timer } },
+ },
+ [['x', 'y'], 37n, 11n],
+ );
+ testTriple(
+ {
+ want: {
+ Winnings: {
+ brand: moolaBrand,
+ value: makeCopyBagFromElements([
+ { foo: 'a' },
+ { foo: 'b' },
+ { foo: 'c' },
+ ]),
+ },
+ },
+ give: { Bid: { brand, value: 37n } },
+ exit: { afterDeadline: { deadline: 11n, timer } },
+ },
+ {
+ want: {
+ Winnings: {
+ brand: moolaBrand,
+ value: M.bagOf(harden({ foo: M.string() }), 1n),
+ },
+ },
+ give: { Bid: { brand, value: M.nat() } },
+ exit: { afterDeadline: { deadline: M.gte(10n), timer } },
+ },
+ [[['c'], ['b'], ['a']], 37n, 11n],
+ );
+ testTriple(
+ 'orange',
+ M.or('red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'),
+ [[1, []]],
+ );
+ testTriple(
+ { x: 3, y: 5 },
+ M.or(harden({ x: M.number(), y: M.number() }), M.bigint(), M.record()),
+ [[0, [5, 3]]],
+ );
+ testTriple(
+ [5n],
+ M.or(harden({ x: M.number(), y: M.number() }), [M.bigint()], M.record()),
+ [[1, [5n]]],
+ );
+ testTriple(
+ { x: 3, y: 5, z: 9 },
+ M.or(harden({ x: M.number(), y: M.number() }), M.bigint(), M.record()),
+ [[2, { x: 3, y: 5, z: 9 }]],
+ );
+ testTriple(
+ {
+ brand,
+ value: [{ bar: 2 }, { bar: 1 }],
+ },
+ {
+ brand,
+ value: M.arrayOf(M.and(M.key(), { bar: M.number() })),
+ },
+ [[[[2]], [[1]]]],
+ );
+ testTriple(
+ ['a', 'b', 'c', 'd', 'e'],
+ M.splitArray(['a', M.string()], [M.any()], M.any()),
+ [[['b'], [[1, 'c']], ['d', 'e']]],
+ );
+ testTriple(
+ ['a', 'b', undefined, 'd'],
+ M.splitArray(['a', M.string()], ['c', 'd', 'e'], M.any()),
+ [
+ [
+ ['b'],
+ [
+ [0, []],
+ [1, []],
+ ],
+ [],
+ ],
+ ],
+ );
+ testTriple(
+ { a: 1, b: 2, c: undefined, d: 4, e: 5 },
+ M.splitRecord({ a: 1, b: M.number() }, { c: M.any(), d: 4, f: 6 }, M.any()),
+ [[[2], [null, [1, []], [0, []]], { e: 5 }]],
+ );
+};
+
+test('compression', t => {
+ const testCompress = (specimen, pattern, compressed, message = undefined) => {
+ if (!message) {
+ t.deepEqual(
+ mustCompress(harden(specimen), harden(pattern)),
+ harden(compressed),
+ );
+ }
+ };
+ runTests(testCompress);
+});
+
+test('test mustCompress', t => {
+ const testCompress = (specimen, pattern, compressed, message = undefined) => {
+ if (message === undefined) {
+ t.deepEqual(
+ mustCompress(harden(specimen), harden(pattern), 'test mustCompress'),
+ harden(compressed),
+ );
+ } else {
+ t.throws(
+ () =>
+ mustCompress(harden(specimen), harden(pattern), 'test mustCompress'),
+ { message },
+ );
+ }
+ };
+ runTests(testCompress);
+});
+
+test('decompression', t => {
+ const testDecompress = (
+ specimen,
+ pattern,
+ compressed,
+ message = undefined,
+ ) => {
+ if (message === undefined) {
+ t.deepEqual(
+ mustDecompress(harden(compressed), harden(pattern)),
+ harden(specimen),
+ );
+ }
+ };
+ runTests(testDecompress);
+});
+
+test('demo compression ratio', t => {
+ const { toCapData } = makeMarshal(() => 's', undefined, {
+ serializeBodyFormat: 'smallcaps',
+ });
+
+ const testCompress = (specimen, pattern, compressed, message = undefined) => {
+ harden(specimen);
+ harden(pattern);
+ harden(compressed);
+ if (message === undefined) {
+ const { body: big } = toCapData(specimen);
+ const { body: small } = toCapData(compressed);
+ const ratio = small.length / big.length;
+ console.log('\n', big, '\n', small, '\n', ratio);
+ const { body: patt } = toCapData(pattern);
+ console.log('Pattern: ', patt);
+ t.assert(ratio <= 2.0);
+ }
+ };
+ runTests(testCompress);
+});
diff --git a/packages/patterns/test/test-patterns.js b/packages/patterns/test/test-patterns.js
index bb76225edc..c1f6127ead 100644
--- a/packages/patterns/test/test-patterns.js
+++ b/packages/patterns/test/test-patterns.js
@@ -39,7 +39,7 @@ const runTests = (successCase, failCase) => {
failCase(specimen, M.gte('x'), '3 - Must be >= "x"');
failCase(specimen, M.and(3, 4), '3 - Must be: 4');
failCase(specimen, M.or(4, 4), '3 - Must match one of [4,4]');
- failCase(specimen, M.or(), '3 - no pattern disjuncts to match: []');
+ failCase(specimen, M.or(), '3 - Must fail negated pattern: "[match:any]"');
}
{
const specimen = [3, 4];