Skip to content

Commit

Permalink
feat(types): generic Passable
Browse files Browse the repository at this point in the history
  • Loading branch information
turadg committed Jan 11, 2024
1 parent 935718b commit ae6ad15
Show file tree
Hide file tree
Showing 17 changed files with 306 additions and 200 deletions.
4 changes: 4 additions & 0 deletions packages/marshal/src/deeplyFulfilled.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,22 @@ export const deeplyFulfilled = async val => {
const passStyle = passStyleOf(val);
switch (passStyle) {
case 'copyRecord': {
// @ts-expect-error FIXME narrowed
const names = ownKeys(val);
// @ts-expect-error FIXME narrowed
const valPs = names.map(name => deeplyFulfilled(val[name]));
return E.when(Promise.all(valPs), vals =>
harden(fromEntries(vals.map((c, i) => [names[i], c]))),
);
}
case 'copyArray': {
// @ts-expect-error FIXME narrowed
const valPs = val.map(p => deeplyFulfilled(p));
return E.when(Promise.all(valPs), vals => harden(vals));
}
case 'tagged': {
const tag = getTag(val);
// @ts-expect-error FIXME narrowed
return E.when(deeplyFulfilled(val.payload), payload =>
makeTagged(tag, payload),
);
Expand Down
4 changes: 2 additions & 2 deletions packages/marshal/src/encodePassable.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const { ownKeys } = Reflect;
* string-named own properties. `recordNames` returns those name *reverse*
* sorted, because that's how records are compared, encoded, and sorted.
*
* @template T
* @template {Passable} T
* @param {CopyRecord<T>} record
* @returns {string[]}
*/
Expand All @@ -44,7 +44,7 @@ harden(recordNames);
* Assuming that `record` is a CopyRecord and `names` is `recordNames(record)`,
* return the corresponding array of property values.
*
* @template T
* @template {Passable} T
* @param {CopyRecord<T>} record
* @param {string[]} names
* @returns {T[]}
Expand Down
14 changes: 8 additions & 6 deletions packages/marshal/src/rankOrder.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,18 +382,20 @@ export const coveredEntries = (sorted, [leftIndex, rightIndex]) => {
harden(coveredEntries);

/**
* @template {Passable} T
* @param {RankCompare} compare
* @param {Passable} a
* @param {Passable} b
* @returns {Passable}
* @param {T} a
* @param {T} b
* @returns {T}
*/
const maxRank = (compare, a, b) => (compare(a, b) >= 0 ? a : b);

/**
* @template {Passable} T
* @param {RankCompare} compare
* @param {Passable} a
* @param {Passable} b
* @returns {Passable}
* @param {T} a
* @param {T} b
* @returns {T}
*/
const minRank = (compare, a, b) => (compare(a, b) <= 0 ? a : b);

Expand Down
2 changes: 2 additions & 0 deletions packages/pass-style/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# typescript-eslint errors on this because it has no typecheck information, because tsc produced it for the `types.d.ts` instead
/src/types.js
1 change: 1 addition & 0 deletions packages/pass-style/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"module": "./index.js",
"exports": {
".": "./index.js",
"./src/types.js": "./src/types.js",
"./tools.js": "./tools.js",
"./endow.js": "./endow.js",
"./package.json": "./package.json"
Expand Down
10 changes: 6 additions & 4 deletions packages/pass-style/src/make-far.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { assertChecker, PASS_STYLE } from './passStyle-helpers.js';
import { assertIface, getInterfaceOf, RemotableHelper } from './remotable.js';

/** @typedef {import('./types.js').InterfaceSpec} InterfaceSpec */
/** @template L,R @typedef {import('@endo/eventual-send').RemotableBrand<L, R>} RemotableBrand */
/** @template L,R @typedef {import('@endo/eventual-send/src/types').RemotableBrand<L, R>} RemotableBrand */

const { quote: q, Fail } = assert;

Expand Down Expand Up @@ -62,7 +62,8 @@ const assertCanBeRemotable = candidate =>
* // https://github.com/Agoric/agoric-sdk/issues/804
*
* @template {{}} T
* @param {InterfaceSpec} [iface] The interface specification for
* @template {InterfaceSpec} I
* @param {I} [iface] The interface specification for
* the remotable. For now, a string iface must be "Remotable" or begin with
* "Alleged: " or "DebugName: ", to serve as the alleged name. More
* general ifaces are not yet implemented. This is temporary. We include the
Expand All @@ -75,9 +76,10 @@ const assertCanBeRemotable = candidate =>
* @param {undefined} [props] Currently may only be undefined.
* That plan is that own-properties are copied to the remotable
* @param {T} [remotable] The object used as the remotable
* @returns {T & RemotableBrand<{}, T>} remotable, modified for debuggability
* @returns {T & import('./types.js').RemotableObject<I> & RemotableBrand<{}, T>}} remotable, modified for debuggability
*/
export const Remotable = (
// @ts-expect-error I could have different subtype than string
iface = 'Remotable',
props = undefined,
remotable = /** @type {T} */ ({}),
Expand Down Expand Up @@ -125,7 +127,7 @@ export const Remotable = (
// COMMITTED!
// We're committed, so keep the interface for future reference.
assert(iface !== undefined); // To make TypeScript happy
return /** @type {T & RemotableBrand<{}, T>} */ (remotable);
return /** @type {any} */ (remotable);
};
harden(Remotable);

Expand Down
2 changes: 1 addition & 1 deletion packages/pass-style/src/makeTagged.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { Fail } = assert;

/**
* @template {string} T
* @template {any} P
* @template {import('./types.js').Passable} P
* @param {T} tag
* @param {P} payload
* @returns {import('./types.js').CopyTagged<T,P>}
Expand Down
8 changes: 7 additions & 1 deletion packages/pass-style/src/passStyle-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const hasOwnPropertyOf = (obj, prop) =>
apply(objectHasOwnProperty, obj, [prop]);
harden(hasOwnPropertyOf);

/** @type {(val) => val is {}} */
export const isObject = val => Object(val) === val;
harden(isObject);

Expand Down Expand Up @@ -122,6 +123,11 @@ export const checkNormalProperty = (
};
harden(checkNormalProperty);

/**
* @template {import('./types.js').InterfaceSpec} T
* @param {import('./types.js').TaggedRecord<any, T>} tagRecord
* @returns {T}
*/
export const getTag = tagRecord => tagRecord[Symbol.toStringTag];
harden(getTag);

Expand All @@ -138,7 +144,7 @@ harden(checkPassStyle);

const makeCheckTagRecord = checkProto => {
/**
* @param {{ [PASS_STYLE]: string }} tagRecord
* @param {import('./types.js').TaggedRecord<any, any>} tagRecord
* @param {PassStyle} passStyle
* @param {Checker} [check]
* @returns {boolean}
Expand Down
18 changes: 8 additions & 10 deletions packages/pass-style/src/passStyleOf.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,28 +81,26 @@ const makePassStyleOf = passStyleHelpers => {
* structures, so without this cache, these algorithms could be
* O(N**2) or worse.
*
* @type {WeakMap<Passable, PassStyle>}
* @type {WeakMap<WeakKey, PassStyle>}
*/
const passStyleMemo = new WeakMap();

/**
* @type {PassStyleOf}
*/
// @ts-expect-error cast
const passStyleOf = passable => {
// Even when a WeakSet is correct, when the set has a shorter lifetime
// than its keys, we prefer a Set due to expected implementation
// tradeoffs.
const inProgress = new Set();

/**
* @type {PassStyleOf}
*/
const passStyleOfRecur = inner => {
const innerIsObject = isObject(inner);
if (innerIsObject) {
if (passStyleMemo.has(inner)) {
// @ts-ignore TypeScript doesn't know that `get` after `has` is safe
return passStyleMemo.get(inner);
const innerMemo = passStyleMemo.get(inner);
if (innerMemo) {
return innerMemo;
}
!inProgress.has(inner) ||
Fail`Pass-by-copy data cannot be cyclic ${inner}`;
Expand All @@ -117,9 +115,6 @@ const makePassStyleOf = passStyleHelpers => {
return passStyle;
};

/**
* @type {PassStyleOf}
*/
const passStyleOfInternal = inner => {
const typestr = typeof inner;
switch (typestr) {
Expand Down Expand Up @@ -164,10 +159,12 @@ const makePassStyleOf = passStyleHelpers => {
}
for (const helper of passStyleHelpers) {
if (helper.canBeValid(inner)) {
// @ts-expect-error XXX
helper.assertValid(inner, passStyleOfRecur);
return helper.styleName;
}
}
// @ts-expect-error XXX
remotableHelper.assertValid(inner, passStyleOfRecur);
return 'remotable';
}
Expand All @@ -176,6 +173,7 @@ const makePassStyleOf = passStyleHelpers => {
Fail`Cannot pass non-frozen objects like ${inner}. Use harden()`;
typeof inner.then !== 'function' ||
Fail`Cannot pass non-promise thenables`;
// @ts-expect-error XXX
remotableHelper.assertValid(inner, passStyleOfRecur);
return 'remotable';
}
Expand Down
14 changes: 12 additions & 2 deletions packages/pass-style/src/remotable.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {

/** @typedef {import('./types.js').Checker} Checker */
/** @typedef {import('./types.js').InterfaceSpec} InterfaceSpec */
/** @typedef {import('./types.js').MarshalGetInterfaceOf} MarshalGetInterfaceOf */
/** @typedef {import('./internal-types.js').PassStyleHelper} PassStyleHelper */
/** @typedef {import('./types.js').RemotableObject} Remotable */

Expand Down Expand Up @@ -167,15 +166,26 @@ const checkRemotable = (val, check) => {
return result;
};

/** @type {MarshalGetInterfaceOf} */
/**
* Simple semantics, just tell what interface (or undefined) a remotable has.
* @type {{
* <T extends string>(val: import('./types.js').TaggedRecord<any, T>): T;
* (val: any): string | undefined;
* }}
* @returns the interface specification, or undefined
* if not a deemed to be a Remotable
*/
export const getInterfaceOf = val => {
if (
!isObject(val) ||
val[PASS_STYLE] !== 'remotable' ||
// @ts-expect-error FIXME
!checkRemotable(val)
) {
// @ts-expect-error FIXME
return undefined;
}
// @ts-expect-error FIXME
return getTag(val);
};
harden(getInterfaceOf);
Expand Down
17 changes: 4 additions & 13 deletions packages/pass-style/src/typeGuards.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const { Fail, quote: q } = assert;
* Check whether the argument is a pass-by-copy array, AKA a "copyArray"
* in @endo/marshal terms
*
* @param {Passable} arr
* @param {any} arr
* @returns {arr is CopyArray<any>}
*/
const isCopyArray = arr => passStyleOf(arr) === 'copyArray';
Expand All @@ -27,7 +27,7 @@ harden(isCopyArray);
* Check whether the argument is a pass-by-copy record, AKA a
* "copyRecord" in @endo/marshal terms
*
* @param {Passable} record
* @param {any} record
* @returns {record is CopyRecord<any>}
*/
const isRecord = record => passStyleOf(record) === 'copyRecord';
Expand All @@ -43,13 +43,10 @@ const isRemotable = remotable => passStyleOf(remotable) === 'remotable';
harden(isRemotable);

/**
* @callback AssertArray
* @param {Passable} array
* @param {any} array
* @param {string=} optNameOfArray
* @returns {asserts array is CopyArray<any>}
*/

/** @type {AssertArray} */
const assertCopyArray = (array, optNameOfArray = 'Alleged array') => {
const passStyle = passStyleOf(array);
passStyle === 'copyArray' ||
Expand All @@ -60,13 +57,10 @@ const assertCopyArray = (array, optNameOfArray = 'Alleged array') => {
harden(assertCopyArray);

/**
* @callback AssertRecord
* @param {Passable} record
* @param {any} record
* @param {string=} optNameOfRecord
* @returns {asserts record is CopyRecord<any>}
*/

/** @type {AssertRecord} */
const assertRecord = (record, optNameOfRecord = 'Alleged record') => {
const passStyle = passStyleOf(record);
passStyle === 'copyRecord' ||
Expand All @@ -77,13 +71,10 @@ const assertRecord = (record, optNameOfRecord = 'Alleged record') => {
harden(assertRecord);

/**
* @callback AssertRemotable
* @param {Passable} remotable
* @param {string=} optNameOfRemotable
* @returns {asserts remotable is Remotable}
*/

/** @type {AssertRemotable} */
const assertRemotable = (
remotable,
optNameOfRemotable = 'Alleged remotable',
Expand Down
Loading

0 comments on commit ae6ad15

Please sign in to comment.