All files / kernel-agents-repl/src/strategies/repl evaluator.ts

85.29% Statements 29/34
70% Branches 14/20
66.66% Functions 4/6
85.29% Lines 29/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124                                        1x     19x   1x   18x   1x           17x                     17x     1x                   15x   15x         19x 19x 2x 2x 2x       17x           17x 19x 19x     19x       19x   19x 19x                                 16x     16x 40x       19x 19x   19x      
import { EvaluatorError } from '@metamask/kernel-errors';
import { mergeDisjointRecords } from '@metamask/kernel-utils';
import type { Logger } from '@metamask/logger';
import { extractCapabilities } from '@ocap/kernel-agents/capabilities/capability';
import type { CapabilityRecord } from '@ocap/kernel-agents/types';
import { ifDefined } from '@ocap/kernel-agents/utils';
 
import { makeCompartment } from './compartment.ts';
import { processEvaluationError } from './evaluator-error.ts';
import {
  CommentMessage,
  EvaluationMessage,
  ImportMessage,
  ResultMessage,
} from './messages.ts';
import type { ReplTranscript, StatementMessage } from './messages.ts';
import { prepareEvaluation } from './prepare-evaluation.ts';
import { ERROR, RETURN } from './symbols.ts';
import type { EvaluatorState } from './types.ts';
 
const validateStatement = (
  statement: StatementMessage,
): { earlyResult?: ResultMessage | null } => {
  if (statement instanceof CommentMessage) {
    // Comments are not evaluated.
    return { earlyResult: null };
  }
  if (statement instanceof ImportMessage) {
    // Imports are not implemented yet.
    return {
      earlyResult: new ResultMessage({
        [ERROR]: new SyntaxError('Additional imports are not allowed.'),
      }),
    };
  }
  Iif (!(statement instanceof EvaluationMessage)) {
    // This should never happen.
    throw new Error(
      [
        'Internal: Unknown statement',
        `statement: ${statement.messageBody.node.text}`,
        `type: ${statement.messageBody.node.toString()}`,
      ].join('\n'),
    );
  }
  // Otherwise, proceed with the evaluation.
  return {};
};
 
export const makeEvaluator = ({
  capabilities = {},
  logger,
  // For testing purposes.
  initState = () => ({ consts: {}, lets: {} }),
}: {
  capabilities?: CapabilityRecord;
  logger?: Logger;
  initState?: () => EvaluatorState;
}) => {
  const state: EvaluatorState = initState();
 
  return async (
    history: ReplTranscript,
    statement: StatementMessage,
  ): Promise<ResultMessage | null> => {
    // Validate the statement.
    const validation = validateStatement(statement);
    if ('earlyResult' in validation) {
      const { earlyResult } = validation;
      history.push(statement, ...(earlyResult ? [earlyResult] : []));
      return earlyResult;
    }
 
    // Prepare the evaluation.
    const { code, endowments, result, commit } = prepareEvaluation(
      state,
      statement.messageBody.node,
      ifDefined({ logger }),
    );
 
    logger?.info('capabilities:', capabilities);
    logger?.info('endowments:', endowments);
    logger?.info('evaluating:', code);
 
    // Prepare the compartment.
    const compartmentEndowments = mergeDisjointRecords(
      endowments,
      extractCapabilities(capabilities),
    );
    const compartment = makeCompartment(compartmentEndowments);
 
    try {
      await compartment.evaluate(code);
    } catch (cause) {
      const asError = (error: unknown): Error =>
        error instanceof Error ? error : new Error(String(error));
      // Errors that evade $catch are always an EvaluationError
      throw new EvaluatorError(
        'REPL evaluation failed',
        code,
        // If the error is already an EvaluatorError, we rethrow with the code,
        cause instanceof EvaluatorError
          ? (cause.cause as Error)
          : // Otherwise, wrap the error as EvaluatorError
            asError(cause),
      );
    }
 
    // Handle errors caught by $catch (user code errors)
    processEvaluationError(result, code);
 
    // Update the state and return the result
    const stepResult = [ERROR, RETURN, 'value'].some((key) =>
      Object.hasOwn(result, key),
    )
      ? new ResultMessage(result)
      : null;
    history.push(statement, ...(stepResult ? [stepResult] : []));
    commit();
 
    return stepResult;
  };
};