Skip to content

Commit

Permalink
Merge pull request #1712 from endojs/markm-tag-guards-2
Browse files Browse the repository at this point in the history
fix(patterns)!: tag and retype guards
  • Loading branch information
erights authored Sep 18, 2023
2 parents 1bfb9c4 + 25ded7a commit 25c44ba
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 98 deletions.
5 changes: 4 additions & 1 deletion packages/marshal/src/encodePassable.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import {
/** @typedef {import('@endo/pass-style').PassStyle} PassStyle */
/** @typedef {import('@endo/pass-style').Passable} Passable */
/** @typedef {import('@endo/pass-style').RemotableObject} Remotable */
/** @template T @typedef {import('@endo/pass-style').CopyRecord<T>} CopyRecord */
/**
* @template {Passable} [T=Passable]
* @typedef {import('@endo/pass-style').CopyRecord<T>} CopyRecord
*/
/** @typedef {import('./types.js').RankCover} RankCover */

const { quote: q, Fail } = assert;
Expand Down
10 changes: 8 additions & 2 deletions packages/pass-style/src/typeGuards.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { passStyleOf } from './passStyleOf.js';

/** @typedef {import('./types.js').Passable} Passable */
/** @template T @typedef {import('./types.js').CopyArray<T>} CopyArray */
/** @template T @typedef {import('./types.js').CopyRecord<T>} CopyRecord */
/**
* @template {Passable} [T=Passable]
* @typedef {import('./types.js').CopyArray<T>} CopyArray
*/
/**
* @template {Passable} [T=Passable]
* @typedef {import('./types.js').CopyRecord<T>} CopyRecord
*/
/** @typedef {import('./types.js').RemotableObject} Remotable */

const { Fail, quote: q } = assert;
Expand Down
53 changes: 32 additions & 21 deletions packages/pass-style/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,26 @@ export {};
/**
* @typedef {*} Passable
*
* A Passable is acyclic data that can be marshalled. It must be hardened to remain
* A Passable is acyclic data that can be marshalled. It must be hardened to
* remain
* stable (even if some components are proxies; see PureData restriction below),
* and is classified by PassStyle:
* * Atomic primitive values have a PrimitiveStyle (PassStyle
* 'undefined' | 'null' | 'boolean' | 'number' | 'bigint' | 'string' | 'symbol').
* 'undefined' | 'null' | 'boolean' | 'number' | 'bigint'
* | 'string' | 'symbol').
* * Containers aggregate other Passables into
* * sequences as CopyArrays (PassStyle 'copyArray'), or
* * string-keyed dictionaries as CopyRecords (PassStyle 'copyRecord'), or
* * higher-order types as CopyTaggeds (PassStyle 'tagged').
* * PassableCaps (PassStyle 'remotable' | 'promise') expose local values to remote
* interaction.
* * As a special case to support system observability, error objects are Passable
* (PassStyle 'error').
* * PassableCaps (PassStyle 'remotable' | 'promise') expose local values to
* remote interaction.
* * As a special case to support system observability, error objects are
* Passable (PassStyle 'error').
*
* A Passable is essentially a pass-by-copy superstructure with a pass-by-reference
* exit point at the site of each PassableCap (which marshalling represents using
* 'slots').
* A Passable is essentially a pass-by-copy superstructure with a
* pass-by-reference
* exit point at the site of each PassableCap (which marshalling represents
* using 'slots').
*/

/**
Expand All @@ -50,14 +53,16 @@ export {};
*
* A Passable is PureData when its entire data structure is free of PassableCaps
* (remotables and promises) and error objects.
* PureData is an arbitrary composition of primitive values into CopyArray and/or
* PureData is an arbitrary composition of primitive values into CopyArray
* and/or
* CopyRecord and/or CopyTagged containers (or a single primitive value with no
* container), and is fully pass-by-copy.
*
* This restriction assures absence of side effects and interleaving risks *given*
* that none of the containers can be a Proxy instance.
* TODO SECURITY BUG we plan to enforce this, giving PureData the same security
* properties as the proposed [Records and Tuples](https://github.com/tc39/proposal-record-tuple).
* properties as the proposed
* [Records and Tuples](https://github.com/tc39/proposal-record-tuple).
*
* Given this (currently counter-factual) assumption, a PureData value cannot
* be used as a communications channel,
Expand All @@ -82,34 +87,40 @@ export {};
*/

/**
* @template {Passable} T
* @template {Passable} [T=Passable]
* @typedef {T[]} CopyArray
*
* A Passable sequence of Passable values.
*/

/**
* @template {Passable} T
* @template {Passable} [T=Passable]
* @typedef {Record<string, T>} CopyRecord
*
* A Passable dictionary in which each key is a string and each value is Passable.
*/

/**
* @template {string} [Tag=string]
* @template {Passable} [Payload=Passable]
* @typedef {{
* [Symbol.toStringTag]: string,
* payload: Passable,
* [Symbol.toStringTag]: Tag,
* payload: Payload,
* [passStyle: symbol]: 'tagged' | string,
* }} CopyTagged
*
* A Passable "tagged record" with semantics specific to the tag identified in
* the `[Symbol.toStringTag]` property (such as 'copySet', 'copyBag', or 'copyMap').
* It must have a property with key equal to the `PASS_STYLE` export and value 'tagged'
* and no other properties except `[Symbol.toStringTag]` and `payload`,
* but TypeScript complains about a declaration like `[PASS_STYLE]: 'tagged'`
* because importing packages do not know what `PASS_STYLE` is
* the `[Symbol.toStringTag]` property (such as 'copySet', 'copyBag',
* or 'copyMap').
* It must have a property with key equal to the `PASS_STYLE` export and
* value 'tagged'
* and no other properties except `[Symbol.toStringTag]` and `payload`.
*
* TODO
* But TypeScript complains about a declaration like `[PASS_STYLE]: 'tagged'`
* because importing packages do not know what `PASS_STYLE` is,
* so we appease it with a looser but less accurate definition
* using symbol index properties.
* using symbol index properties and `| string`.
*/

/**
Expand Down
22 changes: 16 additions & 6 deletions packages/patterns/src/patterns/internal-types.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
/// <reference types="ses"/>

/** @typedef {import('@endo/marshal').Passable} Passable */
/** @typedef {import('@endo/marshal').PassStyle} PassStyle */
/** @typedef {import('@endo/marshal').CopyTagged} CopyTagged */
/** @template T @typedef {import('@endo/marshal').CopyRecord<T>} CopyRecord */
/** @template T @typedef {import('@endo/marshal').CopyArray<T>} CopyArray */
/** @typedef {import('@endo/marshal').Checker} Checker */
/** @typedef {import('@endo/pass-style').Passable} Passable */
/** @typedef {import('@endo/pass-style').PassStyle} PassStyle */
/**
* @template {string} [Tag=string]
* @template {Passable} [Payload=Passable]
* @typedef {import('@endo/pass-style').CopyTagged<Tag,Payload>} CopyTagged
*/
/**
* @template {Passable} [T=Passable]
* @typedef {import('@endo/pass-style').CopyRecord<T>} CopyRecord
*/
/**
* @template {Passable} [T=Passable]
* @typedef {import('@endo/pass-style').CopyArray<T>} CopyArray
*/
/** @typedef {import('@endo/pass-style').Checker} Checker */
/** @typedef {import('@endo/marshal').RankCompare} RankCompare */
/** @typedef {import('@endo/marshal').RankCover} RankCover */

Expand Down
69 changes: 45 additions & 24 deletions packages/patterns/src/patterns/patternMatchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,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 @@ -201,6 +213,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 @@ -1710,11 +1728,12 @@ MM = M;

// //////////////////////////// Guards ///////////////////////////////////////

const AwaitArgGuardShape = harden({
klass: 'awaitArg',
const AwaitArgGuardPayloadShape = harden({
argGuard: M.pattern(),
});

const AwaitArgGuardShape = M.kind('guard:awaitArgGuard');

/**
* @param {any} specimen
* @returns {specimen is AwaitArgGuard}
Expand All @@ -1741,9 +1760,7 @@ harden(assertAwaitArgGuard);
*/
export const getAwaitArgGuardPayload = awaitArgGuard => {
assertAwaitArgGuard(awaitArgGuard);
const { klass: _, ...payload } = awaitArgGuard;
/** @type {AwaitArgGuardPayload} */
return payload;
return awaitArgGuard.payload;
};
harden(getAwaitArgGuardPayload);

Expand All @@ -1753,8 +1770,7 @@ harden(getAwaitArgGuardPayload);
*/
const makeAwaitArgGuard = argPattern => {
/** @type {AwaitArgGuard} */
const result = harden({
klass: 'awaitArg',
const result = makeTagged('guard:awaitArgGuard', {
argGuard: argPattern,
});
assertAwaitArgGuard(result);
Expand All @@ -1766,25 +1782,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');

/**
* @param {any} specimen
Expand All @@ -1804,9 +1823,7 @@ harden(assertMethodGuard);
*/
export const getMethodGuardPayload = methodGuard => {
assertMethodGuard(methodGuard);
const { klass: _, ...payload } = methodGuard;
/** @type {MethodGuardPayload} */
return payload;
return methodGuard.payload;
};
harden(getMethodGuardPayload);

Expand Down Expand Up @@ -1842,8 +1859,7 @@ const makeMethodGuardMaker = (
},
returns: (returnGuard = M.undefined()) => {
/** @type {MethodGuard} */
const result = harden({
klass: 'methodGuard',
const result = makeTagged('guard:methodGuard', {
callKind,
argGuards,
optionalArgGuards,
Expand All @@ -1855,9 +1871,8 @@ const makeMethodGuardMaker = (
},
});

const InterfaceGuardShape = M.splitRecord(
const InterfaceGuardPayloadShape = M.splitRecord(
{
klass: 'Interface',
interfaceName: M.string(),
methodGuards: M.recordOf(M.string(), MethodGuardShape),
sloppy: M.boolean(),
Expand All @@ -1867,6 +1882,8 @@ const InterfaceGuardShape = M.splitRecord(
},
);

const InterfaceGuardShape = M.kind('guard:interfaceGuard');

/**
* @param {any} specimen
* @returns {asserts specimen is InterfaceGuard}
Expand All @@ -1886,9 +1903,7 @@ harden(assertInterfaceGuard);
*/
export const getInterfaceGuardPayload = interfaceGuard => {
assertInterfaceGuard(interfaceGuard);
const { klass: _, ...payload } = interfaceGuard;
/** @type {InterfaceGuardPayload} */
return payload;
return interfaceGuard.payload;
};
harden(getInterfaceGuardPayload);

Expand Down Expand Up @@ -1934,8 +1949,8 @@ const makeInterfaceGuard = (interfaceName, methodGuards, options = {}) => {
stringMethodGuards[key] = value;
}
}
const result = harden({
klass: 'Interface',
/** @type {InterfaceGuard} */
const result = makeTagged('guard:interfaceGuard', {
interfaceName,
methodGuards: stringMethodGuards,
...(symbolMethodGuardsEntries.length
Expand All @@ -1946,3 +1961,9 @@ const makeInterfaceGuard = (interfaceName, methodGuards, options = {}) => {
assertInterfaceGuard(result);
return /** @type {InterfaceGuard<M>} */ (result);
};

const GuardPayloadShapes = harden({
'guard:awaitArgGuard': AwaitArgGuardPayloadShape,
'guard:methodGuard': MethodGuardPayloadShape,
'guard:interfaceGuard': InterfaceGuardPayloadShape,
});
Loading

0 comments on commit 25c44ba

Please sign in to comment.