diff --git a/packages/marshal/src/deeplyFulfilled.js b/packages/marshal/src/deeplyFulfilled.js index ebedc93a6d..eda563da37 100644 --- a/packages/marshal/src/deeplyFulfilled.js +++ b/packages/marshal/src/deeplyFulfilled.js @@ -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), ); diff --git a/packages/marshal/src/encodePassable.js b/packages/marshal/src/encodePassable.js index 387a88b402..002d3be0f7 100644 --- a/packages/marshal/src/encodePassable.js +++ b/packages/marshal/src/encodePassable.js @@ -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} record * @returns {string[]} */ @@ -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} record * @param {string[]} names * @returns {T[]} diff --git a/packages/marshal/src/rankOrder.js b/packages/marshal/src/rankOrder.js index f8d8d9d95c..cdeea95af4 100644 --- a/packages/marshal/src/rankOrder.js +++ b/packages/marshal/src/rankOrder.js @@ -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); diff --git a/packages/pass-style/.eslintignore b/packages/pass-style/.eslintignore new file mode 100644 index 0000000000..d76bcff161 --- /dev/null +++ b/packages/pass-style/.eslintignore @@ -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 diff --git a/packages/pass-style/package.json b/packages/pass-style/package.json index ecd9cd9602..0ef01e72f3 100644 --- a/packages/pass-style/package.json +++ b/packages/pass-style/package.json @@ -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" diff --git a/packages/pass-style/src/make-far.js b/packages/pass-style/src/make-far.js index 1e92acbbc3..2bbc912794 100644 --- a/packages/pass-style/src/make-far.js +++ b/packages/pass-style/src/make-far.js @@ -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} RemotableBrand */ +/** @template L,R @typedef {import('@endo/eventual-send/src/types').RemotableBrand} RemotableBrand */ const { quote: q, Fail } = assert; @@ -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 @@ -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 & 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} */ ({}), @@ -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); diff --git a/packages/pass-style/src/makeTagged.js b/packages/pass-style/src/makeTagged.js index b4e6c08c2e..90f5ffad5f 100644 --- a/packages/pass-style/src/makeTagged.js +++ b/packages/pass-style/src/makeTagged.js @@ -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} diff --git a/packages/pass-style/src/passStyle-helpers.js b/packages/pass-style/src/passStyle-helpers.js index 840156641c..4afb6e0efb 100644 --- a/packages/pass-style/src/passStyle-helpers.js +++ b/packages/pass-style/src/passStyle-helpers.js @@ -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); @@ -122,6 +123,11 @@ export const checkNormalProperty = ( }; harden(checkNormalProperty); +/** + * @template {import('./types.js').InterfaceSpec} T + * @param {import('./types.js').TaggedRecord} tagRecord + * @returns {T} + */ export const getTag = tagRecord => tagRecord[Symbol.toStringTag]; harden(getTag); @@ -138,7 +144,7 @@ harden(checkPassStyle); const makeCheckTagRecord = checkProto => { /** - * @param {{ [PASS_STYLE]: string }} tagRecord + * @param {import('./types.js').TaggedRecord} tagRecord * @param {PassStyle} passStyle * @param {Checker} [check] * @returns {boolean} diff --git a/packages/pass-style/src/passStyleOf.js b/packages/pass-style/src/passStyleOf.js index 0a49fe71a1..f635d47a21 100644 --- a/packages/pass-style/src/passStyleOf.js +++ b/packages/pass-style/src/passStyleOf.js @@ -81,28 +81,26 @@ const makePassStyleOf = passStyleHelpers => { * structures, so without this cache, these algorithms could be * O(N**2) or worse. * - * @type {WeakMap} + * @type {WeakMap} */ 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}`; @@ -117,9 +115,6 @@ const makePassStyleOf = passStyleHelpers => { return passStyle; }; - /** - * @type {PassStyleOf} - */ const passStyleOfInternal = inner => { const typestr = typeof inner; switch (typestr) { @@ -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'; } @@ -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'; } diff --git a/packages/pass-style/src/remotable.js b/packages/pass-style/src/remotable.js index 476dff5387..8d4add0968 100644 --- a/packages/pass-style/src/remotable.js +++ b/packages/pass-style/src/remotable.js @@ -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 */ @@ -167,15 +166,26 @@ const checkRemotable = (val, check) => { return result; }; -/** @type {MarshalGetInterfaceOf} */ +/** + * Simple semantics, just tell what interface (or undefined) a remotable has. + * @type {{ + * (val: import('./types.js').TaggedRecord): 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); diff --git a/packages/pass-style/src/typeGuards.js b/packages/pass-style/src/typeGuards.js index 2d2f13e267..ffc6e7c398 100644 --- a/packages/pass-style/src/typeGuards.js +++ b/packages/pass-style/src/typeGuards.js @@ -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} */ const isCopyArray = arr => passStyleOf(arr) === 'copyArray'; @@ -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} */ const isRecord = record => passStyleOf(record) === 'copyRecord'; @@ -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} */ - -/** @type {AssertArray} */ const assertCopyArray = (array, optNameOfArray = 'Alleged array') => { const passStyle = passStyleOf(array); passStyle === 'copyArray' || @@ -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} */ - -/** @type {AssertRecord} */ const assertRecord = (record, optNameOfRecord = 'Alleged record') => { const passStyle = passStyleOf(record); passStyle === 'copyRecord' || @@ -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', diff --git a/packages/pass-style/src/types.d.ts b/packages/pass-style/src/types.d.ts new file mode 100644 index 0000000000..040865c05d --- /dev/null +++ b/packages/pass-style/src/types.d.ts @@ -0,0 +1,203 @@ +/* eslint-disable no-use-before-define */ +import { PASS_STYLE } from './passStyle-helpers.js'; + +/** + * Matches any [primitive value](https://developer.mozilla.org/en-US/docs/Glossary/Primitive). + */ +export type Primitive = + | null + | undefined + | string + | number + | boolean + | symbol + | bigint; + +export type PrimitiveStyle = + | 'undefined' + | 'null' + | 'boolean' + | 'number' + | 'bigint' + | 'string' + | 'symbol'; + +export type ContainerStyle = 'copyRecord' | 'copyArray' | 'tagged'; + +export type PassStyle = + | PrimitiveStyle + | ContainerStyle + | 'remotable' + | 'error' + | 'promise'; + +export type TaggedOrRemotable = 'tagged' | 'remotable'; + +export type PassStyled = { + [PASS_STYLE]: S; +}; + +export type ExtractStyle

