Skip to content

Commit

Permalink
fix: interface guards
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Apr 22, 2022
1 parent a1784de commit 66676bd
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 20 deletions.
39 changes: 19 additions & 20 deletions packages/ERTP/src/interfaces.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable no-use-before-define */
/* global foo */
const M = foo();
import { M, I } from '@agoric/store';

export const MatchDisplayInfo = M.rest(
{
Expand All @@ -10,36 +9,36 @@ export const MatchDisplayInfo = M.rest(
}),
);

export const BrandI = M.interface({
isMyIssuer: M.callWhen(M.await(IssuerI)).returns(M.boolean()),
getAllegedName: M.call().returns(M.string()),
getDisplayInfo: M.call().returns(MatchDisplayInfo),
export const BrandI = I.interface('Brand', {
isMyIssuer: I.callWhen(I.await(IssuerI)).returns(M.boolean()),
getAllegedName: I.call().returns(M.string()),
getDisplayInfo: I.call().returns(MatchDisplayInfo),
});

export const MatchAmount = {
brand: BrandI,
value: M.or(M.bigint(), M.array()),
};

export const IssuerI = M.interface({
getBrand: M.call().returns(BrandI),
getAllegedName: M.call().returns(M.string()),
getAssetKind: M.call().returns(M.or('nat', 'set')),
getDisplayInfo: M.call().returns(MatchDisplayInfo),
makeEmptyPurse: M.call().returns(PurseI),
export const IssuerI = I.interface('Issuer', {
getBrand: I.call().returns(BrandI),
getAllegedName: I.call().returns(M.string()),
getAssetKind: I.call().returns(M.or('nat', 'set')),
getDisplayInfo: I.call().returns(MatchDisplayInfo),
makeEmptyPurse: I.call().returns(PurseI),

isLive: M.callWhen(M.await(PaymentI)).returns(M.boolean()),
getAmountOf: M.callWhen(M.await(PaymentI)).returns(MatchAmount),
isLive: I.callWhen(I.await(PaymentI)).returns(M.boolean()),
getAmountOf: I.callWhen(I.await(PaymentI)).returns(MatchAmount),
});

export const PaymentI = M.interface({
getAllegedBrand: M.call().returns(BrandI),
export const PaymentI = I.interface('Payment', {
getAllegedBrand: I.call().returns(BrandI),
});

export const PurseI = M.interface({
getAllegedBrand: M.call().returns(BrandI),
deposit: M.apply(M.rest([PaymentI]).optionals([MatchAmount])).returns(
export const PurseI = I.interface('Purse', {
getAllegedBrand: I.call().returns(BrandI),
deposit: I.apply(M.rest([PaymentI], M.partial([MatchAmount]))).returns(
MatchAmount,
),
withdraw: M.call(MatchAmount).returns(PaymentI),
withdraw: I.call(MatchAmount).returns(PaymentI),
});
1 change: 1 addition & 0 deletions packages/store/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export {
matches,
fit,
} from './patterns/patternMatchers.js';
export { I } from './patterns/interface-tools.js';
export { compareRank, isRankSorted, sortByRank } from './patterns/rankOrder.js';
export {
makeDecodePassable,
Expand Down
28 changes: 28 additions & 0 deletions packages/store/src/patterns/defineHeapKind.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { makeScalarWeakMapStore } from '../stores/scalarWeakMapStore.js';
import { defendVTable } from './interface-tools.js';

const { create } = Object;

export const defineHeapKind = (
iface,
init,
rawVTable,
{ finish = undefined } = {},
) => {
const { klass } = iface;
assert(klass === 'Interface');
const contextMapStore = makeScalarWeakMapStore();
const defensiveVTable = defendVTable(rawVTable, contextMapStore, iface);
const makeInstance = (...args) => {
const state = init(...args);
const self = create(defensiveVTable);
const context = harden({ state, self });
contextMapStore.init(self, context);
if (finish) {
finish(context);
}
return self;
};
return harden(makeInstance);
};
harden(defineHeapKind);
161 changes: 161 additions & 0 deletions packages/store/src/patterns/interface-tools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { PASS_STYLE } from '@endo/marshal';
import { E } from '@endo/eventual-send';
import { M, fit } from './patternMatchers.js';

const { details: X, quote: q } = assert;
const { apply, ownKeys } = Reflect;
const { fromEntries, entries, defineProperties } = Object;

const makeMethodGuardMaker = (callKind, argGuards) =>
harden({
returns: (returnGuard = M.any()) =>
harden({
klass: 'methodGuard',
callKind,
argGuards,
returnGuard,
}),
});

const makeAwaitArgGuard = argGuard =>
harden({
klass: 'awaitArg',
argGuard,
});

const isAwaitArgGuard = argGuard =>
argGuard && typeof argGuard === 'object' && argGuard.klass === 'awaitArg';

export const I = harden({
interface: (name, methodGuards) => {
for (const [_, methodGuard] of entries(methodGuards)) {
assert(
methodGuard.klass === 'methodGuard',
X`unrecognize method guard ${methodGuard}`,
);
}
return harden({
klass: 'Interface',
name,
methodGuards,
});
},
call: (...argGuards) => makeMethodGuardMaker('sync', argGuards),
callWhen: (...argGuards) => makeMethodGuardMaker('async', argGuards),
apply: argGuards => makeMethodGuardMaker('sync', argGuards),
applyWhen: argGuards => makeMethodGuardMaker('async', argGuards),

await: argGuard => makeAwaitArgGuard(argGuard),
});

const defendSyncMethod = (rawMethod, contextMapStore, methodGuard) => {
const { argGuards, returnGuard } = methodGuard;

const defensiveSyncMethod = (...args) => {
// Note purposeful use of `this`
assert(
contextMapStore.has(this),
X`method can only be used on its own instances: ${rawMethod}`,
);
const context = contextMapStore.get(this);
fit(args, argGuards);
const result = apply(rawMethod, undefined, [context, ...args]);
fit(result, returnGuard);
return result;
};
return harden(defensiveSyncMethod);
};

const defendAsyncMethod = (rawMethod, contextMapStore, methodGuard) => {
const { argGuards, returnGuard } = methodGuard;

const rawArgGuards = [];
const awaitIndexes = [];
for (let i = 0; i < argGuards.length; i += 1) {
const argGuard = argGuards[i];
if (isAwaitArgGuard(argGuard)) {
rawArgGuards.push(argGuard.argGuard);
awaitIndexes.push(i);
} else {
rawArgGuards.push(argGuard);
}
}
harden(rawArgGuards);
harden(awaitIndexes);
const defensiveAsyncMethod = (...args) => {
// Note purposeful use of `this`
assert(
contextMapStore.has(this),
X`method can only be used on its own instances: ${rawMethod}`,
);
const context = contextMapStore.get(this);
const awaitList = awaitIndexes.map(i => args[i]);
const p = Promise.all(awaitList);
const rawArgs = [...args];
return E.when(p, awaitedArgs => {
for (let j = 0; j < awaitIndexes.length; j += 1) {
rawArgs[awaitIndexes[j]] = awaitedArgs[j];
}
fit(rawArgs, rawArgGuards);
const resultP = apply(rawMethod, undefined, [context, ...rawArgs]);
return E.when(resultP, result => {
fit(result, returnGuard);
return result;
});
});
};
return harden(defensiveAsyncMethod);
};

const defendMethod = (rawMethod, contextMapStore, methodGuard) => {
const { klass, callKind } = methodGuard;
assert(klass === 'methodGuard');

if (callKind === 'sync') {
return defendSyncMethod(rawMethod, contextMapStore, methodGuard);
} else {
assert(callKind === 'async');
return defendAsyncMethod(rawMethod, contextMapStore, methodGuard);
}
};

const defaultMethodGuard = I.apply(M.array()).returns();

export const defendVTable = (rawVTable, contextMapStore, iface) => {
const { klass, name, methodGuards } = iface;
assert(klass === 'Interface');
assert.typeof(name, 'string');

const methodGuardNames = ownKeys(methodGuards);
for (const methodGuardName of methodGuardNames) {
assert(
methodGuardName in rawVTable,
X`${q(methodGuardName)} not implemented by ${rawVTable}`,
);
}
const methodNames = ownKeys(rawVTable);
// like Object.entries, but unenumerable and symbol as well.
const rawMethodEntries = methodNames.map(mName => [mName, rawVTable[mName]]);
const defensiveMethodEntries = rawMethodEntries.map(([mName, rawMethod]) => {
const methodGuard = methodGuards[mName] || defaultMethodGuard;
const defensiveMethod = defendMethod(
rawMethod,
contextMapStore,
methodGuard,
);
return [mName, defensiveMethod];
});
// Return the defensive VTable, which can be use on a shared
// prototype and shared by instances, avoiding the per-object-per-method
// allocation cost of the objects as closure pattern. That's why we
// use `this` above. To make it safe, each defensive method starts with
// a fail-fast brand check on `this`, ensuring that the methods can only be
// applied to legitimate instances.
const defensiveVTable = fromEntries(defensiveMethodEntries);
defineProperties(defensiveVTable, {
[PASS_STYLE]: { value: 'remotable' },
[Symbol.toStringTag]: { value: name },
});
return harden(defensiveVTable);
};
harden(defendVTable);
20 changes: 20 additions & 0 deletions packages/store/test/test-interface-tools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @ts-check

import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js';
import { passStyleOf } from '@endo/marshal';

import { I } from '../src/patterns/interface-tools.js';
import { defineHeapKind } from '../src/patterns/defineHeapKind.js';
import { M } from '../src/patterns/patternMatchers.js';

test('how far-able is defineHeapKind', t => {
const bobIFace = I.interface('bob', {
foo: I.call(M.any()).returns(M.undefined()),
});
const makeBob = defineHeapKind(bobIFace, () => ({}), {
// @ts-ignore
foo: ({ _state, _self }, _carol) => console.log('got here'),
});
const bob = makeBob();
t.assert(passStyleOf(bob) === 'remotable');
});

0 comments on commit 66676bd

Please sign in to comment.