Skip to content

Commit

Permalink
Merge pull request #1902 from endojs/markm-8664-receive-amplifier
Browse files Browse the repository at this point in the history
feat(exo): lightweight inter-facet rights amplification
  • Loading branch information
erights authored Dec 31, 2023
2 parents 2179108 + bf29a3a commit 3e9e0dd
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 10 deletions.
45 changes: 43 additions & 2 deletions packages/exo/src/exo-makers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { objectMap } from '@endo/patterns';

import { defendPrototype, defendPrototypeKit } from './exo-tools.js';

const { Fail, quote: q } = assert;
const { create, seal, freeze, defineProperty, values } = Object;

const { getEnvironmentOption } = makeEnvironmentCaptor(globalThis);
Expand Down Expand Up @@ -80,12 +81,26 @@ export const initEmpty = () => emptyRecord;
* @returns {void}
*/

/**
* @callback Amplifier
* @param {any} exo
* @param {string} facetName
* @returns {any}
*/

/**
* @callback ReceiveAmplifier
* @param {Amplifier} amplifier
* @returns {void}
*/

/**
* @template C
* @typedef {object} FarClassOptions
* @property {(context: C) => void} [finish]
* @property {StateShape} [stateShape]
* @property {ReceiveRevoker} [receiveRevoker]
* @property {ReceiveAmplifier} [receiveAmplifier]
*/

/**
Expand Down Expand Up @@ -123,7 +138,14 @@ export const defineExoClass = (
options = {},
) => {
harden(methods);
const { finish = undefined, receiveRevoker = undefined } = options;
const {
finish = undefined,
receiveRevoker = undefined,
receiveAmplifier = undefined,
} = options;
receiveAmplifier === undefined ||
Fail`Only facets of an exo class kit can be amplified ${q(tag)}`;

/** @type {WeakMap<M,ClassContext<ReturnType<I>, M>>} */
const contextMap = new WeakMap();
const proto = defendPrototype(
Expand Down Expand Up @@ -183,7 +205,11 @@ export const defineExoClassKit = (
options = {},
) => {
harden(methodsKit);
const { finish = undefined, receiveRevoker = undefined } = options;
const {
finish = undefined,
receiveRevoker = undefined,
receiveAmplifier = undefined,
} = options;
const contextMapKit = objectMap(methodsKit, () => new WeakMap());
const getContextKit = objectMap(
contextMapKit,
Expand Down Expand Up @@ -228,6 +254,21 @@ export const defineExoClassKit = (
receiveRevoker(revoke);
}

if (receiveAmplifier) {
const amplify = (aFacet, facetName) => {
for (const contextMap of values(contextMapKit)) {
if (contextMap.has(aFacet)) {
const otherFacet = contextMap.get(aFacet).facets[facetName];
otherFacet || Fail`${q(facetName)} must be a facet name of ${q(tag)}`;
return otherFacet;
}
}
throw Fail`Must be an unrevoked facet of ${q(tag)}: ${aFacet}`;
};
harden(amplify);
receiveAmplifier(amplify);
}

return harden(makeInstanceKit);
};
harden(defineExoClassKit);
Expand Down
106 changes: 106 additions & 0 deletions packages/exo/test/test-amplify-heap-class-kits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// modeled on test-revoke-heap-classes.js

// eslint-disable-next-line import/order
import { test } from './prepare-test-env-ava.js';

// eslint-disable-next-line import/order
import { M } from '@endo/patterns';
import { defineExoClass, defineExoClassKit } from '../src/exo-makers.js';

const UpCounterI = M.interface('UpCounter', {
incr: M.call().optional(M.gte(0)).returns(M.number()),
});

const DownCounterI = M.interface('DownCounter', {
decr: M.call().optional(M.gte(0)).returns(M.number()),
});

test('test amplify defineExoClass fails', t => {
t.throws(
() =>
defineExoClass(
'UpCounter',
UpCounterI,
/** @param {number} x */
(x = 0) => ({ x }),
{
incr(y = 1) {
const { state } = this;
state.x += y;
return state.x;
},
},
{
receiveAmplifier(_) {},
},
),
{
message: 'Only facets of an exo class kit can be amplified "UpCounter"',
},
);
});

test('test amplify defineExoClassKit', t => {
let revoke;
let amp;
const makeCounterKit = defineExoClassKit(
'Counter',
{ up: UpCounterI, down: DownCounterI },
/** @param {number} x */
(x = 0) => ({ x }),
{
up: {
incr(y = 1) {
const { state } = this;
state.x += y;
return state.x;
},
},
down: {
decr(y = 1) {
const { state } = this;
state.x -= y;
return state.x;
},
},
},
{
receiveRevoker(r) {
revoke = r;
},
receiveAmplifier(a) {
amp = a;
},
},
);
const { up: upCounter, down: downCounter } = makeCounterKit(3);
t.is(upCounter.incr(5), 8);
t.is(downCounter.decr(), 7);

t.throws(() => amp(upCounter, 'sideways'), {
message: '"sideways" must be a facet name of "Counter"',
});
t.throws(() => amp(harden({}), 'down'), {
message: 'Must be an unrevoked facet of "Counter": {}',
});
t.is(amp(upCounter, 'down'), downCounter);
t.is(amp(upCounter, 'up'), upCounter);
t.is(amp(downCounter, 'up'), upCounter);
t.is(amp(downCounter, 'down'), downCounter);

t.is(revoke(upCounter), true);

t.throws(() => amp(upCounter, 'down'), {
message: 'Must be an unrevoked facet of "Counter": "[Alleged: Counter up]"',
});
t.throws(() => amp(upCounter, 'up'), {
message: 'Must be an unrevoked facet of "Counter": "[Alleged: Counter up]"',
});
t.is(amp(downCounter, 'up'), upCounter);
t.throws(() => upCounter.incr(3), {
message:
'"In \\"incr\\" method of (Counter up)" may only be applied to a valid instance: "[Alleged: Counter up]"',
});
t.is(amp(downCounter, 'down'), downCounter);
t.is(downCounter.decr(), 6);
});
10 changes: 2 additions & 8 deletions packages/exo/test/test-revoke-heap-classes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,11 @@ import { defineExoClass, defineExoClassKit } from '../src/exo-makers.js';
const { apply } = Reflect;

const UpCounterI = M.interface('UpCounter', {
incr: M.call()
// TODO M.number() should not be needed to get a better error message
.optional(M.and(M.number(), M.gte(0)))
.returns(M.number()),
incr: M.call().optional(M.gte(0)).returns(M.number()),
});

const DownCounterI = M.interface('DownCounter', {
decr: M.call()
// TODO M.number() should not be needed to get a better error message
.optional(M.and(M.number(), M.gte(0)))
.returns(M.number()),
decr: M.call().optional(M.gte(0)).returns(M.number()),
});

test('test revoke defineExoClass', t => {
Expand Down

0 comments on commit 3e9e0dd

Please sign in to comment.