Skip to content

Commit

Permalink
feat: implement multifaceted virtual objects
Browse files Browse the repository at this point in the history
closes #3834
  • Loading branch information
FUDCo committed Mar 18, 2022
1 parent 3d75325 commit 0696866
Show file tree
Hide file tree
Showing 13 changed files with 1,664 additions and 707 deletions.
126 changes: 93 additions & 33 deletions packages/SwingSet/docs/virtual-objects.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,44 +25,42 @@ The `descriptionTag` parameter is a short description string for the kind. This

The `init` parameter is a function that will be called when new instances are first created. It is expected to return a simple JavaScript object that represents the initialized state for the new virtual object instance. Any parameters passed to the maker function returned by `defineKind` are passed directly to the `init` function.

The `actualize` parameter is a function that binds an in-memory instance (the "Representation) of the virtual object with the virtual object's state, associating such instances with the virtual object's behavior. It is passed the virtual object's state as a parameter and is expected to return a new Javascript object with methods that close over the given state. This returned object will become the body of the new instance. It is called whenever a new virtual object instance is created, whenever such an instance is swapped in from secondary storage, and whenever a reference to a virtual object is received as a parameter of a message and deserialized.
The `actualize` parameter is a function that binds an in-memory instance (the "Representative") of the virtual object with the virtual object's state, associating such instances with the virtual object's behavior. It is passed the virtual object's state as a parameter and is expected to return either:

1. A new JavaScript object with methods that close over the given state. This returned object will become the body of the new instance. This object can be empty; in such a case it can serve as a powerless but unforgeable "marker" handle.

2. A new JavaScript object populated with objects as described in (1). These will become facets of the new instance. The returned object will be an object mapping to the facets by name.

The `actualize` function is called whenever a new virtual object instance is created, whenever such an instance is swapped in from secondary storage, and whenever a reference to a virtual object is received as a parameter of a message and deserialized. The `actualize` function for a given VDO kind must always return the same shape of result: if it returns a single instance body, it must always return a single instance body; if it returns a object full of facets, it must always return an object with the exact same facet names.

The `finish` parameter will, if present (it is optional), be called exactly once as part of instance initialization. It will be invoked immediately after the `actualize` function is called for the first time. In other words, it will be called after the instance per se exists but before that instance is returned from the maker function and thus becomes available to whoever requested its creation. `finish` is passed two parameters: the virtual object's state (exactly as passed to the `actualize` function) and the virtual object itself. The `finish` function can modify the object's state in the context of knowing the object's identity, and thus can be used in cases where a validly initialized instance requires it to participate in some kind of cyclical object graph with other virtual objects. It can also be used, for example, to register the object with outside tracking data structures, or do whatever other post-creation setup is needed for the object to do its job. In particular, if one or more of the object's methods need to refer to the object itself (for example, so it can pass itself to other objects), the `finish` function provides a way to capture that identity as part of the object's state.

For example:

```javascript
function initCounter(name) {
return {
counter: 0,
name,
};
}
function actualizeCounter(state) {
return {
inc() {
state.counter += 1;
},
dec() {
state.counter -= 1;
},
reset() {
state.counter = 0;
},
rename(newName) {
state.name = newName;
},
getCount() {
return state.counter;
},
getName() {
return state.name;
},
};
}
function finishCounter(state, counter) {
const initCounter = (name) => ({ counter: 0, name });

const actualizeCounter = (state) => ({
inc: () => {
state.counter += 1;
},
dec: () => {
state.counter -= 1;
},
reset: () => {
state.counter = 0;
},
rename: (newName) => {
state.name = newName;
},
getCount: () => state.counter,
getName: () => state.name,
});

const finishCounter = (state, counter) => {
addToCounterRegistry(counter, state.name);
}
};

const makeCounter = defineKind('counter', initCounter, actualizeCounter, finishCounter);
```

Expand All @@ -80,11 +78,73 @@ This defines a simple counter object with two properties in its state, a count a
console.log(`${barCounter.getName()} count is ${barCounter.getCount()`); // "new bar count is 1"
```
Note that the `init`, `actualize`, and `finish` functions are defined explicitly here for clarity of exposition, but in practice you'd usually declare them inline as arrow functions in the parameters of the `defineKind` call.
Suppose you instead wanted to provide the the increment and decrement capabilities as independent facets. A simplified version of the above (without the name property, counter registry, and `reset` method) might look like:
```javascript
const initFacetedCounter = () => ({ counter: 0 });
const actualizeFacetedCounter = (state) => {
const getCount = () => state.counter;
return {
incr: {
step: () => {
state.counter += 1;
},
getCount,
},
decr: {
step: () => {
state.counter -= 1;
},
getCount,
},
};
}
const makeFacetedCounter = defineKind('counter', initCounter, actualizeCounter);
```
Which you'd use like:
```javascript
const { incr, decr } = makeFacetedCounter('foo');
incr.step();
incr.step();
console.log(`count is ${decr.getCount()`); // "count is 2"
decr.step();
console.log(`count is ${incr.getCount()`); // "count is 1"
```
Note that the `init`, `actualize`, and `finish` functions are defined explicitly in the above examples for clarity of exposition, but in practice you'd usually declare them inline in the parameters of the `defineKind` call:
```javascript
const makeFacetedCounter = defineKind(
'counter',
() => ({ counter: 0 }),
(state) => {
const getCount = () => state.counter;
return {
incr: {
step: () => {
state.counter += 1;
},
getCount,
},
decr: {
step: () => {
state.counter -= 1;
},
getCount,
},
};
},
);
```
Additional important details:
- The set of state properties is fully determined by the `init` function. That is, the set of properties that exist on `state` is completely determined by the enumerable properties of the object that `init` returns. State properties cannot thereafter be added or removed.
- The set of state properties of an instance is fully determined by the `init` function. That is, the set of properties that exist on in instance's `state` is completely determined by the enumerable properties of the object that `init` returns. State properties cannot thereafter be added or removed. Currently there is no requirement that all instances of a given kind have the same set of properties, but code authors should not rely on this as such enforcement may be added in the future.
- The values a state property may take are limited to things that are serializable and which may be hardened (and, in fact, _are_ hardened and serialized the moment they are assigned). That is, you can replace what value a state property _has_, but you cannot modify a state property's value in situ. In other words, you can do things like:
Expand Down
112 changes: 91 additions & 21 deletions packages/SwingSet/src/lib/parseVatSlots.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,100 @@ import { assert, details as X } from '@agoric/assert';
// as badly. Also, "slot" is a short, single syllable word, which is nice.
// But still "slot" implies containership which I think is wrong.)

// Object/promise references (in vats) contain a three-tuple of (type,
// allocator flag, index). The 'ownership' flag is expressed as a sign: "-"
// means the index was allocated by the kernel, and thus the actual object
// lives in some other vat (and any messages sent to it must go into the
// kernel). "+" means the index was allocated by this vat, hence the object
// lives in this vat, and we can expect to receive messages for it.

/**
* Parse a vat slot reference string into a vat slot object:
* Parse a vref string into its component parts:
* {
* type: STRING, // 'object', 'device', 'promise'
* allocatedByVat: BOOL, // true=>allocated by vat, false=>by the kernel
* id: Nat
* id: Nat,
* subid: Nat,
* baseRef: STRING,
* facet: Nat
* }
*
* @param {string} s The string to be parsed, as described above.
* A vref string can take one of the forms:
*
* T-N
* T+N
* T+N/I
* T+N/I:F
*
* Where:
*
* T is a single character encoding the type of entity being referenced: 'd'
* for 'device', 'o' for 'object', or 'p' for 'promise'. One of the string
* values 'device', 'object', or 'promise' is returned as the `type`
* property of the result.
*
* '+' or '-' encodes who allocated the reference: '-' for the kernel
* (typically an import) or '+ for the vat (typically an export). This is
* returned in the `allocatedByVat` property of the result as a boolean.
*
* N is a decimal integer representing the identity of the referenced entity.
* This is returned in the `id` property of the result as a BigInt.
*
* I if present (only allowed if T is 'o') is a decimal integer representing
* the instance id of the referenced object. In this case N denotes a
* category of objects that share a common shape, either one of the store
* types or a virtual object kind, and I indicates which instance of that
* category is being referred to. If present this is returned as the
* `subid` property of the result as a BigInt.
*
* F if present (only allowed if I is also present) is a decimal integer
* referencing a facet of the referenced object. In this case N/I denotes
* a particular object instance and F indicates which of several possible
* facets of that instance is being addressed. If present this is returned
* in the `facet` property of the result as a BigInt.
*
* @returns {*} a vat slot object corresponding to the parameter.
* The `baseRef` property of the result is `vref` stripped of any facet indicator.
*
* @throws if the given string is syntactically incorrect.
* A "vref" identifies an entity visible to vat code to which messages may be
* sent and which may be compared for equality to other such entities. Let's
* call such an entity an "addressable object".
*
* A "baseRef" designates an entity that is managed by LiveSlots, both as a unit
* of garbage collection specifically and as a unit of memory management more
* generally. Such an entity may be a promise or remotable object or imported
* presence, all of which will always be JavaScript objects in memory, or it may
* be a virtual object or collection, which can be in memory or on disk or both.
* Let's call such an entity a "base object". In most cases this is one and the
* same with the addressable object that the vref designates, but in the case of
* a faceted object it is the faceted object as a whole (represented in memory,
* though not on disk, as the cohort array) rather than any particular
* individual facet (the faceted object per se is never exposed directly to code
* running within the vat; only its facets are).
*
* XXX TODO: The previous comment suggests some renaming is warranted:
*
* `slotToVal` maps a baseRef to a base object (actually to a weakRef that
* points to a base object)
* `getValForSlot` maps a baseRef to a base object, or to undefined if it is not
* resident in memory
* `convertSlotToVal` maps a vref to to an addressable object, loading it from
* disk if necessary
*
* `valToSlot` maps an addressable object to a vref
* `getSlotForVal` maps an addressable object to a vref
* `convertValToSlot` maps an addressable object to a vref, generating a new
* vref if necessary
*
* @param {string} vref The string to be parsed, as described above.
*
* @returns {*} a vref components descriptor corresponding to the vref string
* parameter, assuming it is syntactically well formed.
*
* @throws if the given vref string is syntactically incorrect.
*/
export function parseVatSlot(s) {
assert.typeof(s, 'string');
export function parseVatSlot(vref) {
assert.typeof(vref, 'string');
const parts = vref.split(':');
assert(parts.length === 1 || parts.length === 2, X`invalid vref ${vref}`);
const [baseRef, facetStr] = parts;
let type;
let allocatedByVat;
const typechar = s[0];
const allocchar = s[1];
const idSuffix = s.slice(2);
const typechar = baseRef[0];
const allocchar = baseRef[1];
const idSuffix = baseRef.slice(2);

if (typechar === 'o') {
type = 'object';
Expand All @@ -48,31 +114,35 @@ export function parseVatSlot(s) {
} else if (typechar === 'p') {
type = 'promise';
} else {
assert.fail(X`invalid vatSlot ${s}`);
assert.fail(X`invalid vref ${vref}`);
}

if (allocchar === '+') {
allocatedByVat = true;
} else if (allocchar === '-') {
allocatedByVat = false;
} else {
assert.fail(X`invalid vatSlot ${s}`);
assert.fail(X`invalid vref ${vref}`);
}

const delim = idSuffix.indexOf('/');
let id;
let subid;
let facet;
let virtual = false;
if (delim > 0) {
assert(type === 'object' && allocatedByVat, X`invalid vatSlot ${s}`);
assert(type === 'object' && allocatedByVat, X`invalid vref ${vref}`);
virtual = true;
id = Nat(BigInt(idSuffix.substr(0, delim)));
subid = Nat(BigInt(idSuffix.slice(delim + 1)));
} else {
id = Nat(BigInt(idSuffix));
}
if (subid !== undefined && facetStr !== undefined) {
facet = Nat(BigInt(facetStr));
}

return { type, allocatedByVat, virtual, id, subid };
return { type, allocatedByVat, virtual, id, subid, baseRef, facet };
}

/**
Expand Down
10 changes: 5 additions & 5 deletions packages/SwingSet/src/liveslots/collectionManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function makeCollectionManager(
allocateExportID,
convertValToSlot,
convertSlotToVal,
registerEntry,
registerValue,
serialize,
unserialize,
) {
Expand Down Expand Up @@ -660,7 +660,7 @@ export function makeCollectionManager(
valueSchema,
);
const store = collectionToMapStore(collection);
registerEntry(vobjID, store);
registerValue(vobjID, store, false);
return store;
}

Expand All @@ -687,7 +687,7 @@ export function makeCollectionManager(
valueSchema,
);
const store = collectionToWeakMapStore(collection);
registerEntry(vobjID, store);
registerValue(vobjID, store, false);
return store;
}

Expand All @@ -712,7 +712,7 @@ export function makeCollectionManager(
valueSchema,
);
const store = collectionToSetStore(collection);
registerEntry(vobjID, store);
registerValue(vobjID, store, false);
return store;
}

Expand All @@ -739,7 +739,7 @@ export function makeCollectionManager(
valueSchema,
);
const store = collectionToWeakSetStore(collection);
registerEntry(vobjID, store);
registerValue(vobjID, store, false);
return store;
}

Expand Down
Loading

0 comments on commit 0696866

Please sign in to comment.