Skip to content

Commit

Permalink
Merge pull request #2034: feat(cli,daemon): Support dot-delimited pet…
Browse files Browse the repository at this point in the history
…name paths for CLI `eval` command

Ref: #2023 

## Description

Adds support for dot-delimited petname paths to the CLI `eval` command. This required modifications to the `evaluate` method of the daemon's `host.js`, and the introduction of a new formula type, `lookup`.

The `lookup` formula type is necessary because the names in a lookup path may have neither petnames nor formula identifiers associated with them. Consider:

```text
> endo eval '10' --name ten
10
> endo eval 'foo' foo:INFO.ten.source
10
```

The only way retrieve the value for `source` is to call `E(HOST).lookup('INFO', 'ten', 'source')`, and since the `eval` formula expects its values to be mediated by formula identifiers, the lookup of `INFO.ten.source` must itself be stored as a formula.

The `lookup` formula is an `-id512` formula with the following interface:
```typescript
type LookupFormula = {
  type: 'lookup';

  /**
   * The formula identifier of the naming hub to call lookup on.
   * A "naming hub" is an object with a variadic `lookup()` method.
   */
  hub: string;

  /**
   * The pet name path.
   */
  path: Array<string>;
};
```

The `lookup` formula introduces the language of "naming hubs" into the daemon codebase. At present the only such objects are guests and hosts, which are also agents, but the category of naming hubs is expected to grow to include non-agent objects.
  • Loading branch information
rekmarks authored Feb 8, 2024
2 parents e02e9f2 + e6f36a1 commit 4a3c8c0
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 16 deletions.
5 changes: 3 additions & 2 deletions packages/cli/src/commands/eval.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os from 'os';
import { E } from '@endo/far';
import { withEndoParty } from '../context.js';
import { parsePetNamePath } from '../pet-name.js';

