diff --git a/engines/execution-controller.ts b/engines/execution-controller.ts index ea1ecee..d88cf7d 100644 --- a/engines/execution-controller.ts +++ b/engines/execution-controller.ts @@ -12,6 +12,7 @@ type ExecuteAllArgs = { class ExecutionController { private _engine: LanguageEngine; + private _breakpoints: number[] = []; private _result: StepExecutionResult | null; /** @@ -42,12 +43,20 @@ class ExecutionController { 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) { 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; } diff --git a/engines/worker-constants.ts b/engines/worker-constants.ts index 2e2f27c..c1ac3a6 100644 --- a/engines/worker-constants.ts +++ b/engines/worker-constants.ts @@ -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 = - | { type: "state"; data: "empty" | "ready" } + | { type: "ack"; data: WorkerAckType } | { type: "result"; data: StepExecutionResult }; diff --git a/engines/worker.ts b/engines/worker.ts index 215d503..8042537 100644 --- a/engines/worker.ts +++ b/engines/worker.ts @@ -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 | null = null; -/** Create a worker response for state update */ -const stateMessage = ( - state: "empty" | "ready" -): WorkerResponseData => ({ - type: "state", +/** Create a worker response for update acknowledgement */ +const ackMessage = (state: WorkerAckType): WorkerResponseData => ({ + type: "ack", data: state, }); @@ -26,10 +27,9 @@ const resultMessage = ( * 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) => { 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"); }); diff --git a/ui/Mainframe.tsx b/ui/Mainframe.tsx index 311bd9e..1c26c0f 100644 --- a/ui/Mainframe.tsx +++ b/ui/Mainframe.tsx @@ -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(null); const inputEditorRef = React.useRef(null); - // const providerRef = React.useRef>(SampleLangProvider); const providerRef = React.useRef>(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={() => ( diff --git a/ui/use-exec-controller.ts b/ui/use-exec-controller.ts index 63827b3..490727f 100644 --- a/ui/use-exec-controller.ts +++ b/ui/use-exec-controller.ts @@ -25,7 +25,7 @@ export const useExecController = () => { const [workerState, setWorkerState] = React.useState("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 = () => { * * @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 = () => { }); }; + /** Utility to throw error on unexpected response */ + const throwUnexpectedRes = ( + fnName: string, + res: WorkerResponseData + ): 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 = () => { 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 = () => { 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 = () => { */ 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 = () => { 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 = () => { [] ); - return React.useMemo( - () => ({ state: workerState, resetState, prepare, executeAll }), - [workerState, resetState, prepare, executeAll] - ); + return { + state: workerState, + resetState, + prepare, + executeAll, + updateBreakpoints, + }; };