From bf593d8e83ba7eb231b4d3a909c41751ab24fe66 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Tue, 15 Aug 2023 18:28:03 -0700 Subject: [PATCH 01/10] feat(exo): opt out individual arguments --- packages/exo/src/exo-tools.js | 14 +++- packages/patterns/index.js | 2 + .../patterns/src/patterns/internal-types.js | 2 + .../patterns/src/patterns/patternMatchers.js | 55 ++++++++++++--- packages/patterns/src/types.js | 68 +++++++++++++++---- 5 files changed, 116 insertions(+), 25 deletions(-) diff --git a/packages/exo/src/exo-tools.js b/packages/exo/src/exo-tools.js index e17d7ba2e7..75e8686a6a 100644 --- a/packages/exo/src/exo-tools.js +++ b/packages/exo/src/exo-tools.js @@ -10,10 +10,12 @@ import { getMethodGuardPayload, getInterfaceGuardPayload, getCopyMapEntries, + // isRawValueGuard, } from '@endo/patterns'; /** @typedef {import('@endo/patterns').Method} Method */ /** @typedef {import('@endo/patterns').MethodGuard} MethodGuard */ +/** @typedef {import('@endo/patterns').MethodGuardPayload} MethodGuardPayload */ /** * @template {Record} [T=Record] * @typedef {import('@endo/patterns').InterfaceGuard} InterfaceGuard @@ -24,11 +26,18 @@ const { quote: q, Fail } = assert; const { apply, ownKeys } = Reflect; const { defineProperties, fromEntries } = Object; +/** + * A method guard, for inclusion in an interface guard, that does not + * enforce any constraints of incoming arguments or return results. + */ +// const RawMethodGuard = M.call().rest(M.rawValue()).returns(M.rawValue()); + /** * A method guard, for inclusion in an interface guard, that enforces only that * all arguments are passable and that the result is passable. (In far classes, - * "any" means any *passable*.) This is the least possible enforcement for a - * method guard, and is implied by all other method guards. + * "any" means any *passable*.) This is the least possible non-raw + * enforcement for a method guard, and is implied by all other + * non-raw method guards. */ const MinMethodGuard = M.call().rest(M.any()).returns(M.any()); @@ -308,6 +317,7 @@ export const defendPrototype = ( }); { const methodNames = ownKeys(behaviorMethods); + assert(methodGuards); const methodGuardNames = ownKeys(methodGuards); const unimplemented = listDifference(methodGuardNames, methodNames); unimplemented.length === 0 || diff --git a/packages/patterns/index.js b/packages/patterns/index.js index 7b3d6ff1ab..b08b8acefa 100644 --- a/packages/patterns/index.js +++ b/packages/patterns/index.js @@ -61,6 +61,8 @@ export { isAwaitArgGuard, assertAwaitArgGuard, getAwaitArgGuardPayload, + isRawValueGuard, + assertRawValueGuard, assertMethodGuard, getMethodGuardPayload, getInterfaceMethodKeys, diff --git a/packages/patterns/src/patterns/internal-types.js b/packages/patterns/src/patterns/internal-types.js index 072cac4fe7..0e36c9a817 100644 --- a/packages/patterns/src/patterns/internal-types.js +++ b/packages/patterns/src/patterns/internal-types.js @@ -21,8 +21,10 @@ /** @typedef {import('../types.js').AwaitArgGuardPayload} AwaitArgGuardPayload */ /** @typedef {import('../types.js').AwaitArgGuard} AwaitArgGuard */ +/** @typedef {import('../types.js').RawValueGuard} RawValueGuard */ /** @typedef {import('../types.js').ArgGuard} ArgGuard */ /** @typedef {import('../types.js').MethodGuardPayload} MethodGuardPayload */ +/** @typedef {import('../types.js').SyncValueGuard} SyncValueGuard */ /** @typedef {import('../types.js').MethodGuard} MethodGuard */ /** * @template {Record} [T=Record] diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index 2a22fa6744..438d6660bf 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -1706,6 +1706,9 @@ const makePatternKit = () => { await: argPattern => // eslint-disable-next-line no-use-before-define makeAwaitArgGuard(argPattern), + rawValue: () => + // eslint-disable-next-line no-use-before-define + makeRawValueGuard(), }); return harden({ @@ -1739,6 +1742,7 @@ MM = M; // //////////////////////////// Guards /////////////////////////////////////// +// M.await(...) const AwaitArgGuardPayloadShape = harden({ argGuard: M.pattern(), }); @@ -1788,25 +1792,52 @@ const makeAwaitArgGuard = argPattern => { return result; }; -const PatternListShape = M.arrayOf(M.pattern()); +// M.rawValue() + +// TODO does not need to be a singleton, and would not be if we added a +// parameter, like a description string. +/** @type {RawValueGuard} */ +const TheRawValueGuard = harden({ + klass: 'rawValueGuard', +}); + +const RawValueGuardShape = TheRawValueGuard; + +export const isRawValueGuard = specimen => + matches(specimen, RawValueGuardShape); + +export const assertRawValueGuard = specimen => + mustMatch(specimen, RawValueGuardShape, 'rawValueGuard'); + +/** + * @returns {RawValueGuard} + */ +const makeRawValueGuard = () => TheRawValueGuard; + +// M.call(...) +// M.callWhen(...) + +const SyncValueGuardShape = M.or(RawValueGuardShape, M.pattern()); + +const SyncValueGuardListShape = M.arrayOf(SyncValueGuardShape); -const ArgGuardShape = M.or(M.pattern(), AwaitArgGuardShape); +const ArgGuardShape = M.or(RawValueGuardShape, AwaitArgGuardShape, M.pattern()); const ArgGuardListShape = M.arrayOf(ArgGuardShape); const SyncMethodGuardPayloadShape = harden({ callKind: 'sync', - argGuards: PatternListShape, - optionalArgGuards: M.opt(PatternListShape), - restArgGuard: M.opt(M.pattern()), - returnGuard: M.pattern(), + argGuards: SyncValueGuardListShape, + optionalArgGuards: M.opt(SyncValueGuardListShape), + restArgGuard: M.opt(SyncValueGuardShape), + returnGuard: SyncValueGuardShape, }); const AsyncMethodGuardPayloadShape = harden({ callKind: 'async', argGuards: ArgGuardListShape, optionalArgGuards: M.opt(ArgGuardListShape), - restArgGuard: M.opt(M.pattern()), - returnGuard: M.pattern(), + restArgGuard: M.opt(SyncValueGuardShape), + returnGuard: SyncValueGuardShape, }); const MethodGuardPayloadShape = M.or( @@ -1842,7 +1873,7 @@ harden(getMethodGuardPayload); * @param {'sync'|'async'} callKind * @param {ArgGuard[]} argGuards * @param {ArgGuard[]} [optionalArgGuards] - * @param {ArgGuard} [restArgGuard] + * @param {SyncValueGuard} [restArgGuard] * @returns {MethodGuardMaker0} */ const makeMethodGuardMaker = ( @@ -1887,6 +1918,7 @@ const InterfaceGuardPayloadShape = M.splitRecord( interfaceName: M.string(), methodGuards: M.recordOf(M.string(), MethodGuardShape), sloppy: M.boolean(), + raw: M.boolean(), }, { symbolMethodGuards: M.mapOf(M.symbol(), MethodGuardShape), @@ -1940,11 +1972,11 @@ harden(getInterfaceMethodKeys); * @template {Record} [M = Record] * @param {string} interfaceName * @param {M} methodGuards - * @param {{ sloppy?: boolean }} [options] + * @param {{ sloppy?: boolean, raw?: boolean }} [options] * @returns {InterfaceGuard} */ const makeInterfaceGuard = (interfaceName, methodGuards, options = {}) => { - const { sloppy = false } = options; + const { sloppy = false, raw = false } = options; // For backwards compatibility, string-keyed method guards are represented in // a CopyRecord. But symbol-keyed methods cannot be, so we put those in a // CopyMap when present. @@ -1968,6 +2000,7 @@ const makeInterfaceGuard = (interfaceName, methodGuards, options = {}) => { ? { symbolMethodGuards: makeCopyMap(symbolMethodGuardsEntries) } : {}), sloppy, + raw, }); assertInterfaceGuard(result); return /** @type {InterfaceGuard} */ (result); diff --git a/packages/patterns/src/types.js b/packages/patterns/src/types.js index 2c18776e73..221483f05d 100644 --- a/packages/patterns/src/types.js +++ b/packages/patterns/src/types.js @@ -478,14 +478,42 @@ export {}; * @property {MakeInterfaceGuard} interface * Guard the interface of an exo object * - * @property {(...argPatterns: Pattern[]) => MethodGuardMaker0} call + * @property {(...argPatterns: SyncValueGuard[]) => MethodGuardMaker0} call * Guard a synchronous call * * @property {(...argGuards: ArgGuard[]) => MethodGuardMaker0} callWhen * Guard an async call * * @property {(argPattern: Pattern) => AwaitArgGuard} await - * Guard an await + * In parameter position, guard a parameter by awaiting it. Can only be used in + * parameter position of an `M.callWhen`. + * `M.await(M.nat())`, for example, with `await` the corresponding argument, + * check that the fulfillment of the `await` satisfies the `M.nat()` + * pattern, and only then proceed to call the raw method with that fulfillment. + * If the argument already passes the `M.nat()` pattern, then the result of + * `await`ing it will still pass, and the `M.callWhen` will still delay the + * raw method call to a future turn. + * If the argument is a promise that rejects rather than fulfills, or if its + * fulfillment does not satisfy the nested pattern, then the call is rejected + * without ever calling the raw method. + * + * Any `AwaitArgGuard` may not appear as a rest pattern or a result pattern, + * only a top-level single parameter pattern. + * + * HAZARD: Until https://github.com/endojs/endo/pull/1712 an `AwaitArgGuard` + * is itself a `CopyRecord`. If used nested within a pattern, rather than + * at top level, it may be mistaken for a CopyRecord pattern that would match + * only a specimen shaped like an `AwaitArgGuard`. + * + * @property {(() => RawValueGuard)} rawValue + * In parameter position, pass this argument through without any checking. + * In rest position, pass the rest of the arguments through without any checking. + * In return position, return the result without any checking. + * + * HAZARD: Until https://github.com/endojs/endo/pull/1712 a `RawValueGuard` + * is itself a `CopyRecord`. If used nested within a pattern, rather than + * at top level, it may be mistaken for a CopyRecord pattern that would match + * only a specimen shaped like a `RawValueGuard`. */ /** @@ -503,7 +531,10 @@ export {}; * symbolMethodGuards?: * CopyMap, T[Extract]>, * sloppy?: boolean, + * raw?: boolean, * }} InterfaceGuardPayload + * + * At most one of `sloppy` or `raw` can be true. */ /** @@ -529,8 +560,8 @@ export {}; * } * ``` * @property {(...optArgGuards: ArgGuard[]) => MethodGuardMaker1} optional - * @property {(rArgGuard: Pattern) => MethodGuardMaker2} rest - * @property {(returnGuard?: Pattern) => MethodGuard} returns + * @property {(rArgGuard: SyncValueGuard) => MethodGuardMaker2} rest + * @property {(returnGuard?: SyncValueGuard) => MethodGuard} returns */ /** @@ -546,8 +577,8 @@ export {}; * foo: M.call(AShape, BShape).optional(CShape).rest(EShape).returns(FShape), * } * ``` - * @property {(rArgGuard: Pattern) => MethodGuardMaker2} rest - * @property {(returnGuard?: Pattern) => MethodGuard} returns + * @property {(rArgGuard: SyncValueGuard) => MethodGuardMaker2} rest + * @property {(returnGuard?: SyncValueGuard) => MethodGuard} returns */ /** @@ -563,16 +594,16 @@ export {}; * foo: M.call(AShape, BShape).optional(CShape).rest(EShape).returns(FShape), * } * ``` - * @property {(returnGuard?: Pattern) => MethodGuard} returns + * @property {(returnGuard?: SyncValueGuard) => MethodGuard} returns */ /** * @typedef {{ * callKind: 'sync' | 'async', - * argGuards: ArgGuard[] - * optionalArgGuards?: ArgGuard[] - * restArgGuard?: Pattern - * returnGuard: Pattern + * argGuards: ArgGuard[], + * optionalArgGuards?: ArgGuard[], + * restArgGuard?: SyncValueGuard, + * returnGuard: SyncValueGuard, * }} MethodGuardPayload */ @@ -590,4 +621,17 @@ export {}; * @typedef {CopyTagged<'guard:awaitArgGuard', AwaitArgGuardPayload>} AwaitArgGuard */ -/** @typedef {AwaitArgGuard | Pattern} ArgGuard */ +/** + * @typedef {{ + * klass: 'rawValueGuard' + * }} RawValueGuard + * + * 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 RawValueGuard it is a correctness + * issue, so that the guard not be mistaken for the copyRecord as key/pattern. + */ + +/** @typedef {RawValueGuard | Pattern} SyncValueGuard */ + +/** @typedef {AwaitArgGuard | RawValueGuard | Pattern} ArgGuard */ From 58a3d42a92102336d814690430e0feb3773227d4 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Wed, 11 Oct 2023 15:04:02 -0600 Subject: [PATCH 02/10] feat(defaultGuards): absorb `sloppy` and `raw` --- packages/exo/src/exo-tools.js | 4 +- .../patterns/src/patterns/patternMatchers.js | 21 ++++----- packages/patterns/src/types.js | 45 +++++++++---------- 3 files changed, 33 insertions(+), 37 deletions(-) diff --git a/packages/exo/src/exo-tools.js b/packages/exo/src/exo-tools.js index 75e8686a6a..ed0089170d 100644 --- a/packages/exo/src/exo-tools.js +++ b/packages/exo/src/exo-tools.js @@ -308,7 +308,7 @@ export const defendPrototype = ( interfaceName, methodGuards: mg, symbolMethodGuards, - sloppy = false, + defaultGuards, } = getInterfaceGuardPayload(interfaceGuard); methodGuards = harden({ ...mg, @@ -322,7 +322,7 @@ export const defendPrototype = ( const unimplemented = listDifference(methodGuardNames, methodNames); unimplemented.length === 0 || Fail`methods ${q(unimplemented)} not implemented by ${q(tag)}`; - if (!sloppy) { + if (defaultGuards === 'never') { const unguarded = listDifference(methodNames, methodGuardNames); unguarded.length === 0 || Fail`methods ${q(unguarded)} not guarded by ${q(interfaceName)}`; diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index 438d6660bf..7771b1b5a5 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -1794,14 +1794,14 @@ const makeAwaitArgGuard = argPattern => { // M.rawValue() +const RawValueGuardPayloadShape = M.record(); + // TODO does not need to be a singleton, and would not be if we added a // parameter, like a description string. /** @type {RawValueGuard} */ -const TheRawValueGuard = harden({ - klass: 'rawValueGuard', -}); +const TheRawValueGuard = makeTagged('guard:rawValueGuard', {}); -const RawValueGuardShape = TheRawValueGuard; +const RawValueGuardShape = M.kind('guard:rawValueGuard'); export const isRawValueGuard = specimen => matches(specimen, RawValueGuardShape); @@ -1917,10 +1917,10 @@ const InterfaceGuardPayloadShape = M.splitRecord( { interfaceName: M.string(), methodGuards: M.recordOf(M.string(), MethodGuardShape), - sloppy: M.boolean(), - raw: M.boolean(), + defaultGuards: M.or('never', 'passable', 'raw'), }, { + sloppy: M.boolean(), symbolMethodGuards: M.mapOf(M.symbol(), MethodGuardShape), }, ); @@ -1972,11 +1972,12 @@ harden(getInterfaceMethodKeys); * @template {Record} [M = Record] * @param {string} interfaceName * @param {M} methodGuards - * @param {{ sloppy?: boolean, raw?: boolean }} [options] + * @param {{ sloppy?: boolean, defaultGuards?: import('../types.js').DefaultGuardType }} [options] * @returns {InterfaceGuard} */ const makeInterfaceGuard = (interfaceName, methodGuards, options = {}) => { - const { sloppy = false, raw = false } = options; + const { sloppy = false, defaultGuards = sloppy ? 'passable' : 'never' } = + options; // For backwards compatibility, string-keyed method guards are represented in // a CopyRecord. But symbol-keyed methods cannot be, so we put those in a // CopyMap when present. @@ -1999,8 +2000,7 @@ const makeInterfaceGuard = (interfaceName, methodGuards, options = {}) => { ...(symbolMethodGuardsEntries.length ? { symbolMethodGuards: makeCopyMap(symbolMethodGuardsEntries) } : {}), - sloppy, - raw, + defaultGuards, }); assertInterfaceGuard(result); return /** @type {InterfaceGuard} */ (result); @@ -2008,6 +2008,7 @@ const makeInterfaceGuard = (interfaceName, methodGuards, options = {}) => { const GuardPayloadShapes = harden({ 'guard:awaitArgGuard': AwaitArgGuardPayloadShape, + 'guard:rawValueGuard': RawValueGuardPayloadShape, 'guard:methodGuard': MethodGuardPayloadShape, 'guard:interfaceGuard': InterfaceGuardPayloadShape, }); diff --git a/packages/patterns/src/types.js b/packages/patterns/src/types.js index 221483f05d..298be27029 100644 --- a/packages/patterns/src/types.js +++ b/packages/patterns/src/types.js @@ -456,21 +456,32 @@ export {}; * Matches any Passable that is matched by `subPatt` or is the exact value `undefined`. */ +/** + * @typedef {'never' | 'passable' | 'raw'} DefaultGuardType + */ + +/** + * @typedef {>( + * interfaceName: string, + * methodGuards: M, + * options: {defaultGuards?: 'never', sloppy?: false }) => InterfaceGuard + * } MakeInterfaceGuardStrict + */ /** * @typedef {( * interfaceName: string, * methodGuards: any, - * options: {sloppy: true}) => InterfaceGuard> + * options: {defaultGuards?: 'passable' | 'raw', sloppy?: true }) => InterfaceGuard> * } MakeInterfaceGuardSloppy */ /** * @typedef {>( * interfaceName: string, * methodGuards: M, - * options?: {sloppy?: boolean}) => InterfaceGuard + * options?: {defaultGuards?: DefaultGuardType, sloppy?: boolean}) => InterfaceGuard * } MakeInterfaceGuardGeneral */ -/** @typedef {MakeInterfaceGuardSloppy & MakeInterfaceGuardGeneral} MakeInterfaceGuard */ +/** @typedef {MakeInterfaceGuardStrict & MakeInterfaceGuardSloppy & MakeInterfaceGuardGeneral} MakeInterfaceGuard */ /** * @typedef {object} GuardMakers @@ -500,20 +511,10 @@ export {}; * Any `AwaitArgGuard` may not appear as a rest pattern or a result pattern, * only a top-level single parameter pattern. * - * HAZARD: Until https://github.com/endojs/endo/pull/1712 an `AwaitArgGuard` - * is itself a `CopyRecord`. If used nested within a pattern, rather than - * at top level, it may be mistaken for a CopyRecord pattern that would match - * only a specimen shaped like an `AwaitArgGuard`. - * * @property {(() => RawValueGuard)} rawValue * In parameter position, pass this argument through without any checking. * In rest position, pass the rest of the arguments through without any checking. * In return position, return the result without any checking. - * - * HAZARD: Until https://github.com/endojs/endo/pull/1712 a `RawValueGuard` - * is itself a `CopyRecord`. If used nested within a pattern, rather than - * at top level, it may be mistaken for a CopyRecord pattern that would match - * only a specimen shaped like a `RawValueGuard`. */ /** @@ -530,11 +531,8 @@ export {}; * Omit & Partial<{ [K in Extract]: never }>, * symbolMethodGuards?: * CopyMap, T[Extract]>, - * sloppy?: boolean, - * raw?: boolean, + * defaultGuards: DefaultGuardType, * }} InterfaceGuardPayload - * - * At most one of `sloppy` or `raw` can be true. */ /** @@ -622,14 +620,11 @@ export {}; */ /** - * @typedef {{ - * klass: 'rawValueGuard' - * }} RawValueGuard - * - * 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 RawValueGuard it is a correctness - * issue, so that the guard not be mistaken for the copyRecord as key/pattern. + * @typedef {{}} RawValueGuardPayload + */ + +/** + * @typedef {CopyTagged<'guard:rawValueGuard', RawValueGuardPayload>} RawValueGuard */ /** @typedef {RawValueGuard | Pattern} SyncValueGuard */ From c8126dc9d863fbb69cc53d57514368ba931df7fe Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Wed, 11 Oct 2023 15:06:43 -0600 Subject: [PATCH 03/10] feat(defendSyncMethod): implement raw exo methods --- packages/exo/src/exo-tools.js | 181 ++++++++++++++++++++----- packages/exo/test/test-heap-classes.js | 17 +++ packages/patterns/src/types.js | 3 +- 3 files changed, 165 insertions(+), 36 deletions(-) diff --git a/packages/exo/src/exo-tools.js b/packages/exo/src/exo-tools.js index ed0089170d..7aa007db99 100644 --- a/packages/exo/src/exo-tools.js +++ b/packages/exo/src/exo-tools.js @@ -6,11 +6,11 @@ import { mustMatch, M, isAwaitArgGuard, + isRawGuard, getAwaitArgGuardPayload, getMethodGuardPayload, getInterfaceGuardPayload, getCopyMapEntries, - // isRawValueGuard, } from '@endo/patterns'; /** @typedef {import('@endo/patterns').Method} Method */ @@ -20,7 +20,6 @@ import { * @template {Record} [T=Record] * @typedef {import('@endo/patterns').InterfaceGuard} InterfaceGuard */ -/** @typedef {import('@endo/patterns').InterfaceGuardKit} InterfaceGuardKit */ const { quote: q, Fail } = assert; const { apply, ownKeys } = Reflect; @@ -30,7 +29,9 @@ const { defineProperties, fromEntries } = Object; * A method guard, for inclusion in an interface guard, that does not * enforce any constraints of incoming arguments or return results. */ -// const RawMethodGuard = M.call().rest(M.rawValue()).returns(M.rawValue()); +const RawMethodGuard = M.call().rest(M.raw()).returns(M.raw()); + +const REDACTED_RAW_ARG = ''; /** * A method guard, for inclusion in an interface guard, that enforces only that @@ -39,28 +40,60 @@ const { defineProperties, fromEntries } = Object; * enforcement for a method guard, and is implied by all other * non-raw method guards. */ -const MinMethodGuard = M.call().rest(M.any()).returns(M.any()); +const PassableMethodGuard = M.call().rest(M.any()).returns(M.any()); + +/** + * @typedef {object} MatchConfig + * @property {number} declaredLen + * @property {boolean} hasRestArgGuard + * @property {boolean} restArgGuardIsRaw + * @property {Pattern} paramsPattern + * @property {number[]} redactedIndices + */ /** * @param {Passable[]} syncArgs - * @param {MethodGuardPayload} methodGuardPayload + * @param {MatchConfig} matchConfig * @param {string} [label] * @returns {Passable[]} Returns the args that should be passed to the * raw method */ -const defendSyncArgs = (syncArgs, methodGuardPayload, label = undefined) => { - const { argGuards, optionalArgGuards, restArgGuard } = methodGuardPayload; - const paramsPattern = M.splitArray( - argGuards, - optionalArgGuards, - restArgGuard, - ); - mustMatch(harden(syncArgs), paramsPattern, label); - if (restArgGuard !== undefined) { +const defendSyncArgs = (syncArgs, matchConfig, label = undefined) => { + const { + declaredLen, + hasRestArgGuard, + restArgGuardIsRaw, + paramsPattern, + redactedIndices, + } = matchConfig; + + // Use syncArgs if possible, but copy it when necessary to implement redactions. + let matchableArgs = syncArgs; + if (restArgGuardIsRaw && syncArgs.length > declaredLen) { + const restLen = syncArgs.length - declaredLen; + const redactedRest = Array(restLen).fill(REDACTED_RAW_ARG); + matchableArgs = [...syncArgs.slice(0, declaredLen), ...redactedRest]; + } else if ( + redactedIndices.length > 0 && + redactedIndices[0] < syncArgs.length + ) { + // Copy the arguments array, avoiding hardening the redacted ones (which are + // trivially matched using REDACTED_RAW_ARG as a sentinel value). + matchableArgs = [...syncArgs]; + } + + for (const i of redactedIndices) { + if (i >= matchableArgs.length) { + break; + } + matchableArgs[i] = REDACTED_RAW_ARG; + } + + mustMatch(harden(matchableArgs), paramsPattern, label); + + if (hasRestArgGuard) { return syncArgs; } - const declaredLen = - argGuards.length + (optionalArgGuards ? optionalArgGuards.length : 0); if (syncArgs.length <= declaredLen) { return syncArgs; } @@ -68,6 +101,60 @@ const defendSyncArgs = (syncArgs, methodGuardPayload, label = undefined) => { return syncArgs.slice(0, declaredLen); }; +/** + * Convert a method guard to a match config for more efficient per-call + * execution. This is a one-time conversion, so it's OK to be slow. + * + * Most of the work is done to detect `M.raw()` so that we build a match pattern + * and metadata instead of doing this in the hot path. + * @param {MethodGuardPayload} methodGuardPayload + * @returns {MatchConfig} + */ +const buildMatchConfig = methodGuardPayload => { + const { + argGuards, + optionalArgGuards = [], + restArgGuard, + } = methodGuardPayload; + + const matchableArgGuards = [...argGuards, ...optionalArgGuards]; + + const redactedIndices = []; + for (let i = 0; i < matchableArgGuards.length; i += 1) { + if (isRawGuard(matchableArgGuards[i])) { + matchableArgGuards[i] = REDACTED_RAW_ARG; + redactedIndices.push(i); + } + } + + // Pass through raw rest arguments without matching. + let matchableRestArgGuard = restArgGuard; + if (isRawGuard(matchableRestArgGuard)) { + matchableRestArgGuard = M.arrayOf(REDACTED_RAW_ARG); + } + const matchableMethodGuardPayload = harden({ + ...methodGuardPayload, + argGuards: matchableArgGuards.slice(0, argGuards.length), + optionalArgGuards: matchableArgGuards.slice(argGuards.length), + restArgGuard: matchableRestArgGuard, + }); + + const paramsPattern = M.splitArray( + matchableMethodGuardPayload.argGuards, + matchableMethodGuardPayload.optionalArgGuards, + matchableMethodGuardPayload.restArgGuard, + ); + + return harden({ + declaredLen: matchableArgGuards.length, + hasRestArgGuard: restArgGuard !== undefined, + restArgGuardIsRaw: restArgGuard !== matchableRestArgGuard, + paramsPattern, + redactedIndices, + matchableMethodGuardPayload, + }); +}; + /** * @param {Method} method * @param {MethodGuardPayload} methodGuardPayload @@ -76,16 +163,17 @@ const defendSyncArgs = (syncArgs, methodGuardPayload, label = undefined) => { */ const defendSyncMethod = (method, methodGuardPayload, label) => { const { returnGuard } = methodGuardPayload; + const isRawReturn = isRawGuard(returnGuard); + const matchConfig = buildMatchConfig(methodGuardPayload); const { syncMethod } = { // Note purposeful use of `this` and concise method syntax syncMethod(...syncArgs) { - const realArgs = defendSyncArgs( - harden(syncArgs), - methodGuardPayload, - label, - ); + // Only harden args and return value if not dealing with a raw value guard. + const realArgs = defendSyncArgs(syncArgs, matchConfig, label); const result = apply(method, this, realArgs); - mustMatch(harden(result), returnGuard, `${label}: result`); + if (!isRawReturn) { + mustMatch(harden(result), returnGuard, `${label}: result`); + } return result; }, }; @@ -193,6 +281,7 @@ const defendMethod = (method, methodGuard, label) => { * @param {CallableFunction} behaviorMethod * @param {boolean} [thisfulMethods] * @param {MethodGuard} [methodGuard] + * @param {import('@endo/patterns').DefaultGuardType} [defaultGuards] */ const bindMethod = ( methodTag, @@ -200,6 +289,7 @@ const bindMethod = ( behaviorMethod, thisfulMethods = false, methodGuard = undefined, + defaultGuards = 'never', ) => { assert.typeof(behaviorMethod, 'function'); @@ -235,12 +325,23 @@ const bindMethod = ( return apply(behaviorMethod, null, [context, ...args]); }, }; + if (!methodGuard && thisfulMethods) { + switch (defaultGuards) { + case 'never': + case 'passable': + methodGuard = PassableMethodGuard; + break; + case 'raw': + methodGuard = RawMethodGuard; + break; + default: + throw Fail`Unrecognized defaultGuards ${q(defaultGuards)}`; + } + } if (methodGuard) { method = defendMethod(method, methodGuard, methodTag); - } else if (thisfulMethods) { - // For far classes ensure that inputs and outputs are passable. - method = defendMethod(method, MinMethodGuard, methodTag); } + defineProperties(method, { name: { value: methodTag }, length: { @@ -267,7 +368,7 @@ export const GET_INTERFACE_GUARD = Symbol.for('getInterfaceGuard'); * @template {Record} T * @param {T} behaviorMethods * @param {InterfaceGuard<{ [M in keyof T]: MethodGuard }>} interfaceGuard - * @returns {T} + * @returns {T & { [GET_INTERFACE_GUARD]: () => InterfaceGuard<{ [M in keyof T]: MethodGuard }> }} */ const withGetInterfaceGuardMethod = (behaviorMethods, interfaceGuard) => harden({ @@ -284,7 +385,6 @@ const withGetInterfaceGuardMethod = (behaviorMethods, interfaceGuard) => * @param {T} behaviorMethods * @param {boolean} [thisfulMethods] * @param {InterfaceGuard<{ [M in keyof T]: MethodGuard }>} [interfaceGuard] - * @returns {T & import('@endo/eventual-send').RemotableBrand<{}, T>} */ export const defendPrototype = ( tag, @@ -303,18 +403,22 @@ export const defendPrototype = ( } /** @type {Record | undefined} */ let methodGuards; + /** @type {import('@endo/patterns').DefaultGuardType} */ + let defaultGuards = 'never'; if (interfaceGuard) { const { interfaceName, methodGuards: mg, symbolMethodGuards, - defaultGuards, + sloppy, + defaultGuards: dg = sloppy ? 'passable' : defaultGuards, } = getInterfaceGuardPayload(interfaceGuard); methodGuards = harden({ ...mg, ...(symbolMethodGuards && fromEntries(getCopyMapEntries(symbolMethodGuards))), }); + defaultGuards = dg; { const methodNames = ownKeys(behaviorMethods); assert(methodGuards); @@ -333,7 +437,6 @@ export const defendPrototype = ( interfaceGuard, ); } - for (const prop of ownKeys(behaviorMethods)) { prototype[prop] = bindMethod( `In ${q(prop)} method of (${tag})`, @@ -342,19 +445,26 @@ export const defendPrototype = ( thisfulMethods, // TODO some tool does not yet understand the `?.[` syntax methodGuards && methodGuards[prop], + defaultGuards, ); } - return Far(tag, /** @type {T} */ (prototype)); + return Far( + tag, + /** @type {T & { [GET_INTERFACE_GUARD]: () => InterfaceGuard<{ [M in keyof T]: MethodGuard }>}} */ ( + prototype + ), + ); }; harden(defendPrototype); /** + * @template {Record} F * @param {string} tag - * @param {Record} contextProviderKit - * @param {Record>} behaviorMethodsKit + * @param {{ [K in keyof F]: KitContextProvider }} contextProviderKit + * @param {F} behaviorMethodsKit * @param {boolean} [thisfulMethods] - * @param {InterfaceGuardKit} [interfaceGuardKit] + * @param {{ [K in keyof F]: InterfaceGuard> }} [interfaceGuardKit] */ export const defendPrototypeKit = ( tag, @@ -381,13 +491,14 @@ export const defendPrototypeKit = ( const extraFacetNames = listDifference(contextMapNames, facetNames); extraFacetNames.length === 0 || Fail`Facets ${q(extraFacetNames)} of ${q(tag)} missing contexts`; - return objectMap(behaviorMethodsKit, (behaviorMethods, facetName) => + const protoKit = objectMap(behaviorMethodsKit, (behaviorMethods, facetName) => defendPrototype( - `${tag} ${facetName}`, + `${tag} ${String(facetName)}`, contextProviderKit[facetName], behaviorMethods, thisfulMethods, interfaceGuardKit && interfaceGuardKit[facetName], ), ); + return protoKit; }; diff --git a/packages/exo/test/test-heap-classes.js b/packages/exo/test/test-heap-classes.js index 8f5e753a92..c81f650357 100644 --- a/packages/exo/test/test-heap-classes.js +++ b/packages/exo/test/test-heap-classes.js @@ -203,6 +203,23 @@ test('sloppy option', t => { ); }); +const RawGreeterI = M.interface('greeter', {}, { defaultGuards: 'raw' }); + +test('raw defaultGuards', t => { + const greeter = makeExo('greeter', RawGreeterI, { + sayHello(mutable) { + mutable.x = 3; + return 'hello'; + }, + }); + const mutable = {}; + t.is(greeter.sayHello(mutable), 'hello'); + t.deepEqual(mutable, { x: 3 }); + mutable.y = 4; + t.deepEqual(mutable, { x: 3, y: 4 }); + t.deepEqual(greeter[GET_INTERFACE_GUARD](), RawGreeterI); +}); + const GreeterI = M.interface('greeter', { sayHello: M.call().returns('hello'), }); diff --git a/packages/patterns/src/types.js b/packages/patterns/src/types.js index 298be27029..2f875de735 100644 --- a/packages/patterns/src/types.js +++ b/packages/patterns/src/types.js @@ -531,7 +531,8 @@ export {}; * Omit & Partial<{ [K in Extract]: never }>, * symbolMethodGuards?: * CopyMap, T[Extract]>, - * defaultGuards: DefaultGuardType, + * defaultGuards?: DefaultGuardType, + * sloppy?: boolean, * }} InterfaceGuardPayload */ From c50ee18b543c8da921cd095cdc65b56df1761b9f Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Tue, 17 Oct 2023 17:28:38 -0500 Subject: [PATCH 04/10] fix(exo): tighten typing --- packages/exo/src/exo-makers.js | 66 +++++++++++++++++++++++++--------- packages/patterns/src/utils.js | 10 +++--- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/packages/exo/src/exo-makers.js b/packages/exo/src/exo-makers.js index 5d2ca8a8c9..3d7d9b08f3 100644 --- a/packages/exo/src/exo-makers.js +++ b/packages/exo/src/exo-makers.js @@ -3,7 +3,14 @@ import { makeEnvironmentCaptor } from '@endo/env-options'; import { objectMap } from '@endo/patterns'; -import { defendPrototype, defendPrototypeKit } from './exo-tools.js'; +import { + GET_INTERFACE_GUARD, + defendPrototype, + defendPrototypeKit, +} from './exo-tools.js'; + +// Used in typing. +GET_INTERFACE_GUARD; const { create, seal, freeze, defineProperty, values } = Object; @@ -13,6 +20,12 @@ const DEBUG = getEnvironmentOption('DEBUG', ''); // Turn on to give each exo instance its own toStringTag value. const LABEL_INSTANCES = DEBUG.split(',').includes('label-instances'); +/** + * @template {{}} T + * @param {T} proto + * @param {number} instanceCount + * @returns {T} + */ const makeSelf = (proto, instanceCount) => { const self = create(proto); if (LABEL_INSTANCES) { @@ -82,6 +95,28 @@ export const initEmpty = () => emptyRecord; * @property {ReceiveRevoker} [receiveRevoker] */ +/** + * @template {Methods} M + * @typedef {M & import('@endo/eventual-send').RemotableBrand<{}, M>} Farable + */ + +/** + * @template {Methods} M + * @typedef {{ + * [GET_INTERFACE_GUARD]: () => InterfaceGuard<{ [K in keyof M]: MethodGuard }> + * }} GetInterfaceGuard + */ + +/** + * @template {Methods} M + * @typedef {Farable>} Facet + */ + +/** + * @template {Record} F + * @typedef {{ [K in keyof F]: Facet }} FacetKit + */ + /** * @template {(...args: any[]) => any} I init function * @template {Methods} M methods @@ -90,9 +125,9 @@ export const initEmpty = () => emptyRecord; * [K in keyof M]: import("@endo/patterns").MethodGuard * }> | undefined} interfaceGuard * @param {I} init - * @param {M & ThisType<{ self: M, state: ReturnType }>} methods + * @param {M & ThisType<{ self: Facet, state: ReturnType }>} methods * @param {FarClassOptions, M>>} [options] - * @returns {(...args: Parameters) => (M & import('@endo/eventual-send').RemotableBrand<{}, M>)} + * @returns {(...args: Parameters) => Facet} */ export const defineExoClass = ( tag, @@ -120,7 +155,6 @@ export const defineExoClass = ( // Be careful not to freeze the state record const state = seal(init(...args)); instanceCount += 1; - /** @type {M} */ const self = makeSelf(proto, instanceCount); // Be careful not to freeze the state record @@ -130,9 +164,7 @@ export const defineExoClass = ( if (finish) { finish(context); } - return /** @type {M & import('@endo/eventual-send').RemotableBrand<{}, M>} */ ( - self - ); + return self; }; if (receiveRevoker) { @@ -149,13 +181,13 @@ harden(defineExoClass); * @template {(...args: any[]) => any} I init function * @template {Record} F facet methods * @param {string} tag - * @param {{ [K in keyof F]: import("@endo/patterns").InterfaceGuard<{ - * [M in keyof F[K]]: import("@endo/patterns").MethodGuard; - * }> } | undefined} interfaceGuardKit + * @param {{ [K in keyof F]: + * InterfaceGuard<{[M in keyof F[K]]: MethodGuard; }> + * } | undefined} interfaceGuardKit * @param {I} init - * @param {F & ThisType<{ facets: F, state: ReturnType }> } methodsKit - * @param {FarClassOptions,F>>} [options] - * @returns {(...args: Parameters) => F} + * @param {F & { [K in keyof F]: ThisType<{ facets: FacetKit, state: ReturnType }> }} methodsKit + * @param {FarClassOptions, FacetKit>>} [options] + * @returns {(...args: Parameters) => FacetKit} */ export const defineExoClassKit = ( tag, @@ -186,8 +218,8 @@ export const defineExoClassKit = ( // Be careful not to freeze the state record const state = seal(init(...args)); // Don't freeze context until we add facets - /** @type {KitContext,F>} */ - const context = { state, facets: {} }; + /** @type {{ state: ReturnType, facets: unknown }} */ + const context = { state, facets: null }; instanceCount += 1; const facets = objectMap(prototypeKit, (proto, facetName) => { const self = makeSelf(proto, instanceCount); @@ -200,7 +232,7 @@ export const defineExoClassKit = ( if (finish) { finish(context); } - return context.facets; + return /** @type {FacetKit} */ (context.facets); }; if (receiveRevoker) { @@ -222,7 +254,7 @@ harden(defineExoClassKit); * }> | undefined} interfaceGuard CAVEAT: static typing does not yet support `callWhen` transformation * @param {T} methods * @param {FarClassOptions>} [options] - * @returns {T & import('@endo/eventual-send').RemotableBrand<{}, T>} + * @returns {Facet} */ export const makeExo = (tag, interfaceGuard, methods, options = undefined) => { const makeInstance = defineExoClass( diff --git a/packages/patterns/src/utils.js b/packages/patterns/src/utils.js index 557c54474a..e6f7722df7 100644 --- a/packages/patterns/src/utils.js +++ b/packages/patterns/src/utils.js @@ -92,15 +92,17 @@ harden(fromUniqueEntries); * a CopyRecord. * * @template {Record} O + * @template R result * @param {O} original - * @template R map result * @param {(value: O[keyof O], key: keyof O) => R} mapFn - * @returns {{ [P in keyof O]: R}} + * @returns {Record} */ export const objectMap = (original, mapFn) => { const ents = entries(original); - const mapEnts = ents.map(([k, v]) => [k, mapFn(v, k)]); - return harden(fromEntries(mapEnts)); + const mapEnts = ents.map( + ([k, v]) => /** @type {[keyof O, R]} */ ([k, mapFn(v, k)]), + ); + return /** @type {Record} */ (harden(fromEntries(mapEnts))); }; harden(objectMap); From 5b9453042aec993f5876deeed4488f4d32dc4803 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Wed, 25 Oct 2023 14:18:50 -0600 Subject: [PATCH 05/10] fix(patterns): `M.rawValue()` -> `M.raw()` --- packages/patterns/index.js | 4 +-- .../patterns/src/patterns/internal-types.js | 2 +- .../patterns/src/patterns/patternMatchers.js | 32 ++++++++----------- packages/patterns/src/types.js | 16 ++++------ 4 files changed, 22 insertions(+), 32 deletions(-) diff --git a/packages/patterns/index.js b/packages/patterns/index.js index b08b8acefa..9bd5dbb5f5 100644 --- a/packages/patterns/index.js +++ b/packages/patterns/index.js @@ -61,8 +61,8 @@ export { isAwaitArgGuard, assertAwaitArgGuard, getAwaitArgGuardPayload, - isRawValueGuard, - assertRawValueGuard, + isRawGuard, + assertRawGuard, assertMethodGuard, getMethodGuardPayload, getInterfaceMethodKeys, diff --git a/packages/patterns/src/patterns/internal-types.js b/packages/patterns/src/patterns/internal-types.js index 0e36c9a817..0aa8e30b55 100644 --- a/packages/patterns/src/patterns/internal-types.js +++ b/packages/patterns/src/patterns/internal-types.js @@ -21,7 +21,7 @@ /** @typedef {import('../types.js').AwaitArgGuardPayload} AwaitArgGuardPayload */ /** @typedef {import('../types.js').AwaitArgGuard} AwaitArgGuard */ -/** @typedef {import('../types.js').RawValueGuard} RawValueGuard */ +/** @typedef {import('../types.js').RawGuard} RawGuard */ /** @typedef {import('../types.js').ArgGuard} ArgGuard */ /** @typedef {import('../types.js').MethodGuardPayload} MethodGuardPayload */ /** @typedef {import('../types.js').SyncValueGuard} SyncValueGuard */ diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index 7771b1b5a5..3af408e1ce 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -1706,9 +1706,9 @@ const makePatternKit = () => { await: argPattern => // eslint-disable-next-line no-use-before-define makeAwaitArgGuard(argPattern), - rawValue: () => + raw: () => // eslint-disable-next-line no-use-before-define - makeRawValueGuard(), + makeRawGuard(), }); return harden({ @@ -1792,36 +1792,30 @@ const makeAwaitArgGuard = argPattern => { return result; }; -// M.rawValue() +// M.raw() -const RawValueGuardPayloadShape = M.record(); +const RawGuardPayloadShape = M.record(); -// TODO does not need to be a singleton, and would not be if we added a -// parameter, like a description string. -/** @type {RawValueGuard} */ -const TheRawValueGuard = makeTagged('guard:rawValueGuard', {}); +const RawGuardShape = M.kind('guard:rawGuard'); -const RawValueGuardShape = M.kind('guard:rawValueGuard'); +export const isRawGuard = specimen => matches(specimen, RawGuardShape); -export const isRawValueGuard = specimen => - matches(specimen, RawValueGuardShape); - -export const assertRawValueGuard = specimen => - mustMatch(specimen, RawValueGuardShape, 'rawValueGuard'); +export const assertRawGuard = specimen => + mustMatch(specimen, RawGuardShape, 'rawGuard'); /** - * @returns {RawValueGuard} + * @returns {import('../types.js').RawGuard} */ -const makeRawValueGuard = () => TheRawValueGuard; +const makeRawGuard = () => makeTagged('guard:rawGuard', {}); // M.call(...) // M.callWhen(...) -const SyncValueGuardShape = M.or(RawValueGuardShape, M.pattern()); +const SyncValueGuardShape = M.or(RawGuardShape, M.pattern()); const SyncValueGuardListShape = M.arrayOf(SyncValueGuardShape); -const ArgGuardShape = M.or(RawValueGuardShape, AwaitArgGuardShape, M.pattern()); +const ArgGuardShape = M.or(RawGuardShape, AwaitArgGuardShape, M.pattern()); const ArgGuardListShape = M.arrayOf(ArgGuardShape); const SyncMethodGuardPayloadShape = harden({ @@ -2008,7 +2002,7 @@ const makeInterfaceGuard = (interfaceName, methodGuards, options = {}) => { const GuardPayloadShapes = harden({ 'guard:awaitArgGuard': AwaitArgGuardPayloadShape, - 'guard:rawValueGuard': RawValueGuardPayloadShape, + 'guard:rawGuard': RawGuardPayloadShape, 'guard:methodGuard': MethodGuardPayloadShape, 'guard:interfaceGuard': InterfaceGuardPayloadShape, }); diff --git a/packages/patterns/src/types.js b/packages/patterns/src/types.js index 2f875de735..93a4d30625 100644 --- a/packages/patterns/src/types.js +++ b/packages/patterns/src/types.js @@ -471,7 +471,7 @@ export {}; * @typedef {( * interfaceName: string, * methodGuards: any, - * options: {defaultGuards?: 'passable' | 'raw', sloppy?: true }) => InterfaceGuard> + * options: {defaultGuards?: 'passable' | 'raw', sloppy?: true }) => InterfaceGuard * } MakeInterfaceGuardSloppy */ /** @@ -511,7 +511,7 @@ export {}; * Any `AwaitArgGuard` may not appear as a rest pattern or a result pattern, * only a top-level single parameter pattern. * - * @property {(() => RawValueGuard)} rawValue + * @property {() => RawGuard} raw * In parameter position, pass this argument through without any checking. * In rest position, pass the rest of the arguments through without any checking. * In return position, return the result without any checking. @@ -541,10 +541,6 @@ export {}; * @typedef {CopyTagged<'guard:interfaceGuard', InterfaceGuardPayload>}InterfaceGuard */ -/** - * @typedef {Record} InterfaceGuardKit - */ - /** * @typedef {object} MethodGuardMaker0 * A method name and parameter/return signature like: @@ -621,13 +617,13 @@ export {}; */ /** - * @typedef {{}} RawValueGuardPayload + * @typedef {{}} RawGuardPayload */ /** - * @typedef {CopyTagged<'guard:rawValueGuard', RawValueGuardPayload>} RawValueGuard + * @typedef {CopyTagged<'guard:rawGuard', RawGuardPayload>} RawGuard */ -/** @typedef {RawValueGuard | Pattern} SyncValueGuard */ +/** @typedef {RawGuard | Pattern} SyncValueGuard */ -/** @typedef {AwaitArgGuard | RawValueGuard | Pattern} ArgGuard */ +/** @typedef {AwaitArgGuard | RawGuard | Pattern} ArgGuard */ From 12624f5912b12d2665180bfc8fe603aa34541294 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Thu, 12 Oct 2023 11:50:28 -0600 Subject: [PATCH 06/10] test(exo): test explicit `M.raw()` --- packages/exo/test/test-heap-classes.js | 106 +++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 6 deletions(-) diff --git a/packages/exo/test/test-heap-classes.js b/packages/exo/test/test-heap-classes.js index c81f650357..4475296598 100644 --- a/packages/exo/test/test-heap-classes.js +++ b/packages/exo/test/test-heap-classes.js @@ -1,3 +1,4 @@ +// @ts-check // eslint-disable-next-line import/order import { test } from './prepare-test-env-ava.js'; @@ -77,6 +78,7 @@ test('test defineExoClass', t => { }); const foo = makeFoo(); t.deepEqual(foo[GET_INTERFACE_GUARD](), FooI); + // @ts-expect-error intentional for test t.throws(() => foo[symbolic]('invalid arg'), { message: 'In "[Symbol(symbolic)]" method of (Foo): arg 0: string "invalid arg" - Must be a boolean', @@ -203,21 +205,113 @@ test('sloppy option', t => { ); }); +const makeBehavior = () => ({ + behavior() { + return 'something'; + }, +}); + +const PassableGreeterI = M.interface( + 'greeter', + {}, + { defaultGuards: 'passable' }, +); +test('passable guards', t => { + const greeter = makeExo('greeter', PassableGreeterI, { + sayHello(immutabe) { + t.is(Object.isFrozen(immutabe), true); + return 'hello'; + }, + }); + + const mutable = {}; + t.is(greeter.sayHello(mutable), 'hello', `passableGreeter can sayHello`); + t.is(Object.isFrozen(mutable), true, `mutable is frozen`); + t.throws(() => greeter.sayHello(makeBehavior()), { + message: + /In "sayHello" method of \(greeter\): Remotables must be explicitly declared/, + }); +}); + const RawGreeterI = M.interface('greeter', {}, { defaultGuards: 'raw' }); -test('raw defaultGuards', t => { +const testGreeter = (t, greeter, msg) => { + const mutable = {}; + t.is(greeter.sayHello(mutable), 'hello', `${msg} can sayHello`); + t.deepEqual(mutable, { x: 3 }, `${msg} mutable is mutated`); + mutable.y = 4; + t.deepEqual(mutable, { x: 3, y: 4 }, `${msg} mutable is mutated again}`); +}; + +test('raw guards', t => { const greeter = makeExo('greeter', RawGreeterI, { sayHello(mutable) { mutable.x = 3; return 'hello'; }, }); - const mutable = {}; - t.is(greeter.sayHello(mutable), 'hello'); - t.deepEqual(mutable, { x: 3 }); - mutable.y = 4; - t.deepEqual(mutable, { x: 3, y: 4 }); t.deepEqual(greeter[GET_INTERFACE_GUARD](), RawGreeterI); + testGreeter(t, greeter, 'raw defaultGuards'); + + const Greeter2I = M.interface('greeter2', { + sayHello: M.call(M.raw()).returns(M.string()), + rawIn: M.call(M.raw()).returns(M.any()), + rawOut: M.call(M.any()).returns(M.raw()), + passthrough: M.call(M.raw()).returns(M.raw()), + tortuous: M.call(M.any(), M.raw(), M.any()) + .optional(M.any(), M.raw()) + .returns(M.any()), + }); + const greeter2 = makeExo('greeter2', Greeter2I, { + sayHello(mutable) { + mutable.x = 3; + return 'hello'; + }, + rawIn(obj) { + t.is(Object.isFrozen(obj), false); + return obj; + }, + rawOut(obj) { + t.is(Object.isFrozen(obj), true); + return { ...obj }; + }, + passthrough(obj) { + t.is(Object.isFrozen(obj), false); + return obj; + }, + tortuous(hardA, softB, hardC, optHardD, optSoftE = {}) { + // Test that `M.raw()` does not freeze the arguments, unlike `M.any()`. + t.is(Object.isFrozen(hardA), true); + t.is(Object.isFrozen(softB), false); + softB.b = 2; + t.is(Object.isFrozen(hardC), true); + t.is(Object.isFrozen(optHardD), true); + t.is(Object.isFrozen(optSoftE), false); + return {}; + }, + }); + t.deepEqual(greeter2[GET_INTERFACE_GUARD](), Greeter2I); + testGreeter(t, greeter, 'explicit raw'); + + t.is(Object.isFrozen(greeter2.rawIn({})), true); + t.is(Object.isFrozen(greeter2.rawOut({})), false); + t.is(Object.isFrozen(greeter2.passthrough({})), false); + + t.is(Object.isFrozen(greeter2.tortuous({}, {}, {}, {}, {})), true); + t.is(Object.isFrozen(greeter2.tortuous({}, {}, {})), true); + + t.throws( + () => greeter2.tortuous(makeBehavior(), {}, {}), + { + message: + /In "tortuous" method of \(greeter2\): Remotables must be explicitly declared/, + }, + 'passable behavior not allowed', + ); + t.notThrows( + () => greeter2.tortuous({}, makeBehavior(), {}), + 'raw behavior allowed', + ); }); const GreeterI = M.interface('greeter', { From 22bd9c33eec3764d8dfd002325a86245dc6f6709 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Thu, 26 Oct 2023 19:43:20 -0600 Subject: [PATCH 07/10] docs(patterns): describe `M.raw()` vs `call` and `callWhen` --- packages/patterns/src/types.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/patterns/src/types.js b/packages/patterns/src/types.js index 93a4d30625..413de43412 100644 --- a/packages/patterns/src/types.js +++ b/packages/patterns/src/types.js @@ -490,10 +490,12 @@ export {}; * Guard the interface of an exo object * * @property {(...argPatterns: SyncValueGuard[]) => MethodGuardMaker0} call - * Guard a synchronous call + * Guard a synchronous call. Arguments not guarded by `M.raw()` are + * automatically hardened and must be at least Passable. * * @property {(...argGuards: ArgGuard[]) => MethodGuardMaker0} callWhen - * Guard an async call + * Guard an async call. Arguments not guarded by `M.raw()` are automatically + * hardened and must be at least Passable. * * @property {(argPattern: Pattern) => AwaitArgGuard} await * In parameter position, guard a parameter by awaiting it. Can only be used in @@ -512,9 +514,9 @@ export {}; * only a top-level single parameter pattern. * * @property {() => RawGuard} raw - * In parameter position, pass this argument through without any checking. - * In rest position, pass the rest of the arguments through without any checking. - * In return position, return the result without any checking. + * In parameter position, pass this argument through without any hardening or checking. + * In rest position, pass the rest of the arguments through without any hardening or checking. + * In return position, return the result without any hardening or checking. */ /** @@ -555,8 +557,14 @@ export {}; * } * ``` * @property {(...optArgGuards: ArgGuard[]) => MethodGuardMaker1} optional - * @property {(rArgGuard: SyncValueGuard) => MethodGuardMaker2} rest + * Optional arguments not guarded with `M.raw()` are automatically hardened and + * must be Passable. + * @property {(restArgGuard: SyncValueGuard) => MethodGuardMaker2} rest + * If the rest argument guard is not `M.raw()`, all rest arguments are + * automatically hardened and must be Passable. * @property {(returnGuard?: SyncValueGuard) => MethodGuard} returns + * If the return guard is not `M.raw()`, the return value is automatically + * hardened and must be Passable. */ /** @@ -572,8 +580,12 @@ export {}; * foo: M.call(AShape, BShape).optional(CShape).rest(EShape).returns(FShape), * } * ``` - * @property {(rArgGuard: SyncValueGuard) => MethodGuardMaker2} rest + * @property {(restArgGuard: SyncValueGuard) => MethodGuardMaker2} rest + * If the rest argument guard is not `M.raw()`, all rest arguments are + * automatically hardened and must be Passable. * @property {(returnGuard?: SyncValueGuard) => MethodGuard} returns + * If the return guard is not `M.raw()`, the return value is automatically + * hardened and must be Passable. */ /** @@ -590,6 +602,8 @@ export {}; * } * ``` * @property {(returnGuard?: SyncValueGuard) => MethodGuard} returns + * If the return guard is not `M.raw()`, the return value is automatically + * hardened and must be Passable. */ /** From df169782bfb51132254a2450808fb7ebdc1d573e Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Thu, 26 Oct 2023 20:29:06 -0600 Subject: [PATCH 08/10] refactor(exo): break GET_INTERFACE_GUARD loop --- packages/exo/index.js | 2 +- packages/exo/src/exo-makers.js | 34 ++++++++------------------ packages/exo/src/exo-tools.js | 18 +++----------- packages/exo/src/get-interface.js | 22 +++++++++++++++++ packages/exo/test/test-heap-classes.js | 4 +-- 5 files changed, 39 insertions(+), 41 deletions(-) create mode 100644 packages/exo/src/get-interface.js diff --git a/packages/exo/index.js b/packages/exo/index.js index 571fdfedff..0936e295c7 100644 --- a/packages/exo/index.js +++ b/packages/exo/index.js @@ -5,4 +5,4 @@ export { makeExo, } from './src/exo-makers.js'; -export { GET_INTERFACE_GUARD } from './src/exo-tools.js'; +export { GET_INTERFACE_GUARD } from './src/get-interface.js'; diff --git a/packages/exo/src/exo-makers.js b/packages/exo/src/exo-makers.js index 3d7d9b08f3..a8e7325027 100644 --- a/packages/exo/src/exo-makers.js +++ b/packages/exo/src/exo-makers.js @@ -3,14 +3,7 @@ import { makeEnvironmentCaptor } from '@endo/env-options'; import { objectMap } from '@endo/patterns'; -import { - GET_INTERFACE_GUARD, - defendPrototype, - defendPrototypeKit, -} from './exo-tools.js'; - -// Used in typing. -GET_INTERFACE_GUARD; +import { defendPrototype, defendPrototypeKit } from './exo-tools.js'; const { create, seal, freeze, defineProperty, values } = Object; @@ -102,19 +95,12 @@ export const initEmpty = () => emptyRecord; /** * @template {Methods} M - * @typedef {{ - * [GET_INTERFACE_GUARD]: () => InterfaceGuard<{ [K in keyof M]: MethodGuard }> - * }} GetInterfaceGuard - */ - -/** - * @template {Methods} M - * @typedef {Farable>} Facet + * @typedef {Farable>} Guarded */ /** * @template {Record} F - * @typedef {{ [K in keyof F]: Facet }} FacetKit + * @typedef {{ [K in keyof F]: Guarded }} GuardedKit */ /** @@ -125,9 +111,9 @@ export const initEmpty = () => emptyRecord; * [K in keyof M]: import("@endo/patterns").MethodGuard * }> | undefined} interfaceGuard * @param {I} init - * @param {M & ThisType<{ self: Facet, state: ReturnType }>} methods + * @param {M & ThisType<{ self: Guarded, state: ReturnType }>} methods * @param {FarClassOptions, M>>} [options] - * @returns {(...args: Parameters) => Facet} + * @returns {(...args: Parameters) => Guarded} */ export const defineExoClass = ( tag, @@ -185,9 +171,9 @@ harden(defineExoClass); * InterfaceGuard<{[M in keyof F[K]]: MethodGuard; }> * } | undefined} interfaceGuardKit * @param {I} init - * @param {F & { [K in keyof F]: ThisType<{ facets: FacetKit, state: ReturnType }> }} methodsKit - * @param {FarClassOptions, FacetKit>>} [options] - * @returns {(...args: Parameters) => FacetKit} + * @param {F & { [K in keyof F]: ThisType<{ facets: GuardedKit, state: ReturnType }> }} methodsKit + * @param {FarClassOptions, GuardedKit>>} [options] + * @returns {(...args: Parameters) => GuardedKit} */ export const defineExoClassKit = ( tag, @@ -232,7 +218,7 @@ export const defineExoClassKit = ( if (finish) { finish(context); } - return /** @type {FacetKit} */ (context.facets); + return /** @type {GuardedKit} */ (context.facets); }; if (receiveRevoker) { @@ -254,7 +240,7 @@ harden(defineExoClassKit); * }> | undefined} interfaceGuard CAVEAT: static typing does not yet support `callWhen` transformation * @param {T} methods * @param {FarClassOptions>} [options] - * @returns {Facet} + * @returns {Guarded} */ export const makeExo = (tag, interfaceGuard, methods, options = undefined) => { const makeInstance = defineExoClass( diff --git a/packages/exo/src/exo-tools.js b/packages/exo/src/exo-tools.js index 7aa007db99..5dc8ca5552 100644 --- a/packages/exo/src/exo-tools.js +++ b/packages/exo/src/exo-tools.js @@ -13,6 +13,8 @@ import { getCopyMapEntries, } from '@endo/patterns'; +import { GET_INTERFACE_GUARD } from './get-interface.js'; + /** @typedef {import('@endo/patterns').Method} Method */ /** @typedef {import('@endo/patterns').MethodGuard} MethodGuard */ /** @typedef {import('@endo/patterns').MethodGuardPayload} MethodGuardPayload */ @@ -351,24 +353,12 @@ const bindMethod = ( return method; }; -/** - * The name of the automatically added default meta-method for - * obtaining an exo's interface, if it has one. - * - * TODO Name to be bikeshed. Perhaps even whether it is a - * string or symbol to be bikeshed. - * - * TODO Beware that an exo's interface can change across an upgrade, - * so remotes that cache it can become stale. - */ -export const GET_INTERFACE_GUARD = Symbol.for('getInterfaceGuard'); - /** * * @template {Record} T * @param {T} behaviorMethods * @param {InterfaceGuard<{ [M in keyof T]: MethodGuard }>} interfaceGuard - * @returns {T & { [GET_INTERFACE_GUARD]: () => InterfaceGuard<{ [M in keyof T]: MethodGuard }> }} + * @returns {T & import('./get-interface.js').GetInterfaceGuard} */ const withGetInterfaceGuardMethod = (behaviorMethods, interfaceGuard) => harden({ @@ -451,7 +441,7 @@ export const defendPrototype = ( return Far( tag, - /** @type {T & { [GET_INTERFACE_GUARD]: () => InterfaceGuard<{ [M in keyof T]: MethodGuard }>}} */ ( + /** @type {T & import('./get-interface.js').GetInterfaceGuard} */ ( prototype ), ); diff --git a/packages/exo/src/get-interface.js b/packages/exo/src/get-interface.js new file mode 100644 index 0000000000..9fd4baf9d0 --- /dev/null +++ b/packages/exo/src/get-interface.js @@ -0,0 +1,22 @@ +// @ts-check + +/** + * The name of the automatically added default meta-method for + * obtaining an exo's interface, if it has one. + * + * TODO Name to be bikeshed. Perhaps even whether it is a + * string or symbol to be bikeshed. + * + * TODO Beware that an exo's interface can change across an upgrade, + * so remotes that cache it can become stale. + */ +export const GET_INTERFACE_GUARD = Symbol.for('getInterfaceGuard'); + +/** + * @template {Record} M + * @typedef {{ + * [GET_INTERFACE_GUARD]: () => import('@endo/patterns').InterfaceGuard<{ + * [K in keyof M]: import('@endo/patterns').MethodGuard + * }> + * }} GetInterfaceGuard + */ diff --git a/packages/exo/test/test-heap-classes.js b/packages/exo/test/test-heap-classes.js index 4475296598..a5ef1346a5 100644 --- a/packages/exo/test/test-heap-classes.js +++ b/packages/exo/test/test-heap-classes.js @@ -5,11 +5,11 @@ import { test } from './prepare-test-env-ava.js'; // eslint-disable-next-line import/order import { getInterfaceMethodKeys, M } from '@endo/patterns'; import { + GET_INTERFACE_GUARD, defineExoClass, defineExoClassKit, makeExo, -} from '../src/exo-makers.js'; -import { GET_INTERFACE_GUARD } from '../src/exo-tools.js'; +} from '../index.js'; const NoExtraI = M.interface('NoExtra', { foo: M.call().returns(), From 77d04b2902ddf539f10688dfb84fe2aa9e841f16 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Sat, 28 Oct 2023 20:42:54 -0600 Subject: [PATCH 09/10] fix(patterns): remove `defaultGuards: 'never'` for `undefined` --- packages/exo/src/exo-tools.js | 8 ++++---- packages/patterns/src/patterns/patternMatchers.js | 4 ++-- packages/patterns/src/types.js | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/exo/src/exo-tools.js b/packages/exo/src/exo-tools.js index 5dc8ca5552..206ea77fa9 100644 --- a/packages/exo/src/exo-tools.js +++ b/packages/exo/src/exo-tools.js @@ -291,7 +291,7 @@ const bindMethod = ( behaviorMethod, thisfulMethods = false, methodGuard = undefined, - defaultGuards = 'never', + defaultGuards = undefined, ) => { assert.typeof(behaviorMethod, 'function'); @@ -329,7 +329,7 @@ const bindMethod = ( }; if (!methodGuard && thisfulMethods) { switch (defaultGuards) { - case 'never': + case undefined: case 'passable': methodGuard = PassableMethodGuard; break; @@ -394,7 +394,7 @@ export const defendPrototype = ( /** @type {Record | undefined} */ let methodGuards; /** @type {import('@endo/patterns').DefaultGuardType} */ - let defaultGuards = 'never'; + let defaultGuards; if (interfaceGuard) { const { interfaceName, @@ -416,7 +416,7 @@ export const defendPrototype = ( const unimplemented = listDifference(methodGuardNames, methodNames); unimplemented.length === 0 || Fail`methods ${q(unimplemented)} not implemented by ${q(tag)}`; - if (defaultGuards === 'never') { + if (defaultGuards === undefined) { const unguarded = listDifference(methodNames, methodGuardNames); unguarded.length === 0 || Fail`methods ${q(unguarded)} not guarded by ${q(interfaceName)}`; diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index 3af408e1ce..2175190b3d 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -1911,9 +1911,9 @@ const InterfaceGuardPayloadShape = M.splitRecord( { interfaceName: M.string(), methodGuards: M.recordOf(M.string(), MethodGuardShape), - defaultGuards: M.or('never', 'passable', 'raw'), }, { + defaultGuards: M.or(M.undefined(), 'passable', 'raw'), sloppy: M.boolean(), symbolMethodGuards: M.mapOf(M.symbol(), MethodGuardShape), }, @@ -1970,7 +1970,7 @@ harden(getInterfaceMethodKeys); * @returns {InterfaceGuard} */ const makeInterfaceGuard = (interfaceName, methodGuards, options = {}) => { - const { sloppy = false, defaultGuards = sloppy ? 'passable' : 'never' } = + const { sloppy = false, defaultGuards = sloppy ? 'passable' : undefined } = options; // For backwards compatibility, string-keyed method guards are represented in // a CopyRecord. But symbol-keyed methods cannot be, so we put those in a diff --git a/packages/patterns/src/types.js b/packages/patterns/src/types.js index 413de43412..42fcc04a66 100644 --- a/packages/patterns/src/types.js +++ b/packages/patterns/src/types.js @@ -457,14 +457,14 @@ export {}; */ /** - * @typedef {'never' | 'passable' | 'raw'} DefaultGuardType + * @typedef {undefined | 'passable' | 'raw'} DefaultGuardType */ /** * @typedef {>( * interfaceName: string, * methodGuards: M, - * options: {defaultGuards?: 'never', sloppy?: false }) => InterfaceGuard + * options: {defaultGuards?: undefined, sloppy?: false }) => InterfaceGuard * } MakeInterfaceGuardStrict */ /** From a0b3a5c531b22e61419917721c9c25ef107852ea Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Sat, 28 Oct 2023 20:44:11 -0600 Subject: [PATCH 10/10] docs(patterns): dedupe the `MethodGuardMaker` types --- .../patterns/src/patterns/internal-types.js | 2 +- .../patterns/src/patterns/patternMatchers.js | 2 +- packages/patterns/src/types.js | 77 +++++++------------ 3 files changed, 28 insertions(+), 53 deletions(-) diff --git a/packages/patterns/src/patterns/internal-types.js b/packages/patterns/src/patterns/internal-types.js index 0aa8e30b55..ceb3ec4085 100644 --- a/packages/patterns/src/patterns/internal-types.js +++ b/packages/patterns/src/patterns/internal-types.js @@ -34,7 +34,7 @@ * @template {Record} [T = Record] * @typedef {import('../types.js').InterfaceGuard} InterfaceGuard */ -/** @typedef {import('../types.js').MethodGuardMaker0} MethodGuardMaker0 */ +/** @typedef {import('../types.js').MethodGuardMaker} MethodGuardMaker */ /** @typedef {import('../types').MatcherNamespace} MatcherNamespace */ /** @typedef {import('../types').Key} Key */ diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index 2175190b3d..49e266a49b 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -1868,7 +1868,7 @@ harden(getMethodGuardPayload); * @param {ArgGuard[]} argGuards * @param {ArgGuard[]} [optionalArgGuards] * @param {SyncValueGuard} [restArgGuard] - * @returns {MethodGuardMaker0} + * @returns {MethodGuardMaker} */ const makeMethodGuardMaker = ( callKind, diff --git a/packages/patterns/src/types.js b/packages/patterns/src/types.js index 42fcc04a66..5b7ff5577e 100644 --- a/packages/patterns/src/types.js +++ b/packages/patterns/src/types.js @@ -489,26 +489,26 @@ export {}; * @property {MakeInterfaceGuard} interface * Guard the interface of an exo object * - * @property {(...argPatterns: SyncValueGuard[]) => MethodGuardMaker0} call + * @property {(...argPatterns: SyncValueGuard[]) => MethodGuardMaker} call * Guard a synchronous call. Arguments not guarded by `M.raw()` are * automatically hardened and must be at least Passable. * - * @property {(...argGuards: ArgGuard[]) => MethodGuardMaker0} callWhen + * @property {(...argGuards: ArgGuard[]) => MethodGuardMaker} callWhen * Guard an async call. Arguments not guarded by `M.raw()` are automatically * hardened and must be at least Passable. * * @property {(argPattern: Pattern) => AwaitArgGuard} await - * In parameter position, guard a parameter by awaiting it. Can only be used in - * parameter position of an `M.callWhen`. - * `M.await(M.nat())`, for example, with `await` the corresponding argument, - * check that the fulfillment of the `await` satisfies the `M.nat()` - * pattern, and only then proceed to call the raw method with that fulfillment. - * If the argument already passes the `M.nat()` pattern, then the result of - * `await`ing it will still pass, and the `M.callWhen` will still delay the - * raw method call to a future turn. + * Guard a positional parameter in `M.callWhen`, awaiting it and matching its + * fulfillment against the provided pattern. + * For example, `M.callWhen(M.await(M.nat())).returns()` will await the first + * argument, check that its fulfillment satisfies `M.nat()`, and only then call + * the guarded method with that fulfillment. If the argument is a non-promise + * value that already satisfies `M.nat()`, then the result of `await`ing it will + * still pass, and `M.callWhen` will still delay the guarded method call to a + * future turn. * If the argument is a promise that rejects rather than fulfills, or if its * fulfillment does not satisfy the nested pattern, then the call is rejected - * without ever calling the raw method. + * without ever invoking the guarded method. * * Any `AwaitArgGuard` may not appear as a rest pattern or a result pattern, * only a top-level single parameter pattern. @@ -544,7 +544,7 @@ export {}; */ /** - * @typedef {object} MethodGuardMaker0 + * @typedef {MethodGuardOptional & MethodGuardRestReturns} MethodGuardMaker * A method name and parameter/return signature like: * ```js * foo(a, b, c = d, ...e) => f @@ -556,54 +556,29 @@ export {}; * foo: M.call(AShape, BShape).optional(CShape).rest(EShape).returns(FShape), * } * ``` - * @property {(...optArgGuards: ArgGuard[]) => MethodGuardMaker1} optional - * Optional arguments not guarded with `M.raw()` are automatically hardened and - * must be Passable. - * @property {(restArgGuard: SyncValueGuard) => MethodGuardMaker2} rest - * If the rest argument guard is not `M.raw()`, all rest arguments are - * automatically hardened and must be Passable. +/** + * @typedef {object} MethodGuardReturns * @property {(returnGuard?: SyncValueGuard) => MethodGuard} returns + * Arguments have been specified, now finish by creating a `MethodGuard`. * If the return guard is not `M.raw()`, the return value is automatically * hardened and must be Passable. */ - /** - * @typedef {object} MethodGuardMaker1 - * A method name and parameter/return signature like: - * ```js - * foo(a, b, c = d, ...e) => f - * ``` - * should be guarded by something like: - * ```js - * { - * ...otherMethodGuards, - * foo: M.call(AShape, BShape).optional(CShape).rest(EShape).returns(FShape), - * } - * ``` - * @property {(restArgGuard: SyncValueGuard) => MethodGuardMaker2} rest + * @typedef {object} MethodGuardRest + * @property {(restArgGuard: SyncValueGuard) => MethodGuardReturns} rest * If the rest argument guard is not `M.raw()`, all rest arguments are * automatically hardened and must be Passable. - * @property {(returnGuard?: SyncValueGuard) => MethodGuard} returns - * If the return guard is not `M.raw()`, the return value is automatically - * hardened and must be Passable. */ - /** - * @typedef {object} MethodGuardMaker2 - * A method name and parameter/return signature like: - * ```js - * foo(a, b, c = d, ...e) => f - * ``` - * should be guarded by something like: - * ```js - * { - * ...otherMethodGuards, - * foo: M.call(AShape, BShape).optional(CShape).rest(EShape).returns(FShape), - * } - * ``` - * @property {(returnGuard?: SyncValueGuard) => MethodGuard} returns - * If the return guard is not `M.raw()`, the return value is automatically - * hardened and must be Passable. + * @typedef {MethodGuardRest & MethodGuardReturns} MethodGuardRestReturns + * Mandatory and optional arguments have been specified, now specify `rest`, or + * finish with `returns`. + */ +/** + * @typedef {object} MethodGuardOptional + * @property {(...optArgGuards: ArgGuard[]) => MethodGuardRestReturns} optional + * Optional arguments not guarded with `M.raw()` are automatically hardened and + * must be Passable. */ /**