Skip to content

Commit

Permalink
fix(patterns)!: tag and retype guards
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Aug 9, 2023
1 parent a0170df commit b05871a
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 65 deletions.
2 changes: 1 addition & 1 deletion packages/exo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
...
```
41 changes: 24 additions & 17 deletions packages/exo/src/exo-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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];
Expand All @@ -71,17 +75,17 @@ const desync = methodGuard => {
}
return {
awaitIndexes,
rawMethodGuard: {
rawMethodGuardPayload: {
argGuards: rawArgGuards.slice(0, argGuards.length),
optionalArgGuards: rawArgGuards.slice(argGuards.length),
restArgGuard,
},
};
};

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) {
Expand All @@ -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 => {
Expand All @@ -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);
}
};

Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/exo/test/test-heap-classes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
56 changes: 41 additions & 15 deletions packages/patterns/src/patterns/patternMatchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<PassStyle, 'tagged'> |
* 'copySet' | 'copyBag' | 'copyMap' | keyof HelpersByMatchTag
Expand Down Expand Up @@ -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': {
Expand Down Expand Up @@ -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);
Expand All @@ -1722,8 +1741,7 @@ harden(assertAwaitArgGuard);
*/
const makeAwaitArgGuard = argGuard => {
/** @type {AwaitArgGuard} */
const result = harden({
klass: 'awaitArg',
const result = makeTagged('guard:awaitArgGuard', {
argGuard,
});
assertAwaitArgGuard(result);
Expand All @@ -1735,25 +1753,28 @@ 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),
restArgGuard: M.opt(M.pattern()),
returnGuard: M.pattern(),
});

const AsyncMethodGuardShape = harden({
klass: 'methodGuard',
const AsyncMethodGuardPayloadShape = harden({
callKind: 'async',
argGuards: ArgGuardListShape,
optionalArgGuards: M.opt(ArgGuardListShape),
restArgGuard: M.opt(M.pattern()),
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');
Expand Down Expand Up @@ -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,
Expand All @@ -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');
};
Expand All @@ -1826,12 +1847,17 @@ 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,
});
assertInterfaceGuard(result);
return result;
};

const GuardPayloadShapes = harden({
'guard:awaitArgGuard': AwaitArgGuardPayloadShape,
'guard:methodGuard': MethodGuardPayloadShape,
'guard:interfaceGuard': InterfaceGuardPayloadShape,
});
52 changes: 21 additions & 31 deletions packages/patterns/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -511,11 +511,13 @@ export {};

/**
* @template {Record<string | symbol, MethodGuard>} [T=Record<string | symbol, MethodGuard>]
* @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
Expand Down Expand Up @@ -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 */

0 comments on commit b05871a

Please sign in to comment.