Skip to content

Commit

Permalink
feat(cli,daemon): Support dot-delimited petname paths in eval
Browse files Browse the repository at this point in the history
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`, modeled on the `web-bundle`
formula.

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.
  • Loading branch information
rekmarks committed Feb 5, 2024
1 parent e02e9f2 commit 42cdabc
Show file tree
Hide file tree
Showing 7 changed files with 124 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;
};
40 changes: 39 additions & 1 deletion packages/daemon/src/daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,26 @@ const makeEndoBootstrap = (
return { external, internal: undefined };
};

/**
* @param {string} agentFormulaIdentifier
* @param {string[]} path
* @param {import('./types.js').Terminator} terminator
*/
const makeControllerForLookup = async (
agentFormulaIdentifier,
path,
terminator,
) => {
terminator.thisDiesIfThatDies(agentFormulaIdentifier);

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

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

/**
* @param {string} workerFormulaIdentifier
* @param {string} guestFormulaIdentifier
Expand Down Expand Up @@ -325,6 +345,8 @@ const makeEndoBootstrap = (
formula.values,
terminator,
);
} else if (formula.type === 'lookup') {
return makeControllerForLookup(formula.agent, formula.path, terminator);
} else if (formula.type === 'make-unconfined') {
return makeControllerForUnconfinedPlugin(
formula.worker,
Expand Down Expand Up @@ -491,6 +513,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<unknown>} The value of the formula.
*/
const provideValueForNumberedFormula = async (
formulaType,
formulaNumber,
Expand All @@ -508,7 +536,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 @@ -647,6 +675,7 @@ const makeEndoBootstrap = (
if (
![
'eval-id512',
'lookup',
'make-unconfined-id512',
'make-bundle-id512',
'guest-id512',
Expand Down Expand Up @@ -676,6 +705,15 @@ const makeEndoBootstrap = (
worker: provideValueForFormulaIdentifier(formula.worker),
}),
);
} else if (formula.type === 'lookup') {
return makeInspector(
formula.type,
formulaNumber,
harden({
agent: provideValueForFormulaIdentifier(formula.agent),
path: formula.path,
}),
);
} else if (formula.type === 'guest') {
return makeInspector(
formula.type,
Expand Down
52 changes: 40 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 @@ -185,15 +185,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 +203,52 @@ 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 lookupAgent = lookupFormulaIdentifierForName('SELF');
const digester = makeSha512();
digester.updateText(`${lookupAgent},${petNamePath.join(',')}`);
const lookupFormulaNumber = digester.digestHex().slice(32, 64);

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

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

const { formulaIdentifier: lookupFormulaIdentifier } =
await provideValueForNumberedFormula(
'lookup',
lookupFormulaNumber,
lookupFormula,
);
return lookupFormulaIdentifier;
}),
);

const formula = {
const evalFormula = {
/** @type {'eval'} */
type: 'eval',
worker: workerFormulaIdentifier,
Expand All @@ -232,7 +260,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
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|make-unconfined-id512|make-bundle-id512|host-id512|guest-id512):[0-9a-f]{128}|(?:web-bundle|lookup):[0-9a-f]{32})$/;

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

type LookupFormula = {
type: 'lookup';

/**
* The formula identifier of the guest or host to call lookup on.
*/
agent: string;

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

type MakeUnconfinedFormula = {
type: 'make-unconfined';
worker: string;
Expand All @@ -97,6 +111,7 @@ type WebBundleFormula = {
export type Formula =
| GuestFormula
| EvalFormula
| LookupFormula
| MakeUnconfinedFormula
| MakeBundleFormula
| WebBundleFormula;
Expand Down

0 comments on commit 42cdabc

Please sign in to comment.