diff --git a/packages/ERTP/src/typeGuards.js b/packages/ERTP/src/typeGuards.js index 59debca89c7a..d4eb3430d6d6 100644 --- a/packages/ERTP/src/typeGuards.js +++ b/packages/ERTP/src/typeGuards.js @@ -124,18 +124,19 @@ export const MAX_ABSOLUTE_DECIMAL_PLACES = 100; export const AssetKindShape = M.or('nat', 'set', 'copySet', 'copyBag'); -export const DisplayInfoShape = M.partial( - harden({ +export const DisplayInfoShape = M.splitRecord( + {}, + { decimalPlaces: M.and( M.gte(-MAX_ABSOLUTE_DECIMAL_PLACES), M.lte(MAX_ABSOLUTE_DECIMAL_PLACES), ), assetKind: AssetKindShape, - }), - harden({ + }, + { // Including this empty `rest` ensures that there are no other // properties beyond those in the `base` record. - }), + }, ); // //////////////////////// Interfaces ///////////////////////////////////////// diff --git a/packages/ERTP/test/unitTests/test-inputValidation.js b/packages/ERTP/test/unitTests/test-inputValidation.js index b319962d826e..c192eb598939 100644 --- a/packages/ERTP/test/unitTests/test-inputValidation.js +++ b/packages/ERTP/test/unitTests/test-inputValidation.js @@ -34,7 +34,7 @@ test('makeIssuerKit bad displayInfo.decimalPlaces', async t => { ), { message: - 'displayInfo: optional-parts: decimalPlaces: "hello" - Must be >= -100', + 'displayInfo: optional: decimalPlaces: "hello" - Must be >= -100', }, ); @@ -62,8 +62,7 @@ test('makeIssuerKit bad displayInfo.decimalPlaces', async t => { () => makeIssuerKit('myTokens', AssetKind.NAT, harden({ decimalPlaces: 101 })), { - message: - 'displayInfo: optional-parts: decimalPlaces: 101 - Must be <= 100', + message: 'displayInfo: optional: decimalPlaces: 101 - Must be <= 100', }, ); @@ -71,8 +70,7 @@ test('makeIssuerKit bad displayInfo.decimalPlaces', async t => { () => makeIssuerKit('myTokens', AssetKind.NAT, harden({ decimalPlaces: -101 })), { - message: - 'displayInfo: optional-parts: decimalPlaces: -101 - Must be >= -100', + message: 'displayInfo: optional: decimalPlaces: -101 - Must be >= -100', }, ); }); @@ -90,7 +88,7 @@ test('makeIssuerKit bad displayInfo.assetKind', async t => { ), { message: - 'displayInfo: optional-parts: assetKind: "something" - Must match one of ["nat","set","copySet","copyBag"]', + 'displayInfo: optional: assetKind: "something" - Must match one of ["nat","set","copySet","copyBag"]', }, ); }); @@ -107,8 +105,7 @@ test('makeIssuerKit bad displayInfo.whatever', async t => { }), ), { - message: - 'displayInfo: rest-parts: {"whatever":"something"} - Must be: {}', + message: 'displayInfo: rest: {"whatever":"something"} - Must be: {}', }, ); }); @@ -145,7 +142,7 @@ test('brand.isMyIssuer bad issuer', async t => { // @ts-expect-error Intentional wrong type for testing t.throwsAsync(() => brand.isMyIssuer('not an issuer'), { message: - 'In "isMyIssuer" method of (myTokens brand) arg 0: string "not an issuer" - Must be a remotable (Issuer)', + 'In "isMyIssuer" method of (myTokens brand): args: [0]: string "not an issuer" - Must be a remotable (Issuer)', }); const fakeIssuer = /** @type {Issuer} */ ( /** @type {unknown} */ (Far('myTokens issuer', {})) @@ -188,7 +185,7 @@ test('issuer.combine bad payments array', async t => { // @ts-expect-error Intentional wrong type for testing await t.throwsAsync(() => E(issuer).combine(notAnArray), { message: - 'In "combine" method of (fungible issuer) arg 0: cannot serialize Remotables with non-methods like "length" in {"length":2,"split":"[Function split]"}', + 'In "combine" method of (fungible issuer): cannot serialize Remotables with non-methods like "length" in {"length":2,"split":"[Function split]"}', }); const notAnArray2 = Far('notAnArray2', { @@ -198,7 +195,7 @@ test('issuer.combine bad payments array', async t => { // @ts-expect-error Intentional wrong type for testing await t.throwsAsync(() => E(issuer).combine(notAnArray2), { message: - 'In "combine" method of (fungible issuer) arg 0: remotable "[Alleged: notAnArray2]" - Must be a copyArray', + 'In "combine" method of (fungible issuer): args: [0]: remotable "[Alleged: notAnArray2]" - Must be a copyArray', }); }); diff --git a/packages/ERTP/test/unitTests/test-issuerObj.js b/packages/ERTP/test/unitTests/test-issuerObj.js index 72035b850069..f5e924f3d8d1 100644 --- a/packages/ERTP/test/unitTests/test-issuerObj.js +++ b/packages/ERTP/test/unitTests/test-issuerObj.js @@ -44,7 +44,7 @@ test('bad display info', t => { const displayInfo = harden({ somethingUnexpected: 3 }); // @ts-expect-error deliberate invalid arguments for testing t.throws(() => makeIssuerKit('fungible', AssetKind.NAT, displayInfo), { - message: 'displayInfo: rest-parts: {"somethingUnexpected":3} - Must be: {}', + message: 'displayInfo: rest: {"somethingUnexpected":3} - Must be: {}', }); }); @@ -200,7 +200,7 @@ test('purse.deposit promise', async t => { () => E(purse).deposit(exclusivePaymentP, fungible25), { message: - 'In "deposit" method of (fungible Purse purse) arg 0: promise "[Promise]" - Must be a remotable (Payment)', + 'In "deposit" method of (fungible Purse purse): args: [0]: promise "[Promise]" - Must be a remotable (Payment)', }, 'failed to reject a promise for a payment', ); @@ -335,7 +335,7 @@ test('issuer.split bad amount', async t => { _ => E(issuer).split(payment, AmountMath.make(otherBrand, 10n)), { message: - 'In "split" method of (fungible issuer) arg 1: brand: "[Alleged: other fungible brand]" - Must be: "[Alleged: fungible brand]"', + 'In "split" method of (fungible issuer): args: [1]: brand: "[Alleged: other fungible brand]" - Must be: "[Alleged: fungible brand]"', }, 'throws for bad amount', ); diff --git a/packages/ERTP/test/unitTests/test-mintObj.js b/packages/ERTP/test/unitTests/test-mintObj.js index c3f1e45ec3bb..2ad45a334433 100644 --- a/packages/ERTP/test/unitTests/test-mintObj.js +++ b/packages/ERTP/test/unitTests/test-mintObj.js @@ -46,7 +46,7 @@ test('mint.mintPayment set w strings AssetKind', async t => { const badAmount = AmountMath.make(brand, harden([['badElement']])); t.throws(() => mint.mintPayment(badAmount), { message: - 'In "mintPayment" method of (items mint) arg 0: value: [0]: copyArray ["badElement"] - Must be a string', + 'In "mintPayment" method of (items mint): args: [0]: value: [0]: copyArray ["badElement"] - Must be a string', }); }); diff --git a/packages/SwingSet/src/typeGuards.js b/packages/SwingSet/src/typeGuards.js index 41bdcd8c386d..7d00a4cbe24d 100644 --- a/packages/SwingSet/src/typeGuards.js +++ b/packages/SwingSet/src/typeGuards.js @@ -4,16 +4,17 @@ import { M } from '@agoric/store'; export const ManagerType = M.or('xs-worker', 'local'); // TODO: others -const Bundle = M.split({ moduleType: M.string() }, M.partial({})); +const Bundle = M.splitRecord({ moduleType: M.string() }); + +const partial1 = harden({ + creationOptions: M.splitRecord({}, { critial: M.boolean() }), + parameters: M.recordOf(M.string(), M.any()), +}); -const p1 = M.and( - M.partial({ creationOptions: M.partial({ critial: M.boolean() }) }), - M.partial({ parameters: M.recordOf(M.string(), M.any()) }), -); const SwingSetConfigProperties = M.or( - M.split({ sourceSpec: M.string() }, p1), - M.split({ bundleSpec: M.string() }, p1), - M.split({ bundle: Bundle }, p1), + M.splitRecord({ sourceSpec: M.string() }, partial1), + M.splitRecord({ bundleSpec: M.string() }, partial1), + M.splitRecord({ bundle: Bundle }, partial1), ); const SwingSetConfigDescriptor = M.recordOf( M.string(), @@ -31,11 +32,11 @@ const SwingSetConfigDescriptor = M.recordOf( * in ./types-external.js */ export const SwingSetConfig = M.and( - M.partial({ defaultManagerType: ManagerType }), - M.partial({ includeDevDependencies: M.boolean() }), - M.partial({ defaultReapInterval: M.number() }), // not in type decl - M.partial({ snapshotInterval: M.number() }), - M.partial({ vats: SwingSetConfigDescriptor }), - M.partial({ bootstrap: M.string() }), - M.partial({ bundles: SwingSetConfigDescriptor }), + M.splitRecord({}, { defaultManagerType: ManagerType }), + M.splitRecord({}, { includeDevDependencies: M.boolean() }), + M.splitRecord({}, { defaultReapInterval: M.number() }), // not in type decl + M.splitRecord({}, { snapshotInterval: M.number() }), + M.splitRecord({}, { vats: SwingSetConfigDescriptor }), + M.splitRecord({}, { bootstrap: M.string() }), + M.splitRecord({}, { bundles: SwingSetConfigDescriptor }), ); diff --git a/packages/governance/test/unitTests/test-paramGovernance.js b/packages/governance/test/unitTests/test-paramGovernance.js index e378c6c16276..38744faaa52d 100644 --- a/packages/governance/test/unitTests/test-paramGovernance.js +++ b/packages/governance/test/unitTests/test-paramGovernance.js @@ -220,7 +220,7 @@ test('multiple params bad change', async t => { ), { message: - 'In "getAmountOf" method of (Zoe Invitation issuer) arg 0: bigint "[13n]" - Must be a remotable (Payment)', + 'In "getAmountOf" method of (Zoe Invitation issuer): args: [0]: bigint "[13n]" - Must be a remotable (Payment)', }, ); }); diff --git a/packages/inter-protocol/src/psm/psm.js b/packages/inter-protocol/src/psm/psm.js index b82942f018bc..87edeb066715 100644 --- a/packages/inter-protocol/src/psm/psm.js +++ b/packages/inter-protocol/src/psm/psm.js @@ -275,7 +275,7 @@ export const start = async (zcf, privateArgs, baggage) => { wantmintedHook, 'wantMinted', undefined, - M.split({ + M.splitRecord({ give: { In: anchorAmountShape }, want: M.or({ Out: stableAmountShape }, {}), }), @@ -286,7 +286,7 @@ export const start = async (zcf, privateArgs, baggage) => { giveMintedHook, 'giveMinted', undefined, - M.split({ + M.splitRecord({ give: { In: stableAmountShape }, want: M.or({ Out: anchorAmountShape }, {}), }), diff --git a/packages/inter-protocol/src/psm/psmCharter.js b/packages/inter-protocol/src/psm/psmCharter.js index 5bbff37e0a6e..124acdf49a1d 100644 --- a/packages/inter-protocol/src/psm/psmCharter.js +++ b/packages/inter-protocol/src/psm/psmCharter.js @@ -25,17 +25,15 @@ import { E } from '@endo/far'; * @property {Record} params * @property {{paramPath: { key: string }}} [path] */ -const ParamChangesOfferArgsShape = harden( - M.split( - { - deadline: TimestampShape, - instance: InstanceHandleShape, - params: M.recordOf(M.string(), M.any()), - }, - M.partial({ - path: { paramPath: { key: M.string() } }, - }), - ), +const ParamChangesOfferArgsShape = M.splitRecord( + { + deadline: TimestampShape, + instance: InstanceHandleShape, + params: M.recordOf(M.string(), M.any()), + }, + { + path: { paramPath: { key: M.string() } }, + }, ); /** diff --git a/packages/inter-protocol/src/vaultFactory/params.js b/packages/inter-protocol/src/vaultFactory/params.js index 270027b7cbbb..9080b00a81dc 100644 --- a/packages/inter-protocol/src/vaultFactory/params.js +++ b/packages/inter-protocol/src/vaultFactory/params.js @@ -80,16 +80,13 @@ const makeVaultParamManager = (publisherKit, initial) => }); /** @typedef {ReturnType} VaultParamManager */ -export const vaultParamPattern = M.split( - { - liquidationMargin: ratioPattern, - liquidationPenalty: ratioPattern, - interestRate: ratioPattern, - loanFee: ratioPattern, - debtLimit: amountPattern, - }, - M.any(), -); +export const vaultParamPattern = M.splitRecord({ + liquidationMargin: ratioPattern, + liquidationPenalty: ratioPattern, + interestRate: ratioPattern, + loanFee: ratioPattern, + debtLimit: amountPattern, +}); /** * @param {import('@agoric/notifier').StoredPublisherKit} publisherKit diff --git a/packages/inter-protocol/test/psm/test-psm.js b/packages/inter-protocol/test/psm/test-psm.js index ea50f27788d2..6f129f66be8d 100644 --- a/packages/inter-protocol/test/psm/test-psm.js +++ b/packages/inter-protocol/test/psm/test-psm.js @@ -624,7 +624,7 @@ test('wrong give giveMintedInvitation', async t => { ), { message: - 'proposal: required-parts: give: In: brand: "[Alleged: aUSD brand]" - Must be: "[Alleged: IST brand]"', + 'proposal: give: In: brand: "[Alleged: aUSD brand]" - Must be: "[Alleged: IST brand]"', }, ); }); @@ -654,7 +654,7 @@ test('wrong give wantMintedInvitation', async t => { ), { message: - 'proposal: required-parts: give: In: brand: "[Alleged: IST brand]" - Must be: "[Alleged: aUSD brand]"', + 'proposal: give: In: brand: "[Alleged: IST brand]" - Must be: "[Alleged: aUSD brand]"', }, ); }); @@ -676,7 +676,7 @@ test('extra give wantMintedInvitation', async t => { ), { message: - 'proposal: required-parts: give: {"Extra":{"brand":"[Alleged: aUSD brand]","value":"[200000000n]"},"In":{"brand":"[Seen]","value":"[200000000n]"}} - Must not have unexpected properties: ["Extra"]', + 'proposal: give: {"Extra":{"brand":"[Alleged: aUSD brand]","value":"[200000000n]"},"In":{"brand":"[Seen]","value":"[200000000n]"}} - Must not have unexpected properties: ["Extra"]', }, ); }); diff --git a/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js b/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js index 020dc240158e..769bf20c97fd 100644 --- a/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js +++ b/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js @@ -2518,7 +2518,7 @@ test('addVaultType: extra, unexpected params', async t => { E(vaultFactory).addVaultType(chit.issuer, 'Chit', missingParams), { message: - /initialParamValues: required-parts: .* - Must have missing properties \["interestRate"\]/, + /initialParamValues: .* - Must have missing properties \["interestRate"\]/, }, ); diff --git a/packages/internal/src/utils.js b/packages/internal/src/utils.js index a0c3fe8e97bb..be18ea981943 100644 --- a/packages/internal/src/utils.js +++ b/packages/internal/src/utils.js @@ -12,8 +12,8 @@ const { details: X, quote: q } = assert; /** * Throws if multiple entries use the same property name. Otherwise acts - * like `Object.fromEntries`. Use it to protect from property names - * computed from user-provided data. + * like `Object.fromEntries` but hardens the result. + * Use it to protect from property names computed from user-provided data. * * @template K,V * @param {Iterable<[K,V]>} allEntries @@ -21,7 +21,7 @@ const { details: X, quote: q } = assert; */ export const fromUniqueEntries = allEntries => { const entriesArray = [...allEntries]; - const result = fromEntries(entriesArray); + const result = harden(fromEntries(entriesArray)); if (ownKeys(result).length === entriesArray.length) { return result; } diff --git a/packages/smart-wallet/src/typeGuards.js b/packages/smart-wallet/src/typeGuards.js index d10941bdf7da..c8f7e5323120 100644 --- a/packages/smart-wallet/src/typeGuards.js +++ b/packages/smart-wallet/src/typeGuards.js @@ -14,25 +14,25 @@ export const shape = { }, // invitations - ContractInvitationSpec: M.split( + ContractInvitationSpec: M.splitRecord( { source: 'contract', instance: InstanceHandleShape, publicInvitationMaker: M.string(), }, - M.partial({ + { invitationArgs: M.array(), - }), + }, ), - ContinuingInvitationSpec: M.split( + ContinuingInvitationSpec: M.splitRecord( { source: 'continuing', previousOffer: M.number(), invitationMakerName: M.string(), }, - M.partial({ + { invitationArgs: M.array(), - }), + }, ), PurseInvitationSpec: { source: 'purse', @@ -41,24 +41,25 @@ export const shape = { }, // offers - OfferSpec: M.split( + OfferSpec: M.splitRecord( { id: M.number(), // TODO M.unknown() to defer validation invitationSpec: M.any(), proposal: ProposalShape, }, - M.partial({ offerArgs: M.any() }), + { offerArgs: M.any() }, ), // walletFactory - WalletBridgeMsg: M.split( + WalletBridgeMsg: M.splitRecord( { owner: M.string(), type: M.string(), blockHeight: M.number(), blockTime: M.number(), }, + {}, M.or({ action: M.string() }, { spendAction: M.string() }), ), }; diff --git a/packages/smart-wallet/src/walletFactory.js b/packages/smart-wallet/src/walletFactory.js index 1668ec02f03a..bd0992e97e48 100644 --- a/packages/smart-wallet/src/walletFactory.js +++ b/packages/smart-wallet/src/walletFactory.js @@ -15,9 +15,9 @@ import { makeSmartWallet } from './smartWallet.js'; import { shape } from './typeGuards.js'; const PrivateArgsShape = harden( - M.split( + M.splitRecord( { storageNode: M.eref(M.any()) }, - M.partial({ bridgeManager: M.eref(M.any()) }), + { bridgeManager: M.eref(M.any()) }, ), ); diff --git a/packages/store/src/patterns/interface-tools.js b/packages/store/src/patterns/interface-tools.js index e58ea45b47dd..26c1af8d3d9f 100644 --- a/packages/store/src/patterns/interface-tools.js +++ b/packages/store/src/patterns/interface-tools.js @@ -3,7 +3,7 @@ import { Far } from '@endo/marshal'; import { E } from '@endo/eventual-send'; import { listDifference, objectMap } from '@agoric/internal'; -import { fit } from './patternMatchers.js'; +import { fit, M } from './patternMatchers.js'; const { details: X, quote: q } = assert; const { apply, ownKeys } = Reflect; @@ -11,38 +11,8 @@ const { defineProperties, seal, freeze } = Object; const defendSyncArgs = (args, methodGuard, label) => { const { argGuards, optionalArgGuards, restArgGuard } = methodGuard; - if (args.length < argGuards.length) { - assert.fail( - X`${label} args: ${args} - expected ${argGuards.length} arguments`, - ); - } - for (let i = 0; i < args.length; i += 1) { - const arg = args[i]; - const argLabel = `${label} arg ${i}`; - if (i < argGuards.length) { - fit(arg, argGuards[i], argLabel); - } else if ( - optionalArgGuards && - i < argGuards.length + optionalArgGuards.length - ) { - if (arg !== undefined) { - // In the optional section, an `undefined` arg succeeds - // unconditionally - fit(arg, optionalArgGuards[i - argGuards.length], argLabel); - } - } else if (restArgGuard) { - const restArg = harden(args.slice(i)); - fit(restArg, restArgGuard, `${label} rest[${i}]`); - return; - } else { - assert.fail( - X`${argLabel}: ${args} - expected fewer than ${i + 1} arguments`, - ); - } - } - if (restArgGuard) { - fit(harden([]), restArgGuard, `${label} rest[]`); - } + const paramsPattern = M.splitArgs(argGuards, optionalArgGuards, restArgGuard); + fit(harden(args), paramsPattern, label); }; /** diff --git a/packages/store/src/patterns/patternMatchers.js b/packages/store/src/patterns/patternMatchers.js index 2cb1dfdc9ba1..99fb6fcf7114 100644 --- a/packages/store/src/patterns/patternMatchers.js +++ b/packages/store/src/patterns/patternMatchers.js @@ -10,7 +10,12 @@ import { nameForPassableSymbol, } from '@endo/marshal'; import { identChecker } from '@agoric/assert'; -import { applyLabelingError, listDifference } from '@agoric/internal'; +import { + applyLabelingError, + fromUniqueEntries, + listDifference, + objectMap, +} from '@agoric/internal'; import { compareRank, @@ -625,8 +630,8 @@ const makePatternKit = () => { // const [leftEntriesLimit, rightEntriesLimit] = // getRankCover(pattEntries); // return harden([ - // fromEntries(leftEntriesLimit), - // fromEntries(rightEntriesLimit), + // fromUniqueEntries(leftEntriesLimit), + // fromUniqueEntries(rightEntriesLimit), // ]); break; } @@ -764,6 +769,16 @@ const makePatternKit = () => { X`${specimen} - no pattern disjuncts to match: ${patts}`, ); } + if ( + patts.length === 2 && + !matches(specimen, patts[0]) && + checkKind(patts[0], 'match:kind', identChecker) && + patts[0].payload === 'undefined' + ) { + // Worth special casing the optional pattern for + // better error messages. + return checkMatches(specimen, patts[1], check); + } if (patts.some(patt => matches(specimen, patt))) { return true; } @@ -1312,133 +1327,293 @@ const makePatternKit = () => { check(false, X`CopyMap not yet supported as keys`), }); + /** + * @param {Passable[]} specimen + * @param {Pattern[]} requiredPatt + * @param {Pattern[]} optionalPatt + * @returns {{ + * requiredSpecimen: Passable[], + * optionalSpecimen: Passable[], + * restSpecimen: Passable[] + * }} + */ + const splitArgsParts = (specimen, requiredPatt, optionalPatt) => { + const numRequired = requiredPatt.length; + const numOptional = optionalPatt.length; + const requiredSpecimen = specimen.slice(0, numRequired); + const optionalSpecimen = specimen.slice( + numRequired, + numRequired + numOptional, + ); + const restSpecimen = specimen.slice(numRequired + numOptional); + return harden({ requiredSpecimen, optionalSpecimen, restSpecimen }); + }; + + /** + * Optional specimen elements which are `undefined` pass unconditionally. + * We encode this with the `M.or` pattern so it also produces a good + * compression distinguishing `undefined` from absence. + * + * @param {Pattern[]} optionalPatt + * @param {number} length + * @returns {Pattern[]} The partialPatt + */ + const adaptArrayPattern = (optionalPatt, length) => + harden(optionalPatt.slice(0, length).map(patt => MM.opt(patt))); + /** @type {MatchHelper} */ - const matchSplitHelper = Far('match:split helper', { - checkMatches: (specimen, [base, rest = undefined], check) => { - const baseStyle = passStyleOf(base); - if (!checkKind(specimen, baseStyle, check)) { + const matchSplitArrayHelper = Far('match:splitArgs helper', { + checkMatches: ( + specimen, + [requiredPatt, optionalPatt = [], restPatt = MM.any()], + check, + ) => { + if (!checkKind(specimen, 'copyArray', check)) { return false; } - 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 entries(specimen)) { - if (hasOwnPropertyOf(base, name)) { - specB[name] = value; - } else { - specR[name] = value; - } - } - } - harden(specB); - harden(specR); + const { requiredSpecimen, optionalSpecimen, restSpecimen } = + splitArgsParts(specimen, requiredPatt, optionalPatt); + const partialPatt = adaptArrayPattern( + optionalPatt, + optionalSpecimen.length, + ); return ( - checkMatches(specB, base, check, 'required-parts') && - (rest === undefined || checkMatches(specR, rest, check, 'rest-parts')) + checkMatches(requiredSpecimen, requiredPatt, check, 'args') && + checkMatches(optionalSpecimen, partialPatt, check, 'optional args') && + checkMatches(restSpecimen, restPatt, check, 'rest args') ); }, + compress: ( + specimen, + [requiredPatt, optionalPatt = [], restPatt = MM.any()], + compress, + ) => { + if (!checkKind(specimen, 'copyArray', identChecker)) { + return undefined; + } + const { requiredSpecimen, optionalSpecimen, restSpecimen } = + splitArgsParts(specimen, requiredPatt, optionalPatt); + const partialPatt = adaptArrayPattern( + optionalPatt, + optionalSpecimen.length, + ); + const bindings = harden([ + compress(requiredSpecimen, requiredPatt), + compress(optionalSpecimen, partialPatt), + compress(restSpecimen, restPatt), + ]); + if (bindings.some(subBinding => subBinding === undefined)) { + return undefined; + } + return bindings; + }, + + decompress: ( + [requiredBindings, partialBindings, restBindings], + [requiredPatt, optionalPatt = [], restPatt = MM.any()], + decompress, + ) => { + const partialPatt = adaptArrayPattern( + optionalPatt, + partialBindings.length, + ); + return [ + ...decompress(requiredBindings, requiredPatt), + ...decompress(partialBindings, partialPatt), + ...decompress(restBindings, restPatt), + ]; + }, + checkIsWellFormed: (splitArgs, check) => { if ( passStyleOf(splitArgs) === 'copyArray' && - (splitArgs.length === 1 || splitArgs.length === 2) + (splitArgs.length >= 1 || splitArgs.length <= 3) ) { - const [base, rest = undefined] = splitArgs; - const baseStyle = passStyleOf(base); + const [requiredPatt, optionalPatt = undefined, restPatt = undefined] = + splitArgs; if ( - isPattern(base) && - (baseStyle === 'copyArray' || baseStyle === 'copyRecord') && - (rest === undefined || isPattern(rest)) + isPattern(requiredPatt) && + passStyleOf(requiredPatt) === 'copyArray' && + (optionalPatt === undefined || + (isPattern(optionalPatt) && + passStyleOf(optionalPatt) === 'copyArray')) && + (restPatt === undefined || isPattern(restPatt)) ) { return true; } } return check( false, - X`Must be an array of a base structure and an optional rest pattern: ${splitArgs}`, + X`Must be an array of a requiredPatt array, an optional optionalPatt array, and an optional restPatt: ${q( + splitArgs, + )}`, ); }, - getRankCover: ([base, _rest = undefined]) => - getPassStyleCover(passStyleOf(base)), + getRankCover: ([ + _requiredPatt, + _optionalPatt = undefined, + _restPatt = undefined, + ]) => getPassStyleCover('copyArray'), - checkKeyPattern: ([base, _rest = undefined], check) => - check(false, X`${q(passStyleOf(base))} not yet supported as keys`), + checkKeyPattern: ( + [_requiredPatt, _optionalPatt = undefined, _restPatt = undefined], + check, + ) => check(false, X`copyRecord not yet supported as keys`), }); + /** + * @param {CopyRecord} specimen + * @param {CopyRecord} requiredPatt + * @param {CopyRecord} optionalPatt + * @returns {{ + * requiredSpecimen: CopyRecord, + * optionalSpecimen: CopyRecord, + * restSpecimen: CopyRecord + * }} + */ + const splitRecordParts = (specimen, requiredPatt, optionalPatt) => { + // Not frozen! Mutated in place + /** @type {[string, Passable][]} */ + const requiredEntries = []; + /** @type {[string, Passable][]} */ + const optionalEntries = []; + /** @type {[string, Passable][]} */ + const restEntries = []; + for (const [name, value] of entries(specimen)) { + if (hasOwnPropertyOf(requiredPatt, name)) { + requiredEntries.push([name, value]); + } else if (hasOwnPropertyOf(optionalPatt, name)) { + optionalEntries.push([name, value]); + } else { + restEntries.push([name, value]); + } + } + return harden({ + requiredSpecimen: fromUniqueEntries(requiredEntries), + optionalSpecimen: fromUniqueEntries(optionalEntries), + restSpecimen: fromUniqueEntries(restEntries), + }); + }; + + /** + * Optional specimen values which are `undefined` pass unconditionally. + * We encode this with the `M.or` pattern so it also produces a good + * compression distinguishing `undefined` from absence. + * + * @param {CopyRecord} optionalPatt + * @param {string[]} names + * @returns {CopyRecord} The partialPatt + */ + const adaptRecordPattern = (optionalPatt, names) => + fromUniqueEntries(names.map(name => [name, MM.opt(optionalPatt[name])])); + /** @type {MatchHelper} */ - const matchPartialHelper = Far('match:partial helper', { - checkMatches: (specimen, [base, rest = undefined], check) => { - const baseStyle = passStyleOf(base); - if (!checkKind(specimen, baseStyle, check)) { + const matchSplitRecordHelper = Far('match:splitRecord helper', { + checkMatches: ( + specimen, + [requiredPatt, optionalPatt = {}, restPatt = MM.any()], + check, + ) => { + if (!checkKind(specimen, 'copyRecord', check)) { return false; } - let specB; - let specR; - let newBase; - if (baseStyle === 'copyArray') { - const { length: specimenLen } = specimen; - const { length: baseLen } = base; - if (specimenLen < baseLen) { - newBase = harden(base.slice(0, specimenLen)); - specB = specimen; - // eslint-disable-next-line no-use-before-define - specR = []; - } else { - newBase = [...base]; - specB = specimen.slice(0, baseLen); - specR = specimen.slice(baseLen); - } - for (let i = 0; i < newBase.length; i += 1) { - // For the optional base array parts, an undefined specimen element - // matches unconditionally. - if (specB[i] === undefined) { - // eslint-disable-next-line no-use-before-define - newBase[i] = M.any(); - } - } - } else { - assert(baseStyle === 'copyRecord'); - // Not yet frozen! Mutated in place - specB = {}; - specR = {}; - newBase = {}; - for (const [name, value] of entries(specimen)) { - if (hasOwnPropertyOf(base, name)) { - // For the optional base record parts, an undefined specimen value - // matches unconditionally. - if (value !== undefined) { - specB[name] = value; - newBase[name] = base[name]; - } - } else { - specR[name] = value; - } - } - } - harden(specB); - harden(specR); - harden(newBase); + const { requiredSpecimen, optionalSpecimen, restSpecimen } = + splitRecordParts(specimen, requiredPatt, optionalPatt); + + const partialNames = /** @type {string[]} */ (ownKeys(optionalSpecimen)); + const partialPatt = adaptRecordPattern(optionalPatt, partialNames); return ( - checkMatches(specB, newBase, check, 'optional-parts') && - (rest === undefined || checkMatches(specR, rest, check, 'rest-parts')) + checkMatches(requiredSpecimen, requiredPatt, check) && + checkMatches(optionalSpecimen, partialPatt, check, 'optional') && + checkMatches(restSpecimen, restPatt, check, 'rest') ); }, - checkIsWellFormed: matchSplitHelper.checkIsWellFormed, + compress: ( + specimen, + [requiredPatt, optionalPatt = {}, restPatt = MM.any()], + compress, + ) => { + if (!checkKind(specimen, 'copyArray', identChecker)) { + return undefined; + } + const { requiredSpecimen, optionalSpecimen, restSpecimen } = + splitRecordParts(specimen, requiredPatt, optionalPatt); + const partialNames = /** @type {string[]} */ (ownKeys(optionalSpecimen)); + const partialPatt = adaptRecordPattern(optionalPatt, partialNames); + const bindings = harden([ + compress(requiredSpecimen, requiredPatt), + // The bindings must record which optional field names were + // present + objectMap(partialPatt, (fieldPatt, fieldName) => + compress(optionalSpecimen[fieldName], fieldPatt), + ), + compress(restSpecimen, restPatt), + ]); + if (bindings.some(subBinding => subBinding === undefined)) { + return undefined; + } + return bindings; + }, + + decompress: ( + [requiredBindings, partialBindings, restBindings], + [requiredPatt, optionalPatt = {}, restPatt = MM.any()], + decompress, + ) => { + const partialNames = /** @type {string[]} */ (ownKeys(partialBindings)); + const partialPatt = adaptRecordPattern(optionalPatt, partialNames); + + const allEntries = [ + ...entries(decompress(requiredBindings, requiredPatt)), + ...entries( + objectMap(partialPatt, (fieldPatt, fieldName) => + decompress(partialBindings[fieldName], fieldPatt), + ), + ), + ...entries(decompress(restBindings, restPatt)), + ]; + return fromUniqueEntries(allEntries); + }, + + checkIsWellFormed: (splitArgs, check) => { + if ( + passStyleOf(splitArgs) === 'copyArray' && + (splitArgs.length >= 1 || splitArgs.length <= 3) + ) { + const [requiredPatt, optionalPatt = undefined, restPatt = undefined] = + splitArgs; + if ( + isPattern(requiredPatt) && + passStyleOf(requiredPatt) === 'copyRecord' && + (optionalPatt === undefined || + (isPattern(optionalPatt) && + passStyleOf(optionalPatt) === 'copyRecord')) && + (restPatt === undefined || isPattern(restPatt)) + ) { + return true; + } + } + return check( + false, + X`Must be an array of a requiredPatt record, an optional optionalPatt record, and an optional restPatt: ${q( + splitArgs, + )}`, + ); + }, - getRankCover: matchSplitHelper.getRankCover, + getRankCover: ([ + requiredPatt, + _optionalPatt = undefined, + _restPatt = undefined, + ]) => getPassStyleCover(passStyleOf(requiredPatt)), - checkKeyPattern: matchSplitHelper.checkKeyPattern, + checkKeyPattern: ( + [_requiredPatt, _optionalPatt = undefined, _restPatt = undefined], + check, + ) => check(false, X`copyRecord not yet supported as keys`), }); /** @type {Record} */ @@ -1468,8 +1643,8 @@ const makePatternKit = () => { 'match:setOf': matchSetOfHelper, 'match:bagOf': matchBagOfHelper, 'match:mapOf': matchMapOfHelper, - 'match:split': matchSplitHelper, - 'match:partial': matchPartialHelper, + 'match:splitArgs': matchSplitArrayHelper, + 'match:splitRecord': matchSplitRecordHelper, }); const makeMatcher = (tag, payload) => { @@ -1625,13 +1800,49 @@ const makePatternKit = () => { makeLimitsMatcher('match:bagOf', [keyPatt, countPatt, limits]), mapOf: (keyPatt = M.any(), valuePatt = M.any(), limits = undefined) => makeLimitsMatcher('match:mapOf', [keyPatt, valuePatt, limits]), - split: (base, rest = undefined) => - makeMatcher('match:split', rest === undefined ? [base] : [base, rest]), - partial: (base, rest = undefined) => - makeMatcher('match:partial', rest === undefined ? [base] : [base, rest]), + splitArgs: (base, optional = undefined, rest = undefined) => + makeMatcher( + 'match:splitArgs', + // eslint-disable-next-line no-nested-ternary + optional === undefined + ? rest === undefined + ? [base] + : [base, [], rest] + : rest === undefined + ? [base, optional] + : [base, optional, rest], + ), + splitRecord: (base, optional = undefined, rest = undefined) => + makeMatcher( + 'match:splitRecord', + // eslint-disable-next-line no-nested-ternary + optional === undefined + ? rest === undefined + ? [base] + : [base, {}, rest] + : rest === undefined + ? [base, optional] + : [base, optional, rest], + ), + split: (base, rest = undefined) => { + if (passStyleOf(harden(base)) === 'copyArray') { + // @ts-expect-error We know it should be an array + return M.splitArgs(base, rest && [], rest); + } else { + return M.splitRecord(base, rest && {}, rest); + } + }, + partial: (base, rest = undefined) => { + if (passStyleOf(harden(base)) === 'copyArray') { + // @ts-expect-error We know it should be an array + return M.splitArgs([], base, rest); + } else { + return M.splitRecord({}, base, rest); + } + }, eref: t => M.or(t, M.promise()), - opt: t => M.or(t, M.undefined()), + opt: t => M.or(M.undefined(), t), interface: (interfaceName, methodGuards, { sloppy = false } = {}) => { for (const [_, methodGuard] of entries(methodGuards)) { diff --git a/packages/store/src/types.js b/packages/store/src/types.js index d0d0b1707b9c..04a34869c811 100644 --- a/packages/store/src/types.js +++ b/packages/store/src/types.js @@ -568,15 +568,45 @@ * valuePatt?: Pattern, * limits?: Limits * ) => Matcher} mapOf - * @property {(base: CopyRecord<*> | CopyArray<*>, + * + * @property {(required: Pattern[], + * optional?: Pattern[], + * rest?: Pattern, + * ) => Matcher} splitArgs + * Splits an array --- typically an arguments list --- into an initial + * portion that matches the `required` pattern, then the portion that matches + * the `optional` pattern, and the remainder which is matched against the + * `rest` pattern. The specimen must be at least as long as the `required` + * pattern, but the remainder can be shorter than the `optional` pattern. + * Within the optional portion, an `undefined` arg matches unconditionally. + * Any elements beyond the `optional` pattern are matched against the + * `rest` pattern. + * @property {(required: CopyRecord, + * optional?: CopyRecord, + * rest?: Pattern, + * ) => Matcher} splitRecord + * Splits a copyRecord into those properties that match the `required` + * pattern, the remainder which is matched against the `optional` pattern. + * Any properties not matched by either are gathered into a record that + * is matched against the `rest` pattern. + * The specimen must have all the properties that appear on the `required` + * pattern. It may omit properties that appear on the optional pattern. + * For these purposes, `undefined` is like omission --- such properties + * pass unconditionally. + * + * @property {(required: CopyRecord<*> | CopyArray<*>, * rest?: Pattern, * ) => Matcher} split + * Deprecated. Use `M.splitArgs` or `M.splitRecord` instead. + * * 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 + * Deprecated. Use `M.splitArgs` or `M.splitRecord` instead. + * * 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. diff --git a/packages/store/test/test-heap-classes.js b/packages/store/test/test-heap-classes.js index 6640dcf0323b..4bffd29cc0de 100644 --- a/packages/store/test/test-heap-classes.js +++ b/packages/store/test/test-heap-classes.js @@ -40,12 +40,13 @@ test('test defineHeapFarClass', t => { t.is(upCounter.incr(5), 8); t.is(upCounter.incr(1), 9); t.throws(() => upCounter.incr(-3), { - message: 'In "incr" method of (UpCounter) arg 0: -3 - Must be >= 0', + message: + 'In "incr" method of (UpCounter): optional args: [0]: -3 - Must be >= 0', }); // @ts-expect-error bad arg t.throws(() => upCounter.incr('foo'), { message: - 'In "incr" method of (UpCounter) arg 0: string "foo" - Must be a number', + 'In "incr" method of (UpCounter): optional args: [0]: string "foo" - Must be a number', }); }); @@ -78,12 +79,13 @@ test('test defineHeapFarClassKit', t => { t.is(downCounter.decr(), 7); t.is(upCounter.incr(3), 10); t.throws(() => upCounter.incr(-3), { - message: 'In "incr" method of (Counter up) arg 0: -3 - Must be >= 0', + message: + 'In "incr" method of (Counter up): optional args: [0]: -3 - Must be >= 0', }); // @ts-expect-error the type violation is what we're testing t.throws(() => downCounter.decr('foo'), { message: - 'In "decr" method of (Counter down) arg 0: string "foo" - Must be a number', + 'In "decr" method of (Counter down): optional args: [0]: string "foo" - Must be a number', }); // @ts-expect-error bad arg t.throws(() => upCounter.decr(3), { @@ -102,11 +104,12 @@ test('test makeHeapFarInstance', t => { t.is(upCounter.incr(5), 8); t.is(upCounter.incr(1), 9); t.throws(() => upCounter.incr(-3), { - message: 'In "incr" method of (upCounter) arg 0: -3 - Must be >= 0', + message: + 'In "incr" method of (upCounter): optional args: [0]: -3 - Must be >= 0', }); t.throws(() => upCounter.incr('foo'), { message: - 'In "incr" method of (upCounter) arg 0: string "foo" - Must be a number', + 'In "incr" method of (upCounter): optional args: [0]: string "foo" - Must be a number', }); }); diff --git a/packages/store/test/test-patterns.js b/packages/store/test/test-patterns.js index 615bedbe0d96..43686b520a64 100644 --- a/packages/store/test/test-patterns.js +++ b/packages/store/test/test-patterns.js @@ -84,13 +84,13 @@ const runTests = (successCase, failCase) => { failCase( specimen, M.split([3, 4, 5, 6]), - 'required-parts: [3,4] - Must be: [3,4,5,6]', + 'args: [3,4] - Must be: [3,4,5,6]', ); - failCase(specimen, M.split([5]), 'required-parts: [3] - Must be: [5]'); + failCase(specimen, M.split([5]), 'args: [3] - Must be: [5]'); failCase(specimen, M.split({}), 'copyArray [3,4] - Must be a copyRecord'); - failCase(specimen, M.split([3], 'x'), 'rest-parts: [4] - Must be: "x"'); + failCase(specimen, M.split([3], 'x'), 'rest args: [4] - Must be: "x"'); - failCase(specimen, M.partial([5]), 'optional-parts: [3] - Must be: [5]'); + failCase(specimen, M.partial([5]), 'optional args: [0]: 3 - Must be: 5'); failCase( specimen, @@ -196,13 +196,19 @@ const runTests = (successCase, failCase) => { '{"foo":3,"bar":4} - Must be >= {"baz":3}', ); + failCase( + specimen, + M.splitRecord({ foo: M.number() }, { bar: M.string(), baz: M.number() }), + 'optional: bar: number 4 - Must be a string', + ); + failCase( specimen, M.split( { foo: M.number() }, M.and(M.partial({ bar: M.string() }), M.partial({ baz: M.number() })), ), - 'rest-parts: optional-parts: bar: number 4 - Must be a string', + 'rest: optional: bar: number 4 - Must be a string', ); failCase( @@ -213,22 +219,22 @@ const runTests = (successCase, failCase) => { failCase( specimen, M.split({ foo: 3, z: 4 }), - 'required-parts: {"foo":3} - Must be: {"foo":3,"z":4}', + '{"foo":3} - Must be: {"foo":3,"z":4}', ); failCase( specimen, M.split({ foo: 3 }, { foo: 3, bar: 4 }), - 'rest-parts: {"bar":4} - Must be: {"foo":3,"bar":4}', + 'rest: {"bar":4} - Must be: {"foo":3,"bar":4}', ); failCase( specimen, M.split({ foo: 3 }, { foo: M.any(), bar: 4 }), - 'rest-parts: {"bar":4} - Must have missing properties ["foo"]', + 'rest: {"bar":4} - Must have missing properties ["foo"]', ); failCase( specimen, M.partial({ foo: 7, zip: 5 }, { bar: 4 }), - 'optional-parts: {"foo":3} - Must be: {"foo":7}', + 'optional: foo: 3 - Must be: 7', ); failCase( diff --git a/packages/vat-data/test/test-durable-classes.js b/packages/vat-data/test/test-durable-classes.js index e5ef8cc3cf1d..c4e2f77ba1a9 100644 --- a/packages/vat-data/test/test-durable-classes.js +++ b/packages/vat-data/test/test-durable-classes.js @@ -44,12 +44,13 @@ test('test defineDurableFarClass', t => { t.is(upCounter.incr(5), 8); t.is(upCounter.incr(1), 9); t.throws(() => upCounter.incr(-3), { - message: 'In "incr" method of (UpCounter) arg 0: -3 - Must be >= 0', + message: + 'In "incr" method of (UpCounter): optional args: [0]: -3 - Must be >= 0', }); // @ts-expect-error the type violation is what we're testing t.throws(() => upCounter.incr('foo'), { message: - 'In "incr" method of (UpCounter) arg 0: string "foo" - Must be a number', + 'In "incr" method of (UpCounter): optional args: [0]: string "foo" - Must be a number', }); }); @@ -82,12 +83,13 @@ test('test defineDurableFarClassKit', t => { t.is(downCounter.decr(), 7); t.is(upCounter.incr(3), 10); t.throws(() => upCounter.incr(-3), { - message: 'In "incr" method of (Counter up) arg 0: -3 - Must be >= 0', + message: + 'In "incr" method of (Counter up): optional args: [0]: -3 - Must be >= 0', }); // @ts-expect-error the type violation is what we're testing t.throws(() => downCounter.decr('foo'), { message: - 'In "decr" method of (Counter down) arg 0: string "foo" - Must be a number', + 'In "decr" method of (Counter down): optional args: [0]: string "foo" - Must be a number', }); t.throws(() => upCounter.decr(3), { message: 'upCounter.decr is not a function', diff --git a/packages/vat-data/test/test-virtual-classes.js b/packages/vat-data/test/test-virtual-classes.js index 6aae78a63bf8..e23d12239ac7 100644 --- a/packages/vat-data/test/test-virtual-classes.js +++ b/packages/vat-data/test/test-virtual-classes.js @@ -41,12 +41,13 @@ test('test defineVirtualFarClass', t => { t.is(upCounter.incr(5), 8); t.is(upCounter.incr(1), 9); t.throws(() => upCounter.incr(-3), { - message: 'In "incr" method of (UpCounter) arg 0: -3 - Must be >= 0', + message: + 'In "incr" method of (UpCounter): optional args: [0]: -3 - Must be >= 0', }); // @ts-expect-error the type violation is what we're testing t.throws(() => upCounter.incr('foo'), { message: - 'In "incr" method of (UpCounter) arg 0: string "foo" - Must be a number', + 'In "incr" method of (UpCounter): optional args: [0]: string "foo" - Must be a number', }); }); @@ -77,12 +78,13 @@ test('test defineVirtualFarClassKit', t => { t.is(downCounter.decr(), 7); t.is(upCounter.incr(3), 10); t.throws(() => upCounter.incr(-3), { - message: 'In "incr" method of (Counter up) arg 0: -3 - Must be >= 0', + message: + 'In "incr" method of (Counter up): optional args: [0]: -3 - Must be >= 0', }); // @ts-expect-error the type violation is what we're testing t.throws(() => downCounter.decr('foo'), { message: - 'In "decr" method of (Counter down) arg 0: string "foo" - Must be a number', + 'In "decr" method of (Counter down): optional args: [0]: string "foo" - Must be a number', }); t.throws(() => upCounter.decr(3), { message: 'upCounter.decr is not a function', diff --git a/packages/vat-data/test/test-vivify.js b/packages/vat-data/test/test-vivify.js index b3c7c9b1c531..8b75a66b4dd8 100644 --- a/packages/vat-data/test/test-vivify.js +++ b/packages/vat-data/test/test-vivify.js @@ -46,12 +46,13 @@ test('test vivifyFarClass', t => { t.is(upCounter.incr(5), 8); t.is(upCounter.incr(1), 9); t.throws(() => upCounter.incr(-3), { - message: 'In "incr" method of (UpCounter) arg 0: -3 - Must be >= 0', + message: + 'In "incr" method of (UpCounter): optional args: [0]: -3 - Must be >= 0', }); // @ts-expect-error the type violation is what we're testing t.throws(() => upCounter.incr('foo'), { message: - 'In "incr" method of (UpCounter) arg 0: string "foo" - Must be a number', + 'In "incr" method of (UpCounter): optional args: [0]: string "foo" - Must be a number', }); }); @@ -85,12 +86,13 @@ test('test vivifyFarClassKit', t => { t.is(downCounter.decr(), 7); t.is(upCounter.incr(3), 10); t.throws(() => upCounter.incr(-3), { - message: 'In "incr" method of (Counter up) arg 0: -3 - Must be >= 0', + message: + 'In "incr" method of (Counter up): optional args: [0]: -3 - Must be >= 0', }); // @ts-expect-error the type violation is what we're testing t.throws(() => downCounter.decr('foo'), { message: - 'In "decr" method of (Counter down) arg 0: string "foo" - Must be a number', + 'In "decr" method of (Counter down): optional args: [0]: string "foo" - Must be a number', }); t.throws(() => upCounter.decr(3), { message: 'upCounter.decr is not a function', @@ -110,11 +112,12 @@ test('test vivifyFarInstance', t => { t.is(upCounter.incr(5), 8); t.is(upCounter.incr(1), 9); t.throws(() => upCounter.incr(-3), { - message: 'In "incr" method of (upCounter) arg 0: -3 - Must be >= 0', + message: + 'In "incr" method of (upCounter): optional args: [0]: -3 - Must be >= 0', }); t.throws(() => upCounter.incr('foo'), { message: - 'In "incr" method of (upCounter) arg 0: string "foo" - Must be a number', + 'In "incr" method of (upCounter): optional args: [0]: string "foo" - Must be a number', }); }); diff --git a/packages/vats/src/core/boot-psm.js b/packages/vats/src/core/boot-psm.js index 7a21f035669b..25741f65305a 100644 --- a/packages/vats/src/core/boot-psm.js +++ b/packages/vats/src/core/boot-psm.js @@ -88,19 +88,22 @@ export const agoricNamesReserved = harden( * decimalPlaces?: number * }} AnchorOptions */ -const AnchorOptionsShape = M.split( +const AnchorOptionsShape = M.splitRecord( { denom: M.string() }, - M.partial({ + { keyword: M.string(), proposedName: M.string(), decimalPlaces: M.number(), - }), + }, ); -export const ParametersShape = M.partial({ - anchorAssets: M.arrayOf(AnchorOptionsShape), - economicCommitteeAddresses: M.recordOf(M.string(), M.string()), -}); +export const ParametersShape = M.splitRecord( + {}, + { + anchorAssets: M.arrayOf(AnchorOptionsShape), + economicCommitteeAddresses: M.recordOf(M.string(), M.string()), + }, +); /** * Build root object of the PSM-only bootstrap vat. diff --git a/packages/zoe/src/contracts/coveredCall-durable.js b/packages/zoe/src/contracts/coveredCall-durable.js index ac38535fccb1..ea185cd1ed22 100644 --- a/packages/zoe/src/contracts/coveredCall-durable.js +++ b/packages/zoe/src/contracts/coveredCall-durable.js @@ -62,7 +62,7 @@ const start = async (zcf, _privateArgs, instanceBaggage) => { const makeOption = sellSeat => { fit( sellSeat.getProposal(), - M.split({ exit: { afterDeadline: M.any() } }), + M.splitRecord({ exit: { afterDeadline: M.any() } }), 'exit afterDeadline', ); const sellSeatExitRule = sellSeat.getProposal().exit; diff --git a/packages/zoe/src/contracts/coveredCall.js b/packages/zoe/src/contracts/coveredCall.js index 968a67b65bfe..a63b5a9f8ea4 100644 --- a/packages/zoe/src/contracts/coveredCall.js +++ b/packages/zoe/src/contracts/coveredCall.js @@ -76,7 +76,7 @@ const start = zcf => { const makeOption = sellSeat => { fit( sellSeat.getProposal(), - M.split({ exit: { afterDeadline: M.any() } }), + M.splitRecord({ exit: { afterDeadline: M.any() } }), 'exit afterDeadline', ); const sellSeatExitRule = sellSeat.getProposal().exit; diff --git a/packages/zoe/src/typeGuards.js b/packages/zoe/src/typeGuards.js index 56f21c412d5f..c53c78da91f7 100644 --- a/packages/zoe/src/typeGuards.js +++ b/packages/zoe/src/typeGuards.js @@ -30,7 +30,8 @@ export const FullProposalShape = harden({ // To accept only one, we could use M.or rather than M.partial, // but the error messages would have been worse. Rather, // cleanProposal's assertExit checks that there's exactly one. - exit: M.partial( + exit: M.splitRecord( + {}, { onDemand: null, waived: null, @@ -43,7 +44,7 @@ export const FullProposalShape = harden({ ), }); /** @see {Proposal} type */ -export const ProposalShape = M.partial(FullProposalShape); +export const ProposalShape = M.splitRecord({}, FullProposalShape, {}); export const isOnDemandExitRule = exit => { const [exitKey] = Object.getOwnPropertyNames(exit); @@ -68,7 +69,7 @@ export const isAfterDeadlineExitRule = exit => { return exitKey === 'afterDeadline'; }; -export const InvitationElementShape = M.split({ +export const InvitationElementShape = M.splitRecord({ description: M.string(), handle: InvitationHandleShape, instance: InstanceHandleShape, diff --git a/packages/zoe/test/unitTests/test-cleanProposal.js b/packages/zoe/test/unitTests/test-cleanProposal.js index 13721ebd5d58..843e1b679030 100644 --- a/packages/zoe/test/unitTests/test-cleanProposal.js +++ b/packages/zoe/test/unitTests/test-cleanProposal.js @@ -228,7 +228,7 @@ test('cleanProposal - other wrong stuff', t => { t, { exit: { onDemand: 'foo' } }, 'nat', - 'proposal: exit: optional-parts: {"onDemand":"foo"} - Must be: {"onDemand":null}', + 'proposal: exit: optional: onDemand: "foo" - Must be: null', ); proposeBad( t, @@ -240,43 +240,43 @@ test('cleanProposal - other wrong stuff', t => { t, { exit: { afterDeadline: { timer: 'foo', deadline: 3n } } }, 'nat', - 'proposal: exit: optional-parts: afterDeadline: timer: "foo" - Must match one of ["[match:remotable]","[match:kind]"]', + 'proposal: exit: optional: afterDeadline: timer: "foo" - Must match one of ["[match:remotable]","[match:kind]"]', ); proposeBad( t, { exit: { afterDeadline: { timer, deadline: 'foo' } } }, 'nat', - 'proposal: exit: optional-parts: afterDeadline: deadline: string "foo" - Must be a bigint', + 'proposal: exit: optional: afterDeadline: deadline: string "foo" - Must be a bigint', ); proposeBad( t, { exit: { afterDeadline: { timer, deadline: 3n, extra: 'foo' } } }, 'nat', - 'proposal: exit: optional-parts: afterDeadline: {"timer":"[Alleged: ManualTimer]","deadline":"[3n]","extra":"foo"} - Must not have unexpected properties: ["extra"]', + 'proposal: exit: optional: afterDeadline: {"timer":"[Alleged: ManualTimer]","deadline":"[3n]","extra":"foo"} - Must not have unexpected properties: ["extra"]', ); proposeBad( t, { exit: { afterDeadline: { timer } } }, 'nat', - 'proposal: exit: optional-parts: afterDeadline: {"timer":"[Alleged: ManualTimer]"} - Must have missing properties ["deadline"]', + 'proposal: exit: optional: afterDeadline: {"timer":"[Alleged: ManualTimer]"} - Must have missing properties ["deadline"]', ); proposeBad( t, { exit: { afterDeadline: { deadline: 3n } } }, 'nat', - 'proposal: exit: optional-parts: afterDeadline: {"deadline":"[3n]"} - Must have missing properties ["timer"]', + 'proposal: exit: optional: afterDeadline: {"deadline":"[3n]"} - Must have missing properties ["timer"]', ); proposeBad( t, { exit: { afterDeadline: { timer, deadline: 3 } } }, 'nat', - 'proposal: exit: optional-parts: afterDeadline: deadline: number 3 - Must be a bigint', + 'proposal: exit: optional: afterDeadline: deadline: number 3 - Must be a bigint', ); proposeBad( t, { exit: { afterDeadline: { timer, deadline: -3n } } }, 'nat', - 'proposal: exit: optional-parts: afterDeadline: deadline: "[-3n]" - Must be non-negative', + 'proposal: exit: optional: afterDeadline: deadline: "[-3n]" - Must be non-negative', ); proposeBad(t, { exit: {} }, 'nat', /exit {} should only have one key/); proposeBad( diff --git a/packages/zoe/test/unitTests/zcf/test-zoeHelpersWZcf.js b/packages/zoe/test/unitTests/zcf/test-zoeHelpersWZcf.js index aaaa154cecae..296edcb847d6 100644 --- a/packages/zoe/test/unitTests/zcf/test-zoeHelpersWZcf.js +++ b/packages/zoe/test/unitTests/zcf/test-zoeHelpersWZcf.js @@ -305,7 +305,7 @@ test(`zoeHelper with zcf - fit proposal patterns`, async t => { () => fit(proposal, M.split({ want: { C: M.any() } })), { message: - 'required-parts: want: {"A":{"brand":"[Alleged: moola brand]","value":"[20n]"}} - Must have missing properties ["C"]', + 'want: {"A":{"brand":"[Alleged: moola brand]","value":"[20n]"}} - Must have missing properties ["C"]', }, 'empty keywordRecord does not match', ); @@ -315,14 +315,14 @@ test(`zoeHelper with zcf - fit proposal patterns`, async t => { () => fit(proposal, M.split({ give: { c: M.any() } })), { message: - 'required-parts: give: {"B":{"brand":"[Alleged: simoleans brand]","value":"[3n]"}} - Must have missing properties ["c"]', + 'give: {"B":{"brand":"[Alleged: simoleans brand]","value":"[3n]"}} - Must have missing properties ["c"]', }, 'wrong key in keywordRecord does not match', ); t.throws( () => fit(proposal, M.split({ exit: { onDemaind: M.any() } })), { - message: 'required-parts: {} - Must have missing properties ["exit"]', + message: '{} - Must have missing properties ["exit"]', }, 'missing exit rule', );