Skip to content

Commit

Permalink
feat(pass-style,exo): label remotable instances
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Apr 13, 2023
1 parent e3760e1 commit 56edc68
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 26 deletions.
38 changes: 33 additions & 5 deletions packages/exo/src/exo-makers.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
/* global globalThis */
import { objectMap } from '@endo/patterns';

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

const { seal, freeze } = Object;
const { seal, freeze, defineProperty } = Object;

// TODO Use environment-options.js currently in ses/src after factoring it out
// to a new package.
const env = (globalThis.process || {}).env || {};

// Turn on to give each exo instance its own toStringTag value.
const LABEL_INSTANCES = (env.DEBUG || '')
.split(':')
.includes('label-instances');

const makeSelf = (proto, instanceCount) => {
const self = { __proto__: proto };
if (LABEL_INSTANCES) {
defineProperty(self, Symbol.toStringTag, {
value: `${proto[Symbol.toStringTag]}#${instanceCount}`,
writable: false,
enumerable: false,
configurable: false,
});
}
return harden(self);
};

const emptyRecord = harden({});

Expand Down Expand Up @@ -66,22 +89,25 @@ export const defineExoClass = (
) => {
/** @type {WeakMap<M,ClassContext<ReturnType<I>, M>>} */
const contextMap = new WeakMap();
const prototype = defendPrototype(
const proto = defendPrototype(
tag,
self => /** @type {any} */ (contextMap.get(self)),
methods,
true,
interfaceGuard,
);
let instanceCount = 0;
/**
* @param {Parameters<I>} args
*/
const makeInstance = (...args) => {
// Be careful not to freeze the state record
const state = seal(init(...args));
instanceCount += 1;
/** @type {M} */
// @ts-expect-error could be instantiated with different subtype
const self = harden({ __proto__: prototype });
const self = makeSelf(proto, instanceCount);

// Be careful not to freeze the state record
/** @type {ClassContext<ReturnType<I>,M>} */
const context = freeze({ state, self });
Expand Down Expand Up @@ -125,6 +151,7 @@ export const defineExoClassKit = (
true,
interfaceGuardKit,
);
let instanceCount = 0;
/**
* @param {Parameters<I>} args
*/
Expand All @@ -133,8 +160,9 @@ export const defineExoClassKit = (
const state = seal(init(...args));
// Don't freeze context until we add facets
const context = { state };
const facets = objectMap(prototypeKit, (prototype, facetName) => {
const self = harden({ __proto__: prototype });
instanceCount += 1;
const facets = objectMap(prototypeKit, (proto, facetName) => {
const self = makeSelf(proto, instanceCount);
contextMapKit[facetName].set(self, context);
return self;
});
Expand Down
9 changes: 9 additions & 0 deletions packages/exo/test/prepare-test-env-ava-label-instances.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* global globalThis */

export * from './prepare-test-env-ava.js';

// TODO Use environment-options.js currently in ses/src after factoring it out
// to a new package.
const env = (globalThis.process || {}).env || {};

env.DEBUG = 'label-instances';
3 changes: 3 additions & 0 deletions packages/exo/test/test-heap-classes.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// 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 {
Expand Down Expand Up @@ -107,6 +109,7 @@ test('test makeExo', t => {
t.throws(() => upCounter.incr(-3), {
message: 'In "incr" method of (upCounter): arg 0?: -3 - Must be >= 0',
});
// @ts-expect-error deliberately bad arg for testing
t.throws(() => upCounter.incr('foo'), {
message:
'In "incr" method of (upCounter): arg 0?: string "foo" - Must be a number',
Expand Down
113 changes: 113 additions & 0 deletions packages/exo/test/test-label-instances.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// eslint-disable-next-line import/order
import { test } from './prepare-test-env-ava-label-instances.js';

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

const { quote: q } = assert;

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

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

test('test defineExoClass', t => {
const makeUpCounter = defineExoClass(
'UpCounter',
UpCounterI,
/** @param {number} x */
(x = 0) => ({ x }),
{
incr() {
const { state } = this;
state.x += 1;
return state.x;
},
},
);
const up1 = makeUpCounter(3);
const up2 = makeUpCounter(7);
t.is(passStyleOf(up1), 'remotable');
t.is(`${up1}`, '[object Alleged: UpCounter#1]');
t.is(`${q(up1)}`, '"[Alleged: UpCounter#1]"');

t.is(passStyleOf(up2), 'remotable');
t.is(`${up2}`, '[object Alleged: UpCounter#2]');
t.is(`${q(up2)}`, '"[Alleged: UpCounter#2]"');
});

test('test defineExoClassKit', t => {
const makeCounterKit = defineExoClassKit(
'Counter',
{ up: UpCounterI, down: DownCounterI },
/** @param {number} x */
(x = 0) => ({ x }),
{
up: {
incr() {
const { state } = this;
state.x += 1;
return state.x;
},
},
down: {
decr() {
const { state } = this;
state.x -= 1;
return state.x;
},
},
},
);
const { up: up1, down: down1 } = makeCounterKit(3);
const { up: up2, down: down2 } = makeCounterKit(7);

t.is(passStyleOf(up1), 'remotable');
t.is(`${up1}`, '[object Alleged: Counter up#1]');
t.is(`${q(up1)}`, '"[Alleged: Counter up#1]"');

t.is(passStyleOf(up2), 'remotable');
t.is(`${up2}`, '[object Alleged: Counter up#2]');
t.is(`${q(up2)}`, '"[Alleged: Counter up#2]"');

t.is(passStyleOf(down1), 'remotable');
t.is(`${down1}`, '[object Alleged: Counter down#1]');
t.is(`${q(down1)}`, '"[Alleged: Counter down#1]"');

t.is(passStyleOf(down2), 'remotable');
t.is(`${down2}`, '[object Alleged: Counter down#2]');
t.is(`${q(down2)}`, '"[Alleged: Counter down#2]"');
});

test('test makeExo', t => {
let x = 3;
const up1 = makeExo('upCounterA', UpCounterI, {
incr() {
x += 1;
return x;
},
});
const up2 = makeExo('upCounterB', UpCounterI, {
incr() {
x += 1;
return x;
},
});

t.is(passStyleOf(up1), 'remotable');
t.is(`${up1}`, '[object Alleged: upCounterA#1]');
t.is(`${q(up1)}`, '"[Alleged: upCounterA#1]"');

t.is(passStyleOf(up2), 'remotable');
t.is(`${up2}`, '[object Alleged: upCounterB#1]');
t.is(`${q(up2)}`, '"[Alleged: upCounterB#1]"');
});
2 changes: 1 addition & 1 deletion packages/far/test/test-marshal-far-obj.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ const NON_METHOD = {
};
const IFACE_ALLEGED = {
message:
/For now, iface "Bad remotable proto" must be "Remotable" or begin with "Alleged: "; unimplemented/,
/For now, iface "Bad remotable proto" must be "Remotable" or begin with "Alleged: " or "DebugName: "; unimplemented/,
};
const UNEXPECTED_PROPS = {
message: /Unexpected properties on Remotable Proto .*/,
Expand Down
2 changes: 1 addition & 1 deletion packages/marshal/test/test-marshal-far-obj.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const NON_METHOD = {
};
const IFACE_ALLEGED = {
message:
/For now, iface "Bad remotable proto" must be "Remotable" or begin with "Alleged: "; unimplemented/,
/For now, iface "Bad remotable proto" must be "Remotable" or begin with "Alleged: " or "DebugName: "; unimplemented/,
};
const UNEXPECTED_PROPS = {
message: /Unexpected properties on Remotable Proto .*/,
Expand Down
6 changes: 4 additions & 2 deletions packages/marshal/test/test-marshal-smallcaps.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,11 +301,13 @@ test('smallcaps encoding examples', t => {

// Remotables
const foo = Far('foo', {});
const bar = Far('bar', {});
const bar = Far('bar', {
[Symbol.toStringTag]: 'DebugName: Bart',
});
assertRoundTrip(foo, '#"$0.Alleged: foo"', [foo], 'Remotable object');
assertRoundTrip(
harden([foo, bar, foo, bar]),
'#["$0.Alleged: foo","$1.Alleged: bar","$0","$1"]',
'#["$0.Alleged: foo","$1.DebugName: Bart","$0","$1"]',
[foo, bar],
'Only show iface once',
);
Expand Down
7 changes: 4 additions & 3 deletions packages/pass-style/src/make-far.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ const assertCanBeRemotable = candidate =>
* @template {{}} T
* @param {InterfaceSpec} [iface='Remotable'] The interface specification for
* the remotable. For now, a string iface must be "Remotable" or begin with
* "Alleged: ", to serve as the alleged name. More general ifaces are not yet
* implemented. This is temporary. We include the
* "Alleged" as a reminder that we do not yet have SwingSet or Comms Vat
* "Alleged: " or "DebugName: ", to serve as the alleged name. More
* general ifaces are not yet implemented. This is temporary. We include the
* "Alleged" or "DebugName" as a reminder that we do not yet have SwingSet
* or Comms Vat
* support for ensuring this is according to the vat hosting the object.
* Currently, Alice can tell Bob about Carol, where VatA (on Alice's behalf)
* misrepresents Carol's `iface`. VatB and therefore Bob will then see
Expand Down
39 changes: 27 additions & 12 deletions packages/pass-style/src/remotable.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ const checkIface = (iface, check) => {
))) &&
(iface === 'Remotable' ||
iface.startsWith('Alleged: ') ||
iface.startsWith('DebugName: ') ||
(reject &&
reject(
X`For now, iface ${q(
iface,
)} must be "Remotable" or begin with "Alleged: "; unimplemented`,
)} must be "Remotable" or begin with "Alleged: " or "DebugName: "; unimplemented`,
)))
);
};
Expand Down Expand Up @@ -213,22 +214,29 @@ export const RemotableHelper = harden({
String(key),
)} in ${candidate}`,
))) &&
(canBeMethod(candidate[key]) ||
(reject &&
reject(
X`cannot serialize Remotables with non-methods like ${q(
String(key),
)} in ${candidate}`,
))) &&
(key !== PASS_STYLE ||
(reject &&
reject(X`A pass-by-remote cannot shadow ${q(PASS_STYLE)}`)))
((key === Symbol.toStringTag && checkIface(candidate[key], check)) ||
((canBeMethod(candidate[key]) ||
(reject &&
reject(
X`cannot serialize Remotables with non-methods like ${q(
String(key),
)} in ${candidate}`,
))) &&
(key !== PASS_STYLE ||
(reject &&
reject(X`A pass-by-remote cannot shadow ${q(PASS_STYLE)}`)))))
);
});
} else if (typeof candidate === 'function') {
// Far functions cannot be methods, and cannot have methods.
// They must have exactly expected `.name` and `.length` properties
const { name: nameDesc, length: lengthDesc, ...restDescs } = descs;
const {
name: nameDesc,
length: lengthDesc,
// @ts-ignore TS doesn't like symbols as computed indexes??
[Symbol.toStringTag]: toStringTagDesc,
...restDescs
} = descs;
const restKeys = ownKeys(restDescs);
return (
((nameDesc && typeof nameDesc.value === 'string') ||
Expand All @@ -239,6 +247,13 @@ export const RemotableHelper = harden({
reject(
X`Far function length must be a number, in ${candidate}`,
))) &&
(toStringTagDesc === undefined ||
((typeof toStringTagDesc.value === 'string' ||
(reject &&
reject(
X`Far function @@toStringTag must be a string, in ${candidate}`,
))) &&
checkIface(toStringTagDesc.value, check))) &&
(restKeys.length === 0 ||
(reject &&
reject(
Expand Down
37 changes: 35 additions & 2 deletions packages/pass-style/test/test-passStyleOf.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* eslint-disable max-classes-per-file */
import { test } from './prepare-test-env-ava.js';

import { passStyleOf } from '../src/passStyleOf.js';
import { Far } from '../src/make-far.js';
import { makeTagged } from '../src/makeTagged.js';
import { PASS_STYLE } from '../src/passStyle-helpers.js';

const { getPrototypeOf } = Object;
const { quote: q } = assert;
const { getPrototypeOf, defineProperty } = Object;
const { ownKeys } = Reflect;

test('passStyleOf basic success cases', t => {
Expand Down Expand Up @@ -213,7 +215,7 @@ test('passStyleOf testing remotables', t => {
});
t.throws(() => passStyleOf(farObj5), {
message:
/For now, iface "Not alleging" must be "Remotable" or begin with "Alleged: "; unimplemented/,
/For now, iface "Not alleging" must be "Remotable" or begin with "Alleged: " or "DebugName: "; unimplemented/,
});

const tagRecord6 = makeTagishRecord('Alleged: manually constructed');
Expand Down Expand Up @@ -397,3 +399,34 @@ test('remotables - safety from the gibson042 attack', t => {
'Errors must inherit from an error class .prototype "[undefined: undefined]"',
});
});

test('Allow toStringTag overrides', t => {
const alice = Far('Alice', { [Symbol.toStringTag]: 'DebugName: Allison' });
t.is(passStyleOf(alice), 'remotable');
t.is(`${alice}`, '[object DebugName: Allison]');
t.is(`${q(alice)}`, '"[DebugName: Allison]"');

const carol = harden({ __proto__: alice });
t.is(passStyleOf(carol), 'remotable');
t.is(`${carol}`, '[object DebugName: Allison]');
t.is(`${q(carol)}`, '"[DebugName: Allison]"');

const bob = harden({
__proto__: carol,
[Symbol.toStringTag]: 'DebugName: Robert',
});
t.is(passStyleOf(bob), 'remotable');
t.is(`${bob}`, '[object DebugName: Robert]');
t.is(`${q(bob)}`, '"[DebugName: Robert]"');

const fred = () => {};
t.is(fred.name, 'fred');
defineProperty(fred, Symbol.toStringTag, { value: 'DebugName: Friedrich' });
const f = Far('Fred', fred);
t.is(f, fred);
t.is(passStyleOf(fred), 'remotable');
t.is(`${fred}`, '() => {}');
t.is(Object.prototype.toString.call(fred), '[object DebugName: Friedrich]');
t.is(fred.name, 'fred');
t.is(`${q(fred)}`, '"[Function fred]"');
});

0 comments on commit 56edc68

Please sign in to comment.