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
1 change: 1 addition & 0 deletions packages/kernel-exo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.17.4",
"@endo/eventual-send": "^1.3.4",
"@metamask/auto-changelog": "^5.3.0",
"@metamask/eslint-config": "^15.0.0",
"@metamask/eslint-config-nodejs": "^15.0.0",
Expand Down
8 changes: 8 additions & 0 deletions packages/kernel-exo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,11 @@ export { makeDefaultInterface, makeDefaultExo } from './exo.ts';
export { GET_DESCRIPTION, makeDiscoverableExo } from './discoverable.ts';
export type { DiscoverableExo } from './discoverable.ts';
export type { JsonSchema, MethodSchema } from './schema.ts';

// Sheaf types
export type { Section, Germ, Lift, Presheaf } from './sheaf/types.ts';

// Sheaf functions
export { makePresheaf } from './sheaf/presheaf.ts';
export { sheafify } from './sheaf/sheafify.ts';
export { collectSheafGuard } from './sheaf/guard.ts';
117 changes: 117 additions & 0 deletions packages/kernel-exo/src/sheaf/guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { makeExo } from '@endo/exo';
import {
M,
matches,
getInterfaceGuardPayload,
getMethodGuardPayload,
} from '@endo/patterns';
import type { MethodGuard, Pattern } from '@endo/patterns';
import { describe, it, expect } from 'vitest';

import { collectSheafGuard } from './guard.ts';
import type { Section } from './types.ts';

const makeSection = (
tag: string,
guards: Record<string, MethodGuard>,
methods: Record<string, (...args: unknown[]) => unknown>,
): Section => {
const interfaceGuard = M.interface(tag, guards);
return makeExo(tag, interfaceGuard, methods) as unknown as Section;
};

describe('collectSheafGuard', () => {
it('variable arity: add with 1, 2, and 3 args', () => {
const sections = [
makeSection(
'Calc:0',
{ add: M.call(M.number()).returns(M.number()) },
{ add: (a: number) => a },
),
makeSection(
'Calc:1',
{ add: M.call(M.number(), M.number()).returns(M.number()) },
{ add: (a: number, b: number) => a + b },
),
makeSection(
'Calc:2',
{
add: M.call(M.number(), M.number(), M.number()).returns(M.number()),
},
{ add: (a: number, b: number, cc: number) => a + b + cc },
),
];

const guard = collectSheafGuard('Calc', sections);
const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as {
methodGuards: Record<string, MethodGuard>;
};
const payload = getMethodGuardPayload(methodGuards.add) as unknown as {
argGuards: Pattern[];
optionalArgGuards?: Pattern[];
};

// 1 required arg (present in all), 2 optional (variable arity)
expect(payload.argGuards).toHaveLength(1);
expect(payload.optionalArgGuards).toHaveLength(2);
});

it('return guard union', () => {
const sections = [
makeSection(
'S:0',
{ f: M.call(M.eq(0)).returns(M.eq(0)) },
{ f: (_: number) => 0 },
),
makeSection(
'S:1',
{ f: M.call(M.eq(1)).returns(M.eq(1)) },
{ f: (_: number) => 1 },
),
];

const guard = collectSheafGuard('S', sections);
const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as {
methodGuards: Record<string, MethodGuard>;
};
const { returnGuard } = getMethodGuardPayload(
methodGuards.f,
) as unknown as { returnGuard: Pattern };

// Return guard is union of eq(0) and eq(1)
expect(matches(0, returnGuard)).toBe(true);
expect(matches(1, returnGuard)).toBe(true);
});

it('multi-method guard collection', () => {
const sections = [
makeSection(
'Multi:0',
{
translate: M.call(M.string(), M.string()).returns(M.string()),
},
{
translate: (from: string, to: string) => `${from}->${to}`,
},
),
makeSection(
'Multi:1',
{
translate: M.call(M.string(), M.string()).returns(M.string()),
summarize: M.call(M.string()).returns(M.string()),
},
{
translate: (from: string, to: string) => `${from}->${to}`,
summarize: (text: string) => `summary: ${text}`,
},
),
];

const guard = collectSheafGuard('Multi', sections);
const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as {
methodGuards: Record<string, MethodGuard>;
};
expect('translate' in methodGuards).toBe(true);
expect('summarize' in methodGuards).toBe(true);
});
});
103 changes: 103 additions & 0 deletions packages/kernel-exo/src/sheaf/guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { GET_INTERFACE_GUARD } from '@endo/exo';
import type { Methods } from '@endo/exo';
import {
M,
getInterfaceGuardPayload,
getMethodGuardPayload,
} from '@endo/patterns';
import type { InterfaceGuard, MethodGuard, Pattern } from '@endo/patterns';

