Skip to content

Commit 2519237

Browse files
grypezclaude
andcommitted
fix(kernel-utils): fix false negative in collectSheafGuard for rest-arg sections
`getGuardAt` was returning `undefined` for positions beyond a section's fixed argument range, even when a `restArgGuard` was present. This caused rest-arg sections to be absent from optional-position unions, producing a false negative: e.g. `M.call().rest(M.string())` would not cover position 0 in the union, so a call `['hello']` would fail the collected guard even though the section accepts it. Fix: fall through to `payload.restArgGuard` after exhausting the optional array, so rest-arg sections contribute to every optional position in the union. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent bba1d0c commit 2519237

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

packages/kernel-utils/src/sheaf/guard.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { MethodGuard, Pattern } from '@endo/patterns';
99
import { describe, it, expect } from 'vitest';
1010

1111
import { collectSheafGuard } from './guard.ts';
12+
import { guardCoversPoint } from './stalk.ts';
1213
import type { Section } from './types.ts';
1314

1415
const makeSection = (
@@ -159,6 +160,30 @@ describe('collectSheafGuard', () => {
159160
expect(matches(42, restArgGuard)).toBe(true);
160161
});
161162

163+
it('rest-arg section covers optional positions (no false negative)', () => {
164+
// Section A requires 1 number; Section B requires 0 args but accepts any
165+
// number of strings via rest. A call ['hello'] is covered by B — the
166+
// collected guard must pass it too.
167+
const sections = [
168+
makeSection(
169+
'AB:0',
170+
{ f: M.call(M.number()).returns(M.any()) },
171+
{ f: (_: number) => undefined },
172+
),
173+
makeSection(
174+
'AB:1',
175+
{ f: M.call().rest(M.string()).returns(M.any()) },
176+
{ f: (..._args: string[]) => undefined },
177+
),
178+
];
179+
180+
const guard = collectSheafGuard('AB', sections);
181+
182+
expect(guardCoversPoint(guard, 'f', ['hello'])).toBe(true); // covered by B
183+
expect(guardCoversPoint(guard, 'f', [42])).toBe(true); // covered by A
184+
expect(guardCoversPoint(guard, 'f', [])).toBe(true); // covered by B (0 required)
185+
});
186+
162187
it('multi-method guard collection', () => {
163188
const sections = [
164189
makeSection(

packages/kernel-utils/src/sheaf/guard.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,14 @@ export const collectSheafGuard = <Core extends Methods>(
7474
if (idx < payload.argGuards.length) {
7575
return payload.argGuards[idx];
7676
}
77-
return payload.optionalArgGuards?.[idx - payload.argGuards.length];
77+
const optIdx = idx - payload.argGuards.length;
78+
if (
79+
payload.optionalArgGuards &&
80+
optIdx < payload.optionalArgGuards.length
81+
) {
82+
return payload.optionalArgGuards[optIdx];
83+
}
84+
return payload.restArgGuard;
7885
};
7986

8087
const unionMethodGuards: Record<string, MethodGuard> = {};

0 commit comments

Comments
 (0)