diff --git a/packages/kernel-test/src/narrowing.test.ts b/packages/kernel-test/src/narrowing.test.ts index 1fedadbee..7fedaf4b5 100644 --- a/packages/kernel-test/src/narrowing.test.ts +++ b/packages/kernel-test/src/narrowing.test.ts @@ -127,8 +127,7 @@ describe('narrowing', () => { ); }); - // Unmarks at PR-8, which synthesizes a guard for a `makeDefaultExo` base. - it.fails('narrows a default-guarded exo', async () => { + it('narrows a default-guarded exo', async () => { const kernel = await launchNarrowingVat(); expect( await probe(kernel, 'probeDefaultGuarded', [['srv', 'data', 'x']]), diff --git a/packages/kernel-utils/CHANGELOG.md b/packages/kernel-utils/CHANGELOG.md index 7576e7dff..bdc27d899 100644 --- a/packages/kernel-utils/CHANGELOG.md +++ b/packages/kernel-utils/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `narrow` and `narrowInterfaceGuard` accept a base built with `makeDefaultExo`, whose interface guard carries `defaultGuards: 'passable'` and so names no method to conjoin onto: for such a method the delta's patterns become the whole guard. Because the base names no methods, a delta naming one the base does not implement can no longer be refused while narrowing — calling it rejects instead ([#1054](https://github.com/MetaMask/ocap-kernel/pull/1054)) - Add `narrowInterfaceGuard` and the `NarrowingDelta` type, which derive the interface guard of a narrowing: each delta pattern is conjoined onto the base's guard at the argument position it addresses, arity and return guards are inherited verbatim, methods the delta does not name are dropped, and every method guard is asyncified for forwarding ([#1051](https://github.com/MetaMask/ocap-kernel/pull/1051)) - Add `pathUnder(segments)`, which builds an `@endo/patterns` pattern matching segment arrays under a prefix, with `..` excluded past the prefix so that traversal out of it is unrepresentable. Empty `segments` matches every `..`-free segment array ([#1049](https://github.com/MetaMask/ocap-kernel/pull/1049)) - Add `narrow({ name, base, delta })`, which reads the base's interface guard over `E()` and returns an exo under the derived guard whose methods forward to the base. Narrowing a narrowing flattens: the result forwards straight to the original base under both deltas conjoined, so a chain of any depth stays one hop deep and costs one guard read. Every method of the result returns a promise ([#1052](https://github.com/MetaMask/ocap-kernel/pull/1052)) diff --git a/packages/kernel-utils/src/narrow-interface-guard.test.ts b/packages/kernel-utils/src/narrow-interface-guard.test.ts index df526ab7d..e106f667d 100644 --- a/packages/kernel-utils/src/narrow-interface-guard.test.ts +++ b/packages/kernel-utils/src/narrow-interface-guard.test.ts @@ -132,8 +132,8 @@ describe('narrowInterfaceGuard', () => { message: 'the base has arity 1 and no rest guard', }, { - scenario: 'narrows a method the base guards by default', - makeGuard: () => M.interface('Any', {}, { defaultGuards: 'passable' }), + scenario: 'narrows a method the base guards with raw defaults', + makeGuard: () => M.interface('Raw', {}, { defaultGuards: 'raw' }), delta: { read: [M.eq('a')] }, message: 'there is no guard to conjoin onto', }, @@ -142,6 +142,50 @@ describe('narrowInterfaceGuard', () => { }); }); +describe('narrowInterfaceGuard, against a default-guarded base', () => { + const makePassableGuard = (): InterfaceGuard => + M.interface('Any', {}, { defaultGuards: 'passable' }); + + it('synthesizes a guard from the delta alone', () => { + const result = narrowFrom(makePassableGuard(), { read: [M.eq('a')] }); + const { argGuards, restArgGuard } = payloadOf(result, 'read'); + + expect(matches('a', argGuards[0])).toBe(true); + expect(matches('b', argGuards[0])).toBe(false); + expect(matches('anything', restArgGuard)).toBe(true); + }); + + it('leaves a method unconstrained when its delta is empty', () => { + const result = narrowFrom(makePassableGuard(), { read: [] }); + const { argGuards, optionalArgGuards, restArgGuard } = payloadOf( + result, + 'read', + ); + + expect(argGuards).toStrictEqual([]); + expect(optionalArgGuards ?? []).toStrictEqual([]); + expect(matches('anything', restArgGuard)).toBe(true); + expect(matches(42, restArgGuard)).toBe(true); + }); + + it('treats a hole as unconstrained', () => { + const result = narrowFrom(makePassableGuard(), { + read: [undefined, M.eq('x')], + }); + const { argGuards } = payloadOf(result, 'read'); + + expect(matches(42, argGuards[0])).toBe(true); + expect(matches('x', argGuards[1])).toBe(true); + expect(matches('y', argGuards[1])).toBe(false); + }); + + it('still drops methods the delta does not name', () => { + const result = narrowFrom(makePassableGuard(), {}); + + expect(getInterfaceMethodGuards(result)).toStrictEqual({}); + }); +}); + describe('conjoinDeltas', () => { it('keeps only the methods the incoming delta names', () => { const combined = conjoinDeltas({ read: [], write: [] }, { read: [] }); diff --git a/packages/kernel-utils/src/narrow-interface-guard.ts b/packages/kernel-utils/src/narrow-interface-guard.ts index 5ea17f413..82178d56d 100644 --- a/packages/kernel-utils/src/narrow-interface-guard.ts +++ b/packages/kernel-utils/src/narrow-interface-guard.ts @@ -153,6 +153,32 @@ const narrowMethodGuard = ( ); }; +/** + * Synthesize a method guard for a method its base admits by default. + * + * `defaultGuards: 'passable'` admits any passable arguments, so there is + * nothing to conjoin onto and the delta's patterns are the whole guard. The + * result still admits no more calls than the base did — a delta of length 0 + * synthesizes `M.callWhen().rest(M.any()).returns(M.any())`, which admits + * exactly what the base admits. + * + * Such a base names no methods, so a delta naming one it does not implement is + * indistinguishable from one it does, and no error can be raised here. The + * forward rejects at call time instead. + * + * @param patterns - The delta's patterns for this method. + * @returns The synthesized guard. + */ +const synthesizeMethodGuard = ( + patterns: (Pattern | undefined)[], +): MethodGuard => + buildMethodGuard( + M.callWhen(...patterns.map((pattern) => pattern ?? M.any())), + [], + M.any(), + M.any(), + ); + /** * Derive the interface guard of a narrowing of a base capability. * @@ -191,6 +217,10 @@ export const narrowInterfaceGuard = ({ for (const [methodName, patterns] of Object.entries(delta)) { const baseMethodGuard = baseMethodGuards[methodName]; if (baseMethodGuard === undefined) { + if (defaultGuards === 'passable') { + narrowedMethodGuards[methodName] = synthesizeMethodGuard(patterns); + continue; + } throw new Error( defaultGuards === undefined ? `Cannot narrow method "${methodName}": the base has no such method.`