import type { Section } from './types.ts';

type MethodGuardPayload = {
argGuards: Pattern[];
optionalArgGuards?: Pattern[];
returnGuard: Pattern;
};

/**
* Naive union of guards via M.or — no pattern canonicalization.
*
* @param guards - Guards to union.
* @returns A single guard representing the union.
*/
const unionGuard = (guards: Pattern[]): Pattern => {
if (guards.length === 1) {
const [first] = guards;
return first;
}
return M.or(...guards);
};

/**
* Compute the known space K = union of section guards.
*
* For each method name across all sections, collects the arg guards at each
* position and produces a union via M.or. Sections with fewer args than
* the maximum contribute to required args; the remainder become optional.
*
* @param name - The name for the collected interface guard.
* @param sections - The sections whose guards are collected.
* @returns An interface guard covering all sections.
*/
export const collectSheafGuard = <Core extends Methods>(
name: string,
sections: Section<Core>[],
): InterfaceGuard => {
const payloadsByMethod = new Map<string, MethodGuardPayload[]>();

for (const section of sections) {
const interfaceGuard = section[GET_INTERFACE_GUARD]?.();
if (!interfaceGuard) {
continue;
}
const { methodGuards } = getInterfaceGuardPayload(
interfaceGuard,
) as unknown as { methodGuards: Record<string, MethodGuard> };
for (const [methodName, methodGuard] of Object.entries(methodGuards)) {
const payload = getMethodGuardPayload(
methodGuard,
) as unknown as MethodGuardPayload;
if (!payloadsByMethod.has(methodName)) {
payloadsByMethod.set(methodName, []);
}
const existing = payloadsByMethod.get(methodName);
existing?.push(payload);
}
}

const unionMethodGuards: Record<string, MethodGuard> = {};
for (const [methodName, payloads] of payloadsByMethod) {
const arities = payloads.map((payload) => payload.argGuards.length);
const minArity = Math.min(...arities);
const maxArity = Math.max(...arities);

const requiredArgGuards = [];
for (let idx = 0; idx < minArity; idx++) {
requiredArgGuards.push(
unionGuard(payloads.map((payload) => payload.argGuards[idx])),
);
}

const optionalArgGuards = [];
for (let idx = minArity; idx < maxArity; idx++) {
const guards = payloads
.filter((payload) => idx < payload.argGuards.length)
.map((payload) => payload.argGuards[idx]);
optionalArgGuards.push(unionGuard(guards));
}

const returnGuard = unionGuard(
payloads.map((payload) => payload.returnGuard),
);

unionMethodGuards[methodName] =
optionalArgGuards.length > 0
? M.callWhen(...requiredArgGuards)
.optional(...optionalArgGuards)
.returns(returnGuard)
: M.callWhen(...requiredArgGuards).returns(returnGuard);
}

return M.interface(name, unionMethodGuards);
};
114 changes: 114 additions & 0 deletions packages/kernel-exo/src/sheaf/presheaf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { makeExo } from '@endo/exo';
import { M } from '@endo/patterns';
import { describe, it, expect } from 'vitest';

import { makePresheaf } from './presheaf.ts';

