Add error handling logic

This commit is contained in:
Nilay Majorwar
2022-01-22 13:53:57 +05:30
parent d8481c097b
commit 0efd4c79ef
9 changed files with 291 additions and 131 deletions

View File

@ -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";
}
}

View File

@ -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! });
});
}

View File

@ -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");
});
};

View File

@ -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
View 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 };
};