Add error handling logic
This commit is contained in:
		@@ -1,33 +0,0 @@
 | 
			
		||||
import { DocumentRange } from "./types";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Special error class, to be thrown when encountering a
 | 
			
		||||
 * syntax error while parsing a program.
 | 
			
		||||
 */
 | 
			
		||||
export class ParseError extends Error {
 | 
			
		||||
  /** Location of syntax error in the program */
 | 
			
		||||
  range: DocumentRange;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Create an instance of ParseError
 | 
			
		||||
   * @param message Error message
 | 
			
		||||
   * @param range Location of syntactically incorrect code
 | 
			
		||||
   */
 | 
			
		||||
  constructor(message: string, range: DocumentRange) {
 | 
			
		||||
    super(message);
 | 
			
		||||
    this.range = range;
 | 
			
		||||
    this.name = "ParseError";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Special error class, to be thrown when something happens
 | 
			
		||||
 * that is indicative of a bug in the language implementation.
 | 
			
		||||
 */
 | 
			
		||||
export class UnexpectedError extends Error {
 | 
			
		||||
  /** Create an instance of UnexpectedError */
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super("Something unexpected occured");
 | 
			
		||||
    this.name = "UnexpectedError";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,9 @@
 | 
			
		||||
import { LanguageEngine, StepExecutionResult } from "./types";
 | 
			
		||||
import {
 | 
			
		||||
  isRuntimeError,
 | 
			
		||||
  serializeRuntimeError,
 | 
			
		||||
  WorkerRuntimeError,
 | 
			
		||||
} from "./worker-errors";
 | 
			
		||||
 | 
			
		||||
type ExecuteAllArgs<RS> = {
 | 
			
		||||
  /** Interval between two execution steps, in milliseconds */
 | 
			
		||||
@@ -94,30 +99,53 @@ class ExecutionController<RS> {
 | 
			
		||||
   * Run a single step of execution
 | 
			
		||||
   * @returns Result of execution
 | 
			
		||||
   */
 | 
			
		||||
  executeStep(): StepExecutionResult<RS> {
 | 
			
		||||
    this._result = this._engine.executeStep();
 | 
			
		||||
    this._result.signal = "paused";
 | 
			
		||||
    return this._result;
 | 
			
		||||
  executeStep(): {
 | 
			
		||||
    result: StepExecutionResult<RS>;
 | 
			
		||||
    error?: WorkerRuntimeError;
 | 
			
		||||
  } {
 | 
			
		||||
    try {
 | 
			
		||||
      this._result = this._engine.executeStep();
 | 
			
		||||
      this._result.signal = "paused";
 | 
			
		||||
      return { result: this._result };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      if (isRuntimeError(error))
 | 
			
		||||
        return { result: this._result!, error: serializeRuntimeError(error) };
 | 
			
		||||
      else throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Execute the loaded program until stopped.
 | 
			
		||||
   * Execute the loaded program until stopped. Throws if an error other than RuntimeError is encountered.
 | 
			
		||||
   * @param param0.interval Interval between two execution steps
 | 
			
		||||
   * @param param0.onResult Callback called with result on each execution step
 | 
			
		||||
   * @returns Promise that resolves with result of last execution step
 | 
			
		||||
   * @returns Promise that resolves with result of last execution step and RuntimeError if any
 | 
			
		||||
   */
 | 
			
		||||
  executeAll({ interval, onResult }: ExecuteAllArgs<RS>) {
 | 
			
		||||
  executeAll({ interval, onResult }: ExecuteAllArgs<RS>): Promise<{
 | 
			
		||||
    result: StepExecutionResult<RS>;
 | 
			
		||||
    error?: WorkerRuntimeError;
 | 
			
		||||
  }> {
 | 
			
		||||
    // Clear paused state
 | 
			
		||||
    this._isPaused = false;
 | 
			
		||||
 | 
			
		||||
    return new Promise(async (resolve) => {
 | 
			
		||||
    return new Promise(async (resolve, reject) => {
 | 
			
		||||
      while (true) {
 | 
			
		||||
        const doBreak = this.runExecLoopIteration();
 | 
			
		||||
        onResult(this._result!);
 | 
			
		||||
        if (doBreak) break;
 | 
			
		||||
        await this.sleep(interval);
 | 
			
		||||
        try {
 | 
			
		||||
          const doBreak = this.runExecLoopIteration();
 | 
			
		||||
          onResult(this._result!);
 | 
			
		||||
          if (doBreak) break;
 | 
			
		||||
          await this.sleep(interval);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          if (isRuntimeError(error)) {
 | 
			
		||||
            this._isPaused = true;
 | 
			
		||||
            resolve({
 | 
			
		||||
              result: this._result!,
 | 
			
		||||
              error: serializeRuntimeError(error),
 | 
			
		||||
            });
 | 
			
		||||
          } else reject(error);
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      resolve(this._result!);
 | 
			
		||||
      resolve({ result: this._result! });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +1,33 @@
 | 
			
		||||
import ExecutionController from "./execution-controller";
 | 
			
		||||
import { LanguageEngine, StepExecutionResult } from "./types";
 | 
			
		||||
import {
 | 
			
		||||
  WorkerAckType,
 | 
			
		||||
  WorkerRequestData,
 | 
			
		||||
  WorkerResponseData,
 | 
			
		||||
} from "./worker-constants";
 | 
			
		||||
import * as E from "./worker-errors";
 | 
			
		||||
import * as C from "./worker-constants";
 | 
			
		||||
 | 
			
		||||
/** Create a worker response for update acknowledgement */
 | 
			
		||||
const ackMessage = <RS>(state: WorkerAckType): WorkerResponseData<RS> => ({
 | 
			
		||||
/** Create a worker response for acknowledgement */
 | 
			
		||||
const ackMessage = <RS, A extends C.WorkerAckType>(
 | 
			
		||||
  ackType: A,
 | 
			
		||||
  error?: C.WorkerAckError[A]
 | 
			
		||||
): C.WorkerResponseData<RS, A> => ({
 | 
			
		||||
  type: "ack",
 | 
			
		||||
  data: state,
 | 
			
		||||
  data: ackType,
 | 
			
		||||
  error,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/** Create a worker response for execution result */
 | 
			
		||||
const resultMessage = <RS>(
 | 
			
		||||
  result: StepExecutionResult<RS>
 | 
			
		||||
): WorkerResponseData<RS> => ({
 | 
			
		||||
const resultMessage = <RS, A extends C.WorkerAckType>(
 | 
			
		||||
  result: StepExecutionResult<RS>,
 | 
			
		||||
  error?: E.WorkerRuntimeError
 | 
			
		||||
): C.WorkerResponseData<RS, A> => ({
 | 
			
		||||
  type: "result",
 | 
			
		||||
  data: result,
 | 
			
		||||
  error,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/** Create a worker response for unexpected errors */
 | 
			
		||||
const errorMessage = <RS, A extends C.WorkerAckType>(
 | 
			
		||||
  error: Error
 | 
			
		||||
): C.WorkerResponseData<RS, A> => ({ type: "error", error });
 | 
			
		||||
 | 
			
		||||
/** Initialize the execution controller */
 | 
			
		||||
const initController = () => {
 | 
			
		||||
  postMessage(ackMessage("init"));
 | 
			
		||||
@@ -42,8 +50,14 @@ const prepare = <RS>(
 | 
			
		||||
  controller: ExecutionController<RS>,
 | 
			
		||||
  { code, input }: { code: string; input: string }
 | 
			
		||||
) => {
 | 
			
		||||
  controller.prepare(code, input);
 | 
			
		||||
  postMessage(ackMessage("prepare"));
 | 
			
		||||
  try {
 | 
			
		||||
    controller.prepare(code, input);
 | 
			
		||||
    postMessage(ackMessage("prepare"));
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    if (E.isParseError(error))
 | 
			
		||||
      postMessage(ackMessage("prepare", E.serializeParseError(error)));
 | 
			
		||||
    else throw error;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -62,11 +76,15 @@ const updateBreakpoints = <RS>(
 | 
			
		||||
 * Execute the entire program loaded on engine,
 | 
			
		||||
 * and return result of execution.
 | 
			
		||||
 */
 | 
			
		||||
const execute = <RS>(controller: ExecutionController<RS>, interval: number) => {
 | 
			
		||||
  controller.executeAll({
 | 
			
		||||
const execute = async <RS>(
 | 
			
		||||
  controller: ExecutionController<RS>,
 | 
			
		||||
  interval: number
 | 
			
		||||
) => {
 | 
			
		||||
  const { result, error } = await controller.executeAll({
 | 
			
		||||
    interval,
 | 
			
		||||
    onResult: (res) => postMessage(resultMessage(res)),
 | 
			
		||||
  });
 | 
			
		||||
  if (error) postMessage(resultMessage(result, error));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Trigger pause in program execution */
 | 
			
		||||
@@ -77,8 +95,8 @@ const pauseExecution = async <RS>(controller: ExecutionController<RS>) => {
 | 
			
		||||
 | 
			
		||||
/** Run a single execution step */
 | 
			
		||||
const executeStep = <RS>(controller: ExecutionController<RS>) => {
 | 
			
		||||
  const result = controller.executeStep();
 | 
			
		||||
  postMessage(resultMessage(result));
 | 
			
		||||
  const { result, error } = controller.executeStep();
 | 
			
		||||
  postMessage(resultMessage(result, error));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -88,16 +106,23 @@ const executeStep = <RS>(controller: ExecutionController<RS>) => {
 | 
			
		||||
export const setupWorker = <RS>(engine: LanguageEngine<RS>) => {
 | 
			
		||||
  const controller = new ExecutionController(engine);
 | 
			
		||||
 | 
			
		||||
  addEventListener("message", async (ev: MessageEvent<WorkerRequestData>) => {
 | 
			
		||||
    if (ev.data.type === "Init") return initController();
 | 
			
		||||
    if (ev.data.type === "Reset") return resetController(controller);
 | 
			
		||||
    if (ev.data.type === "Prepare") return prepare(controller, ev.data.params);
 | 
			
		||||
    if (ev.data.type === "Execute")
 | 
			
		||||
      return execute(controller, ev.data.params.interval);
 | 
			
		||||
    if (ev.data.type === "Pause") return await pauseExecution(controller);
 | 
			
		||||
    if (ev.data.type === "ExecuteStep") return executeStep(controller);
 | 
			
		||||
    if (ev.data.type === "UpdateBreakpoints")
 | 
			
		||||
      return updateBreakpoints(controller, ev.data.params.points);
 | 
			
		||||
  addEventListener("message", async (ev: MessageEvent<C.WorkerRequestData>) => {
 | 
			
		||||
    try {
 | 
			
		||||
      if (ev.data.type === "Init") return initController();
 | 
			
		||||
      if (ev.data.type === "Reset") return resetController(controller);
 | 
			
		||||
      if (ev.data.type === "Prepare")
 | 
			
		||||
        return prepare(controller, ev.data.params);
 | 
			
		||||
      if (ev.data.type === "Execute")
 | 
			
		||||
        return execute(controller, ev.data.params.interval);
 | 
			
		||||
      if (ev.data.type === "Pause") return await pauseExecution(controller);
 | 
			
		||||
      if (ev.data.type === "ExecuteStep") return executeStep(controller);
 | 
			
		||||
      if (ev.data.type === "UpdateBreakpoints")
 | 
			
		||||
        return updateBreakpoints(controller, ev.data.params.points);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      // Error here indicates an implementation bug
 | 
			
		||||
      console.error(error);
 | 
			
		||||
      postMessage(errorMessage(error as Error));
 | 
			
		||||
    }
 | 
			
		||||
    throw new Error("Invalid worker message type");
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { StepExecutionResult } from "./types";
 | 
			
		||||
import { DocumentRange, StepExecutionResult } from "./types";
 | 
			
		||||
import * as E from "./worker-errors";
 | 
			
		||||
 | 
			
		||||
/** Types of requests the worker handles */
 | 
			
		||||
export type WorkerRequestData =
 | 
			
		||||
@@ -39,7 +40,28 @@ export type WorkerAckType =
 | 
			
		||||
  | "prepare" // on preparing for execution
 | 
			
		||||
  | "pause"; // on pausing execution
 | 
			
		||||
 | 
			
		||||
/** Errors associated with each response ack type */
 | 
			
		||||
export type WorkerAckError = {
 | 
			
		||||
  init: undefined;
 | 
			
		||||
  reset: undefined;
 | 
			
		||||
  "bp-update": undefined;
 | 
			
		||||
  prepare: E.WorkerParseError;
 | 
			
		||||
  pause: undefined;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Types of responses the worker can send */
 | 
			
		||||
export type WorkerResponseData<RS> =
 | 
			
		||||
  | { type: "ack"; data: WorkerAckType }
 | 
			
		||||
  | { type: "result"; data: StepExecutionResult<RS> };
 | 
			
		||||
export type WorkerResponseData<RS, A extends WorkerAckType> =
 | 
			
		||||
  /** Ack for one-off requests, optionally containing error occured (if any) */
 | 
			
		||||
  | {
 | 
			
		||||
      type: "ack";
 | 
			
		||||
      data: A;
 | 
			
		||||
      error?: WorkerAckError[A];
 | 
			
		||||
    }
 | 
			
		||||
  /** Response containing step execution result, and runtime error (if any) */
 | 
			
		||||
  | {
 | 
			
		||||
      type: "result";
 | 
			
		||||
      data: StepExecutionResult<RS>;
 | 
			
		||||
      error?: E.WorkerRuntimeError;
 | 
			
		||||
    }
 | 
			
		||||
  /** Response indicating a bug in worker/engine logic */
 | 
			
		||||
  | { type: "error"; error: Error };
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										71
									
								
								engines/worker-errors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								engines/worker-errors.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
			
		||||
import { DocumentRange } from "./types";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Special error class, to be thrown when encountering a
 | 
			
		||||
 * syntax error while parsing a program.
 | 
			
		||||
 */
 | 
			
		||||
export class ParseError extends Error {
 | 
			
		||||
  /** Location of syntax error in the program */
 | 
			
		||||
  range: DocumentRange;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Create an instance of ParseError
 | 
			
		||||
   * @param message Error message
 | 
			
		||||
   * @param range Location of syntactically incorrect code
 | 
			
		||||
   */
 | 
			
		||||
  constructor(message: string, range: DocumentRange) {
 | 
			
		||||
    super(message);
 | 
			
		||||
    this.name = "ParseError";
 | 
			
		||||
    this.range = range;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Special error class, to be thrown when encountering an error
 | 
			
		||||
 * at runtime that is indicative of a bug in the user's program.
 | 
			
		||||
 */
 | 
			
		||||
export class RuntimeError extends Error {
 | 
			
		||||
  /**
 | 
			
		||||
   * Create an instance of RuntimeError
 | 
			
		||||
   * @param message Error message
 | 
			
		||||
   */
 | 
			
		||||
  constructor(message: string) {
 | 
			
		||||
    super(message);
 | 
			
		||||
    this.name = "RuntimeError";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Check if an error object is instance of a ParseError */
 | 
			
		||||
export const isParseError = (error: any): error is ParseError => {
 | 
			
		||||
  return error instanceof ParseError || error.name === "ParseError";
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Check if an error object is instance of a RuntimeError */
 | 
			
		||||
export const isRuntimeError = (error: any): error is RuntimeError => {
 | 
			
		||||
  return error instanceof RuntimeError || error.name === "RuntimeError";
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Error sent by worker in case of parsing error */
 | 
			
		||||
export type WorkerParseError = {
 | 
			
		||||
  name: "ParseError";
 | 
			
		||||
  message: string;
 | 
			
		||||
  range: DocumentRange;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Error sent by worker in case error at runtime */
 | 
			
		||||
export type WorkerRuntimeError = {
 | 
			
		||||
  name: "RuntimeError";
 | 
			
		||||
  message: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Serialize a RuntimeError instance into a plain object */
 | 
			
		||||
export const serializeRuntimeError = (
 | 
			
		||||
  error: RuntimeError
 | 
			
		||||
): WorkerRuntimeError => {
 | 
			
		||||
  return { name: "RuntimeError", message: error.message };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Serialize a ParseError instance into a plain object */
 | 
			
		||||
export const serializeParseError = (error: ParseError): WorkerParseError => {
 | 
			
		||||
  return { name: "ParseError", message: error.message, range: error.range };
 | 
			
		||||
};
 | 
			
		||||
@@ -7,6 +7,7 @@ import { LanguageProvider, StepExecutionResult } from "../engines/types";
 | 
			
		||||
import { OutputViewer, OutputViewerRef } from "../ui/output-viewer";
 | 
			
		||||
import { ExecutionControls } from "./execution-controls";
 | 
			
		||||
import { RendererRef, RendererWrapper } from "./renderer-wrapper";
 | 
			
		||||
import { WorkerRuntimeError } from "../engines/worker-errors";
 | 
			
		||||
 | 
			
		||||
type Props<RS> = {
 | 
			
		||||
  langName: string;
 | 
			
		||||
@@ -36,10 +37,14 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
 | 
			
		||||
  const [execInterval, setExecInterval] = React.useState(20);
 | 
			
		||||
 | 
			
		||||
  /** Utility that updates UI with the provided execution result */
 | 
			
		||||
  const updateWithResult = (result: StepExecutionResult<any>) => {
 | 
			
		||||
  const updateWithResult = (
 | 
			
		||||
    result: StepExecutionResult<any>,
 | 
			
		||||
    error?: WorkerRuntimeError
 | 
			
		||||
  ) => {
 | 
			
		||||
    rendererRef.current!.updateState(result.rendererState);
 | 
			
		||||
    codeEditorRef.current!.updateHighlights(result.nextStepLocation);
 | 
			
		||||
    outputEditorRef.current!.append(result.output);
 | 
			
		||||
    if (error) outputEditorRef.current!.setError(error);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /** Reset and begin a new execution */
 | 
			
		||||
@@ -54,13 +59,14 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
 | 
			
		||||
    // Reset any existing execution state
 | 
			
		||||
    outputEditorRef.current!.reset();
 | 
			
		||||
    await execController.resetState();
 | 
			
		||||
    await execController.prepare(
 | 
			
		||||
    const error = await execController.prepare(
 | 
			
		||||
      codeEditorRef.current!.getValue(),
 | 
			
		||||
      inputEditorRef.current!.getValue()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Begin execution
 | 
			
		||||
    await execController.execute(updateWithResult, execInterval);
 | 
			
		||||
    // Check for ParseError, else begin execution
 | 
			
		||||
    if (error) outputEditorRef.current!.setError(error);
 | 
			
		||||
    else await execController.execute(updateWithResult, execInterval);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /** Pause the ongoing execution */
 | 
			
		||||
@@ -82,8 +88,8 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Run and update execution states
 | 
			
		||||
    const result = await execController.executeStep();
 | 
			
		||||
    updateWithResult(result);
 | 
			
		||||
    const response = await execController.executeStep();
 | 
			
		||||
    updateWithResult(response.result, response.error);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /** Resume the currently paused execution */
 | 
			
		||||
@@ -101,7 +107,7 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
 | 
			
		||||
  /** Stop the currently active execution */
 | 
			
		||||
  const stopExecution = async () => {
 | 
			
		||||
    // Check if controller has execution
 | 
			
		||||
    if (!["paused", "processing"].includes(execController.state)) {
 | 
			
		||||
    if (!["paused", "processing", "error"].includes(execController.state)) {
 | 
			
		||||
      console.error("No active execution in controller");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -112,7 +118,6 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
 | 
			
		||||
 | 
			
		||||
    // Reset all execution states
 | 
			
		||||
    await execController.resetState();
 | 
			
		||||
    outputEditorRef.current!.reset();
 | 
			
		||||
    rendererRef.current!.updateState(null);
 | 
			
		||||
    codeEditorRef.current!.updateHighlights(null);
 | 
			
		||||
  };
 | 
			
		||||
@@ -122,6 +127,7 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
 | 
			
		||||
    const currState = execController.state;
 | 
			
		||||
    if (currState === "processing") return "running";
 | 
			
		||||
    else if (currState === "paused") return "paused";
 | 
			
		||||
    else if (currState === "error") return "error";
 | 
			
		||||
    else return "off";
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,9 @@ const styles = {
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Possible states of the debug controls component */
 | 
			
		||||
type DebugControlsState = "off" | "running" | "paused" | "error";
 | 
			
		||||
 | 
			
		||||
/** Input field for changing execution interval */
 | 
			
		||||
const IntervalInput = (props: {
 | 
			
		||||
  disabled: boolean;
 | 
			
		||||
@@ -62,25 +65,30 @@ const RunButton = ({ onClick }: { onClick: () => void }) => (
 | 
			
		||||
 | 
			
		||||
/** Button group for debugging controls */
 | 
			
		||||
const DebugControls = (props: {
 | 
			
		||||
  paused: boolean;
 | 
			
		||||
  state: DebugControlsState;
 | 
			
		||||
  onPause: () => void;
 | 
			
		||||
  onResume: () => void;
 | 
			
		||||
  onStep: () => void;
 | 
			
		||||
  onStop: () => void;
 | 
			
		||||
}) => {
 | 
			
		||||
  const paused = props.state === "paused" || props.state === "error";
 | 
			
		||||
  const pauseDisabled = props.state === "error";
 | 
			
		||||
  const stepDisabled = ["off", "running", "error"].includes(props.state);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ButtonGroup>
 | 
			
		||||
      <Button
 | 
			
		||||
        small
 | 
			
		||||
        title={props.paused ? "Pause" : "Resume"}
 | 
			
		||||
        onClick={props.paused ? props.onResume : props.onPause}
 | 
			
		||||
        icon={<Icon icon={props.paused ? "play" : "pause"} intent="primary" />}
 | 
			
		||||
        title={paused ? "Pause" : "Resume"}
 | 
			
		||||
        disabled={pauseDisabled}
 | 
			
		||||
        onClick={paused ? props.onResume : props.onPause}
 | 
			
		||||
        icon={<Icon icon={paused ? "play" : "pause"} intent="primary" />}
 | 
			
		||||
      />
 | 
			
		||||
      <Button
 | 
			
		||||
        small
 | 
			
		||||
        title="Step"
 | 
			
		||||
        onClick={props.onStep}
 | 
			
		||||
        disabled={!props.paused}
 | 
			
		||||
        disabled={stepDisabled}
 | 
			
		||||
        icon={<Icon icon="step-forward" intent="warning" />}
 | 
			
		||||
      />
 | 
			
		||||
      <Button
 | 
			
		||||
@@ -94,7 +102,7 @@ const DebugControls = (props: {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  state: "off" | "running" | "paused";
 | 
			
		||||
  state: DebugControlsState;
 | 
			
		||||
  onRun: () => void;
 | 
			
		||||
  onPause: () => void;
 | 
			
		||||
  onResume: () => void;
 | 
			
		||||
@@ -110,7 +118,7 @@ export const ExecutionControls = (props: Props) => {
 | 
			
		||||
        <RunButton onClick={props.onRun} />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <DebugControls
 | 
			
		||||
          paused={props.state === "paused"}
 | 
			
		||||
          state={props.state}
 | 
			
		||||
          onPause={props.onPause}
 | 
			
		||||
          onResume={props.onResume}
 | 
			
		||||
          onStep={props.onStep}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,18 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { TextArea } from "@blueprintjs/core";
 | 
			
		||||
import { Colors, Text } from "@blueprintjs/core";
 | 
			
		||||
import { WorkerParseError, WorkerRuntimeError } from "../engines/worker-errors";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * For aesthetic reasons, we use readonly textarea for displaying output.
 | 
			
		||||
 * Textarea displays placeholder if value passed is empty string, which is undesired.
 | 
			
		||||
 * This function is a fake-whitespace workaround.
 | 
			
		||||
 *
 | 
			
		||||
 * @param value Value received from parent. Placeholder shown on `null`.
 | 
			
		||||
 * @returns Value to pass as prop to Blueprint TextArea
 | 
			
		||||
 */
 | 
			
		||||
const toTextareaValue = (value: string | null): string | undefined => {
 | 
			
		||||
  if (value == null) return undefined; // Placeholder shown
 | 
			
		||||
  if (value === "") return "\u0020"; // Fake whitespace to hide placeholder
 | 
			
		||||
  return value; // Non-empty output value
 | 
			
		||||
/** Format a ParseError for displaying as output */
 | 
			
		||||
const formatParseError = (error: WorkerParseError): string => {
 | 
			
		||||
  const line = error.range.line + 1;
 | 
			
		||||
  const start = error.range.charRange?.start;
 | 
			
		||||
  const end = error.range.charRange?.end;
 | 
			
		||||
  console.log(line, start, end);
 | 
			
		||||
  let cols: string | null = null;
 | 
			
		||||
  if (start != null && end != null) cols = `col ${start + 1}-${end + 1}`;
 | 
			
		||||
  else if (start != null) cols = `col ${start + 1}`;
 | 
			
		||||
  else if (end != null) cols = `col ${end + 1}`;
 | 
			
		||||
  return `ParseError: line ${line}, ${cols}\n${error.message}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface OutputViewerRef {
 | 
			
		||||
@@ -20,26 +20,36 @@ export interface OutputViewerRef {
 | 
			
		||||
  reset: () => void;
 | 
			
		||||
  /** Append string to the displayed output */
 | 
			
		||||
  append: (str?: string) => void;
 | 
			
		||||
  /** Add error text below the output text */
 | 
			
		||||
  setError: (error: WorkerRuntimeError | WorkerParseError | null) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const OutputViewerComponent = (_: {}, ref: React.Ref<OutputViewerRef>) => {
 | 
			
		||||
  const [value, setValue] = React.useState<string | null>(null);
 | 
			
		||||
  const [error, setError] = React.useState<string | null>(null);
 | 
			
		||||
 | 
			
		||||
  React.useImperativeHandle(ref, () => ({
 | 
			
		||||
    reset: () => setValue(null),
 | 
			
		||||
    reset: () => {
 | 
			
		||||
      setValue(null);
 | 
			
		||||
      setError(null);
 | 
			
		||||
    },
 | 
			
		||||
    append: (s) => setValue((o) => (o || "") + (s || "")),
 | 
			
		||||
    setError: (error: WorkerRuntimeError | WorkerParseError | null) => {
 | 
			
		||||
      if (!error) setError(null);
 | 
			
		||||
      else if (error.name === "RuntimeError")
 | 
			
		||||
        setError("RuntimeError: " + error.message);
 | 
			
		||||
      else if (error.name === "ParseError") setError(formatParseError(error));
 | 
			
		||||
    },
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <TextArea
 | 
			
		||||
      fill
 | 
			
		||||
      large
 | 
			
		||||
      readOnly
 | 
			
		||||
      growVertically
 | 
			
		||||
      value={toTextareaValue(value)}
 | 
			
		||||
      placeholder="Run code to see output..."
 | 
			
		||||
      style={{ height: "100%", resize: "none", boxShadow: "none" }}
 | 
			
		||||
    />
 | 
			
		||||
    <div style={{ padding: 10, fontSize: 16 }}>
 | 
			
		||||
      <Text style={{ fontFamily: "monospace" }}>{value}</Text>
 | 
			
		||||
      {value && <div style={{ height: 10 }} />}
 | 
			
		||||
      <Text style={{ fontFamily: "monospace", color: Colors.RED3 }}>
 | 
			
		||||
        {error}
 | 
			
		||||
      </Text>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import {
 | 
			
		||||
  WorkerRequestData,
 | 
			
		||||
  WorkerResponseData,
 | 
			
		||||
} from "../engines/worker-constants";
 | 
			
		||||
import { WorkerParseError, WorkerRuntimeError } from "../engines/worker-errors";
 | 
			
		||||
 | 
			
		||||
/** Possible states for the worker to be in */
 | 
			
		||||
type WorkerState =
 | 
			
		||||
@@ -12,6 +13,7 @@ type WorkerState =
 | 
			
		||||
  | "ready" // Ready to start execution
 | 
			
		||||
  | "processing" // Executing code
 | 
			
		||||
  | "paused" // Execution currently paused
 | 
			
		||||
  | "error" // Execution ended due to error
 | 
			
		||||
  | "done"; // Program ended, reset now
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -40,10 +42,10 @@ export const useExecController = <RS>(langName: string) => {
 | 
			
		||||
   */
 | 
			
		||||
  const requestWorker = (
 | 
			
		||||
    request: WorkerRequestData,
 | 
			
		||||
    onData?: (data: WorkerResponseData<RS>) => boolean
 | 
			
		||||
  ): Promise<WorkerResponseData<RS>> => {
 | 
			
		||||
    onData?: (data: WorkerResponseData<RS, any>) => boolean
 | 
			
		||||
  ): Promise<WorkerResponseData<RS, any>> => {
 | 
			
		||||
    return new Promise((resolve) => {
 | 
			
		||||
      const handler = (ev: MessageEvent<WorkerResponseData<RS>>) => {
 | 
			
		||||
      const handler = (ev: MessageEvent<WorkerResponseData<RS, any>>) => {
 | 
			
		||||
        if (!onData) {
 | 
			
		||||
          // Normal mode
 | 
			
		||||
          workerRef.current!.removeEventListener("message", handler);
 | 
			
		||||
@@ -65,7 +67,7 @@ export const useExecController = <RS>(langName: string) => {
 | 
			
		||||
  /** Utility to throw error on unexpected response */
 | 
			
		||||
  const throwUnexpectedRes = (
 | 
			
		||||
    fnName: string,
 | 
			
		||||
    res: WorkerResponseData<RS>
 | 
			
		||||
    res: WorkerResponseData<RS, any>
 | 
			
		||||
  ): never => {
 | 
			
		||||
    throw new Error(`Unexpected response on ${fnName}: ${JSON.stringify(res)}`);
 | 
			
		||||
  };
 | 
			
		||||
@@ -75,6 +77,9 @@ export const useExecController = <RS>(langName: string) => {
 | 
			
		||||
    (async () => {
 | 
			
		||||
      if (workerRef.current) throw new Error("Tried to reinitialize worker");
 | 
			
		||||
      workerRef.current = new Worker(`../workers/${langName}.js`);
 | 
			
		||||
      workerRef.current!.addEventListener("error", (ev) =>
 | 
			
		||||
        console.log("Ev: ", ev)
 | 
			
		||||
      );
 | 
			
		||||
      const res = await requestWorker({ type: "Init" });
 | 
			
		||||
      if (res.type === "ack" && res.data === "init") setWorkerState("empty");
 | 
			
		||||
      else throwUnexpectedRes("init", res);
 | 
			
		||||
@@ -92,14 +97,19 @@ export const useExecController = <RS>(langName: string) => {
 | 
			
		||||
   * @param code Code content
 | 
			
		||||
   * @param input User input
 | 
			
		||||
   */
 | 
			
		||||
  const prepare = React.useCallback(async (code: string, input: string) => {
 | 
			
		||||
    const res = await requestWorker({
 | 
			
		||||
      type: "Prepare",
 | 
			
		||||
      params: { code, input },
 | 
			
		||||
    });
 | 
			
		||||
    if (res.type === "ack" && res.data === "prepare") setWorkerState("ready");
 | 
			
		||||
    else throwUnexpectedRes("loadCode", res);
 | 
			
		||||
  }, []);
 | 
			
		||||
  const prepare = React.useCallback(
 | 
			
		||||
    async (code: string, input: string): Promise<WorkerParseError | void> => {
 | 
			
		||||
      const res = await requestWorker({
 | 
			
		||||
        type: "Prepare",
 | 
			
		||||
        params: { code, input },
 | 
			
		||||
      });
 | 
			
		||||
      if (res.type === "ack" && res.data === "prepare") {
 | 
			
		||||
        if (res.error) return res.error;
 | 
			
		||||
        else setWorkerState("ready");
 | 
			
		||||
      } else throwUnexpectedRes("loadCode", res);
 | 
			
		||||
    },
 | 
			
		||||
    []
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Update debugging breakpoints in the execution controller.
 | 
			
		||||
@@ -142,14 +152,18 @@ export const useExecController = <RS>(langName: string) => {
 | 
			
		||||
   * Run a single step of execution
 | 
			
		||||
   * @return Execution result
 | 
			
		||||
   */
 | 
			
		||||
  const executeStep = React.useCallback(async () => {
 | 
			
		||||
  const executeStep = React.useCallback(async (): Promise<{
 | 
			
		||||
    result: StepExecutionResult<RS>;
 | 
			
		||||
    error?: WorkerRuntimeError;
 | 
			
		||||
  }> => {
 | 
			
		||||
    const res = await requestWorker(
 | 
			
		||||
      { type: "ExecuteStep" },
 | 
			
		||||
      (res) => res.type !== "result"
 | 
			
		||||
    );
 | 
			
		||||
    if (res.type !== "result") throw new Error("Something unexpected happened");
 | 
			
		||||
    if (!res.data.nextStepLocation) setWorkerState("done");
 | 
			
		||||
    return res.data;
 | 
			
		||||
 | 
			
		||||
    return { result: res.data, error: res.error };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@@ -158,20 +172,29 @@ export const useExecController = <RS>(langName: string) => {
 | 
			
		||||
   */
 | 
			
		||||
  const execute = React.useCallback(
 | 
			
		||||
    async (
 | 
			
		||||
      onResult: (result: StepExecutionResult<RS>) => void,
 | 
			
		||||
      onResult: (
 | 
			
		||||
        result: StepExecutionResult<RS>,
 | 
			
		||||
        error?: WorkerRuntimeError
 | 
			
		||||
      ) => void,
 | 
			
		||||
      interval: number
 | 
			
		||||
    ) => {
 | 
			
		||||
      setWorkerState("processing");
 | 
			
		||||
      // Set up a streaming-response cycle with the worker
 | 
			
		||||
      await requestWorker({ type: "Execute", params: { interval } }, (res) => {
 | 
			
		||||
        if (res.type !== "result") return true;
 | 
			
		||||
        onResult(res.data);
 | 
			
		||||
        onResult(res.data, res.error);
 | 
			
		||||
        if (!res.data.nextStepLocation) {
 | 
			
		||||
          // Program execution complete
 | 
			
		||||
          setWorkerState("done");
 | 
			
		||||
          return false;
 | 
			
		||||
        } else if (res.data.signal === "paused") {
 | 
			
		||||
          // Execution paused by user or breakpoint
 | 
			
		||||
          setWorkerState("paused");
 | 
			
		||||
          return false;
 | 
			
		||||
        } else if (res.error) {
 | 
			
		||||
          // Runtime error occured
 | 
			
		||||
          setWorkerState("error");
 | 
			
		||||
          return false;
 | 
			
		||||
        } else return true;
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user