// ---------------------------------------------------------------------------
// Unit: makePresheaf
// ---------------------------------------------------------------------------

describe('makePresheaf', () => {
it('addSection stores operational metadata alongside section', () => {
const presheaf = makePresheaf<{ cost: number }>('Wallet');
presheaf.addSection(
{ getBalance: M.call(M.string()).returns(M.number()) },
{ getBalance: (_acct: string) => 100 },
{ cost: 10 },
);

expect(presheaf.entries).toHaveLength(1);
expect(presheaf.sections).toHaveLength(1);
expect(presheaf.entries[0]!.operational).toStrictEqual({ cost: 10 });
});

it('chaining returns the same presheaf', () => {
const presheaf = makePresheaf<{ cost: number }>('Wallet');
const ret = presheaf.addSection(
{ getBalance: M.call(M.string()).returns(M.number()) },
{ getBalance: (_acct: string) => 100 },
{ cost: 10 },
);
expect(ret).toBe(presheaf);
});

it('parallel arrays stay in sync', () => {
const presheaf = makePresheaf<{ tag: string }>('Multi');
presheaf.addSection(
{ f: M.call(M.number()).returns(M.number()) },
{ f: (val: number) => val },
{ tag: 'first' },
);
presheaf.addSection(
{ f: M.call(M.number()).returns(M.number()) },
{ f: (val: number) => val * 2 },
{ tag: 'second' },
);

expect(presheaf.entries).toHaveLength(2);
expect(presheaf.sections).toHaveLength(2);
// entries[i].section === sections[i]
expect(presheaf.entries[0]!.section).toBe(presheaf.sections[0]);
expect(presheaf.entries[1]!.section).toBe(presheaf.sections[1]);
expect(presheaf.entries[0]!.operational.tag).toBe('first');
expect(presheaf.entries[1]!.operational.tag).toBe('second');
});
});

// ---------------------------------------------------------------------------
// Unit: makePresheaf addExo
// ---------------------------------------------------------------------------

describe('makePresheaf addExo', () => {
it('stores exo + operational metadata', () => {
const presheaf = makePresheaf<{ cost: number }>('Wallet');
const exo = makeExo(
'bal',
M.interface('bal', {
getBalance: M.call(M.string()).returns(M.number()),
}),
{ getBalance: (_acct: string) => 100 },
);
presheaf.addExo(exo, { cost: 10 });

expect(presheaf.entries).toHaveLength(1);
expect(presheaf.sections).toHaveLength(1);
expect(presheaf.entries[0]!.operational).toStrictEqual({ cost: 10 });
expect(presheaf.entries[0]!.section).toBe(presheaf.sections[0]);
});

it('chaining returns the same presheaf', () => {
const presheaf = makePresheaf<{ cost: number }>('Wallet');
const exo = makeExo(
'bal',
M.interface('bal', {
getBalance: M.call(M.string()).returns(M.number()),
}),
{ getBalance: (_acct: string) => 100 },
);
const ret = presheaf.addExo(exo, { cost: 10 });
expect(ret).toBe(presheaf);
});

it('mixing addSection and addExo keeps arrays in sync', () => {
const presheaf = makePresheaf<{ tag: string }>('Multi');
presheaf.addSection(
{ f: M.call(M.number()).returns(M.number()) },
{ f: (val: number) => val },
{ tag: 'section' },
);
const exo = makeExo(
'g',
M.interface('g', { g: M.call(M.number()).returns(M.number()) }),
{ g: (val: number) => val * 2 },
);
presheaf.addExo(exo, { tag: 'exo' });

expect(presheaf.entries).toHaveLength(2);
expect(presheaf.sections).toHaveLength(2);
expect(presheaf.entries[0]!.section).toBe(presheaf.sections[0]);
expect(presheaf.entries[1]!.section).toBe(presheaf.sections[1]);
expect(presheaf.entries[0]!.operational.tag).toBe('section');
expect(presheaf.entries[1]!.operational.tag).toBe('exo');
});
});
Loading
Loading