From b05871a164ac1c34bf91c0094bfc54776da3bb8b Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Mon, 7 Aug 2023 18:46:38 -0700 Subject: [PATCH] fix(patterns)!: tag and retype guards --- packages/exo/README.md | 2 +- packages/exo/src/exo-tools.js | 41 ++++++++------ packages/exo/test/test-heap-classes.js | 2 +- .../patterns/src/patterns/patternMatchers.js | 56 ++++++++++++++----- packages/patterns/src/types.js | 52 +++++++---------- 5 files changed, 88 insertions(+), 65 deletions(-) diff --git a/packages/exo/README.md b/packages/exo/README.md index a50d9a0247..47828cc3cf 100644 --- a/packages/exo/README.md +++ b/packages/exo/README.md @@ -17,6 +17,6 @@ import { GET_INTERFACE_GUARD } from `@endo/exo`; // `methodNames` omits names of automatically added meta-methods like // the value of `GET_INTERFACE_GUARD`. // Others may also be omitted if `interfaceGuard.partial` - const methodNames = Reflect.ownKeys(interfaceGuard.methodGuards); + const methodNames = Reflect.ownKeys(interfaceGuard.payload.methodGuards); ... ``` diff --git a/packages/exo/src/exo-tools.js b/packages/exo/src/exo-tools.js index 0f3bd637cb..76e25012b8 100644 --- a/packages/exo/src/exo-tools.js +++ b/packages/exo/src/exo-tools.js @@ -25,8 +25,8 @@ const { defineProperties } = Object; */ const MinMethodGuard = M.call().rest(M.any()).returns(M.any()); -const defendSyncArgs = (args, methodGuard, label) => { - const { argGuards, optionalArgGuards, restArgGuard } = methodGuard; +const defendSyncArgs = (args, methodGuardPayload, label) => { + const { argGuards, optionalArgGuards, restArgGuard } = methodGuardPayload; const paramsPattern = M.splitArray( argGuards, optionalArgGuards, @@ -37,16 +37,16 @@ const defendSyncArgs = (args, methodGuard, label) => { /** * @param {Method} method - * @param {MethodGuard} methodGuard + * @param {MethodGuard['payload']} methodGuardPayload * @param {string} label * @returns {Method} */ -const defendSyncMethod = (method, methodGuard, label) => { - const { returnGuard } = methodGuard; +const defendSyncMethod = (method, methodGuardPayload, label) => { + const { returnGuard } = methodGuardPayload; const { syncMethod } = { // Note purposeful use of `this` and concise method syntax syncMethod(...args) { - defendSyncArgs(harden(args), methodGuard, label); + defendSyncArgs(harden(args), methodGuardPayload, label); const result = apply(method, this, args); mustMatch(harden(result), returnGuard, `${label}: result`); return result; @@ -55,8 +55,12 @@ const defendSyncMethod = (method, methodGuard, label) => { return syncMethod; }; -const desync = methodGuard => { - const { argGuards, optionalArgGuards = [], restArgGuard } = methodGuard; +const desync = methodGuardPayload => { + const { + argGuards, + optionalArgGuards = [], + restArgGuard, + } = methodGuardPayload; !isAwaitArgGuard(restArgGuard) || Fail`Rest args may not be awaited: ${restArgGuard}`; const rawArgGuards = [...argGuards, ...optionalArgGuards]; @@ -71,7 +75,7 @@ const desync = methodGuard => { } return { awaitIndexes, - rawMethodGuard: { + rawMethodGuardPayload: { argGuards: rawArgGuards.slice(0, argGuards.length), optionalArgGuards: rawArgGuards.slice(argGuards.length), restArgGuard, @@ -79,9 +83,9 @@ const desync = methodGuard => { }; }; -const defendAsyncMethod = (method, methodGuard, label) => { - const { returnGuard } = methodGuard; - const { awaitIndexes, rawMethodGuard } = desync(methodGuard); +const defendAsyncMethod = (method, methodGuardPayload, label) => { + const { returnGuard } = methodGuardPayload; + const { awaitIndexes, rawMethodGuardPayload } = desync(methodGuardPayload); const { asyncMethod } = { // Note purposeful use of `this` and concise method syntax asyncMethod(...args) { @@ -92,7 +96,7 @@ const defendAsyncMethod = (method, methodGuard, label) => { for (let j = 0; j < awaitIndexes.length; j += 1) { rawArgs[awaitIndexes[j]] = awaitedArgs[j]; } - defendSyncArgs(rawArgs, rawMethodGuard, label); + defendSyncArgs(rawArgs, rawMethodGuardPayload, label); return apply(method, this, rawArgs); }); return E.when(resultP, result => { @@ -112,12 +116,13 @@ const defendAsyncMethod = (method, methodGuard, label) => { */ const defendMethod = (method, methodGuard, label) => { assertMethodGuard(methodGuard); - const { callKind } = methodGuard; + const { payload } = methodGuard; + const { callKind } = payload; if (callKind === 'sync') { - return defendSyncMethod(method, methodGuard, label); + return defendSyncMethod(method, payload, label); } else { assert(callKind === 'async'); - return defendAsyncMethod(method, methodGuard, label); + return defendAsyncMethod(method, payload, label); } }; @@ -263,7 +268,9 @@ export const defendPrototype = ( let methodGuards; if (interfaceGuard) { assertInterfaceGuard(interfaceGuard); - const { interfaceName, methodGuards: mg, sloppy = false } = interfaceGuard; + const { + payload: { interfaceName, methodGuards: mg, sloppy = false }, + } = interfaceGuard; methodGuards = mg; { const methodNames = ownKeys(behaviorMethods); diff --git a/packages/exo/test/test-heap-classes.js b/packages/exo/test/test-heap-classes.js index 613ec584fc..c35d449686 100644 --- a/packages/exo/test/test-heap-classes.js +++ b/packages/exo/test/test-heap-classes.js @@ -52,7 +52,7 @@ test('test defineExoClass', t => { 'In "incr" method of (UpCounter): arg 0?: string "foo" - Must be a number', }); t.deepEqual(upCounter[GET_INTERFACE_GUARD](), UpCounterI); - t.deepEqual(ownKeys(UpCounterI.methodGuards), ['incr']); + t.deepEqual(ownKeys(UpCounterI.payload.methodGuards), ['incr']); }); test('test defineExoClassKit', t => { diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index a23a2397bb..f4403b4f03 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -158,6 +158,18 @@ const makePatternKit = () => { // eslint-disable-next-line no-use-before-define HelpersByMatchTag[tag]; + /** + * Note that this function indicates absence by returning `undefined`, + * even though `undefined` is a valid pattern. To evade this confusion, + * to register a payload shape with that meaning, use `MM.undefined()`. + * + * @param {string} tag + * @returns {Pattern | undefined} + */ + const maybePayloadShape = tag => + // eslint-disable-next-line no-use-before-define + GuardPayloadShapes[tag]; + /** * @typedef {Exclude | * 'copySet' | 'copyBag' | 'copyMap' | keyof HelpersByMatchTag @@ -197,6 +209,12 @@ const makePatternKit = () => { // Buried here is the important case, where we process // the various patternNodes return matchHelper.checkIsWellFormed(tagged.payload, check); + } else { + const payloadShape = maybePayloadShape(tag); + if (payloadShape !== undefined) { + // eslint-disable-next-line no-use-before-define + return checkMatches(tagged.payload, payloadShape, check, tag); + } } switch (tag) { case 'copySet': { @@ -1702,11 +1720,12 @@ MM = M; // //////////////////////////// Guards /////////////////////////////////////// -const AwaitArgGuardShape = harden({ - klass: 'awaitArg', +const AwaitArgGuardPayloadShape = harden({ argGuard: M.pattern(), }); +const AwaitArgGuardShape = M.kind('guard:awaitArgGuard'); + export const isAwaitArgGuard = specimen => matches(specimen, AwaitArgGuardShape); harden(isAwaitArgGuard); @@ -1722,8 +1741,7 @@ harden(assertAwaitArgGuard); */ const makeAwaitArgGuard = argGuard => { /** @type {AwaitArgGuard} */ - const result = harden({ - klass: 'awaitArg', + const result = makeTagged('guard:awaitArgGuard', { argGuard, }); assertAwaitArgGuard(result); @@ -1735,8 +1753,7 @@ const PatternListShape = M.arrayOf(M.pattern()); const ArgGuardShape = M.or(M.pattern(), AwaitArgGuardShape); const ArgGuardListShape = M.arrayOf(ArgGuardShape); -const SyncMethodGuardShape = harden({ - klass: 'methodGuard', +const SyncMethodGuardPayloadShape = harden({ callKind: 'sync', argGuards: PatternListShape, optionalArgGuards: M.opt(PatternListShape), @@ -1744,8 +1761,7 @@ const SyncMethodGuardShape = harden({ returnGuard: M.pattern(), }); -const AsyncMethodGuardShape = harden({ - klass: 'methodGuard', +const AsyncMethodGuardPayloadShape = harden({ callKind: 'async', argGuards: ArgGuardListShape, optionalArgGuards: M.opt(ArgGuardListShape), @@ -1753,7 +1769,12 @@ const AsyncMethodGuardShape = harden({ returnGuard: M.pattern(), }); -const MethodGuardShape = M.or(SyncMethodGuardShape, AsyncMethodGuardShape); +const MethodGuardPayloadShape = M.or( + SyncMethodGuardPayloadShape, + AsyncMethodGuardPayloadShape, +); + +const MethodGuardShape = M.kind('guard:methodGuard'); export const assertMethodGuard = specimen => { mustMatch(specimen, MethodGuardShape, 'methodGuard'); @@ -1792,8 +1813,7 @@ const makeMethodGuardMaker = ( }, returns: (returnGuard = M.undefined()) => { /** @type {MethodGuard} */ - const result = harden({ - klass: 'methodGuard', + const result = makeTagged('guard:methodGuard', { callKind, argGuards, optionalArgGuards, @@ -1805,13 +1825,14 @@ const makeMethodGuardMaker = ( }, }); -const InterfaceGuardShape = harden({ - klass: 'Interface', +const InterfaceGuardPayloadShape = harden({ interfaceName: M.string(), methodGuards: M.recordOf(M.string(), MethodGuardShape), sloppy: M.boolean(), }); +const InterfaceGuardShape = M.kind('guard:interfaceGuard'); + export const assertInterfaceGuard = specimen => { mustMatch(specimen, InterfaceGuardShape, 'interfaceGuard'); }; @@ -1826,8 +1847,7 @@ harden(assertInterfaceGuard); const makeInterfaceGuard = (interfaceName, methodGuards, options = {}) => { const { sloppy = false } = options; /** @type {InterfaceGuard} */ - const result = harden({ - klass: 'Interface', + const result = makeTagged('guard:interfaceGuard', { interfaceName, methodGuards, sloppy, @@ -1835,3 +1855,9 @@ const makeInterfaceGuard = (interfaceName, methodGuards, options = {}) => { assertInterfaceGuard(result); return result; }; + +const GuardPayloadShapes = harden({ + 'guard:awaitArgGuard': AwaitArgGuardPayloadShape, + 'guard:methodGuard': MethodGuardPayloadShape, + 'guard:interfaceGuard': InterfaceGuardPayloadShape, +}); diff --git a/packages/patterns/src/types.js b/packages/patterns/src/types.js index 0935301790..35bf988f3e 100644 --- a/packages/patterns/src/types.js +++ b/packages/patterns/src/types.js @@ -511,11 +511,13 @@ export {}; /** * @template {Record} [T=Record] - * @typedef {{ - * klass: 'Interface', - * interfaceName: string, - * methodGuards: T - * sloppy?: boolean + * @typedef {CopyTagged & { + * [Symbol.toStringTag]: 'guard:interfaceGuard', + * payload: { + * interfaceName: string, + * methodGuards: T + * sloppy?: boolean + * } * }} InterfaceGuard * * TODO https://github.com/endojs/endo/pull/1712 to make it into a genuine @@ -574,37 +576,25 @@ export {}; */ /** - * @typedef {{ - * klass: 'methodGuard', - * callKind: 'sync' | 'async', - * argGuards: ArgGuard[] - * optionalArgGuards?: ArgGuard[] - * restArgGuard?: Pattern - * returnGuard: Pattern + * @typedef {CopyTagged & { + * [Symbol.toStringTag]: 'guard:methodGuard', + * payload: { + * callKind: 'sync' | 'async', + * argGuards: ArgGuard[] + * optionalArgGuards?: ArgGuard[] + * restArgGuard?: Pattern + * returnGuard?: Pattern + * } * }} MethodGuard - * - * TODO https://github.com/endojs/endo/pull/1712 to make it into a genuine - * guard that is distinct from a copyRecord. - * Once we're ready for such a compat break, we might also take the - * opportunity to rename `restArgGuard` and `returnGuard` - * to reflect that their value must be a Pattern rather that a - * non-pattern guard. */ /** - * @typedef {{ - * klass: 'awaitArg', - * argGuard: Pattern + * @typedef {CopyTagged & { + * [Symbol.toStringTag]: 'guard:awaitArgGuard' + * payload: { + * argGuard: Pattern + * } * }} AwaitArgGuard - * - * TODO https://github.com/endojs/endo/pull/1712 to make it into a genuine - * guard that is distinct from a copyRecord. - * Unlike InterfaceGuard or MethodGuard, for AwaitArgGuard it is a correctness - * issue, so that the guard not be mistaken for the copyRecord as key/pattern. - * Once we're ready for such a compat break, we might also take the - * opportunity to rename `argGuard` - * to reflect that its value must be a Pattern rather that a - * non-pattern guard. */ /** @typedef {AwaitArgGuard | Pattern} ArgGuard */