Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/kernel-test/src/narrowing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']]),
Expand Down
1 change: 1 addition & 0 deletions packages/kernel-utils/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
48 changes: 46 additions & 2 deletions packages/kernel-utils/src/narrow-interface-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand All @@ -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: [] });
Expand Down
30 changes: 30 additions & 0 deletions packages/kernel-utils/src/narrow-interface-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.`
Expand Down
Loading