From 5c8b0d96551c7326711c0c093ef2e71794a7cd10 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 13 Jan 2026 10:35:04 -0800 Subject: [PATCH 01/17] fix(patterns)!: Account for the compactOrdered "~" prefix when computing coverage --- packages/marshal/NEWS.md | 5 + packages/marshal/index.js | 1 + packages/marshal/package.json | 1 + packages/marshal/src/rankOrder.js | 46 ++++++++- .../patterns/src/patterns/patternMatchers.js | 99 ++++++++++++------- yarn.lock | 3 +- 6 files changed, 113 insertions(+), 42 deletions(-) diff --git a/packages/marshal/NEWS.md b/packages/marshal/NEWS.md index fc44a026e4..30d4138875 100644 --- a/packages/marshal/NEWS.md +++ b/packages/marshal/NEWS.md @@ -1,5 +1,10 @@ User-visible changes in `@endo/marshal`: +# Next release + +- `FullRankCover` is deprecated. Instead, use `provideStaticRanks(encodePassable)['*'].cover`. +- `getPassStyleCover(passStyle)` is deprecated. Instead, use `provideStaticRanks(encodePassable)[passStyle].cover`. + # 1.8.0 (2025-07-11) - Introduces an environment variable config option `ENDO_RANK_STRINGS` to change the rank ordering of strings from the current (incorrect) ordering by UTF-16 code unit used by JavaScript's `<` and `.sort()` operations to (correct and OCapN-conformant) ordering by Unicode code point. It currently defaults to "utf16-code-unit-order", matching the previously-unconditional behavior. diff --git a/packages/marshal/index.js b/packages/marshal/index.js index ac2de6a37b..d7516e6580 100644 --- a/packages/marshal/index.js +++ b/packages/marshal/index.js @@ -24,6 +24,7 @@ export { compareAntiRank, makeFullOrderComparatorKit, getPassStyleCover, + provideStaticRanks, intersectRankCovers, unionRankCovers, } from './src/rankOrder.js'; diff --git a/packages/marshal/package.json b/packages/marshal/package.json index c48b9ee3ee..9f0928d33b 100644 --- a/packages/marshal/package.json +++ b/packages/marshal/package.json @@ -48,6 +48,7 @@ "@endo/env-options": "workspace:^", "@endo/errors": "workspace:^", "@endo/eventual-send": "workspace:^", + "@endo/memoize": "workspace:^", "@endo/nat": "workspace:^", "@endo/pass-style": "workspace:^", "@endo/promise-kit": "workspace:^" diff --git a/packages/marshal/src/rankOrder.js b/packages/marshal/src/rankOrder.js index a8d8ed9f44..0b22e99ac2 100644 --- a/packages/marshal/src/rankOrder.js +++ b/packages/marshal/src/rankOrder.js @@ -1,5 +1,7 @@ +import { objectMap } from '@endo/common/object-map.js'; import { getEnvironmentOption as getenv } from '@endo/env-options'; import { Fail, q } from '@endo/errors'; +import { memoize } from '@endo/memoize'; import { getTag, passStyleOf, nameForPassableSymbol } from '@endo/pass-style'; import { passStylePrefixes, @@ -116,6 +118,7 @@ harden(compareNumerics); /** * @typedef {Record} PassStyleRanksRecord + * @typedef {PassStyleRanksRecord & { '*': { cover: RankCover } }} StaticRanksRecord */ const passStyleRanks = /** @type {PassStyleRanksRecord} */ ( @@ -156,10 +159,41 @@ harden(passStyleRanks); * * @param {PassStyle} passStyle * @returns {RankCover} + * @deprecated Coverage depends upon format; use provideStaticRanks instead. */ export const getPassStyleCover = passStyle => passStyleRanks[passStyle].cover; harden(getPassStyleCover); +// Use singleton null as a sentinel for detecting an encodePassable output +// prefix (e.g., the "~" that starts compactOrdered output). +const { null: nullTypePrefix } = passStylePrefixes; +nullTypePrefix.length === 1 || + Fail`internal: null pass style prefix must be a single character, not ${q(nullTypePrefix)}`; + +export const provideStaticRanks = memoize( + /** @type {(encodePassable: ((p: Passable) => string)) => StaticRanksRecord} */ + encodePassable => { + const encodedNull = encodePassable(null); + encodedNull.endsWith(nullTypePrefix) || + Fail`expected null encoding ${q(nullTypePrefix)} not found in ${q(encodedNull)}`; + const prefix = encodedNull.slice(0, -nullTypePrefix.length); + return harden({ + __proto__: null, + ...objectMap(passStyleRanks, ({ index, cover }) => { + const [lower, upper] = cover; + const prefixedCover = /** @type {RankCover} */ ([ + `${prefix}${lower}`, + `${prefix}${upper}`, + ]); + return { index, cover: prefixedCover }; + }), + // The full upper bound ends with `{` because `|` is reserved to exceed + // all pass style prefixes; @see {@link passStylePrefixes}. + '*': { cover: [prefix, `${prefix}{`] }, + }); + }, +); + /** * @type {WeakMap>} */ @@ -474,7 +508,11 @@ export const getIndexCover = (sorted, compare, [leftKey, rightKey]) => { }; harden(getIndexCover); -/** @type {RankCover} */ +/** + * @type {RankCover} + * @deprecated Coverage depends upon format; use + * `provideStaticRanks(encodePassable)['*'].cover` instead. + */ export const FullRankCover = harden(['', '{']); /** @@ -528,6 +566,7 @@ const minRank = (compare, a, b) => (compare(a, b) <= 0 ? a : b); * @returns {RankCover} */ export const unionRankCovers = (compare, covers) => { + covers.length > 0 || Fail`Cannot union empty covers`; /** * @param {RankCover} a * @param {RankCover} b @@ -537,7 +576,7 @@ export const unionRankCovers = (compare, covers) => { minRank(compare, leftA, leftB), maxRank(compare, rightA, rightB), ]; - return covers.reduce(unionRankCoverPair, ['{', '']); + return covers.reduce(unionRankCoverPair); }; harden(unionRankCovers); @@ -547,6 +586,7 @@ harden(unionRankCovers); * @returns {RankCover} */ export const intersectRankCovers = (compare, covers) => { + covers.length > 0 || Fail`Cannot intersect empty covers`; /** * @param {RankCover} a * @param {RankCover} b @@ -556,7 +596,7 @@ export const intersectRankCovers = (compare, covers) => { maxRank(compare, leftA, leftB), minRank(compare, rightA, rightB), ]; - return covers.reduce(intersectRankCoverPair, ['', '{']); + return covers.reduce(intersectRankCoverPair); }; harden(intersectRankCovers); diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index 2260045eef..ebe43dba42 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -21,7 +21,7 @@ import { } from '@endo/pass-style'; import { compareRank, - getPassStyleCover, + provideStaticRanks, intersectRankCovers, unionRankCovers, recordNames, @@ -51,6 +51,7 @@ import { generateCollectionPairEntries } from '../keys/keycollection-operators.j * @import {CopyArray, CopyRecord, CopyTagged, Passable} from '@endo/pass-style'; * @import {CopySet, CopyBag, ArgGuard, AwaitArgGuard, ConfirmPattern, GetRankCover, InterfaceGuard, MatcherNamespace, MethodGuard, MethodGuardMaker, Pattern, RawGuard, SyncValueGuard, Kind, Limits, AllLimits, Key, DefaultGuardType} from '../types.js'; * @import {MatchHelper, PatternKit} from './types.js'; + * @import {KeyToDBKey} from '../types.js'; */ const { entries, values, hasOwn } = Object; @@ -640,14 +641,22 @@ const makePatternKit = () => { // /////////////////////// getRankCover ////////////////////////////////////// + /** @type {(passStyle: PassStyle, encodePassable: KeyToDBKey) => RankCover} */ + const getPassStyleCover = (passStyle, encodePassable) => + provideStaticRanks(encodePassable)[passStyle].cover; + /** @type {GetRankCover} */ const getRankCover = (patt, encodePassable) => { + // This partially validates encodePassable. + provideStaticRanks(encodePassable); + if (isKey(patt)) { const encoded = encodePassable(patt); if (encoded !== undefined) { return [encoded, `${encoded}~`]; } } + const passStyle = passStyleOf(patt); switch (passStyle) { case 'copyArray': { @@ -670,7 +679,7 @@ const makePatternKit = () => { // const pattKeys = ownKeys(patt); // const pattEntries = harden(pattKeys.map(key => [key, patt[key]])); // const [leftEntriesLimit, rightEntriesLimit] = - // getRankCover(pattEntries); + // getRankCover(pattEntries, encodePassable); // return harden([ // fromUniqueEntries(leftEntriesLimit), // fromUniqueEntries(rightEntriesLimit), @@ -701,6 +710,7 @@ const makePatternKit = () => { // // const [leftElementLimit, rightElementLimit] = getRankCover( // patt.payload[0], + // encodePassable, // ); // return harden([ // makeCopySet([leftElementLimit]), @@ -747,7 +757,7 @@ const makePatternKit = () => { break; // fall through to default } } - return getPassStyleCover(passStyle); + return getPassStyleCover(passStyle, encodePassable); }; /** @@ -778,7 +788,8 @@ const makePatternKit = () => { (reject && reject`match:any payload: ${matcherPayload} - Must be undefined`), - getRankCover: (_matchPayload, _encodePassable) => ['', '{'], + getRankCover: (_matchPayload, encodePassable) => + provideStaticRanks(encodePassable)['*'].cover, }); /** @type {MatchHelper} */ @@ -855,7 +866,8 @@ const makePatternKit = () => { confirmIsWellFormed: confirmPattern, - getRankCover: (_patt, _encodePassable) => ['', '{'], + getRankCover: (_patt, encodePassable) => + matchAnyHelper.getRankCover(undefined, encodePassable), }); /** @type {MatchHelper} */ @@ -897,7 +909,7 @@ const makePatternKit = () => { (reject && reject`match:kind: payload: ${allegedKeyKind} - A kind name must be a string`), - getRankCover: (kind, _encodePassable) => { + getRankCover: (kind, encodePassable) => { let style; switch (kind) { case 'copySet': @@ -910,7 +922,7 @@ const makePatternKit = () => { break; } } - return getPassStyleCover(style); + return getPassStyleCover(style, encodePassable); }, }); @@ -939,7 +951,8 @@ const makePatternKit = () => { reject, ), - getRankCover: (_kind, _encodePassable) => getPassStyleCover('tagged'), + getRankCover: (_kind, encodePassable) => + getPassStyleCover('tagged', encodePassable), }); /** @type {MatchHelper} */ @@ -960,8 +973,8 @@ const makePatternKit = () => { reject, ), - getRankCover: (_matchPayload, _encodePassable) => - getPassStyleCover('bigint'), + getRankCover: (_matchPayload, encodePassable) => + getPassStyleCover('bigint', encodePassable), }); /** @type {MatchHelper} */ @@ -985,9 +998,9 @@ const makePatternKit = () => { reject, ), - getRankCover: (_matchPayload, _encodePassable) => + getRankCover: (_matchPayload, encodePassable) => // TODO Could be more precise - getPassStyleCover('bigint'), + getPassStyleCover('bigint', encodePassable), }); /** @type {MatchHelper} */ @@ -1011,8 +1024,8 @@ const makePatternKit = () => { reject, ), - getRankCover: (_matchPayload, _encodePassable) => - getPassStyleCover('string'), + getRankCover: (_matchPayload, encodePassable) => + getPassStyleCover('string', encodePassable), }); /** @type {MatchHelper} */ @@ -1044,8 +1057,8 @@ const makePatternKit = () => { reject, ), - getRankCover: (_matchPayload, _encodePassable) => - getPassStyleCover('symbol'), + getRankCover: (_matchPayload, encodePassable) => + getPassStyleCover('symbol', encodePassable), }); /** @type {MatchHelper} */ @@ -1080,8 +1093,8 @@ const makePatternKit = () => { reject, ), - getRankCover: (_remotableDesc, _encodePassable) => - getPassStyleCover('remotable'), + getRankCover: (_remotableDesc, encodePassable) => + getPassStyleCover('remotable', encodePassable), }); /** @type {MatchHelper} */ @@ -1097,7 +1110,10 @@ const makePatternKit = () => { // The prefer-const makes no sense when some of the variables need // to be `let` // eslint-disable-next-line prefer-const - let [leftBound, rightBound] = getPassStyleCover(passStyle); + let [leftBound, rightBound] = getPassStyleCover( + passStyle, + encodePassable, + ); const newRightBound = `${encodePassable(rightOperand)}~`; if (newRightBound !== undefined) { rightBound = newRightBound; @@ -1130,7 +1146,10 @@ const makePatternKit = () => { // The prefer-const makes no sense when some of the variables need // to be `let` // eslint-disable-next-line prefer-const - let [leftBound, rightBound] = getPassStyleCover(passStyle); + let [leftBound, rightBound] = getPassStyleCover( + passStyle, + encodePassable, + ); const newLeftBound = encodePassable(rightOperand); if (newLeftBound !== undefined) { leftBound = newLeftBound; @@ -1195,7 +1214,8 @@ const makePatternKit = () => { reject, ), - getRankCover: _entryPatt => getPassStyleCover('copyRecord'), + getRankCover: (_matchPayload, encodePassable) => + getPassStyleCover('copyRecord', encodePassable), }); /** @type {MatchHelper} */ @@ -1219,7 +1239,8 @@ const makePatternKit = () => { reject, ), - getRankCover: () => getPassStyleCover('copyArray'), + getRankCover: (_matchPayload, encodePassable) => + getPassStyleCover('copyArray', encodePassable), }); /** @type {MatchHelper} */ @@ -1242,8 +1263,8 @@ const makePatternKit = () => { reject, ), - getRankCover: (_matchPayload, _encodePassable) => - getPassStyleCover('byteArray'), + getRankCover: (_matchPayload, encodePassable) => + getPassStyleCover('byteArray', encodePassable), }); /** @type {MatchHelper} */ @@ -1275,7 +1296,8 @@ const makePatternKit = () => { reject, ), - getRankCover: () => getPassStyleCover('tagged'), + getRankCover: (_matchPayload, encodePassable) => + getPassStyleCover('tagged', encodePassable), }); /** @type {MatchHelper} */ @@ -1316,7 +1338,8 @@ const makePatternKit = () => { reject, ), - getRankCover: () => getPassStyleCover('tagged'), + getRankCover: (_matchPayload, encodePassable) => + getPassStyleCover('tagged', encodePassable), }); /** @@ -1540,7 +1563,8 @@ const makePatternKit = () => { reject, ), - getRankCover: () => getPassStyleCover('tagged'), + getRankCover: (_matchPayload, encodePassable) => + getPassStyleCover('tagged', encodePassable), }); /** @type {MatchHelper} */ @@ -1582,7 +1606,8 @@ const makePatternKit = () => { reject, ), - getRankCover: _entryPatt => getPassStyleCover('tagged'), + getRankCover: (_matchPayload, encodePassable) => + getPassStyleCover('tagged', encodePassable), }); /** @@ -1694,11 +1719,10 @@ const makePatternKit = () => { ); }, - getRankCover: ([ - _requiredPatt, - _optionalPatt = undefined, - _restPatt = undefined, - ]) => getPassStyleCover('copyArray'), + getRankCover: ( + [_requiredPatt, _optionalPatt = undefined, _restPatt = undefined], + encodePassable, + ) => getPassStyleCover('copyArray', encodePassable), }); /** @@ -1806,11 +1830,10 @@ const makePatternKit = () => { ); }, - getRankCover: ([ - requiredPatt, - _optionalPatt = undefined, - _restPatt = undefined, - ]) => getPassStyleCover(passStyleOf(requiredPatt)), + getRankCover: ( + [requiredPatt, _optionalPatt = undefined, _restPatt = undefined], + encodePassable, + ) => getPassStyleCover(passStyleOf(requiredPatt), encodePassable), }); /** @type {Record} */ diff --git a/yarn.lock b/yarn.lock index 9782b31e35..225fc0d1dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -545,6 +545,7 @@ __metadata: "@endo/eventual-send": "workspace:^" "@endo/init": "workspace:^" "@endo/lockdown": "workspace:^" + "@endo/memoize": "workspace:^" "@endo/nat": "workspace:^" "@endo/pass-style": "workspace:^" "@endo/promise-kit": "workspace:^" @@ -557,7 +558,7 @@ __metadata: languageName: unknown linkType: soft -"@endo/memoize@workspace:packages/memoize": +"@endo/memoize@workspace:^, @endo/memoize@workspace:packages/memoize": version: 0.0.0-use.local resolution: "@endo/memoize@workspace:packages/memoize" dependencies: From 7e18395fad8166dbdbc160cd73c47e7a47159815 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 13 Jan 2026 10:35:04 -0800 Subject: [PATCH 02/17] refactor(patterns): Simplify some match helper `getRankCover` implementations --- .../patterns/src/patterns/patternMatchers.js | 48 ++++--------------- 1 file changed, 9 insertions(+), 39 deletions(-) diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index ebe43dba42..0c6200efdc 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -909,21 +909,11 @@ const makePatternKit = () => { (reject && reject`match:kind: payload: ${allegedKeyKind} - A kind name must be a string`), - getRankCover: (kind, encodePassable) => { - let style; - switch (kind) { - case 'copySet': - case 'copyMap': { - style = 'tagged'; - break; - } - default: { - style = kind; - break; - } - } - return getPassStyleCover(style, encodePassable); - }, + getRankCover: (kind, encodePassable) => + getPassStyleCover( + kind === 'copySet' || kind === 'copyMap' ? 'tagged' : kind, + encodePassable, + ), }); /** @type {MatchHelper} */ @@ -1107,18 +1097,8 @@ const makePatternKit = () => { getRankCover: (rightOperand, encodePassable) => { const passStyle = passStyleOf(rightOperand); - // The prefer-const makes no sense when some of the variables need - // to be `let` - // eslint-disable-next-line prefer-const - let [leftBound, rightBound] = getPassStyleCover( - passStyle, - encodePassable, - ); - const newRightBound = `${encodePassable(rightOperand)}~`; - if (newRightBound !== undefined) { - rightBound = newRightBound; - } - return [leftBound, rightBound]; + const [lowerBound] = getPassStyleCover(passStyle, encodePassable); + return [lowerBound, `${encodePassable(rightOperand)}~`]; }, }); @@ -1143,18 +1123,8 @@ const makePatternKit = () => { getRankCover: (rightOperand, encodePassable) => { const passStyle = passStyleOf(rightOperand); - // The prefer-const makes no sense when some of the variables need - // to be `let` - // eslint-disable-next-line prefer-const - let [leftBound, rightBound] = getPassStyleCover( - passStyle, - encodePassable, - ); - const newLeftBound = encodePassable(rightOperand); - if (newLeftBound !== undefined) { - leftBound = newLeftBound; - } - return [leftBound, rightBound]; + const [, upperBound] = getPassStyleCover(passStyle, encodePassable); + return [encodePassable(rightOperand), upperBound]; }, }); From 374edcfaa826b2e0383d71a550780046af96eea9 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 13 Jan 2026 10:35:04 -0800 Subject: [PATCH 03/17] feat(patterns): Improve some `getRankCover` implementations * Same upper and lower bound for a Key. * Bounds for M.nat() match M.gte(0n). * The inclusive upper bound for M.lte(x) is x. --- packages/patterns/src/patterns/patternMatchers.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index 0c6200efdc..ab2bc8a076 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -653,7 +653,7 @@ const makePatternKit = () => { if (isKey(patt)) { const encoded = encodePassable(patt); if (encoded !== undefined) { - return [encoded, `${encoded}~`]; + return [encoded, encoded]; } } @@ -989,8 +989,8 @@ const makePatternKit = () => { ), getRankCover: (_matchPayload, encodePassable) => - // TODO Could be more precise - getPassStyleCover('bigint', encodePassable), + // eslint-disable-next-line no-use-before-define + matchGTEHelper.getRankCover(0n, encodePassable), }); /** @type {MatchHelper} */ @@ -1098,7 +1098,7 @@ const makePatternKit = () => { getRankCover: (rightOperand, encodePassable) => { const passStyle = passStyleOf(rightOperand); const [lowerBound] = getPassStyleCover(passStyle, encodePassable); - return [lowerBound, `${encodePassable(rightOperand)}~`]; + return [lowerBound, encodePassable(rightOperand)]; }, }); From add869d0af640dbaac8cea7e22e439d1675d3fb1 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 13 Jan 2026 10:35:04 -0800 Subject: [PATCH 04/17] feat(patterns): Tighten bounds for `getRankCover([key, ..., nonKey, ...])` Use a prefix derived from the leading key elements. Fixes #3046 --- .../patterns/src/patterns/patternMatchers.js | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index ab2bc8a076..9b1b5cd544 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -645,6 +645,12 @@ const makePatternKit = () => { const getPassStyleCover = (passStyle, encodePassable) => provideStaticRanks(encodePassable)[passStyle].cover; + /** @type {(encodePassable: KeyToDBKey) => number} */ + const getEncodingPrefixLength = encodePassable => { + const [encodingPrefix] = provideStaticRanks(encodePassable)['*'].cover; + return encodingPrefix.length; + }; + /** @type {GetRankCover} */ const getRankCover = (patt, encodePassable) => { // This partially validates encodePassable. @@ -660,16 +666,29 @@ const makePatternKit = () => { const passStyle = passStyleOf(patt); switch (passStyle) { case 'copyArray': { - // XXX this doesn't get along with the world of cover === pair of - // strings. In the meantime, fall through to the default which - // returns a cover that covers all copyArrays. - // - // const rankCovers = patt.map(p => getRankCover(p, encodePassable)); - // return harden([ - // rankCovers.map(([left, _right]) => left), - // rankCovers.map(([_left, right]) => right), - // ]); - break; + // The fallback below would cover all CopyArrays, but we can do better + // by leveraging a run of initial Keys. + const nonKeyIdx = patt.findIndex(v => !isKey(v)); + nonKeyIdx !== -1 || + Fail`internal: all-Key copyArray ${q(patt)} must itself be a Key`; + // Discover the prefix that will start both bounds by encoding a + // CopyArray consisting of those Keys followed by a null sentinel element. + const epLen = getEncodingPrefixLength(encodePassable); + const sentinel = null; + const embeddedSentinel = encodePassable(sentinel).slice(epLen); + const keyArr = harden([...patt.slice(0, nonKeyIdx), sentinel]); + const encodedKeyArr = encodePassable(keyArr); + const prefixLength = encodedKeyArr.lastIndexOf(embeddedSentinel); + const prefix = encodedKeyArr.slice(0, prefixLength); + // Combine that prefix with the RankCover of the first non-Key element. + const [lowerSuffix, upperSuffix] = getRankCover( + patt[nonKeyIdx], + encodePassable, + ); + return [ + `${prefix}${lowerSuffix.slice(epLen)}`, + `${prefix}${upperSuffix.slice(epLen)}`, + ]; } case 'copyRecord': { // XXX this doesn't get along with the world of cover === pair of From ebed59fdd2dcab11412950141bfd11aefe692c9a Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 15 Jan 2026 23:43:15 -0800 Subject: [PATCH 05/17] test(patterns): Add property-based testing for getRankCover --- .yarnrc.yml | 2 +- packages/marshal/test/encodePassable.test.js | 2 +- packages/pass-style/tools/arb-passable.js | 301 ++++++++++++------ packages/patterns/test/rankCover.test.js | 106 ++++++ .../test/snapshots/rankCover.test.js.md | 227 +++++++++++++ .../test/snapshots/rankCover.test.js.snap | Bin 0 -> 1252 bytes yarn.lock | 30 +- 7 files changed, 559 insertions(+), 109 deletions(-) create mode 100644 packages/patterns/test/rankCover.test.js create mode 100644 packages/patterns/test/snapshots/rankCover.test.js.md create mode 100644 packages/patterns/test/snapshots/rankCover.test.js.snap diff --git a/.yarnrc.yml b/.yarnrc.yml index 91e03cefdc..ed0f43849b 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -9,6 +9,6 @@ catalogs: eslint-plugin-eslint-comments: ^3.2.0 eslint-plugin-import: ^2.31.0 typescript: ~5.9.2 - "@fast-check/ava": ^1.1.5 + "@fast-check/ava": ^2.0.2 enableScripts: false nodeLinker: node-modules diff --git a/packages/marshal/test/encodePassable.test.js b/packages/marshal/test/encodePassable.test.js index dc6ec48791..f562c13609 100644 --- a/packages/marshal/test/encodePassable.test.js +++ b/packages/marshal/test/encodePassable.test.js @@ -15,7 +15,7 @@ import { import { compareRank, makeFullOrderComparatorKit } from '../src/rankOrder.js'; import { unsortedSample } from '../tools/marshal-test-data.js'; -const { arbPassable } = makeArbitraries(fc, ['byteArray']); +const { arbPassable } = makeArbitraries(fc, { excludePassStyles: ['byteArray'] }); const statelessEncodePassableLegacy = makeEncodePassable(); diff --git a/packages/pass-style/tools/arb-passable.js b/packages/pass-style/tools/arb-passable.js index 911511e010..58b1126582 100644 --- a/packages/pass-style/tools/arb-passable.js +++ b/packages/pass-style/tools/arb-passable.js @@ -1,8 +1,20 @@ // @ts-check +import { objectMap } from '@endo/common/object-map.js'; import '../src/types.js'; import { Far } from '../src/make-far.js'; import { makeTagged } from '../src/makeTagged.js'; -import { passableSymbolForName } from '../src/symbol.js'; +import { nameForPassableSymbol, passableSymbolForName } from '../src/symbol.js'; + +/** + * @import { Arbitrary } from 'fast-check'; + * @import { Key } from '@endo/patterns'; + * @import { Passable, CopyTagged } from '../src/types.js'; + */ + +/** @type ((reason: string) => never) */ +const reject = reason => { + throw Error(reason); +}; /** * The only elements with identity. Everything else should be equal @@ -13,12 +25,41 @@ export const exampleBob = Far('bob', {}); export const exampleCarol = Far('carol', {}); /** + * @template {Passable} T + * @template {Passable} U + * @typedef {[T, U?] | [T[], U[]?] | [Record, Record?] | [CopyTagged>, CopyTagged>?]} LiftedInput + */ + +/** + * Make fast-check arbitraries for various kinds of passables. + * The recursive types arbKey and arbPassable also support "lifting" to replace + * each produced value with a [value, lifted] pair, where `lifted` is the output + * of a `lift([leaf], detail)` or `lift([composite, liftedParts], detail)` + * invocation in which `detail` is drawn from `arbLiftingDetail`. + * + * @template {Passable} [Lifted=Passable] + * @template [LiftingDetail=unknown] * @param {typeof import('@fast-check/ava').fc} fc - * @param {Array<'byteArray'>} [exclusions] + * @param {object} [options] + * @param {Array<'byteArray'>} [options.excludePassStyles] + * @param {(input: LiftedInput, detail: LiftingDetail) => T | Lifted} [options.lift] + * @param {Arbitrary} [options.arbLiftingDetail] */ -export const makeArbitraries = (fc, exclusions = []) => { - const arbString = fc.oneof(fc.string(), fc.fullUnicodeString()); +export const makeArbitraries = ( + fc, + { + excludePassStyles = [], + lift = /** @type {any} */ (([x]) => x), + arbLiftingDetail = /** @type {any} */ (fc.constant(undefined)), + } = {}, +) => { + const arbString = fc.oneof( + fc.string({ unit: 'grapheme-ascii' }), + fc.string(), + ); + const notThen = arbString.filter(s => s !== 'then'); + /** @type {(Arbitrary)[]} */ const keyableLeaves = [ fc.constantFrom(null, undefined, false, true), arbString, @@ -26,7 +67,12 @@ export const makeArbitraries = (fc, exclusions = []) => { // TODO Once we flip symbol representation, we should revisit everywhere // we make a special case of "@@". It may no longer be appropriate. .filter(s => !s.startsWith('@@')) - .map(s => passableSymbolForName(s)), + .map( + s => passableSymbolForName(s), + v => + nameForPassableSymbol(/** @type {any} */ (v)) ?? + reject('not a passable symbol'), + ), // primordial symbols and registered lookalikes fc.constantFrom( ...Object.getOwnPropertyNames(Symbol).flatMap(k => { @@ -41,12 +87,13 @@ export const makeArbitraries = (fc, exclusions = []) => { // because we may go through a phase where only `sliceToImmutable` is // provided when the shim is run on Hermes. // See https://github.com/endojs/endo/pull/2785 - // @ts-expect-error How can the shim add to the `ArrayBuffer` type? ...[fc.uint8Array().map(arr => arr.buffer.sliceToImmutable())].filter( - () => !exclusions.includes('byteArray'), + () => !excludePassStyles.includes('byteArray'), ), fc.constantFrom(-0, NaN, Infinity, -Infinity), - fc.record({}), + // This was producing null-prototype objects: + // fc.record({}), + fc.constant(undefined).map(() => ({})), fc.constantFrom(exampleAlice, exampleBob, exampleCarol), ]; @@ -54,108 +101,176 @@ export const makeArbitraries = (fc, exclusions = []) => { const arbLeaf = fc.oneof( ...keyableLeaves, - arbString.map(s => Error(s)), + arbString.map( + s => Error(s), + v => (v instanceof Error ? v.message : reject('not an Error')), + ), // unresolved promise fc.constant(new Promise(() => {})), ); - const { keyDag } = fc.letrec(tie => { - return { - keyDag: fc.oneof( - { withCrossShrink: true }, - arbKeyLeaf, - fc.array(tie('keyDag')), - fc.dictionary( - arbString.filter(s => s !== 'then'), - tie('keyDag'), + const recoverabilityCache = new WeakMap(); + /** @type {(arb: Arbitrary, mapper: (input: T) => U) => Arbitrary} */ + const recoverableMap = (arb, mapper) => + arb.map( + recoverableInput => { + const result = mapper(recoverableInput); + recoverabilityCache.set(result, recoverableInput); + return result; + }, + v => { + const found = recoverabilityCache.get(/** @type {any} */ (v)); + if (found !== undefined || recoverabilityCache.has(found)) return found; + reject('not a cached output'); + }, + ); + + /** + * @template {Passable} [T=Passable] + * @template {Lifted} [U=Lifted] + * @typedef {[T, U]} LiftedPair + */ + /** @type {(arb: Arbitrary, makeLiftArgs: (input: T) => Parameters[0]) => Arbitrary} */ + const withLiftingDetail = (arb, makeLiftArgs) => + recoverableMap(fc.tuple(arb, arbLiftingDetail), input => { + const [specimen, detail] = input; + const args = makeLiftArgs(specimen); + return /** @type {LiftedPair} */ ([args[0], lift(args, detail)]); + }); + + const recursives = fc.letrec(tie => ({ + liftedKeyDag: fc.oneof( + { withCrossShrink: true }, + // Base case: lift a leaf into a [leaf, lifted] pair. + withLiftingDetail(arbKeyLeaf, leaf => [leaf]), + // Recursive cases: compose lifted pairs, project into an [unlifted, liftedParts] pair, + // and lift that. + withLiftingDetail( + fc.oneof( + // copyArray + recoverableMap(fc.array(tie('liftedKeyDag')), pairsArr => + [0, 1].map(i => pairsArr.map(pair => pair[i])), + ), + // copyRecord + recoverableMap( + fc.dictionary(notThen, tie('liftedKeyDag')), + pairsRec => [0, 1].map(i => objectMap(pairsRec, p => p[i])), + ), ), + ([compositeKey, liftedParts]) => + /** @type {any} */ ([compositeKey, liftedParts]), ), - }; - }); - - const { arbDag } = fc.letrec(tie => { - return { - arbDag: fc.oneof( - { withCrossShrink: true }, - arbLeaf, - fc.array(tie('arbDag')), - fc.dictionary( - arbString.filter(s => s !== 'then'), - tie('arbDag'), - ), - // A promise for a passable. - tie('arbDag').map(v => Promise.resolve(v)), - // A tagged value, either of arbitrary type with arbitrary payload - // or of known type with arbitrary or explicitly valid payload. - // Ordered by increasing complexity. - fc - .oneof( - fc.record({ type: arbString, payload: tie('arbDag') }), - fc.record({ - type: fc.constantFrom('copySet'), - payload: fc.oneof( - tie('arbDag'), - // copySet valid payload is an array of unique passables. - // TODO: A valid copySet payload must be a reverse sorted array, - // so we should generate some of those as well. - fc.uniqueArray(tie('arbDag')), + ), + liftedArbDag: fc.oneof( + { withCrossShrink: true }, + // Base case: lift a leaf into a [leaf, lifted] pair. + withLiftingDetail(arbLeaf, leaf => [leaf]), + // Recursive cases: compose lifted pairs, project into an [unlifted, liftedParts] pair, + // and lift that. + withLiftingDetail( + fc.oneof( + // copyArray + recoverableMap(fc.array(tie('liftedArbDag')), pairsArr => + [0, 1].map(i => pairsArr.map(pair => pair[i])), + ), + // copyRecord + recoverableMap( + fc.dictionary(notThen, tie('liftedArbDag')), + pairsRec => [0, 1].map(i => objectMap(pairsRec, p => p[i])), + ), + // promise + recoverableMap(tie('liftedArbDag'), pair => + [0, 1].map(i => Promise.resolve(pair[i])), + ), + // arbitrary tagged (but maybe using a known tag) + recoverableMap( + fc.tuple( + fc.oneof( + arbString, + fc.constantFrom('copySet', 'copyBag', 'copyMap'), ), + tie('liftedArbDag'), + ), + ([tag, payloadPair]) => + [0, 1].map(i => makeTagged(tag, payloadPair[i])), + ), + // copySet: an array of unique Passables + // TODO: A valid copySet payload must be a reverse sorted array. + recoverableMap( + fc.uniqueArray(tie('liftedKeyDag'), { + selector: pair => pair[0], }), - fc.record({ - type: fc.constantFrom('copyBag'), - payload: fc.oneof( - tie('arbDag'), - // copyBag valid payload is an array of [passable, count] tuples - // in which each passable is unique. - // TODO: A valid copyBag payload must be a reverse sorted array, - // so we should generate some of those as well. - fc.uniqueArray(fc.tuple(tie('arbDag'), fc.bigInt()), { - selector: entry => entry[0], - }), + pairsArr => + [0, 1].map(i => + makeTagged( + 'copySet', + pairsArr.map(pair => pair[i]), + ), ), - }), - fc.record({ - type: fc.constantFrom('copyMap'), - payload: fc.oneof( - tie('arbDag'), - // copyMap valid payload is a - // `{ keys: Passable[], values: Passable[]}` - // record in which keys are unique and both arrays have the - // same length. - // TODO: In a valid copyMap payload, the keys must be a - // reverse sorted array, so we should generate some of - // those as well. - fc - .uniqueArray( - fc.record({ key: tie('arbDag'), value: tie('arbDag') }), - { selector: entry => entry.key }, - ) - .map(entries => ({ - keys: entries.map(({ key }) => key), - values: entries.map(({ value }) => value), - })), + ), + // copyBag: an array of [Passable, count] tuples in which each Passable is unique + // TODO: A valid copyBag payload must be a reverse sorted array. + recoverableMap( + fc.uniqueArray( + fc.tuple(tie('liftedKeyDag'), fc.bigInt({ min: 1n })), + { selector: pairKeyedEntry => pairKeyedEntry[0][0] }, + ), + pairKeyedEntries => + [0, 1].map(i => + makeTagged( + 'copyBag', + pairKeyedEntries.map(([pair, value]) => [pair[i], value]), + ), ), + ), + // copyMap: a `{ keys: Passable[], values: Passable[] }` record in which keys are unique and both arrays have the same length + // TODO: A valid copyMap payload must be a reverse sorted array. + recoverableMap( + fc.uniqueArray(fc.tuple(tie('liftedKeyDag'), tie('liftedArbDag')), { + selector: pairKeyedEntry => pairKeyedEntry[0][0], }), - ) - .map(({ type, payload }) => { - const passable = /** @type {import('../src/types.js').Passable} */ ( - payload - ); - return makeTagged(type, passable); - }), + pairKeyedEntries => + [0, 1].map(i => + makeTagged( + 'copyMap', + pairKeyedEntries.map(([keyPair, valuePair]) => [ + keyPair[i], + valuePair[i], + ]), + ), + ), + ), + ), + ([specimen, liftedParts]) => + /** @type {any} */ ([specimen, liftedParts]), ), - }; - }); + ), + })); + const { liftedKeyDag, liftedArbDag } = /** @type {{ + * liftedKeyDag: Arbitrary>, + * liftedArbDag: Arbitrary>, + * }} + */ (recursives); + + /** + * A factory for arbitrary [unliftedKey, liftedKey] pairs. + */ + const arbLiftedKey = liftedKeyDag.map(pair => harden(pair)); + + /** + * A factory for arbitrary [unliftedPassable, liftedPassable] pairs. + */ + const arbLiftedPassable = liftedArbDag.map(pair => harden(pair)); /** - * A factory for arbitrary keys. + * A factory for arbitrary Keys. */ - const arbKey = keyDag.map(x => harden(x)); + const arbKey = arbLiftedKey.map(pair => pair[0]); /** - * A factory for arbitrary passables. + * A factory for arbitrary Passables. */ - const arbPassable = arbDag.map(x => harden(x)); + const arbPassable = arbLiftedPassable.map(pair => pair[0]); return { exampleAlice, @@ -166,5 +281,7 @@ export const makeArbitraries = (fc, exclusions = []) => { arbLeaf, arbKey, arbPassable, + arbLiftedKey, + arbLiftedPassable, }; }; diff --git a/packages/patterns/test/rankCover.test.js b/packages/patterns/test/rankCover.test.js new file mode 100644 index 0000000000..3ea3eea591 --- /dev/null +++ b/packages/patterns/test/rankCover.test.js @@ -0,0 +1,106 @@ +// @ts-nocheck +import test from '@endo/ses-ava/test.js'; + +import { fc } from '@fast-check/ava'; +import { makeArbitraries } from '@endo/pass-style/tools.js'; + +import { q } from '@endo/errors'; +import { + compareRank, + makePassableKit, + provideStaticRanks, +} from '@endo/marshal'; + +import { getRankCover, kindOf, M } from '../src/patterns/patternMatchers.js'; + +/** @import {Implementation} from 'ava'; */ + +const formats = ['legacyOrdered', 'compactOrdered']; + +const { + arbKey, + arbLiftedPassable: arbPassableAndPattern, +} = makeArbitraries(fc, { + excludePassStyles: ['byteArray'], + arbLiftingDetail: fc.oneof( + { withCrossShrink: true }, + { arbitrary: fc.constant(false), weight: 80 }, + { arbitrary: fc.constant(true), weight: 20 }, + ), + lift: ([x, patt = x], shouldMakeMatcher) => { + try { + const matchKind = shouldMakeMatcher ? kindOf(harden(x)) : undefined; + if (matchKind) return M.kind(matchKind); + } catch (_err) { + // eslint-disable-next-line no-empty + } + return patt; + }, +}); + +/** + * @param {string} titlePrefix + * @param {Implementation<[encodePassable: (val: Passable) => string]>} testFn + */ +const testAcrossFormats = (titlePrefix, testFn) => { + for (const format of formats) { + /** @type {Map } */ + const ids = new Map(); + const encodePrefixed = (prefix, val) => { + const foundId = ids.get(val); + if (foundId) return foundId; + const n = Math.floor(Math.random() * 10_000); + const newId = `${prefix}${n.toString().padStart(4, '0')}`; + ids.set(val, newId); + return newId; + }; + const { encodePassable } = makePassableKit({ + format, + encodeRemotable: r => encodePrefixed('r', r), + encodePromise: p => encodePrefixed('?', p), + encodeError: e => encodePrefixed('!', e), + }); + test(`${titlePrefix} - ${format}`, async t => testFn(t, encodePassable)); + } +}; + +testAcrossFormats('static ranks', async (t, encodePassable) => { + t.snapshot(provideStaticRanks(encodePassable)); +}); + +testAcrossFormats( + 'getRankCover for Keys is tight', + async (t, encodePassable) => { + await fc.assert( + fc.property(arbKey, x => { + const encoded = encodePassable(x); + return t.deepEqual(getRankCover(x, encodePassable), [encoded, encoded]); + }), + ); + }, +); + +testAcrossFormats( + 'getRankCover(pattern, encodePassable) covers matching specimens', + async (t, encodePassable) => { + await fc.assert( + fc.property(arbPassableAndPattern, ([specimen, patt]) => { + const encoded = encodePassable(specimen); + const [lower, upper] = getRankCover(patt, encodePassable); + const lowerOk = compareRank(lower, encoded) <= 0; + const upperOk = compareRank(encoded, upper) <= 0; + if (lowerOk && upperOk) { + t.pass(); + return; + } + + const boundsRepr = `[${q(lower)}, ${q(upper)}]`; + // eslint-disable-next-line no-nested-ternary + const failedBounds = lowerOk ? 'UPPER' : upperOk ? 'LOWER' : 'BOTH'; + t.fail( + `***SPECIMEN*** ${q(specimen)} ***as*** ${q(encoded)} failed ${failedBounds} bound(s) of ***PATTERN*** ${q(patt)} ***as*** ${boundsRepr}`, + ); + }), + ); + }, +); diff --git a/packages/patterns/test/snapshots/rankCover.test.js.md b/packages/patterns/test/snapshots/rankCover.test.js.md new file mode 100644 index 0000000000..750a8cdba2 --- /dev/null +++ b/packages/patterns/test/snapshots/rankCover.test.js.md @@ -0,0 +1,227 @@ +# Snapshot report for `test/rankCover.test.js` + +The actual snapshot is saved in `rankCover.test.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## static ranks - legacyOrdered + +> Snapshot 1 + + { + '*': { + cover: [ + '', + '{', + ], + }, + bigint: { + cover: [ + 'n', + 'q', + ], + index: 8, + }, + boolean: { + cover: [ + 'b', + 'c', + ], + index: 6, + }, + byteArray: { + cover: [ + 'a', + 'b', + ], + index: 5, + }, + copyArray: { + cover: [ + '[', + '_', + ], + index: 4, + }, + copyRecord: { + cover: [ + '(', + ')', + ], + index: 1, + }, + error: { + cover: [ + '!', + '"', + ], + index: 0, + }, + null: { + cover: [ + 'v', + 'w', + ], + index: 11, + }, + number: { + cover: [ + 'f', + 'g', + ], + index: 7, + }, + promise: { + cover: [ + '?', + '@', + ], + index: 3, + }, + remotable: { + cover: [ + 'r', + 's', + ], + index: 9, + }, + string: { + cover: [ + 's', + 't', + ], + index: 10, + }, + symbol: { + cover: [ + 'y', + 'z', + ], + index: 12, + }, + tagged: { + cover: [ + ':', + ';', + ], + index: 2, + }, + undefined: { + cover: [ + 'z', + '{', + ], + index: 13, + }, + } + +## static ranks - compactOrdered + +> Snapshot 1 + + { + '*': { + cover: [ + '~', + '~{', + ], + }, + bigint: { + cover: [ + '~n', + '~q', + ], + index: 8, + }, + boolean: { + cover: [ + '~b', + '~c', + ], + index: 6, + }, + byteArray: { + cover: [ + '~a', + '~b', + ], + index: 5, + }, + copyArray: { + cover: [ + '~[', + '~_', + ], + index: 4, + }, + copyRecord: { + cover: [ + '~(', + '~)', + ], + index: 1, + }, + error: { + cover: [ + '~!', + '~"', + ], + index: 0, + }, + null: { + cover: [ + '~v', + '~w', + ], + index: 11, + }, + number: { + cover: [ + '~f', + '~g', + ], + index: 7, + }, + promise: { + cover: [ + '~?', + '~@', + ], + index: 3, + }, + remotable: { + cover: [ + '~r', + '~s', + ], + index: 9, + }, + string: { + cover: [ + '~s', + '~t', + ], + index: 10, + }, + symbol: { + cover: [ + '~y', + '~z', + ], + index: 12, + }, + tagged: { + cover: [ + '~:', + '~;', + ], + index: 2, + }, + undefined: { + cover: [ + '~z', + '~{', + ], + index: 13, + }, + } diff --git a/packages/patterns/test/snapshots/rankCover.test.js.snap b/packages/patterns/test/snapshots/rankCover.test.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..5f216ee09a361799268ea74e5b09c5e206976aaf GIT binary patch literal 1252 zcmVRg7&dxn|U%9?gYn2ccN+}^!LhT;|Au}Ne z3!BH?an^ZkGk48ZxusF`pdu`ys6Z(UA~1r8lpaL&M}H(m1Y!LXMiEqhNFWvE-t}?! zoIPCbpTX>3Gw1jD&9FQ7vhzJUTB^6odpd`Y1z|0$2m2Ox!eUq}kF<-;J)M#3N9sYf zSf1S0t_1C%QtLE}nQ-jRoaUh|3QL2L};-hyg^u5I+bQ7Ci`ONQhPO zkt44hxusgQ)(qqGHHq_lsnx0n#b!L%nHUsGlVPyA-7ZeXqeqjYa%+6@%INLH$eSA7 z8I)V?N<2H0m^nea-D=0j`JOAsx#mQ@9-rQrICYy7jZ!eJ?@gTN$J?z&trNt9#}b1= zJ7~1RVyQkodMEi>ov>YNR;T|=GtMU)rPlO&7jlX7uvo1I)4#yl#Cc(&SqVmK&8bnZ zkh^ja(w2qtRZ2u5w_F16y?J z8MNZSm;;A&>KpV`2VQgFq)z({`mF=sJ8({?g9aUPVZIADxEj66pj%zo?!s=J4jc3l z7oK$Cm`>*z^g|aub>WOo7Z~)s3;((>FR#(74SG`^*5%<2oh~+LBM+TCJfYKTj2mCX zSLpG4;ub5n_DO7~U@3+vBNy`>mG-=5~cbasU zg|0B^N()_T(q$I9*Q5_v=%`7n7P`@-n=G_#(vF38Od49~q)GQ%=z5cGu+aUpTv?Y& zW{FlZWQf7Ht%B=TblLtG}Ql{!b!-I#? zQsy}iUh?3rw3PYSgU>zqE-ht#^WdBZd6kkfixezXur@7aZc}i(f?`_AOei>@;OVrK zIi}zZ1t-%|=1T?NDEK)oW&TzmeVFT~q|8bmR{L;kTFTtz!#zH<(o*JOA0GGN<*X?q zL}YyBl#!)oWMt-yj4VAPBQt1ZWGNaMnMo7B!0gzJ8W~xtMn-1V$jH()GBU$PMwYUX zk(o9!vb2qi%(#(}rEX+o=8cRjeIp|?aAagDoOobhOXJAMj2szRDn~|U=E%s>Iq|EQ OSKWUtST*3i82|tTSz+h^ literal 0 HcmV?d00001 diff --git a/yarn.lock b/yarn.lock index 225fc0d1dc..5c9ff144a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -916,14 +916,14 @@ __metadata: languageName: node linkType: hard -"@fast-check/ava@npm:^1.1.5": - version: 1.1.5 - resolution: "@fast-check/ava@npm:1.1.5" +"@fast-check/ava@npm:^2.0.2": + version: 2.0.2 + resolution: "@fast-check/ava@npm:2.0.2" dependencies: - fast-check: "npm:^3.0.0" + fast-check: "npm:^3.0.0 || ^4.0.0" peerDependencies: - ava: ">=4.0.0" - checksum: 10c0/0bbde9806b60098ad493b0df13e87e713ad63890c299bd6cb2ff96ba6b2eb19473b78956def72a806ead36dc88c02756c14ce42e4b18596ad7857a751b1e77ea + ava: ^4 || ^5 || ^6 + checksum: 10c0/bace1eb6c45f4a30208866d17762a22cc2c61f906aa6c4df9b5bc0b1c9f823ea0027bbf9acdcb3e6bb93ac101d4fa606a178395c673973414220ba2148b2bac6 languageName: node linkType: hard @@ -5313,12 +5313,12 @@ __metadata: languageName: node linkType: hard -"fast-check@npm:^3.0.0": - version: 3.1.1 - resolution: "fast-check@npm:3.1.1" +"fast-check@npm:^3.0.0 || ^4.0.0": + version: 4.5.3 + resolution: "fast-check@npm:4.5.3" dependencies: - pure-rand: "npm:^5.0.1" - checksum: 10c0/90804b41e296102de5e0648c880655b6a51d7b8623c12681120fde16fab8bf4ff72b3d0db32e738d89d322614c8547207d9931a4093425d134fe9f9c4acf117e + pure-rand: "npm:^7.0.0" + checksum: 10c0/e50846538de208756ecc3fab9d8bdc5d9677d2e27c611ccbbb3269c44635e819eb0a2323fcf4bf9aae648f7066cc5d7bda339549b25b8e35139831adf4975329 languageName: node linkType: hard @@ -9309,10 +9309,10 @@ __metadata: languageName: node linkType: hard -"pure-rand@npm:^5.0.1": - version: 5.0.1 - resolution: "pure-rand@npm:5.0.1" - checksum: 10c0/56fb43edf336ac939564d60535597a4182a6310627c4c7c54ef22499223a3d967bfa6d05a9c95c45ed5324bb7167de57a25e3fac5495dd4409672c08166d7973 +"pure-rand@npm:^7.0.0": + version: 7.0.1 + resolution: "pure-rand@npm:7.0.1" + checksum: 10c0/9cade41030f5ec95f5d55a11a71404cd6f46b69becaad892097cd7f58e2c6248cd0a933349ca7d21336ab629f1da42ffe899699b671bc4651600eaf6e57f837e languageName: node linkType: hard From 7796183115e0abcc49565e7c6d7ce12db9e58b6f Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 15 Jan 2026 23:48:10 -0800 Subject: [PATCH 06/17] fix(patterns): Don't let Object.prototype pollute the collection of match helpers Fixes #3051 --- packages/patterns/src/patterns/patternMatchers.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index 9b1b5cd544..9eb4674365 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -76,6 +76,7 @@ let MM; * Exported primarily for testing. */ export const defaultLimits = harden({ + __proto__: null, decimalDigitsLimit: 100, stringLengthLimit: 100_000, symbolNameLengthLimit: 100, @@ -1827,6 +1828,8 @@ const makePatternKit = () => { /** @type {Record} */ const HelpersByMatchTag = harden({ + __proto__: null, + 'match:any': matchAnyHelper, 'match:and': matchAndHelper, 'match:or': matchOrHelper, @@ -2303,6 +2306,7 @@ const makeInterfaceGuard = (interfaceName, methodGuards, options = {}) => { }; const GuardPayloadShapes = harden({ + __proto__: null, 'guard:awaitArgGuard': AwaitArgGuardPayloadShape, 'guard:rawGuard': RawGuardPayloadShape, 'guard:methodGuard': MethodGuardPayloadShape, From 03a9dd1728b3182eb02dc6191e99d203f32f3a48 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 15 Jan 2026 23:48:10 -0800 Subject: [PATCH 07/17] fix(patterns): Don't confuse "copyBag" as a pass style Fixes #3052 --- packages/patterns/package.json | 1 + .../patterns/src/patterns/patternMatchers.js | 33 +++++++++++-------- yarn.lock | 1 + 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/patterns/package.json b/packages/patterns/package.json index 83ddd42fcc..6a2c05ad3c 100644 --- a/packages/patterns/package.json +++ b/packages/patterns/package.json @@ -37,6 +37,7 @@ "@endo/errors": "workspace:^", "@endo/eventual-send": "workspace:^", "@endo/marshal": "workspace:^", + "@endo/memoize": "workspace:^", "@endo/pass-style": "workspace:^", "@endo/promise-kit": "workspace:^" }, diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index 9eb4674365..23b6085e57 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -28,6 +28,7 @@ import { recordValues, qp, } from '@endo/marshal'; +import { memoize } from '@endo/memoize'; import { keyEQ, keyGT, keyGTE, keyLT, keyLTE } from '../keys/compareKeys.js'; import { @@ -57,6 +58,16 @@ import { generateCollectionPairEntries } from '../keys/keycollection-operators.j const { entries, values, hasOwn } = Object; const { ownKeys } = Reflect; +const provideEncodePassableMetadata = memoize( + /** @param {KeyToDBKey} encodePassable */ + encodePassable => { + const staticRanks = provideStaticRanks(encodePassable); + const [encodingPrefix] = staticRanks['*'].cover; + const encodingPrefixLength = encodingPrefix.length; + return { staticRanks, encodingPrefix, encodingPrefixLength }; + }, +); + /** @type {WeakSet} */ const patternMemo = new WeakSet(); @@ -646,16 +657,11 @@ const makePatternKit = () => { const getPassStyleCover = (passStyle, encodePassable) => provideStaticRanks(encodePassable)[passStyle].cover; - /** @type {(encodePassable: KeyToDBKey) => number} */ - const getEncodingPrefixLength = encodePassable => { - const [encodingPrefix] = provideStaticRanks(encodePassable)['*'].cover; - return encodingPrefix.length; - }; - /** @type {GetRankCover} */ const getRankCover = (patt, encodePassable) => { // This partially validates encodePassable. - provideStaticRanks(encodePassable); + const { encodingPrefixLength: epLen } = + provideEncodePassableMetadata(encodePassable); if (isKey(patt)) { const encoded = encodePassable(patt); @@ -674,7 +680,6 @@ const makePatternKit = () => { Fail`internal: all-Key copyArray ${q(patt)} must itself be a Key`; // Discover the prefix that will start both bounds by encoding a // CopyArray consisting of those Keys followed by a null sentinel element. - const epLen = getEncodingPrefixLength(encodePassable); const sentinel = null; const embeddedSentinel = encodePassable(sentinel).slice(epLen); const keyArr = harden([...patt.slice(0, nonKeyIdx), sentinel]); @@ -929,11 +934,13 @@ const makePatternKit = () => { (reject && reject`match:kind: payload: ${allegedKeyKind} - A kind name must be a string`), - getRankCover: (kind, encodePassable) => - getPassStyleCover( - kind === 'copySet' || kind === 'copyMap' ? 'tagged' : kind, - encodePassable, - ), + getRankCover: (kind, encodePassable) => { + const passStyle = provideEncodePassableMetadata(encodePassable) + .staticRanks[kind] + ? kind + : 'tagged'; + return getPassStyleCover(passStyle, encodePassable); + }, }); /** @type {MatchHelper} */ diff --git a/yarn.lock b/yarn.lock index 5c9ff144a6..40758e7db7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -710,6 +710,7 @@ __metadata: "@endo/eventual-send": "workspace:^" "@endo/init": "workspace:^" "@endo/marshal": "workspace:^" + "@endo/memoize": "workspace:^" "@endo/pass-style": "workspace:^" "@endo/promise-kit": "workspace:^" "@endo/ses-ava": "workspace:^" From 17178d77c1d55f0f6483cf5fd0c5208e5fa89217 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 15 Jan 2026 23:48:10 -0800 Subject: [PATCH 08/17] feat(patterns): Further tighten bounds for `getRankCover([key, ..., nonKey, ...])` Ref #3046 --- .../patterns/src/patterns/patternMatchers.js | 20 ++++++-- packages/patterns/test/rankCover.test.js | 51 +++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index 23b6085e57..785d0a380d 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -64,7 +64,14 @@ const provideEncodePassableMetadata = memoize( const staticRanks = provideStaticRanks(encodePassable); const [encodingPrefix] = staticRanks['*'].cover; const encodingPrefixLength = encodingPrefix.length; - return { staticRanks, encodingPrefix, encodingPrefixLength }; + const inner = harden(['inner']); + const outer = harden(['outer', [inner]]); + const innerEncoded = encodePassable(inner); + const outerEncoded = encodePassable(outer); + const isEmbeddable = outerEncoded.includes( + innerEncoded.slice(encodingPrefixLength), + ); + return { staticRanks, encodingPrefix, encodingPrefixLength, isEmbeddable }; }, ); @@ -660,7 +667,7 @@ const makePatternKit = () => { /** @type {GetRankCover} */ const getRankCover = (patt, encodePassable) => { // This partially validates encodePassable. - const { encodingPrefixLength: epLen } = + const { encodingPrefixLength: epLen, isEmbeddable } = provideEncodePassableMetadata(encodePassable); if (isKey(patt)) { @@ -678,6 +685,8 @@ const makePatternKit = () => { const nonKeyIdx = patt.findIndex(v => !isKey(v)); nonKeyIdx !== -1 || Fail`internal: all-Key copyArray ${q(patt)} must itself be a Key`; + if (!isEmbeddable && nonKeyIdx === 0) break; + // Discover the prefix that will start both bounds by encoding a // CopyArray consisting of those Keys followed by a null sentinel element. const sentinel = null; @@ -686,7 +695,12 @@ const makePatternKit = () => { const encodedKeyArr = encodePassable(keyArr); const prefixLength = encodedKeyArr.lastIndexOf(embeddedSentinel); const prefix = encodedKeyArr.slice(0, prefixLength); - // Combine that prefix with the RankCover of the first non-Key element. + + // If encodePassable is not embeddable, just use the key elements. + if (!isEmbeddable) return [`${prefix}`, `${prefix}~`]; + + // Otherwise, combine that prefix with the RankCover of the first + // non-Key element for even tighter bounds. const [lowerSuffix, upperSuffix] = getRankCover( patt[nonKeyIdx], encodePassable, diff --git a/packages/patterns/test/rankCover.test.js b/packages/patterns/test/rankCover.test.js index 3ea3eea591..42adc4e8c1 100644 --- a/packages/patterns/test/rankCover.test.js +++ b/packages/patterns/test/rankCover.test.js @@ -11,14 +11,19 @@ import { provideStaticRanks, } from '@endo/marshal'; +import { isKey } from '../src/keys/checkKey.js'; import { getRankCover, kindOf, M } from '../src/patterns/patternMatchers.js'; /** @import {Implementation} from 'ava'; */ const formats = ['legacyOrdered', 'compactOrdered']; +const isInteriorRange = (inner, outer) => + compareRank(outer[0], inner[0]) <= 0 && compareRank(inner[1], outer[1]) <= 0; + const { arbKey, + arbPassable, arbLiftedPassable: arbPassableAndPattern, } = makeArbitraries(fc, { excludePassStyles: ['byteArray'], @@ -104,3 +109,49 @@ testAcrossFormats( ); }, ); + +testAcrossFormats( + 'getRankCover(arrayWithInitialKey, encodePassable) is tighter than "any copyArray"', + async (t, encodePassable) => { + const coverAnyCopyArray = + provideStaticRanks(encodePassable).copyArray.cover; + t.true( + Array.isArray(coverAnyCopyArray) && + coverAnyCopyArray.length === 2 && + coverAnyCopyArray.every(el => typeof el === 'string') && + compareRank(coverAnyCopyArray[0], coverAnyCopyArray[1]) < 0, + `precondition: CopyArray static coverage ${q(coverAnyCopyArray)} is a valid range`, + ); + + let foundNotCovered = false; + await fc.assert( + fc.property( + arbKey, + fc.array(arbPassable).filter(x => !isKey(harden(x))), + fc.array(arbPassable), + (first, rest, other) => { + const cover = getRankCover(harden([first, ...rest]), encodePassable); + t.true( + isInteriorRange(cover, coverAnyCopyArray), + `leading-Key CopyArray coverage is a subset of any-CopyArray coverage: ${q(cover)} ⊆ ${q(coverAnyCopyArray)}`, + ); + t.true( + coverAnyCopyArray[0] < cover[0] || cover[1] < coverAnyCopyArray[1], + `leading-Key CopyArray coverage is tighter than any-CopyArray coverage: ${q(cover)} ⊊ ${q(coverAnyCopyArray)}`, + ); + + if (!foundNotCovered) { + const encodedOther = encodePassable(harden(other)); + foundNotCovered = + compareRank(encodedOther, cover[0]) < 0 || + compareRank(cover[1], encodedOther) < 0; + } + }, + ), + ); + t.true( + foundNotCovered, + 'at least one CopyArray must be outside of leading-Key coverage', + ); + }, +); From 8ccff21d949d3bb6e0f07d3c07d67bf813e63c65 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 16 Jan 2026 10:55:26 -0500 Subject: [PATCH 09/17] fixup! test(patterns): Add property-based testing for getRankCover --- packages/marshal/test/encodePassable.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/marshal/test/encodePassable.test.js b/packages/marshal/test/encodePassable.test.js index f562c13609..3b702209c2 100644 --- a/packages/marshal/test/encodePassable.test.js +++ b/packages/marshal/test/encodePassable.test.js @@ -15,7 +15,9 @@ import { import { compareRank, makeFullOrderComparatorKit } from '../src/rankOrder.js'; import { unsortedSample } from '../tools/marshal-test-data.js'; -const { arbPassable } = makeArbitraries(fc, { excludePassStyles: ['byteArray'] }); +const { arbPassable } = makeArbitraries(fc, { + excludePassStyles: ['byteArray'], +}); const statelessEncodePassableLegacy = makeEncodePassable(); From 794e79aa5f2431181334d7b5c66b4d639163f6f4 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 19 Jan 2026 16:38:09 -0800 Subject: [PATCH 10/17] fixup! fix(patterns)!: Account for the compactOrdered "~" prefix when computing coverage --- packages/marshal/src/rankOrder.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/marshal/src/rankOrder.js b/packages/marshal/src/rankOrder.js index 0b22e99ac2..5613e58d66 100644 --- a/packages/marshal/src/rankOrder.js +++ b/packages/marshal/src/rankOrder.js @@ -159,7 +159,8 @@ harden(passStyleRanks); * * @param {PassStyle} passStyle * @returns {RankCover} - * @deprecated Coverage depends upon format; use provideStaticRanks instead. + * @deprecated Coverage depends upon format; use {@link provideStaticRanks} + * instead. */ export const getPassStyleCover = passStyle => passStyleRanks[passStyle].cover; harden(getPassStyleCover); From d118f35bb887e5d8798914c53a29bcc8ec97d86b Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 19 Jan 2026 17:42:19 -0800 Subject: [PATCH 11/17] fixup! feat(patterns): Further tighten bounds for `getRankCover([key, ..., nonKey, ...])` --- packages/patterns/test/rankCover.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/patterns/test/rankCover.test.js b/packages/patterns/test/rankCover.test.js index 42adc4e8c1..f0ce09ea2c 100644 --- a/packages/patterns/test/rankCover.test.js +++ b/packages/patterns/test/rankCover.test.js @@ -127,10 +127,10 @@ testAcrossFormats( await fc.assert( fc.property( arbKey, - fc.array(arbPassable).filter(x => !isKey(harden(x))), + fc.array(arbPassable, { minLength: 1 }).filter(x => !isKey(harden(x))), fc.array(arbPassable), - (first, rest, other) => { - const cover = getRankCover(harden([first, ...rest]), encodePassable); + (key, rest, other) => { + const cover = getRankCover(harden([key, ...rest]), encodePassable); t.true( isInteriorRange(cover, coverAnyCopyArray), `leading-Key CopyArray coverage is a subset of any-CopyArray coverage: ${q(cover)} ⊆ ${q(coverAnyCopyArray)}`, From aaae55e0b46840529a6eb1ea1f1b3e106486b9ce Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 19 Jan 2026 18:14:54 -0800 Subject: [PATCH 12/17] test(patterns): Limite generated the size of generated arrays --- packages/pass-style/tools/arb-passable.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/pass-style/tools/arb-passable.js b/packages/pass-style/tools/arb-passable.js index 58b1126582..d0486d3855 100644 --- a/packages/pass-style/tools/arb-passable.js +++ b/packages/pass-style/tools/arb-passable.js @@ -11,6 +11,9 @@ import { nameForPassableSymbol, passableSymbolForName } from '../src/symbol.js'; * @import { Passable, CopyTagged } from '../src/types.js'; */ +/** Avoid wasting time on overly large data structures. */ +const maxLength = 100; + /** @type ((reason: string) => never) */ const reject = reason => { throw Error(reason); @@ -148,12 +151,13 @@ export const makeArbitraries = ( withLiftingDetail( fc.oneof( // copyArray - recoverableMap(fc.array(tie('liftedKeyDag')), pairsArr => - [0, 1].map(i => pairsArr.map(pair => pair[i])), + recoverableMap( + fc.array(tie('liftedKeyDag'), { maxLength }), + pairsArr => [0, 1].map(i => pairsArr.map(pair => pair[i])), ), // copyRecord recoverableMap( - fc.dictionary(notThen, tie('liftedKeyDag')), + fc.dictionary(notThen, tie('liftedKeyDag'), { maxKeys: maxLength }), pairsRec => [0, 1].map(i => objectMap(pairsRec, p => p[i])), ), ), @@ -170,12 +174,13 @@ export const makeArbitraries = ( withLiftingDetail( fc.oneof( // copyArray - recoverableMap(fc.array(tie('liftedArbDag')), pairsArr => - [0, 1].map(i => pairsArr.map(pair => pair[i])), + recoverableMap( + fc.array(tie('liftedArbDag'), { maxLength }), + pairsArr => [0, 1].map(i => pairsArr.map(pair => pair[i])), ), // copyRecord recoverableMap( - fc.dictionary(notThen, tie('liftedArbDag')), + fc.dictionary(notThen, tie('liftedArbDag'), { maxKeys: maxLength }), pairsRec => [0, 1].map(i => objectMap(pairsRec, p => p[i])), ), // promise @@ -198,6 +203,7 @@ export const makeArbitraries = ( // TODO: A valid copySet payload must be a reverse sorted array. recoverableMap( fc.uniqueArray(tie('liftedKeyDag'), { + maxLength, selector: pair => pair[0], }), pairsArr => @@ -213,7 +219,7 @@ export const makeArbitraries = ( recoverableMap( fc.uniqueArray( fc.tuple(tie('liftedKeyDag'), fc.bigInt({ min: 1n })), - { selector: pairKeyedEntry => pairKeyedEntry[0][0] }, + { maxLength, selector: pairKeyedEntry => pairKeyedEntry[0][0] }, ), pairKeyedEntries => [0, 1].map(i => @@ -227,6 +233,7 @@ export const makeArbitraries = ( // TODO: A valid copyMap payload must be a reverse sorted array. recoverableMap( fc.uniqueArray(fc.tuple(tie('liftedKeyDag'), tie('liftedArbDag')), { + maxLength, selector: pairKeyedEntry => pairKeyedEntry[0][0], }), pairKeyedEntries => From 332961f4ff36e02bfbd1f55a1a7ed2023ed36e2f Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 19 Jan 2026 18:23:14 -0800 Subject: [PATCH 13/17] fixup! test(patterns): Add property-based testing for getRankCover --- packages/pass-style/tools/arb-passable.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/pass-style/tools/arb-passable.js b/packages/pass-style/tools/arb-passable.js index d0486d3855..4b99757fd6 100644 --- a/packages/pass-style/tools/arb-passable.js +++ b/packages/pass-style/tools/arb-passable.js @@ -94,9 +94,9 @@ export const makeArbitraries = ( () => !excludePassStyles.includes('byteArray'), ), fc.constantFrom(-0, NaN, Infinity, -Infinity), - // This was producing null-prototype objects: - // fc.record({}), - fc.constant(undefined).map(() => ({})), + /** @type {Arbitrary>} */ ( + fc.record({}, { noNullPrototype: true }) + ), fc.constantFrom(exampleAlice, exampleBob, exampleCarol), ]; From 5c83d8b6f70d0486b42e37e55e911dad2de870a3 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 19 Jan 2026 18:38:57 -0800 Subject: [PATCH 14/17] fixup! test(patterns): Add property-based testing for getRankCover --- packages/pass-style/tools/arb-passable.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/pass-style/tools/arb-passable.js b/packages/pass-style/tools/arb-passable.js index 4b99757fd6..869c9cd23e 100644 --- a/packages/pass-style/tools/arb-passable.js +++ b/packages/pass-style/tools/arb-passable.js @@ -57,8 +57,9 @@ export const makeArbitraries = ( } = {}, ) => { const arbString = fc.oneof( + { withCrossShrink: true }, fc.string({ unit: 'grapheme-ascii' }), - fc.string(), + fc.string({ unit: 'binary' }), ); const notThen = arbString.filter(s => s !== 'then'); From ca12b127feb5317b5f58469da10796ba2c786261 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 23 Jan 2026 15:29:24 -0500 Subject: [PATCH 15/17] fixup! fix(patterns): Don't confuse "copyBag" as a pass style --- .../patterns/src/patterns/patternMatchers.js | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index 785d0a380d..bb5155c229 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -949,11 +949,26 @@ const makePatternKit = () => { reject`match:kind: payload: ${allegedKeyKind} - A kind name must be a string`), getRankCover: (kind, encodePassable) => { - const passStyle = provideEncodePassableMetadata(encodePassable) - .staticRanks[kind] - ? kind - : 'tagged'; - return getPassStyleCover(passStyle, encodePassable); + const { staticRanks } = provideEncodePassableMetadata(encodePassable); + + // If `kind` is a pass style, that defines the covering range. + const passStyleCover = kind !== '*' ? staticRanks[kind]?.cover : null; + if (passStyleCover) return passStyleCover; + + // If `kind` is a known {@link Kind}, *that* defines the covering range. + // XXX We really need a registry of known tags to avoid such hard-coding. + if ( + kind === 'copySet' || + kind === 'copyBag' || + kind === 'copyMap' || + kind.startsWith('match:*') || + kind.startsWith('guard:') + ) { + return staticRanks.tagged.cover; + } + + // To support future evolution, assume `kind` is an unknown pass style. + return staticRanks['*'].cover; }, }); From 4a911fc79a939cf8c28e9cefa95a12c5498bf093 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 6 Feb 2026 14:15:17 -0500 Subject: [PATCH 16/17] test(patterns): Use fast-check more efficiently --- packages/patterns/test/rankCover.test.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/patterns/test/rankCover.test.js b/packages/patterns/test/rankCover.test.js index f0ce09ea2c..0af1604f58 100644 --- a/packages/patterns/test/rankCover.test.js +++ b/packages/patterns/test/rankCover.test.js @@ -16,6 +16,9 @@ import { getRankCover, kindOf, M } from '../src/patterns/patternMatchers.js'; /** @import {Implementation} from 'ava'; */ +/** Avoid wasting time on overly large data structures. */ +const maxLength = 100; + const formats = ['legacyOrdered', 'compactOrdered']; const isInteriorRange = (inner, outer) => @@ -127,9 +130,19 @@ testAcrossFormats( await fc.assert( fc.property( arbKey, - fc.array(arbPassable, { minLength: 1 }).filter(x => !isKey(harden(x))), - fc.array(arbPassable), - (key, rest, other) => { + fc.array(arbPassable, { minLength: 1, maxLength }), + fc.array(arbPassable, { maxLength }), + fc.constantFrom( + x => new Error(String(x)), + x => Promise.resolve(x), + ), + (key, rest, other, makeNonKey) => { + if (rest.every(x => isKey(x))) { + // We need a non-Key, so to avoid wasting the work with fast-check + // `.filter` (which can lead to https://crbug.com/1201626 crashes), + // we just transform the first element as directed. + rest[0] = makeNonKey(rest[0]); + } const cover = getRankCover(harden([key, ...rest]), encodePassable); t.true( isInteriorRange(cover, coverAnyCopyArray), From 31c8b542012d05e3a15dcda01c11754cd41cc1ff Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 6 Feb 2026 14:40:00 -0500 Subject: [PATCH 17/17] fixup! fix(patterns): Don't confuse "copyBag" as a pass style --- packages/patterns/src/patterns/patternMatchers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index bb5155c229..01facb54b1 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -961,7 +961,7 @@ const makePatternKit = () => { kind === 'copySet' || kind === 'copyBag' || kind === 'copyMap' || - kind.startsWith('match:*') || + kind.startsWith('match:') || kind.startsWith('guard:') ) { return staticRanks.tagged.cover;