Add breakpoints and ack-based comms to worker

This commit is contained in:
Nilay Majorwar 2021-12-15 15:19:44 +05:30
parent c9afb3a68b
commit 29b243d6f2
5 changed files with 87 additions and 32 deletions

View File

@ -12,6 +12,7 @@ type ExecuteAllArgs<RS> = {
class ExecutionController<RS> {
private _engine: LanguageEngine<RS>;
private _breakpoints: number[] = [];
private _result: StepExecutionResult<RS> | null;
/**
@ -42,12 +43,20 @@ class ExecutionController<RS> {
this._engine.prepare(code, input);
}
/**
* Update debugging breakpoints
* @param points Array of line numbers having breakpoints
*/
updateBreakpoints(points: number[]) {
this._breakpoints = points;
}
async executeAll({ interval, onResult }: ExecuteAllArgs<RS>) {
while (true) {
this._result = this._engine.executeStep();
onResult && onResult(this._result);
if (!this._result.nextStepLocation) break;
if (interval) await this.sleep(interval);
await this.sleep(interval || 0);
}
return this._result;
}

View File

@ -1,5 +1,6 @@
import { StepExecutionResult } from "./types";
/** Types of requests the worker handles */
export type WorkerRequestData =
| {
type: "Init";
@ -13,11 +14,19 @@ export type WorkerRequestData =
type: "Prepare";
params: { code: string; input: string };
}
| {
type: "UpdateBreakpoints";
params: { points: number[] };
}
| {
type: "Execute";
params: { interval?: number };
};
/** Kinds of acknowledgement responses the worker can send */
export type WorkerAckType = "init" | "reset" | "bp-update" | "prepare";
/** Types of responses the worker can send */
export type WorkerResponseData<RS> =
| { type: "state"; data: "empty" | "ready" }
| { type: "ack"; data: WorkerAckType }
| { type: "result"; data: StepExecutionResult<RS> };

View File

@ -1,16 +1,17 @@
import BrainfuckLanguageEngine from "./brainfuck/engine";
import ExecutionController from "./execution-controller";
import SampleLanguageEngine from "./sample-lang/engine";
import { StepExecutionResult } from "./types";
import { WorkerRequestData, WorkerResponseData } from "./worker-constants";
import {
WorkerAckType,
WorkerRequestData,
WorkerResponseData,
} from "./worker-constants";
let _controller: ExecutionController<any> | null = null;
/** Create a worker response for state update */
const stateMessage = <RS>(
state: "empty" | "ready"
): WorkerResponseData<RS> => ({
type: "state",
/** Create a worker response for update acknowledgement */
const ackMessage = <RS>(state: WorkerAckType): WorkerResponseData<RS> => ({
type: "ack",
data: state,
});
@ -26,10 +27,9 @@ const resultMessage = <RS>(
* Initialize the execution controller.
*/
const initController = () => {
// const engine = new SampleLanguageEngine();
const engine = new BrainfuckLanguageEngine();
_controller = new ExecutionController(engine);
postMessage(stateMessage("empty"));
postMessage(ackMessage("init"));
};
/**
@ -38,7 +38,7 @@ const initController = () => {
*/
const resetController = () => {
_controller!.resetState();
postMessage(stateMessage("empty"));
postMessage(ackMessage("reset"));
};
/**
@ -47,7 +47,16 @@ const resetController = () => {
*/
const prepare = ({ code, input }: { code: string; input: string }) => {
_controller!.prepare(code, input);
postMessage(stateMessage("ready"));
postMessage(ackMessage("prepare"));
};
/**
* Update debugging breakpoints
* @param points List of line numbers having breakpoints
*/
const updateBreakpoints = (points: number[]) => {
_controller!.updateBreakpoints(points);
postMessage(ackMessage("bp-update"));
};
/**
@ -67,5 +76,7 @@ addEventListener("message", (ev: MessageEvent<WorkerRequestData>) => {
if (ev.data.type === "Reset") return resetController();
if (ev.data.type === "Prepare") return prepare(ev.data.params);
if (ev.data.type === "Execute") return execute(ev.data.params.interval);
if (ev.data.type === "UpdateBreakpoints")
return updateBreakpoints(ev.data.params.points);
throw new Error("Invalid worker message type");
});

View File

@ -4,14 +4,12 @@ import { InputEditor, InputEditorRef } from "../ui/input-editor";
import { MainLayout } from "../ui/MainLayout";
import { useExecController } from "../ui/use-exec-controller";
import { DocumentRange, LanguageProvider } from "../engines/types";
import SampleLangProvider from "../engines/sample-lang";
import BrainfuckProvider from "../engines/brainfuck";
import { OutputViewer } from "../ui/output-viewer";
export const Mainframe = () => {
const codeEditorRef = React.useRef<CodeEditorRef>(null);
const inputEditorRef = React.useRef<InputEditorRef>(null);
// const providerRef = React.useRef<LanguageProvider<any>>(SampleLangProvider);
const providerRef = React.useRef<LanguageProvider<any>>(BrainfuckProvider);
const execController = useExecController();
@ -66,7 +64,9 @@ export const Mainframe = () => {
highlights={codeHighlights}
defaultValue={providerRef.current.sampleProgram}
tokensProvider={providerRef.current.editorTokensProvider}
onUpdateBreakpoints={(newPoints) => console.log(newPoints)}
onUpdateBreakpoints={(newPoints) =>
execController.updateBreakpoints(newPoints)
}
/>
)}
renderRenderer={() => (

View File

@ -25,7 +25,7 @@ export const useExecController = <RS>() => {
const [workerState, setWorkerState] = React.useState<WorkerState>("loading");
/**
* Semi-typesafe wrapper to abstract request-response cycle into
* Type-safe wrapper to abstract request-response cycle into
* a simple imperative asynchronous call. Returns Promise that resolves
* with response data.
*
@ -34,7 +34,7 @@ export const useExecController = <RS>() => {
*
* @param request Data to send in request
* @param onData Optional argument - if passed, function enters response-streaming mode.
* Callback called with response data. Return `true` to keep the connection alive, `false` to end.
* Callback is called with response data. Return `true` to keep the connection alive, `false` to end.
* On end, promise resolves with last (already used) response data.
*/
const requestWorker = (
@ -61,6 +61,14 @@ export const useExecController = <RS>() => {
});
};
/** Utility to throw error on unexpected response */
const throwUnexpectedRes = (
fnName: string,
res: WorkerResponseData<RS>
): never => {
throw new Error(`Unexpected response on ${fnName}: ${res.toString()}`);
};
// Initialization and cleanup of web worker
React.useEffect(() => {
(async () => {
@ -68,10 +76,9 @@ export const useExecController = <RS>() => {
workerRef.current = new Worker(
new URL("../engines/worker.ts", import.meta.url)
);
const resp = await requestWorker({ type: "Init" });
if (resp.type === "state" && resp.data === "empty")
setWorkerState("empty");
else throw new Error(`Unexpected response on init: ${resp}`);
const res = await requestWorker({ type: "Init" });
if (res.type === "ack" && res.data === "init") setWorkerState("empty");
else throwUnexpectedRes("init", res);
})();
return () => {
@ -91,8 +98,25 @@ export const useExecController = <RS>() => {
type: "Prepare",
params: { code, input },
});
if (res.type === "state" && res.data === "ready") setWorkerState("ready");
else throw new Error(`Unexpected response on loadCode: ${res.toString()}`);
if (res.type === "ack" && res.data === "prepare") setWorkerState("ready");
else throwUnexpectedRes("loadCode", res);
}, []);
/**
* Update debugging breakpoints in the execution controller.
* @param points Array of line numbers having breakpoints
*/
const updateBreakpoints = React.useCallback(async (points: number[]) => {
await requestWorker(
{
type: "UpdateBreakpoints",
params: { points },
},
(res) => {
if (res.type === "ack" && res.data === "bp-update") return false;
else return true;
}
);
}, []);
/**
@ -100,9 +124,8 @@ export const useExecController = <RS>() => {
*/
const resetState = React.useCallback(async () => {
const res = await requestWorker({ type: "Reset" });
if (res.type === "state" && res.data === "empty") setWorkerState("empty");
else
throw new Error(`Unexpected response on resetState: ${res.toString()}`);
if (res.type === "ack" && res.data === "reset") setWorkerState("empty");
else throwUnexpectedRes("resetState", res);
}, []);
/**
@ -117,7 +140,7 @@ export const useExecController = <RS>() => {
setWorkerState("processing");
// Set up a streaming-response cycle with the worker
await requestWorker({ type: "Execute", params: { interval } }, (res) => {
if (res.type !== "result") return false; // TODO: Throw error here
if (res.type !== "result") return true;
onResult(res.data);
if (res.data.nextStepLocation) return true;
// Clean up and terminate response stream
@ -128,8 +151,11 @@ export const useExecController = <RS>() => {
[]
);
return React.useMemo(
() => ({ state: workerState, resetState, prepare, executeAll }),
[workerState, resetState, prepare, executeAll]
);
return {
state: workerState,
resetState,
prepare,
executeAll,
updateBreakpoints,
};
};