Skip to content

Commit

Permalink
fix: omg far classes starting to work
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Aug 15, 2022
1 parent 49d520a commit bf37be3
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 79 deletions.
37 changes: 0 additions & 37 deletions packages/store/src/patterns/defineHeapKind.js

This file was deleted.

133 changes: 100 additions & 33 deletions packages/store/src/patterns/interface-tools.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { PASS_STYLE, assertRemotable } from '@endo/marshal';
import { E } from '@endo/eventual-send';
import { M, fit } from './patternMatchers.js';
import { makeLegacyWeakMap } from '../legacy/legacyWeakMap.js';

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

const makeMethodGuardMaker = (callKind, argGuards) =>
harden({
Expand All @@ -28,7 +36,7 @@ const isAwaitArgGuard = argGuard =>
argGuard && typeof argGuard === 'object' && argGuard.klass === 'awaitArg';

export const I = harden({
interface: (farName, methodGuards) => {
interface: (interfaceName, methodGuards) => {
for (const [_, methodGuard] of entries(methodGuards)) {
assert(
methodGuard.klass === 'methodGuard',
Expand All @@ -37,7 +45,7 @@ export const I = harden({
}
return harden({
klass: 'Interface',
farName,
interfaceName,
methodGuards,
});
},
Expand All @@ -52,7 +60,7 @@ export const I = harden({
const mimicMethodNameLength = (defensiveMethod, rawMethod) => {
defineProperties(defensiveMethod, {
name: { value: rawMethod.name },
length: { value: rawMethod.length - 1 },
length: { value: rawMethod.length },
});
};

Expand All @@ -64,11 +72,11 @@ const defendSyncMethod = (rawMethod, contextMapStore, methodGuard, label) => {
defensiveSyncMethod(...args) {
assert(
contextMapStore.has(this),
X`method can only be used on its own instances: ${rawMethod}`,
X`method can only be used on its own instances: ${q(label)}`,
);
const context = contextMapStore.get(this);
fit(harden(args), argGuards, `${label}: args`);
const result = apply(rawMethod, undefined, [context, ...args]);
const result = apply(rawMethod, context, args);
fit(result, returnGuard, `${label}: result`);
return result;
},
Expand Down Expand Up @@ -98,22 +106,22 @@ const defendAsyncMethod = (rawMethod, contextMapStore, methodGuard, label) => {
defensiveAsyncMethod(...args) {
assert(
contextMapStore.has(this),
X`method can only be used on its own instances: ${rawMethod}`,
X`method can only be used on its own instances: ${q(label)}`,
);
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 => {
const resultP = E.when(p, awaitedArgs => {
for (let j = 0; j < awaitIndexes.length; j += 1) {
rawArgs[awaitIndexes[j]] = awaitedArgs[j];
}
fit(harden(rawArgs), rawArgGuards, `${label}: args`);
const resultP = apply(rawMethod, undefined, [context, ...rawArgs]);
return E.when(resultP, result => {
fit(result, returnGuard, `${label}: result`);
return result;
});
return apply(rawMethod, context, rawArgs);
});
return E.when(resultP, result => {
fit(result, returnGuard, `${label}: result`);
return result;
});
},
};
Expand All @@ -133,47 +141,106 @@ const defendMethod = (rawMethod, contextMapStore, methodGuard, label) => {
}
};

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

export const defendVTable = (rawVTable, contextMapStore, iface) => {
const { klass, farName, methodGuards } = iface;
const defendPrototype = (
className,
rawPrototype,
contextMapStore,
interfaceGuard,
) => {
const { klass, interfaceName, methodGuards } = interfaceGuard;
assert(klass === 'Interface');
assert.typeof(farName, 'string');
assert.typeof(interfaceName, 'string');

const methodGuardNames = ownKeys(methodGuards);
for (const methodGuardName of methodGuardNames) {
assert(
methodGuardName in rawVTable,
X`${q(methodGuardName)} not implemented by ${rawVTable}`,
methodGuardName in rawPrototype,
X`${q(methodGuardName)} not implemented by ${q(className)}`,
);
}
const methodNames = ownKeys(rawVTable);
const methodNames = ownKeys(rawPrototype).filter(
// Drop the constructor. Do not defend it. `defendClass` will make
// a defensive constructor by other means.
mName => mName !== 'constructor',
);
// like Object.entries, but unenumerable and symbol as well.
const rawMethodEntries = methodNames.map(mName => [mName, rawVTable[mName]]);
const rawMethodEntries = methodNames.map(mName => [
mName,
rawPrototype[mName],
]);
const defensiveMethodEntries = rawMethodEntries.map(([mName, rawMethod]) => {
const methodGuard = methodGuards[mName] || defaultMethodGuard;
assert(
mName in methodGuards,
X`${q(`${className}.${mName}`)} method not guarded by ${interfaceName}`,
);
const methodGuard = methodGuards[mName];
const defensiveMethod = defendMethod(
rawMethod,
contextMapStore,
methodGuard,
`${farName}.${mName}`,
`${className}.${mName}`,
);
return [mName, defensiveMethod];
});
// Return the defensive VTable, which can be use on a shared
// Return the defensivePrototype, 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);
const defensivePrototype = fromEntries(defensiveMethodEntries);
const remotableProto = create(Object.prototype, {
[PASS_STYLE]: { value: 'remotable' },
[Symbol.toStringTag]: { value: `Alleged: ${farName}` },
[Symbol.toStringTag]: { value: `Alleged: ${className}` },
});
setPrototypeOf(defensivePrototype, remotableProto);
harden(defensivePrototype);
assertRemotable(defensivePrototype);
return defensivePrototype;
};

export const defendRepresentative = (
interfaceGuard,
RawClass,
contextMapStore,
) => {
const { name: className, init, finish } = RawClass;
assert.typeof(className, 'string');
assert(interfaceGuard.klass === 'Interface');
const defensivePrototype = defendPrototype(
className,
RawClass.prototype,
contextMapStore,
interfaceGuard,
);

// Purposeful use of 'function' keyword so it works as function and
// as constructor.
const DefensiveClass = function DefensiveClass(...args) {
assert(
new.target === undefined || new.target === DefensiveClass,
X`inheritance of defensive classes not yet implemented: ${q(new.target)}`,
);
// Don't freeze state
const state = seal(init(...args));
const self = harden(create(defensivePrototype));
const context = freeze({ state, self });
contextMapStore.init(self, context);
if (finish) {
finish(context);
}
return self;
};
defineProperties(DefensiveClass, {
name: { value: className },
length: { value: init.length },
prototype: defensivePrototype,
});
setPrototypeOf(defensiveVTable, remotableProto);
harden(defensiveVTable);
assertRemotable(defensiveVTable);
return defensiveVTable;
return harden(DefensiveClass);
};
harden(defendVTable);
harden(defendRepresentative);

export const defendClass = (interfaceGuard, RawClass) =>
// legacyWeakMap to avoid hardening `state`
defendRepresentative(interfaceGuard, RawClass, makeLegacyWeakMap());
harden(defendClass);
41 changes: 32 additions & 9 deletions packages/store/test/test-interface-tools.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,54 @@
/* eslint-disable max-classes-per-file */
// @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 { I, defendClass } from '../src/patterns/interface-tools.js';
import { M } from '../src/patterns/patternMatchers.js';

const UniRawClass = class UniRawClass {
state;

self;
};

// const MultiRawClass = class MultiRawClass {
// state;
//
// facets;
// };

test('how far-able is defineHeapKind', t => {
const bobIFace = I.interface('bob', {
const bobI = I.interface('bobI', {
foo: I.call(M.number()).returns(M.undefined()),
});
const makeBob = defineHeapKind(bobIFace, field => ({ field }), {
foo: ({ state, self }, carol) => {

const BobRawClass = class BobRawClass extends UniRawClass {
// @ts-expect-error Purposely overriding the builtin function.name
static name = 'Bob';

static init(field) {
return { field };
}

foo(carol) {
const { state, self } = this;
t.is(state.field, 8);
t.is(typeof self.foo, 'function');
t.is(self.foo.name, 'foo');
t.is(self.foo.length, 1);
t.is(carol, 77);
state.field += carol;
t.is(state.field, 85);
},
});
const bob = makeBob(8);
}
};

const Bob = defendClass(bobI, BobRawClass);
const bob = Bob(8);
t.is(passStyleOf(bob), 'remotable');
bob.foo(77);
t.throws(() => bob.foo(true), {
message: /^bob.foo: args: \[0\]: boolean true - Must be a number$/,
message: /^Bob.foo: args: \[0\]: boolean true - Must be a number$/,
});
});

0 comments on commit bf37be3

Please sign in to comment.