Add error handling logic
This commit is contained in:
@ -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