> = P[typeof PASS_STYLE]; + +export type PassByCopy = + | Primitive + | Error + | CopyArray + | CopyRecord + | CopyTagged; + +export type PassByRef = + | RemotableObject + | Promise + | Promise; + +/** + * 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'). + * * Containers aggregate other Passables into + * * sequences as CopyArrays (PassStyle 'copyArray'), or + * * string-keyed dictionaries as CopyRecords (PassStyle 'copyRecord'), or + * * higher-level 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'). + * + * 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'). + */ +export type Passable< + PC extends PassableCap = PassableCap, + E extends Error = Error, +> = Primitive | Container | PC | E; + +export type Container = + | CopyArrayI + | CopyRecordI + | CopyTaggedI; +interface CopyArrayI + extends CopyArray> {} +interface CopyRecordI + extends CopyRecord> {} +interface CopyTaggedI + extends CopyTagged> {} + +export type PassStyleOf = { + (p: undefined): 'undefined'; + (p: string): 'string'; + (p: boolean): 'boolean'; + (p: number): 'number'; + (p: bigint): 'bigint'; + (p: symbol): 'symbol'; + (p: null): 'null'; + (p: Promise): 'promise'; + (p: Error): 'error'; + (p: CopyTagged): 'tagged'; + (p: any[]): 'copyArray'; + (p: Iterable): 'remotable'; + (p: Iterator): 'remotable'; + >(p: T): ExtractStyle; + (p: { [key: string]: any }): 'copyRecord'; + (p: any): PassStyle; +}; +/** + * 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 + * 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). + * + * Given this (currently counter-factual) assumption, a PureData value cannot + * be used as a communications channel, + * and can therefore be safely shared with subgraphs that should not be able + * to communicate with each other. + * Without that assumption, such a guarantee requires a marshal-unmarshal round + * trip (as exists between vats) to produce data structures disconnected from + * any potential proxies. + */ +export type PureData = Passable; +export type TaggedRecord< + S extends TaggedOrRemotable, + I extends InterfaceSpec, +> = PassStyled & { + [Symbol.toStringTag]: I; +}; +/** + * An object marked as remotely accessible using the `Far` or `Remotable` + * functions, or a local presence representing such a remote object. + * + * A more natural name would be Remotable, but that could be confused with the + * value of the `Remotable` export of this module (a function). + */ +export type RemotableObject = TaggedRecord< + 'remotable', + I +>; +/** + * The authority-bearing leaves of a Passable's pass-by-copy superstructure. + */ +export type PassableCap = Promise | RemotableObject; +/** + * A Passable sequence of Passable values. + */ +export type CopyArray = Array; + +/** + * A Passable dictionary in which each key is a string and each value is Passable. + */ +export type CopyRecord = Record; +/** + * 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`. + */ +export type CopyTagged< + Tag extends string = string, + Payload extends Passable = any, +> = TaggedRecord<'tagged', Tag> & { + payload: Payload; +}; +/** + * This is an interface specification. + * For now, it is just a string, but we retain the option to make it `PureData`. + * Either way, it must remain pure, so that it can be safely shared by subgraphs + * that are not supposed to be able to communicate. + */ +export type InterfaceSpec = string; +/** + * Internal to a useful pattern for writing checking logic + * (a "checkFoo" function) that can be used to implement a predicate + * (an "isFoo" function) or a validator (an "assertFoo" function). + * + * * A predicate ideally only returns `true` or `false` and rarely throws. + * * A validator throws an informative diagnostic when the predicate + * would have returned `false`, and simply returns `undefined` normally + * when the predicate would have returned `true`. + * * The internal checking function that they share is parameterized by a + * `Checker` that determines how to proceed with a failure condition. + * Predicates pass in an identity function as checker. Validators + * pass in `assertChecker` which is a trivial wrapper around `assert`. + * + * See the various uses for good examples. + */ +export type Checker = ( + cond: boolean, + details?: import('ses').Details | undefined, +) => boolean; diff --git a/packages/pass-style/src/types.js b/packages/pass-style/src/types.js index 438d053605..407ac0b20e 100644 --- a/packages/pass-style/src/types.js +++ b/packages/pass-style/src/types.js @@ -1,161 +1,5 @@ -export {}; - -/** - * @typedef { 'undefined' | 'null' | - * 'boolean' | 'number' | 'bigint' | 'string' | 'symbol' - * } PrimitiveStyle - */ - -/** - * @typedef { PrimitiveStyle | - * 'copyRecord' | 'copyArray' | 'tagged' | - * 'remotable' | - * 'error' | 'promise' - * } PassStyle - */ - -// TODO declare more precise types throughout this file, so the type system -// and IDE can be more helpful. - -/** - * @typedef {any} Passable - * - * 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'). - * * Containers aggregate other Passables into - * * sequences as CopyArrays (PassStyle 'copyArray'), or - * * string-keyed dictionaries as CopyRecords (PassStyle 'copyRecord'), or - * * higher-level 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'). - * - * 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'). - */ - -/** - * @callback PassStyleOf - * @param {Passable} passable - * @returns {PassStyle} - */ - -/** - * @typedef {Passable} PureData - * - * 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 - * 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). - * - * Given this (currently counter-factual) assumption, a PureData value cannot - * be used as a communications channel, - * and can therefore be safely shared with subgraphs that should not be able - * to communicate with each other. - * Without that assumption, such a guarantee requires a marshal-unmarshal round - * trip (as exists between vats) to produce data structures disconnected from - * any potential proxies. - */ - -/** - * @typedef {Passable} RemotableObject - * - * An object marked as remotely accessible using the `Far` or `Remotable` - * functions, or a local presence representing such a remote object. - */ +/** @file Empty twin for .d.ts */ +/* eslint-disable */ +import { PASS_STYLE } from './passStyle-helpers.js'; -/** - * @typedef {Promise | RemotableObject} PassableCap - * - * The authority-bearing leaves of a Passable's pass-by-copy superstructure. - */ - -/** - * @template {Passable} [T=Passable] - * @typedef {T[]} CopyArray - * - * A Passable sequence of Passable values. - */ - -/** - * @template {Passable} [T=Passable] - * @typedef {Record} 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]: 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`. - * - * 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 and `| string`. - */ - -/** - * @typedef {string} InterfaceSpec - * This is an interface specification. - * For now, it is just a string, but will eventually be `PureData`. Either - * way, it must remain pure, so that it can be safely shared by subgraphs that - * are not supposed to be able to communicate. - */ - -/** - * @callback MarshalGetInterfaceOf - * Simple semantics, just tell what interface (or undefined) a remotable has. - * @param {any} maybeRemotable the value to check - * @returns {InterfaceSpec|undefined} the interface specification, or undefined - * if not a deemed to be a Remotable - */ - -/** - * @callback Checker - * Internal to a useful pattern for writing checking logic - * (a "checkFoo" function) that can be used to implement a predicate - * (an "isFoo" function) or a validator (an "assertFoo" function). - * - * * A predicate ideally only returns `true` or `false` and rarely throws. - * * A validator throws an informative diagnostic when the predicate - * would have returned `false`, and simply returns `undefined` normally - * when the predicate would have returned `true`. - * * The internal checking function that they share is parameterized by a - * `Checker` that determines how to proceed with a failure condition. - * Predicates pass in an identity function as checker. Validators - * pass in `assertChecker` which is a trivial wrapper around `assert`. - * - * See the various uses for good examples. - * @param {boolean} cond - * @param {import('ses').Details} [details] - * @returns {boolean} - */ +export {}; diff --git a/packages/pass-style/src/types.test-d.ts b/packages/pass-style/src/types.test-d.ts new file mode 100644 index 0000000000..957f15ff59 --- /dev/null +++ b/packages/pass-style/src/types.test-d.ts @@ -0,0 +1,31 @@ +/* eslint-disable */ +import { expectType, expectNotType } from 'tsd'; +import { Far } from './make-far'; +import { passStyleOf } from './passStyleOf'; +import { makeTagged } from './makeTagged'; +import { CopyTagged, PassStyle } from './types'; +import { PASS_STYLE } from './passStyle-helpers'; + +const remotable = Far('foo', {}); + +const copyTagged = makeTagged('someTag', remotable); +expectType>(copyTagged); + +const someUnknown: unknown = null; + +expectType<'undefined'>(passStyleOf(undefined)); +expectType<'string'>(passStyleOf('str')); +expectType<'boolean'>(passStyleOf(true)); +expectType<'number'>(passStyleOf(1)); +expectType<'bigint'>(passStyleOf(1n)); +expectType<'symbol'>(passStyleOf(Symbol.for('foo'))); +expectType<'null'>(passStyleOf(null)); +expectType<'promise'>(passStyleOf(Promise.resolve())); +expectType<'error'>(passStyleOf(new Error())); +expectType<'tagged'>(passStyleOf(copyTagged)); +expectType<'copyArray'>(passStyleOf([])); +expectType<'copyRecord'>(passStyleOf({})); +// though the object is specifying a PASS_STYLE, it doesn't match the case for extracting it +expectType<'copyRecord'>(passStyleOf({ [PASS_STYLE]: 'arbitrary' } as const)); +expectType<'remotable'>(passStyleOf(remotable)); +expectType(passStyleOf(someUnknown)); diff --git a/packages/pass-style/test/test-passStyleOf.js b/packages/pass-style/test/test-passStyleOf.js index 29771a0a70..5c86478484 100644 --- a/packages/pass-style/test/test-passStyleOf.js +++ b/packages/pass-style/test/test-passStyleOf.js @@ -161,6 +161,11 @@ test('passStyleOf testing tagged records', t => { value: { [PASS_STYLE]: 0 }, message: '0 must be a string', }, + { + label: 'unrecognized', + value: { [PASS_STYLE]: 'arbitrary' }, + message: 'Unrecognized PassStyle: "arbitrary"', + }, ]; for (const testCase of tagRecordBadPayloads) { const { label, message, ...desc } = testCase; diff --git a/packages/pass-style/tools/arb-passable.js b/packages/pass-style/tools/arb-passable.js index 1d6651cdec..22549f5d83 100644 --- a/packages/pass-style/tools/arb-passable.js +++ b/packages/pass-style/tools/arb-passable.js @@ -99,7 +99,12 @@ const { arbDag } = fc.letrec(tie => { ), }), ) - .map(({ type, payload }) => makeTagged(type, payload)), + .map(({ type, payload }) => + makeTagged( + type, + /** @type {import('../src/types.js').Passable} */ (payload), + ), + ), ), }; }); diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index 054d79679f..c492b2d97e 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -1,3 +1,5 @@ +// @ts-nocheck So many errors that the suppressions hamper readability. +// TODO parameterize MatchHelper which will solve most of them import { assertChecker, Far,