@agoric/store
Advanced tools
Comparing version 0.6.9-dev-a9895c4.0 to 0.6.9-dev-cc99f7e.0
{ | ||
"name": "@agoric/store", | ||
"version": "0.6.9-dev-a9895c4.0+a9895c4", | ||
"version": "0.6.9-dev-cc99f7e.0+cc99f7e", | ||
"description": "Wrapper for JavaScript map", | ||
@@ -34,9 +34,9 @@ "type": "module", | ||
"dependencies": { | ||
"@agoric/assert": "0.3.16-dev-a9895c4.0+a9895c4", | ||
"@agoric/eventual-send": "0.14.1-dev-a9895c4.0+a9895c4", | ||
"@agoric/marshal": "0.5.1-dev-a9895c4.0+a9895c4", | ||
"@agoric/promise-kit": "0.2.30-dev-a9895c4.0+a9895c4" | ||
"@agoric/assert": "0.3.16-dev-cc99f7e.0+cc99f7e", | ||
"@agoric/eventual-send": "0.14.1-dev-cc99f7e.0+cc99f7e", | ||
"@agoric/marshal": "0.5.1-dev-cc99f7e.0+cc99f7e", | ||
"@agoric/promise-kit": "0.2.30-dev-cc99f7e.0+cc99f7e" | ||
}, | ||
"devDependencies": { | ||
"@agoric/swingset-vat": "0.24.2-dev-a9895c4.0+a9895c4", | ||
"@agoric/swingset-vat": "0.24.2-dev-cc99f7e.0+cc99f7e", | ||
"ava": "^3.12.1" | ||
@@ -70,3 +70,3 @@ }, | ||
}, | ||
"gitHead": "a9895c41b57d5ab3543a213824e293ee304dbcaa" | ||
"gitHead": "cc99f7ed7f6de1b6ee86b1b813649820e741e1dc" | ||
} |
@@ -6,4 +6,10 @@ // @ts-check | ||
export { makePatternKit } from './patterns/patternMatchers.js'; | ||
export { compareRank } from './patterns/rankOrder.js'; | ||
export { | ||
M, | ||
isPattern, | ||
assertPattern, | ||
matches, | ||
fit, | ||
} from './patterns/patternMatchers.js'; | ||
// export { compareRank } from './patterns/rankOrder.js'; | ||
@@ -10,0 +16,0 @@ export { makeScalarWeakSetStore } from './stores/scalarWeakSetStore.js'; |
@@ -9,3 +9,2 @@ // @ts-check | ||
assertPassable, | ||
everyPassableChild, | ||
getTag, | ||
@@ -98,6 +97,9 @@ isObject, | ||
switch (passStyle) { | ||
case 'copyRecord': | ||
case 'copyRecord': { | ||
// A copyRecord is a key iff all its children are keys | ||
return Object.values(val).every(checkIt); | ||
} | ||
case 'copyArray': { | ||
// A copyRecord or copyArray is a key iff all its children are keys | ||
return everyPassableChild(val, checkIt); | ||
// A copyArray is a key iff all its children are keys | ||
return val.every(checkIt); | ||
} | ||
@@ -104,0 +106,0 @@ case 'tagged': { |
@@ -9,3 +9,3 @@ // @ts-check | ||
} from '@agoric/marshal'; | ||
import { compareRank, sortByRank } from '../patterns/rankOrder.js'; | ||
import { compareAntiRank, sortByRank } from '../patterns/rankOrder.js'; | ||
import { checkCopySetKeys } from './copySet.js'; | ||
@@ -19,3 +19,3 @@ | ||
/** @type WeakSet<CopyMap> */ | ||
/** @type WeakSet<CopyMap<any,any>> */ | ||
const copyMapMemo = new WeakSet(); | ||
@@ -68,26 +68,80 @@ | ||
/** | ||
* @param {CopyMap} m | ||
* @param {(v: Passable, i: number) => boolean} fn | ||
* @returns {boolean} | ||
* @template K,V | ||
* @param {CopyMap<K,V>} m | ||
* @returns {K[]} | ||
*/ | ||
export const everyCopyMapKey = (m, fn) => { | ||
export const getCopyMapKeys = m => { | ||
assertCopyMap(m); | ||
return m.payload.keys.every((v, i) => fn(v, i)); | ||
return m.payload.keys; | ||
}; | ||
harden(getCopyMapKeys); | ||
/** | ||
* @template K,V | ||
* @param {CopyMap<K,V>} m | ||
* @returns {V[]} | ||
*/ | ||
export const getCopyMapValues = m => { | ||
assertCopyMap(m); | ||
return m.payload.values; | ||
}; | ||
harden(getCopyMapValues); | ||
/** | ||
* @template K,V | ||
* @param {CopyMap<K,V>} m | ||
* @returns {Iterable<[K,V]>} | ||
*/ | ||
export const getCopyMapEntries = m => { | ||
assertCopyMap(m); | ||
const { | ||
payload: { keys, values }, | ||
} = m; | ||
const { length } = keys; | ||
return harden({ | ||
[Symbol.iterator]: () => { | ||
let i = 0; | ||
return harden({ | ||
next: () => { | ||
/** @type {IteratorResult<[K,V],void>} */ | ||
let result; | ||
if (i < length) { | ||
result = harden({ done: false, value: [keys[i], values[i]] }); | ||
i += 1; | ||
return result; | ||
} else { | ||
result = harden({ done: true, value: undefined }); | ||
} | ||
return result; | ||
}, | ||
}); | ||
}, | ||
}); | ||
}; | ||
harden(getCopyMapEntries); | ||
/** | ||
* @template K,V | ||
* @param {CopyMap<K,V>} m | ||
* @param {(key: K, index: number) => boolean} fn | ||
* @returns {boolean} | ||
*/ | ||
export const everyCopyMapKey = (m, fn) => | ||
getCopyMapKeys(m).every((key, index) => fn(key, index)); | ||
harden(everyCopyMapKey); | ||
/** | ||
* @param {CopyMap} m | ||
* @param {(v: Passable, i: number) => boolean} fn | ||
* @template K,V | ||
* @param {CopyMap<K,V>} m | ||
* @param {(value: V, index: number) => boolean} fn | ||
* @returns {boolean} | ||
*/ | ||
export const everyCopyMapValue = (m, fn) => { | ||
assertCopyMap(m); | ||
return m.payload.values.every((v, i) => fn(v, i)); | ||
}; | ||
export const everyCopyMapValue = (m, fn) => | ||
getCopyMapValues(m).every((value, index) => fn(value, index)); | ||
harden(everyCopyMapValue); | ||
/** | ||
* @param {CopyMap} m | ||
* @returns {CopySet} | ||
* @template K,V | ||
* @param {CopyMap<K,V>} m | ||
* @returns {CopySet<K>} | ||
*/ | ||
@@ -100,4 +154,5 @@ export const copyMapKeySet = m => | ||
/** | ||
* @param {Iterable<[Passable, Passable]>} entries | ||
* @returns {CopyMap} | ||
* @template K,V | ||
* @param {Iterable<[K, V]>} entries | ||
* @returns {CopyMap<K,V>} | ||
*/ | ||
@@ -108,7 +163,8 @@ export const makeCopyMap = entries => { | ||
// organized by those keys. Also, among values associated with | ||
// keys in the same equivalence class, those are rank sorted. This | ||
// keys in the same equivalence class, those are rank sorted. | ||
// TODO This | ||
// could solve the copyMap cover issue explained in patternMatchers.js. | ||
// But only if we include this criteria in our validation of copyMaps, | ||
// which we currently do not. | ||
const sortedEntries = [...sortByRank(entries, compareRank)].reverse(); | ||
const sortedEntries = sortByRank(entries, compareAntiRank); | ||
const keys = sortedEntries.map(([k, _v]) => k); | ||
@@ -115,0 +171,0 @@ const values = sortedEntries.map(([_k, v]) => v); |
@@ -10,3 +10,3 @@ // @ts-check | ||
import { | ||
compareRank, | ||
compareAntiRank, | ||
isRankSorted, | ||
@@ -33,4 +33,3 @@ sortByRank, | ||
} | ||
const reverseKeys = harden([...keys].reverse()); | ||
if (!isRankSorted(reverseKeys, compareRank)) { | ||
if (!isRankSorted(keys, compareAntiRank)) { | ||
return check( | ||
@@ -45,3 +44,3 @@ false, | ||
/** @type WeakSet<CopySet> */ | ||
/** @type WeakSet<CopySet<any>> */ | ||
const copySetMemo = new WeakSet(); | ||
@@ -77,18 +76,29 @@ | ||
/** | ||
* @param {CopySet} s | ||
* @param {(v: Passable, i: number) => boolean} fn | ||
* @returns {boolean} | ||
* @template K | ||
* @param {CopySet<K>} s | ||
* @returns {K[]} | ||
*/ | ||
export const everyCopySetKey = (s, fn) => { | ||
export const getCopySetKeys = s => { | ||
assertCopySet(s); | ||
return s.payload.every((v, i) => fn(v, i)); | ||
return s.payload; | ||
}; | ||
harden(getCopySetKeys); | ||
/** | ||
* @template K | ||
* @param {CopySet<K>} s | ||
* @param {(key: K, index: number) => boolean} fn | ||
* @returns {boolean} | ||
*/ | ||
export const everyCopySetKey = (s, fn) => | ||
getCopySetKeys(s).every((key, index) => fn(key, index)); | ||
harden(everyCopySetKey); | ||
/** | ||
* @param {Iterable<Passable>} elements | ||
* @returns {CopySet} | ||
* @template K | ||
* @param {Iterable<K>} elements | ||
* @returns {CopySet<K>} | ||
*/ | ||
export const makeCopySet = elements => | ||
makeTagged('copySet', [...sortByRank(elements, compareRank)].reverse()); | ||
makeTagged('copySet', sortByRank(elements, compareAntiRank)); | ||
harden(makeCopySet); |
@@ -5,3 +5,2 @@ // @ts-check | ||
assertChecker, | ||
everyPassableChild, | ||
Far, | ||
@@ -11,4 +10,6 @@ getTag, | ||
passStyleOf, | ||
hasOwnPropertyOf, | ||
} from '@agoric/marshal'; | ||
import { | ||
compareAntiRank, | ||
compareRank, | ||
@@ -41,5 +42,5 @@ getPassStyleCover, | ||
/** | ||
* @returns {Object} | ||
* @returns {PatternKit} | ||
*/ | ||
export const makePatternKit = () => { | ||
const makePatternKit = () => { | ||
/** | ||
@@ -88,7 +89,11 @@ * If this is a recognized match tag, return the MatchHelper. | ||
switch (passStyle) { | ||
case 'copyRecord': | ||
case 'copyRecord': { | ||
// A copyRecord is a pattern iff all its children are | ||
// patterns | ||
return Object.values(patt).every(checkIt); | ||
} | ||
case 'copyArray': { | ||
// A copyRecord or copyArray is a pattern iff all its children are | ||
// A copyArray is a pattern iff all its children are | ||
// patterns | ||
return everyPassableChild(patt, checkIt); | ||
return patt.every(checkIt); | ||
} | ||
@@ -250,5 +255,6 @@ case 'tagged': { | ||
keyEQ(specimen, patt), | ||
X`${specimen} - Must be equivalent to the literal pattern: ${patt}`, | ||
X`${specimen} - Must be equivalent to: ${patt}`, | ||
); | ||
} | ||
assertPattern(patt); | ||
const specStyle = passStyleOf(specimen); | ||
@@ -268,8 +274,6 @@ const pattStyle = passStyleOf(patt); | ||
false, | ||
X`Array ${specimen} - must be as long as copyArray pattern: ${patt}`, | ||
X`Array ${specimen} - Must be as long as copyArray pattern: ${patt}`, | ||
); | ||
} | ||
return everyPassableChild(patt, (p, i) => | ||
checkMatches(specimen[i], p, check), | ||
); | ||
return patt.every((p, i) => checkMatches(specimen[i], p, check)); | ||
} | ||
@@ -283,12 +287,4 @@ case 'copyRecord': { | ||
} | ||
const specNames = harden( | ||
ownKeys(specimen) | ||
.sort() | ||
.reverse(), | ||
); | ||
const pattNames = harden( | ||
ownKeys(patt) | ||
.sort() | ||
.reverse(), | ||
); | ||
const specNames = harden(ownKeys(specimen).sort(compareAntiRank)); | ||
const pattNames = harden(ownKeys(patt).sort(compareAntiRank)); | ||
if (!keyEQ(specNames, pattNames)) { | ||
@@ -379,3 +375,3 @@ return check( | ||
*/ | ||
const assertMatches = (specimen, patt) => { | ||
const fit = (specimen, patt) => { | ||
checkMatches(specimen, patt, assertChecker); | ||
@@ -390,3 +386,5 @@ }; | ||
const encoded = encodeKey(patt); | ||
return [encoded, `${encoded}~`]; | ||
if (encoded !== undefined) { | ||
return [encoded, `${encoded}~`]; | ||
} | ||
} | ||
@@ -396,11 +394,18 @@ const passStyle = passStyleOf(patt); | ||
case 'copyArray': { | ||
const rankCovers = patt.map(p => getRankCover(p, encodeKey)); | ||
return harden([ | ||
rankCovers.map(([left, _right]) => left), | ||
rankCovers.map(([_left, right]) => right), | ||
]); | ||
// 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, encodeKey)); | ||
// return harden([ | ||
// rankCovers.map(([left, _right]) => left), | ||
// rankCovers.map(([_left, right]) => right), | ||
// ]); | ||
break; | ||
} | ||
case 'copyRecord': { | ||
// XXX this doesn't get along with the world of cover === pair of | ||
// strings | ||
// strings. In the meantime, fall through to the default which | ||
// returns a cover that covers all copyRecords. | ||
// | ||
// const pattKeys = ownKeys(patt); | ||
@@ -414,3 +419,3 @@ // const pattEntries = harden(pattKeys.map(key => [key, patt[key]])); | ||
// ]); | ||
assert.fail('not supporting copyRecord patterns yet'); // XXX TEMP | ||
break; | ||
} | ||
@@ -421,2 +426,4 @@ case 'tagged': { | ||
if (matchHelper) { | ||
// Buried here is the important case, where we process | ||
// the various patternNodes | ||
return matchHelper.getRankCover(patt.payload, encodeKey); | ||
@@ -427,3 +434,5 @@ } | ||
// XXX this doesn't get along with the world of cover === pair of | ||
// strings | ||
// strings. In the meantime, fall through to the default which | ||
// returns a cover that covers all copySets. | ||
// | ||
// // Should already be validated by checkPattern. But because this | ||
@@ -444,27 +453,32 @@ // // is a check that may loosen over time, we also assert | ||
// ]); | ||
assert.fail('not supporting copySet patterns yet'); // XXX TEMP | ||
break; | ||
} | ||
case 'copyMap': { | ||
// A matching copyMap must have the same keys, or at most one | ||
// non-key key pattern. Thus we can assume that value positions | ||
// match 1-to-1. | ||
// 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 copyMaps. | ||
// | ||
// TODO I may be overlooking that the less precise rankOrder | ||
// equivalence class may cause values to be out of order, | ||
// making this rankCover not actually cover. In that case, for | ||
// all the values for keys at the same rank, we should union their | ||
// rank covers. TODO POSSIBLE SILENT CORRECTNESS BUG | ||
// | ||
// If this is a bug, it probably affects the getRankCover | ||
// cases if matchLTEHelper and matchGTEHelper on copyMap as | ||
// well. See makeCopyMap for an idea on fixing | ||
// this bug. | ||
const [leftPayloadLimit, rightPayloadLimit] = getRankCover( | ||
patt.payload, | ||
encodeKey, | ||
); | ||
return harden([ | ||
makeTagged('copyMap', leftPayloadLimit), | ||
makeTagged('copyMap', rightPayloadLimit), | ||
]); | ||
// // A matching copyMap must have the same keys, or at most one | ||
// // non-key key pattern. Thus we can assume that value positions | ||
// // match 1-to-1. | ||
// // | ||
// // TODO I may be overlooking that the less precise rankOrder | ||
// // equivalence class may cause values to be out of order, | ||
// // making this rankCover not actually cover. In that case, for | ||
// // all the values for keys at the same rank, we should union their | ||
// // rank covers. TODO POSSIBLE SILENT CORRECTNESS BUG | ||
// // | ||
// // If this is a bug, it probably affects the getRankCover | ||
// // cases of matchLTEHelper and matchGTEHelper on copyMap as | ||
// // well. See makeCopyMap for an idea on fixing | ||
// // this bug. | ||
// const [leftPayloadLimit, rightPayloadLimit] = getRankCover( | ||
// patt.payload, | ||
// encodeKey, | ||
// ); | ||
// return harden([ | ||
// makeTagged('copyMap', leftPayloadLimit), | ||
// makeTagged('copyMap', rightPayloadLimit), | ||
// ]); | ||
break; | ||
} | ||
@@ -488,28 +502,12 @@ default: { | ||
const matchAnyHelper = Far('M.any helper', { | ||
checkIsMatcherPayload: (matcherPayload, check = x => x) => | ||
check( | ||
matcherPayload === undefined, | ||
X`An M.any matcher's .payload must be undefined: ${matcherPayload}`, | ||
), | ||
checkMatches: (_specimen, _matcherPayload, _check = x => x) => true, | ||
getRankCover: (_matchPayload, _encodeKey) => ['', '{'], | ||
checkKeyPattern: (_matcherPayload, _check = x => x) => true, | ||
}); | ||
/** @type {MatchHelper} */ | ||
const matchScalarHelper = Far('M.scalar helper', { | ||
checkIsMatcherPayload: (matcherPayload, check = x => x) => | ||
check( | ||
matcherPayload === undefined, | ||
X`An M.scalar matcher's .payload must be undefined: ${matcherPayload}`, | ||
X`Payload must be undefined: ${matcherPayload}`, | ||
), | ||
checkMatches: (specimen, _matcherPayload, check = x => x) => | ||
checkScalarKey(specimen, check), | ||
getRankCover: (_matchPayload, _encodeKey) => ['', '{'], | ||
getRankCover: (_matchPayload, _encodeKey) => ['a', 'z~'], | ||
checkKeyPattern: (_matcherPayload, _check = x => x) => true, | ||
@@ -519,66 +517,7 @@ }); | ||
/** @type {MatchHelper} */ | ||
const matchKindHelper = Far('M.kind helper', { | ||
checkIsMatcherPayload: (allegedKeyKind, check = x => x) => | ||
check( | ||
// We cannot further restrict this to only possible passStyles | ||
// or tags, because we wish to allow matching of tags that we | ||
// don't know ahead of time. Do we need to separate the namespaces? | ||
// TODO are we asking for trouble by lumping passStyles and tags | ||
// together into kinds? | ||
typeof allegedKeyKind === 'string', | ||
X`A kind name must be a string: ${allegedKeyKind}`, | ||
), | ||
checkMatches: (specimen, kind, check = x => x) => | ||
check( | ||
passStyleOf(specimen) === kind || | ||
(passStyleOf(specimen) === 'tagged' && getTag(specimen) === kind), | ||
X`${specimen} - Must have passStyle or tag ${q(kind)}`, | ||
), | ||
getRankCover: (kind, _encodeKey) => { | ||
switch (kind) { | ||
case 'copySet': { | ||
// The bounds in the cover are not valid copySets, which is fine. | ||
// They only need to be valid copyTagged that bound all possible | ||
// copySets. Thus, we need to call makeTagged directly, rather | ||
// than using makeCopySet. | ||
return [ | ||
makeTagged('copySet', null), | ||
makeTagged('copySet', undefined), | ||
]; | ||
} | ||
case 'copyMap': { | ||
// The bounds in the cover are not valid copyMaps, which is fine. | ||
// They only need to be valid copyTagged that bound all possible | ||
// copyMaps. | ||
return [ | ||
makeTagged('copyMap', null), | ||
makeTagged('copyMap', undefined), | ||
]; | ||
} | ||
default: { | ||
return getPassStyleCover(/** @type {PassStyle} */ (kind)); | ||
} | ||
} | ||
const matchAndHelper = Far('match:and helper', { | ||
checkMatches: (specimen, patts, check = x => x) => { | ||
return patts.every(patt => checkMatches(specimen, patt, check)); | ||
}, | ||
checkKeyPattern: (kind, check = x => x) => { | ||
switch (kind) { | ||
case 'boolean': | ||
case 'number': | ||
case 'bigint': | ||
case 'string': | ||
case 'symbol': | ||
case 'remotable': | ||
case 'undefined': | ||
return true; | ||
default: | ||
return check(false, X`${kind} keys are not supported`); | ||
} | ||
}, | ||
}); | ||
/** @type {MatchHelper} */ | ||
const matchAndHelper = Far('match:and helper', { | ||
checkIsMatcherPayload: (allegedPatts, check = x => x) => { | ||
@@ -590,10 +529,6 @@ const checkIt = patt => checkPattern(patt, check); | ||
X`Needs array of sub-patterns: ${allegedPatts}`, | ||
) && everyPassableChild(allegedPatts, checkIt)) | ||
) && allegedPatts.every(checkIt)) | ||
); | ||
}, | ||
checkMatches: (specimen, patts, check = x => x) => { | ||
return patts.every(patt => checkMatches(specimen, patt, check)); | ||
}, | ||
getRankCover: (patts, encodeKey) => | ||
@@ -612,13 +547,18 @@ intersectRankCovers( | ||
const matchOrHelper = Far('match:or helper', { | ||
checkIsMatcherPayload: matchAndHelper.checkIsMatcherPayload, | ||
checkMatches: (specimen, patts, check = x => x) => { | ||
return ( | ||
(check( | ||
patts.length >= 1, | ||
const { length } = patts; | ||
if (length === 0) { | ||
return check( | ||
false, | ||
X`${specimen} - no pattern disjuncts to match: ${patts}`, | ||
) && !patts.every(patt => !checkMatches(specimen, patt, check))) | ||
); | ||
); | ||
} | ||
if (patts.some(patt => matches(specimen, patt))) { | ||
return true; | ||
} | ||
return check(false, X`${specimen} - Must match one of ${patts}`); | ||
}, | ||
checkIsMatcherPayload: matchAndHelper.checkIsMatcherPayload, | ||
getRankCover: (patts, encodeKey) => | ||
@@ -637,4 +577,2 @@ unionRankCovers( | ||
const matchNotHelper = Far('match:not helper', { | ||
checkIsMatcherPayload: checkPattern, | ||
checkMatches: (specimen, patt, check = x => x) => { | ||
@@ -651,2 +589,4 @@ if (matches(specimen, patt)) { | ||
checkIsMatcherPayload: checkPattern, | ||
getRankCover: (_patt, _encodeKey) => ['', '{'], | ||
@@ -658,5 +598,91 @@ | ||
/** @type {MatchHelper} */ | ||
const matchScalarHelper = Far('M.scalar helper', { | ||
checkMatches: (specimen, _matcherPayload, check = x => x) => | ||
checkScalarKey(specimen, check), | ||
checkIsMatcherPayload: matchAnyHelper.checkIsMatcherPayload, | ||
getRankCover: (_matchPayload, _encodeKey) => ['a', 'z~'], | ||
checkKeyPattern: (_matcherPayload, _check = x => x) => true, | ||
}); | ||
/** @type {MatchHelper} */ | ||
const matchKeyHelper = Far('M.key helper', { | ||
checkMatches: (specimen, _matcherPayload, check = x => x) => | ||
checkKey(specimen, check), | ||
checkIsMatcherPayload: matchAnyHelper.checkIsMatcherPayload, | ||
getRankCover: (_matchPayload, _encodeKey) => ['a', 'z~'], | ||
checkKeyPattern: (_matcherPayload, _check = x => x) => true, | ||
}); | ||
/** @type {MatchHelper} */ | ||
const matchPatternHelper = Far('M.pattern helper', { | ||
checkMatches: (specimen, _matcherPayload, check = x => x) => | ||
checkPattern(specimen, check), | ||
checkIsMatcherPayload: matchAnyHelper.checkIsMatcherPayload, | ||
getRankCover: (_matchPayload, _encodeKey) => ['a', 'z~'], | ||
checkKeyPattern: (_matcherPayload, _check = x => x) => true, | ||
}); | ||
/** @type {MatchHelper} */ | ||
const matchKindHelper = Far('M.kind helper', { | ||
checkMatches: (specimen, kind, check = x => x) => | ||
check( | ||
passStyleOf(specimen) === kind || | ||
(passStyleOf(specimen) === 'tagged' && getTag(specimen) === kind), | ||
X`${specimen} - Must have passStyle or tag ${q(kind)}`, | ||
), | ||
checkIsMatcherPayload: (allegedKeyKind, check = x => x) => | ||
check( | ||
// We cannot further restrict this to only possible passStyles | ||
// or tags, because we wish to allow matching of tags that we | ||
// don't know ahead of time. Do we need to separate the namespaces? | ||
// TODO are we asking for trouble by lumping passStyles and tags | ||
// together into kinds? | ||
typeof allegedKeyKind === 'string', | ||
X`A kind name must be a string: ${allegedKeyKind}`, | ||
), | ||
getRankCover: (kind, _encodeKey) => { | ||
let style; | ||
switch (kind) { | ||
case 'copySet': | ||
case 'copyMap': { | ||
style = 'tagged'; | ||
break; | ||
} | ||
default: { | ||
style = kind; | ||
break; | ||
} | ||
} | ||
return getPassStyleCover(style); | ||
}, | ||
checkKeyPattern: (kind, check = x => x) => { | ||
switch (kind) { | ||
case 'boolean': | ||
case 'number': | ||
case 'bigint': | ||
case 'string': | ||
case 'symbol': | ||
case 'remotable': | ||
case 'undefined': | ||
return true; | ||
default: | ||
return check(false, X`${kind} keys are not supported`); | ||
} | ||
}, | ||
}); | ||
/** @type {MatchHelper} */ | ||
const matchLTEHelper = Far('match:lte helper', { | ||
checkIsMatcherPayload: checkKey, | ||
checkMatches: (specimen, rightOperand, check = x => x) => | ||
@@ -668,2 +694,4 @@ check( | ||
checkIsMatcherPayload: checkKey, | ||
getRankCover: (rightOperand, encodeKey) => { | ||
@@ -674,50 +702,8 @@ const passStyle = passStyleOf(rightOperand); | ||
// eslint-disable-next-line prefer-const | ||
let [leftBound, _rightBound] = getPassStyleCover(passStyle); | ||
switch (passStyle) { | ||
case 'number': { | ||
if (Number.isNaN(rightOperand)) { | ||
// leftBound = NaN; | ||
leftBound = 'f'; // XXX BOGUS | ||
} | ||
break; | ||
} | ||
case 'copyRecord': { | ||
// XXX this doesn't get along with the world of cover === pair of | ||
// strings | ||
// leftBound = harden( | ||
// fromEntries(entries(rightOperand).map(([k, _v]) => [k, null])), | ||
// ); | ||
break; | ||
} | ||
case 'tagged': { | ||
leftBound = makeTagged(getTag(rightOperand), null); | ||
switch (getTag(rightOperand)) { | ||
case 'copyMap': { | ||
const { keys } = rightOperand.payload; | ||
const values = keys.map(_ => null); | ||
// See note in getRankCover for copyMap about why we | ||
// may need to take variable values orders into account | ||
// to be correct. | ||
leftBound = makeTagged('copyMap', harden({ keys, values })); | ||
break; | ||
} | ||
default: { | ||
break; | ||
} | ||
} | ||
break; | ||
} | ||
case 'remotable': { | ||
// This does not make for a tighter rankCover, but if an | ||
// underlying table internally further optimizes, for example with | ||
// an identityHash of a virtual object, then this might | ||
// help it take advantage of that. | ||
leftBound = encodeKey(rightOperand); | ||
break; | ||
} | ||
default: { | ||
break; | ||
} | ||
let [leftBound, rightBound] = getPassStyleCover(passStyle); | ||
const newRightBound = `${encodeKey(rightOperand)}~`; | ||
if (newRightBound !== undefined) { | ||
rightBound = newRightBound; | ||
} | ||
return [leftBound, `${encodeKey(rightOperand)}~`]; | ||
return [leftBound, rightBound]; | ||
}, | ||
@@ -731,4 +717,2 @@ | ||
const matchLTHelper = Far('match:lt helper', { | ||
checkIsMatcherPayload: checkKey, | ||
checkMatches: (specimen, rightOperand, check = x => x) => | ||
@@ -740,2 +724,4 @@ check( | ||
checkIsMatcherPayload: checkKey, | ||
getRankCover: matchLTEHelper.getRankCover, | ||
@@ -749,4 +735,2 @@ | ||
const matchGTEHelper = Far('match:gte helper', { | ||
checkIsMatcherPayload: checkKey, | ||
checkMatches: (specimen, rightOperand, check = x => x) => | ||
@@ -758,2 +742,4 @@ check( | ||
checkIsMatcherPayload: checkKey, | ||
getRankCover: (rightOperand, encodeKey) => { | ||
@@ -764,55 +750,8 @@ const passStyle = passStyleOf(rightOperand); | ||
// eslint-disable-next-line prefer-const | ||
let [_leftBound, rightBound] = getPassStyleCover(passStyle); | ||
switch (passStyle) { | ||
case 'number': { | ||
if (Number.isNaN(rightOperand)) { | ||
// rightBound = NaN; | ||
rightBound = 'f'; | ||
} else { | ||
// rightBound = Infinity; | ||
rightBound = 'f~'; | ||
} | ||
break; | ||
} | ||
case 'copyRecord': { | ||
// XXX this doesn't get along with the world of cover === pair of | ||
// strings | ||
// rightBound = harden( | ||
// fromEntries( | ||
// entries(rightOperand).map(([k, _v]) => [k, undefined]), | ||
// ), | ||
// ); | ||
break; | ||
} | ||
case 'tagged': { | ||
rightBound = makeTagged(getTag(rightOperand), undefined); | ||
switch (getTag(rightOperand)) { | ||
case 'copyMap': { | ||
const { keys } = rightOperand.payload; | ||
const values = keys.map(_ => undefined); | ||
// See note in getRankCover for copyMap about why we | ||
// may need to take variable values orders into account | ||
// to be correct. | ||
rightBound = makeTagged('copyMap', harden({ keys, values })); | ||
break; | ||
} | ||
default: { | ||
break; | ||
} | ||
} | ||
break; | ||
} | ||
case 'remotable': { | ||
// This does not make for a tighter rankCover, but if an | ||
// underlying table internally further optimizes, for example with | ||
// an identityHash of a virtual object, then this might | ||
// help it take advantage of that. | ||
rightBound = encodeKey(rightOperand); | ||
break; | ||
} | ||
default: { | ||
break; | ||
} | ||
let [leftBound, rightBound] = getPassStyleCover(passStyle); | ||
const newLeftBound = encodeKey(rightOperand); | ||
if (newLeftBound !== undefined) { | ||
leftBound = newLeftBound; | ||
} | ||
return [encodeKey(rightOperand), rightBound]; | ||
return [leftBound, rightBound]; | ||
}, | ||
@@ -826,4 +765,2 @@ | ||
const matchGTHelper = Far('match:gt helper', { | ||
getMatchTag: () => 'gt', | ||
checkMatches: (specimen, rightOperand, check = x => x) => | ||
@@ -843,10 +780,212 @@ check( | ||
/** @type {MatchHelper} */ | ||
const matchArrayOfHelper = Far('match:arrayOf helper', { | ||
checkMatches: (specimen, subPatt, check = x => x) => | ||
check( | ||
passStyleOf(specimen) === 'copyArray', | ||
X`${specimen} - Must be an array`, | ||
) && specimen.every(el => checkMatches(el, subPatt, check)), | ||
checkIsMatcherPayload: checkPattern, | ||
getRankCover: () => getPassStyleCover('copyArray'), | ||
checkKeyPattern: (_, check = x => x) => | ||
check(false, X`Arrays not yet supported as keys`), | ||
}); | ||
/** @type {MatchHelper} */ | ||
const matchRecordOfHelper = Far('match:recordOf helper', { | ||
checkMatches: (specimen, entryPatt, check = x => x) => | ||
check( | ||
passStyleOf(specimen) === 'copyRecord', | ||
X`${specimen} - Must be a record`, | ||
) && | ||
Object.entries(specimen).every(el => | ||
checkMatches(harden(el), entryPatt, check), | ||
), | ||
checkIsMatcherPayload: (entryPatt, check = x => x) => | ||
check( | ||
passStyleOf(entryPatt) === 'copyArray' && entryPatt.length === 2, | ||
X`${entryPatt} - Must be an pair of patterns`, | ||
) && checkPattern(entryPatt, check), | ||
getRankCover: _entryPatt => getPassStyleCover('copyRecord'), | ||
checkKeyPattern: (_entryPatt, check = x => x) => | ||
check(false, X`Records not yet supported as keys`), | ||
}); | ||
/** @type {MatchHelper} */ | ||
const matchSetOfHelper = Far('match:setOf helper', { | ||
checkMatches: (specimen, keyPatt, check = x => x) => | ||
check( | ||
passStyleOf(specimen) === 'tagged' && getTag(specimen) === 'copySet', | ||
X`${specimen} - Must be a a CopySet`, | ||
) && specimen.payload.every(el => checkMatches(el, keyPatt)), | ||
checkIsMatcherPayload: checkPattern, | ||
getRankCover: () => getPassStyleCover('tagged'), | ||
checkKeyPattern: (_, check = x => x) => | ||
check(false, X`CopySets not yet supported as keys`), | ||
}); | ||
/** @type {MatchHelper} */ | ||
const matchMapOfHelper = Far('match:mapOf helper', { | ||
checkMatches: (specimen, [keyPatt, valuePatt], check = x => x) => | ||
check( | ||
passStyleOf(specimen) === 'tagged' && getTag(specimen) === 'copyMap', | ||
X`${specimen} - Must be a CopyMap`, | ||
) && | ||
specimen.payload.keys.every(k => checkMatches(k, keyPatt, check)) && | ||
specimen.payload.values.every(v => checkMatches(v, valuePatt, check)), | ||
checkIsMatcherPayload: (entryPatt, check = x => x) => | ||
check( | ||
passStyleOf(entryPatt) === 'copyArray' && entryPatt.length === 2, | ||
X`${entryPatt} - Must be an pair of patterns`, | ||
) && checkPattern(entryPatt, check), | ||
getRankCover: _entryPatt => getPassStyleCover('tagged'), | ||
checkKeyPattern: (_entryPatt, check = x => x) => | ||
check(false, X`CopyMap not yet supported as keys`), | ||
}); | ||
/** @type {MatchHelper} */ | ||
const matchSplitHelper = Far('match:split helper', { | ||
checkMatches: (specimen, [base, rest = undefined], check = x => x) => { | ||
const specimenStyle = passStyleOf(specimen); | ||
const baseStyle = passStyleOf(base); | ||
if (specimenStyle !== baseStyle) { | ||
return check( | ||
false, | ||
X`${specimen} - Must have shape of base: ${q(baseStyle)}`, | ||
); | ||
} | ||
let specB; | ||
let specR; | ||
if (baseStyle === 'copyArray') { | ||
const { length: baseLen } = base; | ||
// Frozen below | ||
specB = specimen.slice(0, baseLen); | ||
specR = specimen.slice(baseLen); | ||
} else { | ||
assert(baseStyle === 'copyRecord'); | ||
// Not yet frozen! Mutated in place | ||
specB = {}; | ||
specR = {}; | ||
for (const [name, value] of Object.entries(specimen)) { | ||
if (hasOwnPropertyOf(base, name)) { | ||
specB[name] = value; | ||
} else { | ||
specR[name] = value; | ||
} | ||
} | ||
} | ||
harden(specB); | ||
harden(specR); | ||
return ( | ||
(checkMatches(specB, base, check) && | ||
(rest === undefined || checkMatches(specR, rest, check))) | ||
); | ||
}, | ||
checkIsMatcherPayload: (splitArgs, check = x => x) => { | ||
if ( | ||
passStyleOf(splitArgs) === 'copyArray' && | ||
(splitArgs.length === 1 || splitArgs.length === 2) | ||
) { | ||
const [base, rest = undefined] = splitArgs; | ||
const baseStyle = passStyleOf(base); | ||
if ( | ||
isPattern(base) && | ||
(baseStyle === 'copyArray' || baseStyle === 'copyRecord') && | ||
(rest === undefined || isPattern(rest)) | ||
) { | ||
return true; | ||
} | ||
} | ||
return check( | ||
false, | ||
X`Must be an array of a base structure and an optional rest pattern: ${splitArgs}`, | ||
); | ||
}, | ||
getRankCover: ([base, _rest = undefined]) => | ||
getPassStyleCover(passStyleOf(base)), | ||
checkKeyPattern: ([base, _rest = undefined], check = x => x) => | ||
check(false, X`${q(passStyleOf(base))} not yet supported as keys`), | ||
}); | ||
/** @type {MatchHelper} */ | ||
const matchPartialHelper = Far('match:partial helper', { | ||
checkMatches: (specimen, [base, rest = undefined], check = x => x) => { | ||
const specimenStyle = passStyleOf(specimen); | ||
const baseStyle = passStyleOf(base); | ||
if (specimenStyle !== baseStyle) { | ||
return check( | ||
false, | ||
X`${specimen} - Must have shape of base: ${q(baseStyle)}`, | ||
); | ||
} | ||
let specB; | ||
let specR; | ||
let newBase = base; | ||
if (baseStyle === 'copyArray') { | ||
const { length: specLen } = specimen; | ||
const { length: baseLen } = base; | ||
if (specLen < baseLen) { | ||
newBase = harden(base.slice(0, specLen)); | ||
} | ||
// Frozen below | ||
specB = specimen.slice(0, baseLen); | ||
specR = specimen.slice(baseLen); | ||
} else { | ||
assert(baseStyle === 'copyRecord'); | ||
// Not yet frozen! Mutated in place | ||
specB = {}; | ||
specR = {}; | ||
newBase = {}; | ||
for (const [name, value] of Object.entries(specimen)) { | ||
if (hasOwnPropertyOf(base, name)) { | ||
specB[name] = value; | ||
newBase[name] = base[name]; | ||
} else { | ||
specR[name] = value; | ||
} | ||
} | ||
} | ||
harden(specB); | ||
harden(specR); | ||
harden(newBase); | ||
return ( | ||
(checkMatches(specB, newBase, check) && | ||
(rest === undefined || checkMatches(specR, rest, check))) | ||
); | ||
}, | ||
checkIsMatcherPayload: matchSplitHelper.checkIsMatcherPayload, | ||
getRankCover: matchSplitHelper.getRankCover, | ||
checkKeyPattern: matchSplitHelper.checkKeyPattern, | ||
}); | ||
/** @type {Record<string, MatchHelper>} */ | ||
const HelpersByMatchTag = harden({ | ||
'match:any': matchAnyHelper, | ||
'match:scalar': matchScalarHelper, | ||
'match:kind': matchKindHelper, | ||
'match:and': matchAndHelper, | ||
'match:or': matchOrHelper, | ||
'match:not': matchNotHelper, | ||
'match:scalar': matchScalarHelper, | ||
'match:key': matchKeyHelper, | ||
'match:pattern': matchPatternHelper, | ||
'match:kind': matchKindHelper, | ||
'match:lt': matchLTHelper, | ||
@@ -856,44 +995,67 @@ 'match:lte': matchLTEHelper, | ||
'match:gt': matchGTHelper, | ||
'match:arrayOf': matchArrayOfHelper, | ||
'match:recordOf': matchRecordOfHelper, | ||
'match:setOf': matchSetOfHelper, | ||
'match:mapOf': matchMapOfHelper, | ||
'match:split': matchSplitHelper, | ||
'match:partial': matchPartialHelper, | ||
}); | ||
const patt = p => { | ||
assertPattern(p); | ||
return p; | ||
const makeMatcher = (tag, payload) => { | ||
const matcher = makeTagged(tag, payload); | ||
assertPattern(matcher); | ||
return matcher; | ||
}; | ||
const makeKindMatcher = kind => makeMatcher('match:kind', kind); | ||
const theAnyPattern = makeMatcher('match:any', undefined); | ||
const theScalarPattern = makeMatcher('match:scalar', undefined); | ||
const theKeyPattern = makeMatcher('match:key', undefined); | ||
const thePatternPattern = makeMatcher('match:pattern', undefined); | ||
const theBooleanPattern = makeKindMatcher('boolean'); | ||
const theNumberPattern = makeKindMatcher('number'); | ||
const theBigintPattern = makeKindMatcher('bigint'); | ||
const theNatPattern = makeMatcher('match:gte', 0n); | ||
const theStringPattern = makeKindMatcher('string'); | ||
const theSymbolPattern = makeKindMatcher('symbol'); | ||
const theRecordPattern = makeKindMatcher('copyRecord'); | ||
const theArrayPattern = makeKindMatcher('copyArray'); | ||
const theSetPattern = makeKindMatcher('copySet'); | ||
const theMapPattern = makeKindMatcher('copyMap'); | ||
const theRemotablePattern = makeKindMatcher('remotable'); | ||
const theErrorPattern = makeKindMatcher('error'); | ||
const thePromisePattern = makeKindMatcher('promise'); | ||
const theUndefinedPattern = makeKindMatcher('undefined'); | ||
/** @type {MatcherNamespace} */ | ||
const M = harden({ | ||
any: () => patt(makeTagged('match:any', undefined)), | ||
scalar: () => patt(makeTagged('match:scalar', undefined)), | ||
and: (...patts) => patt(makeTagged('match:and', patts)), | ||
or: (...patts) => patt(makeTagged('match:or', patts)), | ||
not: subPatt => patt(makeTagged('match:not', subPatt)), | ||
any: () => theAnyPattern, | ||
and: (...patts) => makeMatcher('match:and', patts), | ||
or: (...patts) => makeMatcher('match:or', patts), | ||
not: subPatt => makeMatcher('match:not', subPatt), | ||
kind: kind => patt(makeTagged('match:kind', kind)), | ||
boolean: () => M.kind('boolean'), | ||
number: () => M.kind('number'), | ||
bigint: () => M.kind('bigint'), | ||
string: () => M.kind('string'), | ||
symbol: () => M.kind('symbol'), | ||
record: () => M.kind('copyRecord'), | ||
array: () => M.kind('copyArray'), | ||
set: () => M.kind('copySet'), | ||
map: () => M.kind('copyMap'), | ||
remotable: () => M.kind('remotable'), | ||
error: () => M.kind('error'), | ||
promise: () => M.kind('promise'), | ||
/** | ||
* All keys including `undefined` are already valid patterns and | ||
* so can validly represent themselves. But optional pattern arguments | ||
* `(pattern = undefined) => ...` | ||
* cannot distinguish between `undefined` passed as a pattern vs | ||
* omission of the argument. It will interpret the first as the | ||
* second. Thus, when a passed pattern does not also need to be a key, | ||
* we recommend passing `M.undefined()` instead of `undefined`. | ||
*/ | ||
undefined: () => M.kind('undefined'), | ||
scalar: () => theScalarPattern, | ||
key: () => theKeyPattern, | ||
pattern: () => thePatternPattern, | ||
kind: makeKindMatcher, | ||
boolean: () => theBooleanPattern, | ||
number: () => theNumberPattern, | ||
bigint: () => theBigintPattern, | ||
nat: () => theNatPattern, | ||
string: () => theStringPattern, | ||
symbol: () => theSymbolPattern, | ||
record: () => theRecordPattern, | ||
array: () => theArrayPattern, | ||
set: () => theSetPattern, | ||
map: () => theMapPattern, | ||
remotable: () => theRemotablePattern, | ||
error: () => theErrorPattern, | ||
promise: () => thePromisePattern, | ||
undefined: () => theUndefinedPattern, | ||
null: () => null, | ||
lt: rightSide => patt(makeTagged('match:lt', rightSide)), | ||
lte: rightSide => patt(makeTagged('match:lte', rightSide)), | ||
lt: rightOperand => makeMatcher('match:lt', rightOperand), | ||
lte: rightOperand => makeMatcher('match:lte', rightOperand), | ||
eq: key => { | ||
@@ -904,10 +1066,15 @@ assertKey(key); | ||
neq: key => M.not(M.eq(key)), | ||
gte: rightSide => patt(makeTagged('match:gte', rightSide)), | ||
gt: rightSide => patt(makeTagged('match:gt', rightSide)), | ||
gte: rightOperand => makeMatcher('match:gte', rightOperand), | ||
gt: rightOperand => makeMatcher('match:gt', rightOperand), | ||
// TODO make more precise | ||
arrayOf: _elementPatt => M.array(), | ||
recordOf: _entryPatt => M.record(), | ||
setOf: _elementPatt => M.set(), | ||
mapOf: _entryPatt => M.map(), | ||
arrayOf: (subPatt = M.any()) => makeMatcher('match:arrayOf', subPatt), | ||
recordOf: (keyPatt = M.any(), valuePatt = M.any()) => | ||
makeMatcher('match:recordOf', [keyPatt, valuePatt]), | ||
setOf: (keyPatt = M.any()) => makeMatcher('match:setOf', keyPatt), | ||
mapOf: (keyPatt = M.any(), valuePatt = M.any()) => | ||
makeMatcher('match:mapOf', [keyPatt, valuePatt]), | ||
split: (base, rest = undefined) => | ||
makeMatcher('match:split', rest === undefined ? [base] : [base, rest]), | ||
partial: (base, rest = undefined) => | ||
makeMatcher('match:partial', rest === undefined ? [base] : [base, rest]), | ||
}); | ||
@@ -917,3 +1084,3 @@ | ||
matches, | ||
assertMatches, | ||
fit, | ||
assertPattern, | ||
@@ -927,1 +1094,17 @@ isPattern, | ||
}; | ||
// Only include those whose meaning is independent of an imputed sort order | ||
// of remotables, or of encoding of passable as sortable strings. Thus, | ||
// getRankCover is omitted. To get one, you'd need to instantiate | ||
// `makePatternKit()` yourself. Since there are currently no external | ||
// uses of `getRankCover`, for clarity during development, `makePatternKit` | ||
// is not currently exported. | ||
export const { | ||
matches, | ||
fit, | ||
assertPattern, | ||
isPattern, | ||
assertKeyPattern, | ||
isKeyPattern, | ||
M, | ||
} = makePatternKit(); |
@@ -49,128 +49,154 @@ // @ts-check | ||
/** @type {CompareRank} */ | ||
export const compareRank = (left, right) => { | ||
if (sameValueZero(left, right)) { | ||
return 0; | ||
} | ||
const leftStyle = passStyleOf(left); | ||
const rightStyle = passStyleOf(right); | ||
if (leftStyle !== rightStyle) { | ||
return compareRank(PassStyleRank[leftStyle], PassStyleRank[rightStyle]); | ||
} | ||
switch (leftStyle) { | ||
case 'undefined': | ||
case 'null': | ||
case 'remotable': | ||
case 'error': | ||
case 'promise': { | ||
// For each of these passStyles, all members of that passStyle are tied | ||
// for the same rank. | ||
/** | ||
* @type {WeakMap<CompareRank,WeakSet<Passable[]>>} | ||
*/ | ||
const memoOfSorted = new WeakMap(); | ||
/** | ||
* @type {WeakMap<CompareRank,CompareRank>} | ||
*/ | ||
const comparatorMirrorImages = new WeakMap(); | ||
/** | ||
* @param {CompareRank=} compareRemotables | ||
* An option to create a comparator in which an internal order is | ||
* assigned to remotables. This defaults to a comparator that | ||
* always returns `0`, meaning that all remotables are tied | ||
* for the same rank. | ||
* @returns {ComparatorKit} | ||
*/ | ||
const makeComparatorKit = (compareRemotables = (_x, _y) => 0) => { | ||
/** @type {CompareRank} */ | ||
const comparator = (left, right) => { | ||
if (sameValueZero(left, right)) { | ||
return 0; | ||
} | ||
case 'boolean': | ||
case 'bigint': | ||
case 'string': { | ||
// Within each of these passStyles, the rank ordering agrees with | ||
// JavaScript's relational operators `<` and `>`. | ||
if (left < right) { | ||
return -1; | ||
} else { | ||
assert(left > right); | ||
return 1; | ||
} | ||
const leftStyle = passStyleOf(left); | ||
const rightStyle = passStyleOf(right); | ||
if (leftStyle !== rightStyle) { | ||
return comparator(PassStyleRank[leftStyle], PassStyleRank[rightStyle]); | ||
} | ||
case 'symbol': { | ||
return compareRank( | ||
nameForPassableSymbol(left), | ||
nameForPassableSymbol(right), | ||
); | ||
} | ||
case 'number': { | ||
// `NaN`'s rank is after all other numbers. | ||
if (Number.isNaN(left)) { | ||
assert(!Number.isNaN(right)); | ||
return 1; | ||
} else if (Number.isNaN(right)) { | ||
return -1; | ||
switch (leftStyle) { | ||
case 'remotable': { | ||
return compareRemotables(left, right); | ||
} | ||
// The rank ordering of non-NaN numbers agrees with JavaScript's | ||
// relational operators '<' and '>'. | ||
if (left < right) { | ||
return -1; | ||
} else { | ||
assert(left > right); | ||
return 1; | ||
case 'undefined': | ||
case 'null': | ||
case 'error': | ||
case 'promise': { | ||
// For each of these passStyles, all members of that passStyle are tied | ||
// for the same rank. | ||
return 0; | ||
} | ||
} | ||
case 'copyRecord': { | ||
// Lexicographic by inverse sorted order of property names, then | ||
// lexicographic by corresponding values in that same inverse | ||
// order of their property names. Comparing names by themselves first, | ||
// all records with the exact same set of property names sort next to | ||
// each other in a rank-sort of copyRecords. | ||
case 'boolean': | ||
case 'bigint': | ||
case 'string': { | ||
// Within each of these passStyles, the rank ordering agrees with | ||
// JavaScript's relational operators `<` and `>`. | ||
if (left < right) { | ||
return -1; | ||
} else { | ||
assert(left > right); | ||
return 1; | ||
} | ||
} | ||
case 'symbol': { | ||
return comparator( | ||
nameForPassableSymbol(left), | ||
nameForPassableSymbol(right), | ||
); | ||
} | ||
case 'number': { | ||
// `NaN`'s rank is after all other numbers. | ||
if (Number.isNaN(left)) { | ||
assert(!Number.isNaN(right)); | ||
return 1; | ||
} else if (Number.isNaN(right)) { | ||
return -1; | ||
} | ||
// The rank ordering of non-NaN numbers agrees with JavaScript's | ||
// relational operators '<' and '>'. | ||
if (left < right) { | ||
return -1; | ||
} else { | ||
assert(left > right); | ||
return 1; | ||
} | ||
} | ||
case 'copyRecord': { | ||
// Lexicographic by inverse sorted order of property names, then | ||
// lexicographic by corresponding values in that same inverse | ||
// order of their property names. Comparing names by themselves first, | ||
// all records with the exact same set of property names sort next to | ||
// each other in a rank-sort of copyRecords. | ||
// The copyRecord invariants enforced by passStyleOf ensure that | ||
// all the property names are strings. We need the reverse sorted order | ||
// of these names, which we then compare lexicographically. This ensures | ||
// that if the names of record X are a subset of the names of record Y, | ||
// then record X will have an earlier rank and sort to the left of Y. | ||
const leftNames = harden( | ||
ownKeys(left) | ||
.sort() | ||
// TODO Measure which is faster: a reverse sort by sorting and | ||
// reversing, or by sorting with an inverse comparison function. | ||
// If it makes a significant difference, use the faster one. | ||
.reverse(), | ||
); | ||
const rightNames = harden( | ||
ownKeys(right) | ||
.sort() | ||
.reverse(), | ||
); | ||
const result = compareRank(leftNames, rightNames); | ||
if (result !== 0) { | ||
return result; | ||
} | ||
const leftValues = harden(leftNames.map(name => left[name])); | ||
const rightValues = harden(rightNames.map(name => right[name])); | ||
return compareRank(leftValues, rightValues); | ||
} | ||
case 'copyArray': { | ||
// Lexicographic | ||
const len = Math.min(left.length, right.length); | ||
for (let i = 0; i < len; i += 1) { | ||
const result = compareRank(left[i], right[i]); | ||
// The copyRecord invariants enforced by passStyleOf ensure that | ||
// all the property names are strings. We need the reverse sorted order | ||
// of these names, which we then compare lexicographically. This ensures | ||
// that if the names of record X are a subset of the names of record Y, | ||
// then record X will have an earlier rank and sort to the left of Y. | ||
const leftNames = harden( | ||
ownKeys(left) | ||
.sort() | ||
// TODO Measure which is faster: a reverse sort by sorting and | ||
// reversing, or by sorting with an inverse comparison function. | ||
// If it makes a significant difference, use the faster one. | ||
.reverse(), | ||
); | ||
const rightNames = harden( | ||
ownKeys(right) | ||
.sort() | ||
.reverse(), | ||
); | ||
const result = comparator(leftNames, rightNames); | ||
if (result !== 0) { | ||
return result; | ||
} | ||
const leftValues = harden(leftNames.map(name => left[name])); | ||
const rightValues = harden(rightNames.map(name => right[name])); | ||
return comparator(leftValues, rightValues); | ||
} | ||
// If all matching elements were tied, then according to their lengths. | ||
// If array X is a prefix of array Y, then X has an earlier rank than Y. | ||
return compareRank(left.length, right.length); | ||
} | ||
case 'tagged': { | ||
// Lexicographic by `[Symbol.toStringTag]` then `.payload`. | ||
const labelComp = compareRank(getTag(left), getTag(right)); | ||
if (labelComp !== 0) { | ||
return labelComp; | ||
case 'copyArray': { | ||
// Lexicographic | ||
const len = Math.min(left.length, right.length); | ||
for (let i = 0; i < len; i += 1) { | ||
const result = comparator(left[i], right[i]); | ||
if (result !== 0) { | ||
return result; | ||
} | ||
} | ||
// If all matching elements were tied, then according to their lengths. | ||
// If array X is a prefix of array Y, then X has an earlier rank than Y. | ||
return comparator(left.length, right.length); | ||
} | ||
return compareRank(left.payload, right.payload); | ||
case 'tagged': { | ||
// Lexicographic by `[Symbol.toStringTag]` then `.payload`. | ||
const labelComp = comparator(getTag(left), getTag(right)); | ||
if (labelComp !== 0) { | ||
return labelComp; | ||
} | ||
return comparator(left.payload, right.payload); | ||
} | ||
default: { | ||
assert.fail(X`Unrecognized passStyle: ${q(leftStyle)}`); | ||
} | ||
} | ||
default: { | ||
assert.fail(X`Unrecognized passStyle: ${q(leftStyle)}`); | ||
} | ||
} | ||
}; | ||
harden(compareRank); | ||
}; | ||
/** @type {CompareRank} */ | ||
export const compareAntiRank = (x, y) => compareRank(y, x); | ||
/** @type {CompareRank} */ | ||
const antiComparator = (x, y) => comparator(y, x); | ||
memoOfSorted.set(comparator, new WeakSet()); | ||
memoOfSorted.set(antiComparator, new WeakSet()); | ||
comparatorMirrorImages.set(comparator, antiComparator); | ||
comparatorMirrorImages.set(antiComparator, comparator); | ||
return harden({ comparator, antiComparator }); | ||
}; | ||
/** | ||
* @type {Map<CompareRank,WeakSet<Passable[]>>} | ||
* @param {CompareRank} comparator | ||
* @returns {CompareRank=} | ||
*/ | ||
const memoOfSorted = new Map([ | ||
[compareRank, new WeakSet()], | ||
[compareAntiRank, new WeakSet()], | ||
]); | ||
export const comparatorMirrorImage = comparator => | ||
comparatorMirrorImages.get(comparator); | ||
@@ -351,1 +377,49 @@ /** | ||
}; | ||
export const { | ||
comparator: compareRank, | ||
antiComparator: compareAntiRank, | ||
} = makeComparatorKit(); | ||
/** | ||
* Create a comparator kit in which remotables are fully ordered | ||
* by the order in which they are first seen by *this* comparator kit. | ||
* BEWARE: This is observable mutable state, so such a comparator kit | ||
* should never be shared among subsystems that should not be able | ||
* to communicate. | ||
* | ||
* Note that this order does not meet the requirements for store | ||
* ordering, since it has no memory of deleted keys. | ||
* | ||
* These full order comparator kit is strictly more precise that the | ||
* rank order comparator kits above. As a result, any array which is | ||
* sorted by such a full order will pass the isRankSorted test with | ||
* a corresponding rank order. | ||
* | ||
* An array which is sorted by a *fresh* full order comparator, i.e., | ||
* one that has not yet seen any remotables, will of course remain | ||
* sorted by according to *that* full order comparator. An array *of | ||
* scalars* sorted by a fresh full order will remain sorted even | ||
* according to a new fresh full order comparator, since it will see | ||
* the remotables in the same order again. Unfortunately, this is | ||
* not true of arrays of passables in general. | ||
* | ||
* @returns {ComparatorKit} | ||
*/ | ||
export const makeFullOrderComparatorKit = () => { | ||
let numSeen = 0; | ||
// Could be a WeakMap but would perform poorly. There are a dynamic | ||
// number of these created, and each typically has a short lifetime. | ||
const seen = new Map(); | ||
const tag = r => { | ||
if (seen.has(r)) { | ||
return seen.get(r); | ||
} | ||
numSeen += 1; | ||
seen.set(r, numSeen); | ||
return numSeen; | ||
}; | ||
const compareRemotables = (x, y) => compareRank(tag(x), tag(y)); | ||
return makeComparatorKit(compareRemotables); | ||
}; | ||
harden(makeFullOrderComparatorKit); |
@@ -7,3 +7,3 @@ // @ts-check | ||
import { makeCopyMap } from '../keys/copyMap.js'; | ||
import { makePatternKit } from '../patterns/patternMatchers.js'; | ||
import { fit, assertPattern } from '../patterns/patternMatchers.js'; | ||
import { makeWeakMapStoreMethods } from './scalarWeakMapStore.js'; | ||
@@ -13,3 +13,2 @@ import { makeCursorKit } from './store-utils.js'; | ||
const { details: X, quote: q } = assert; | ||
const { assertMatches, assertPattern } = makePatternKit(); | ||
@@ -115,3 +114,3 @@ /** | ||
if (schema) { | ||
assertMatches(harden([key, value]), schema); | ||
fit(harden([key, value]), schema); | ||
} | ||
@@ -118,0 +117,0 @@ }; |
@@ -7,3 +7,3 @@ // @ts-check | ||
import { makeCopySet } from '../keys/copySet.js'; | ||
import { makePatternKit } from '../patterns/patternMatchers.js'; | ||
import { fit, assertPattern } from '../patterns/patternMatchers.js'; | ||
import { makeWeakSetStoreMethods } from './scalarWeakSetStore.js'; | ||
@@ -13,3 +13,2 @@ import { makeCursorKit } from './store-utils.js'; | ||
const { details: X, quote: q } = assert; | ||
const { assertMatches, assertPattern } = makePatternKit(); | ||
@@ -108,3 +107,3 @@ /** | ||
if (schema) { | ||
assertMatches(key, schema); | ||
fit(key, schema); | ||
} | ||
@@ -111,0 +110,0 @@ }; |
// @ts-check | ||
import { Far, assertPassable, passStyleOf } from '@agoric/marshal'; | ||
import { makePatternKit } from '../patterns/patternMatchers.js'; | ||
import { fit, assertPattern } from '../patterns/patternMatchers.js'; | ||
const { details: X, quote: q } = assert; | ||
const { assertMatches, assertPattern } = makePatternKit(); | ||
@@ -97,3 +96,3 @@ /** | ||
if (schema) { | ||
assertMatches(harden([key, value]), schema); | ||
fit(harden([key, value]), schema); | ||
} | ||
@@ -100,0 +99,0 @@ }; |
// @ts-check | ||
import { Far, passStyleOf } from '@agoric/marshal'; | ||
import { makePatternKit } from '../patterns/patternMatchers.js'; | ||
import { fit, assertPattern } from '../patterns/patternMatchers.js'; | ||
const { details: X, quote: q } = assert; | ||
const { assertMatches, assertPattern } = makePatternKit(); | ||
@@ -81,3 +80,3 @@ /** | ||
if (schema) { | ||
assertMatches(key, schema); | ||
fit(key, schema); | ||
} | ||
@@ -84,0 +83,0 @@ }; |
// @ts-check | ||
import { filterIterable } from '@agoric/marshal'; | ||
import { makePatternKit } from '../patterns/patternMatchers.js'; | ||
import { matches } from '../patterns/patternMatchers.js'; | ||
import { assertRankSorted } from '../patterns/rankOrder.js'; | ||
const { details: X, quote: q } = assert; | ||
const { matches } = makePatternKit(); | ||
@@ -10,0 +9,0 @@ export const makeCursorKit = ( |
117
src/types.js
@@ -16,2 +16,3 @@ // @ts-check | ||
/** | ||
* @template K | ||
* @typedef {CopyTagged} CopySet | ||
@@ -21,2 +22,3 @@ */ | ||
/** | ||
* @template K,V | ||
* @typedef {CopyTagged} CopyMap | ||
@@ -79,4 +81,4 @@ */ | ||
* @property {(keyPattern?: Pattern) => K[]} keys | ||
* @property {(keyPattern?: Pattern) => CopySet} snapshot | ||
* @property {(copySet: CopySet) => void} addAll | ||
* @property {(keyPattern?: Pattern) => CopySet<K>} snapshot | ||
* @property {(copySet: CopySet<K>) => void} addAll | ||
* @property {(keyPattern?: Pattern, | ||
@@ -124,4 +126,4 @@ * direction?: Direction | ||
* @property {(entryPattern?: Pattern) => [K,V][]} entries | ||
* @property {(entryPattern?: Pattern) => CopyMap} snapshot | ||
* @property {(copyMap: CopyMap) => void} addAll | ||
* @property {(entryPattern?: Pattern) => CopyMap<K,V>} snapshot | ||
* @property {(copyMap: CopyMap<K,V>) => void} addAll | ||
* @property {(entryPattern?: Pattern, | ||
@@ -220,2 +222,8 @@ * direction?: Direction | ||
/** | ||
* @typedef {Object} ComparatorKit | ||
* @property {CompareRank} comparator | ||
* @property {CompareRank} antiComparator | ||
*/ | ||
// ///////////////////// Should be internal only types ///////////////////////// | ||
@@ -281,4 +289,7 @@ | ||
* @callback KeyToDBKey | ||
* If this key can be encoded as a DBKey string which sorts correctly, | ||
* return that string. Else return `undefined`. For example, a scalar-only | ||
* encodeKey would return `undefined` for all non-scalar keys. | ||
* @param {Passable} key | ||
* @returns {string} | ||
* @returns {string=} | ||
*/ | ||
@@ -298,2 +309,98 @@ | ||
/** | ||
* @typedef {Object} MatcherNamespace | ||
* @property {() => Matcher} any | ||
* Matches any passable | ||
* @property {(...patts: Pattern[]) => Matcher} and | ||
* Only if it matches all the sub-patterns | ||
* @property {(...patts: Pattern[]) => Matcher} or | ||
* Only if it matches at least one subPattern | ||
* @property {(subPatt: Pattern) => Matcher} not | ||
* Only if it does not match the sub-pattern | ||
* | ||
* @property {() => Matcher} scalar | ||
* The scalars are the primitive values and Remotables. | ||
* All scalars are keys. | ||
* @property {() => Matcher} key | ||
* Can be in a copySet or the key in a CopyMap. | ||
* (Will eventually be able to a key is a MapStore.) | ||
* All keys are patterns that match only themselves. | ||
* @property {() => Matcher} pattern | ||
* If it matches M.pattern(), the it is itself a pattern used | ||
* to match other passables. A pattern cannot contain errors | ||
* or promises, as these are not stable enough to usefully match. | ||
* @property {(kind: string) => Matcher} kind | ||
* @property {() => Matcher} boolean | ||
* @property {() => Matcher} number Only floating point numbers | ||
* @property {() => Matcher} bigint | ||
* @property {() => Matcher} nat | ||
* @property {() => Matcher} string | ||
* @property {() => Matcher} symbol | ||
* Only registered and well-known symbols are passable | ||
* @property {() => Matcher} record A CopyRecord | ||
* @property {() => Matcher} array A CopyArray | ||
* @property {() => Matcher} set A CopySet | ||
* @property {() => Matcher} map A CopyMap | ||
* @property {() => Matcher} remotable A far object or its remote presence | ||
* @property {() => Matcher} error | ||
* Error objects are passable, but are neither keys nor symbols. | ||
* They do not have a useful identity. | ||
* @property {() => Matcher} promise | ||
* Promises are passable, but are neither keys nor symbols. | ||
* They do not have a useful identity. | ||
* @property {() => Matcher} undefined | ||
* All keys including `undefined` are already valid patterns and | ||
* so can validly represent themselves. But optional pattern arguments | ||
* `(pattern = undefined) => ...` | ||
* cannot distinguish between `undefined` passed as a pattern vs | ||
* omission of the argument. It will interpret the first as the | ||
* second. Thus, when a passed pattern does not also need to be a key, | ||
* we recommend passing `M.undefined()` instead of `undefined`. | ||
* | ||
* @property {() => null} null | ||
* | ||
* @property {(rightOperand :Key) => Matcher} lt | ||
* Matches if < the right operand by compareKeys | ||
* @property {(rightOperand :Key) => Matcher} lte | ||
* Matches if <= the right operand by compareKeys | ||
* @property {(key :Key) => Matcher} eq | ||
* @property {(key :Key) => Matcher} neq | ||
* @property {(rightOperand :Key) => Matcher} gte | ||
* Matches if >= the right operand by compareKeys | ||
* @property {(rightOperand :Key) => Matcher} gt | ||
* Matches if > the right operand by compareKeys | ||
* | ||
* @property {(subPatt?: Pattern) => Matcher} arrayOf | ||
* @property {(keyPatt?: Pattern, valuePatt?: Pattern) => Matcher} recordOf | ||
* @property {(keyPatt?: Pattern) => Matcher} setOf | ||
* @property {(keyPatt?: Pattern, valuePatt?: Pattern) => Matcher} mapOf | ||
* @property {( | ||
* base: CopyRecord<*> | CopyArray<*>, | ||
* rest?: Pattern | ||
* ) => Matcher} split | ||
* An array or record is split into the first part that matches the | ||
* base pattern, and the remainder, which matches against the optional | ||
* rest pattern if present. | ||
* @property {( | ||
* base: CopyRecord<*> | CopyArray<*>, | ||
* rest?: Pattern | ||
* ) => Matcher} partial | ||
* An array or record is split into the first part that matches the | ||
* base pattern, and the remainder, which matches against the optional | ||
* rest pattern if present. | ||
* Unlike `M.split`, `M.partial` ignores properties on the base | ||
* pattern that are not present on the specimen. | ||
*/ | ||
/** | ||
* @typedef {Object} PatternKit | ||
* @property {(specimen: Passable, patt: Pattern) => boolean} matches | ||
* @property {(specimen: Passable, patt: Pattern) => void} fit | ||
* @property {(patt: Pattern) => void} assertPattern | ||
* @property {(patt: Passable) => boolean} isPattern | ||
* @property {(patt: Pattern) => void} assertKeyPattern | ||
* @property {(patt: Passable) => boolean} isKeyPattern | ||
* @property {MatcherNamespace} M | ||
*/ | ||
// ///////////////////////////////////////////////////////////////////////////// | ||
@@ -300,0 +407,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
135381
3092