export const evalCommand = async ({
source,
Expand All @@ -27,13 +28,13 @@ export const evalCommand = async ({
return pair;
});
const codeNames = pairs.map(pair => pair[0]);
const endowmentNames = pairs.map(pair => pair[1]);
const petNames = pairs.map(pair => parsePetNamePath(pair[1]));

const result = await E(party).evaluate(
workerName,
source,
codeNames,
endowmentNames,
petNames,
resultName,
);
console.log(result);
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/src/pet-name.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Splits a dot-delimited pet name path into an array of pet names.
* Throws if any of the path segments are empty.
*
* @param {string} petNamePath - A dot-delimited pet name path.
* @returns {string[]} - The pet name path, as an array of pet names.
*/
export const parsePetNamePath = petNamePath => {
const petNames = petNamePath.split('.');
for (const petName of petNames) {
if (petName === '') {
throw new Error(
`Pet name path "${petNamePath}" contains an empty segment.`,
);
}
}
return petNames;
};
45 changes: 44 additions & 1 deletion packages/daemon/src/daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,29 @@ const makeEndoBootstrap = (
return { external, internal: undefined };
};

/**
* Creates a controller for a `lookup` formula. The external facet is the
* resolved value of the lookup.
*
* @param {string} hubFormulaIdentifier
* @param {string[]} path
* @param {import('./types.js').Terminator} terminator
*/
const makeControllerForLookup = async (
hubFormulaIdentifier,
path,
terminator,
) => {
terminator.thisDiesIfThatDies(hubFormulaIdentifier);

// Behold, recursion:
// eslint-disable-next-line no-use-before-define
const hub = provideValueForFormulaIdentifier(hubFormulaIdentifier);

const external = E(hub).lookup(...path);
return { external, internal: undefined };
};

/**
* @param {string} workerFormulaIdentifier
* @param {string} guestFormulaIdentifier
Expand Down Expand Up @@ -325,6 +348,8 @@ const makeEndoBootstrap = (
formula.values,
terminator,
);
} else if (formula.type === 'lookup') {
return makeControllerForLookup(formula.hub, formula.path, terminator);
} else if (formula.type === 'make-unconfined') {
return makeControllerForUnconfinedPlugin(
formula.worker,
Expand Down Expand Up @@ -491,6 +516,12 @@ const makeEndoBootstrap = (
// controllerForFormulaIdentifier and formulaIdentifierForRef, since the
// former bypasses the latter in order to avoid a round trip with disk.

/**
* @param {string} formulaType - The type of the formula.
* @param {string} formulaNumber - The number of the formula.
* @param {import('./types').Formula} formula - The formula.
* @returns {Promise<{ formulaIdentifier: string, value: unknown }>} The value of the formula.
*/
const provideValueForNumberedFormula = async (
formulaType,
formulaNumber,
Expand All @@ -508,7 +539,7 @@ const makeEndoBootstrap = (
// Behold, recursion:
// eslint-disable-next-line no-use-before-define
const terminator = makeTerminator(formulaIdentifier);
partial.catch(error => terminator.terminate());
partial.catch(() => terminator.terminate());
const controller = harden({
terminator,
external: E.get(partial).external.then(value => {
Expand Down Expand Up @@ -599,6 +630,8 @@ const makeEndoBootstrap = (
formulaIdentifierForRef,
provideValueForFormulaIdentifier,
provideControllerForFormulaIdentifier,
makeSha512,
provideValueForNumberedFormula,
});

const makeIdentifiedGuestController = makeGuestMaker({
Expand Down Expand Up @@ -647,6 +680,7 @@ const makeEndoBootstrap = (
if (
![
'eval-id512',
'lookup-id512',
'make-unconfined-id512',
'make-bundle-id512',
'guest-id512',
Expand Down Expand Up @@ -676,6 +710,15 @@ const makeEndoBootstrap = (
worker: provideValueForFormulaIdentifier(formula.worker),
}),
);
} else if (formula.type === 'lookup') {
return makeInspector(
formula.type,
formulaNumber,
harden({
hub: provideValueForFormulaIdentifier(formula.hub),
path: formula.path,
}),
);
} else if (formula.type === 'guest') {
return makeInspector(
formula.type,
Expand Down
35 changes: 23 additions & 12 deletions packages/daemon/src/host.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-check

import { Far } from '@endo/far';
import { assertPetName } from './pet-name.js';
import { assertPetName, petNamePathFrom } from './pet-name.js';

const { quote: q } = assert;

Expand Down Expand Up @@ -43,6 +43,7 @@ export const makeHostMaker = ({
reverseLookup,
lookupFormulaIdentifierForName,
listMessages,
provideLookupFormula,
followMessages,
resolve,
reject,
Expand Down Expand Up @@ -185,15 +186,15 @@ export const makeHostMaker = ({
/**
* @param {string | 'MAIN' | 'NEW'} workerName
* @param {string} source
* @param {Array<string>} codeNames
* @param {Array<string>} petNames
* @param {string[]} codeNames
* @param {(string | string[])[]} petNamePaths
* @param {string} resultName
*/
const evaluate = async (
workerName,
source,
codeNames,
petNames,
petNamePaths,
resultName,
) => {
const workerFormulaIdentifier = await provideWorkerFormulaIdentifier(
Expand All @@ -203,24 +204,34 @@ export const makeHostMaker = ({
if (resultName !== undefined) {
assertPetName(resultName);
}
if (petNames.length !== codeNames.length) {
if (petNamePaths.length !== codeNames.length) {
throw new Error('Evaluator requires one pet name for each code name');
}

const formulaIdentifiers = await Promise.all(
petNames.map(async (petName, index) => {
petNamePaths.map(async (petNameOrPath, index) => {
if (typeof codeNames[index] !== 'string') {
throw new Error(`Invalid endowment name: ${q(codeNames[index])}`);
}
const formulaIdentifier = lookupFormulaIdentifierForName(petName);
if (formulaIdentifier === undefined) {
throw new Error(`Unknown pet name ${q(petName)}`);

const petNamePath = petNamePathFrom(petNameOrPath);
if (petNamePath.length === 1) {
const formulaIdentifier = lookupFormulaIdentifierForName(
petNamePath[0],
);
if (formulaIdentifier === undefined) {
throw new Error(`Unknown pet name ${q(petNamePath[0])}`);
}
return formulaIdentifier;
}
return formulaIdentifier;

const { formulaIdentifier: lookupFormulaIdentifier } =
await provideLookupFormula(petNamePath);
return lookupFormulaIdentifier;
}),
);

const formula = {
const evalFormula = {
/** @type {'eval'} */
type: 'eval',
worker: workerFormulaIdentifier,
Expand All @@ -232,7 +243,7 @@ export const makeHostMaker = ({
// Behold, recursion:
// eslint-disable-next-line no-use-before-define
const { formulaIdentifier, value } = await provideValueForFormula(
formula,
evalFormula,
'eval-id512',
);
if (resultName !== undefined) {
Expand Down
37 changes: 37 additions & 0 deletions packages/daemon/src/mail.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const makeMailboxMaker = ({
provideValueForFormulaIdentifier,
provideControllerForFormulaIdentifier,
formulaIdentifierForRef,
makeSha512,
provideValueForNumberedFormula,
}) => {
const makeMailbox = ({
selfFormulaIdentifier,
Expand Down Expand Up @@ -98,6 +100,40 @@ export const makeMailboxMaker = ({
return reverseLookupFormulaIdentifier(formulaIdentifier);
};

/**
* Takes a sequence of pet names and returns a formula identifier and value
* for the corresponding lookup formula.
*
* @param {string[]} petNamePath - A sequence of pet names.
* @returns {Promise<{ formulaIdentifier: string, value: unknown }>} The formula
* identifier and value of the lookup formula.
*/
const provideLookupFormula = async petNamePath => {
// The lookup formula identifier consists of the hash of the associated
// naming hub's formula identifier and the pet name path.
// A "naming hub" is an objected with a variadic lookup method. At present,
// the only such objects are guests and hosts.
const hubFormulaIdentifier = lookupFormulaIdentifierForName('SELF');
const digester = makeSha512();
digester.updateText(`${hubFormulaIdentifier},${petNamePath.join(',')}`);
const lookupFormulaNumber = digester.digestHex();

// TODO:lookup Check if the lookup formula already exists in the store

const lookupFormula = {
/** @type {'lookup'} */
type: 'lookup',
hub: hubFormulaIdentifier,
path: petNamePath,
};

return provideValueForNumberedFormula(
'lookup-id512',
lookupFormulaNumber,
lookupFormula,
);
};

/**
* @param {import('./types.js').InternalMessage} message
* @returns {import('./types.js').Message | undefined}
Expand Down Expand Up @@ -518,6 +554,7 @@ export const makeMailboxMaker = ({
reverseLookup,
reverseLookupFormulaIdentifier,
lookupFormulaIdentifierForName,
provideLookupFormula,
followMessages,
listMessages,
request,
Expand Down
8 changes: 8 additions & 0 deletions packages/daemon/src/pet-name.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ export const assertPetName = petName => {
throw new Error(`Invalid pet name ${q(petName)}`);
}
};

/**
* @param {string | string[]} petNameOrPetNamePath
*/
export const petNamePathFrom = petNameOrPetNamePath =>
typeof petNameOrPetNamePath === 'string'
? [petNameOrPetNamePath]
: petNameOrPetNamePath;
2 changes: 1 addition & 1 deletion packages/daemon/src/pet-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const { quote: q } = assert;

const validIdPattern = /^[0-9a-f]{128}$/;
const validFormulaPattern =
/^(?:host|pet-store|pet-inspector|(?:readable-blob-sha512|worker-id512|pet-store-id512|eval-id512|make-unconfined-id512|make-bundle-id512|host-id512|guest-id512):[0-9a-f]{128}|web-bundle:[0-9a-f]{32})$/;
/^(?:host|pet-store|pet-inspector|(?:readable-blob-sha512|worker-id512|pet-store-id512|eval-id512|lookup-id512|make-unconfined-id512|make-bundle-id512|host-id512|guest-id512):[0-9a-f]{128}|web-bundle:[0-9a-f]{32})$/;

/**
* @param {import('./types.js').FilePowers} filePowers
Expand Down
16 changes: 16 additions & 0 deletions packages/daemon/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ type EvalFormula = {
// TODO formula slots
};

type LookupFormula = {
type: 'lookup';

/**
* The formula identifier of the naming hub to call lookup on.
* A "naming hub" is an object with a variadic `lookup()` method.
*/
hub: string;

/**
* The pet name path.
*/
path: Array<string>;
};

type MakeUnconfinedFormula = {
type: 'make-unconfined';
worker: string;
Expand All @@ -97,6 +112,7 @@ type WebBundleFormula = {
export type Formula =
| GuestFormula
| EvalFormula
| LookupFormula
| MakeUnconfinedFormula
| MakeBundleFormula
| WebBundleFormula;
Expand Down
30 changes: 30 additions & 0 deletions packages/daemon/test/test-endo.js
Original file line number Diff line number Diff line change
Expand Up @@ -1041,3 +1041,33 @@ test('lookup with petname path (value has no lookup method)', async t => {

await stop(locator);
});

test('evaluate name resolved by lookup path', async t => {
const { promise: cancelled, reject: cancel } = makePromiseKit();
t.teardown(() => cancel(Error('teardown')));
const locator = makeLocator('tmp', 'name-resolved-by-lookup-path');

await stop(locator).catch(() => {});
await reset(locator);
await start(locator);

const { getBootstrap } = await makeEndoClient(
'client',
locator.sockPath,
cancelled,
);
const bootstrap = getBootstrap();
const host = E(bootstrap).host();

await E(host).evaluate('MAIN', '10', [], [], 'ten');

const resolvedValue = await E(host).evaluate(
'MAIN',
'foo',
['foo'],
[['INFO', 'ten', 'source']],
);
t.is(resolvedValue, '10');

await stop(locator);
});

0 comments on commit 4a3c8c0

Please sign in to comment.