diff --git a/engines/brainfuck/engine.ts b/engines/brainfuck/engine.ts index 8158f94..97799a9 100644 --- a/engines/brainfuck/engine.ts +++ b/engines/brainfuck/engine.ts @@ -1,3 +1,4 @@ +import { setupWorker } from "../setup-worker"; import { DocumentRange, LanguageEngine, StepExecutionResult } from "../types"; import { BFAstStep, BFInstruction, BFRS, BF_OP } from "./constants"; @@ -178,4 +179,4 @@ class BrainfuckLanguageEngine implements LanguageEngine { } } -export default BrainfuckLanguageEngine; +setupWorker(new BrainfuckLanguageEngine()); diff --git a/engines/sample-lang/engine.ts b/engines/sample-lang/engine.ts index 15fd8e2..57e2e4f 100644 --- a/engines/sample-lang/engine.ts +++ b/engines/sample-lang/engine.ts @@ -1,3 +1,4 @@ +import { setupWorker } from "../setup-worker"; import { LanguageEngine, StepExecutionResult } from "../types"; import { RS } from "./constants"; @@ -120,4 +121,4 @@ class SampleLanguageEngine implements LanguageEngine { }; } -export default SampleLanguageEngine; +setupWorker(new SampleLanguageEngine()); diff --git a/engines/setup-worker.ts b/engines/setup-worker.ts new file mode 100644 index 0000000..04f2bd3 --- /dev/null +++ b/engines/setup-worker.ts @@ -0,0 +1,103 @@ +import ExecutionController from "./execution-controller"; +import { LanguageEngine, StepExecutionResult } from "./types"; +import { + WorkerAckType, + WorkerRequestData, + WorkerResponseData, +} from "./worker-constants"; + +/** Create a worker response for update acknowledgement */ +const ackMessage = (state: WorkerAckType): WorkerResponseData => ({ + type: "ack", + data: state, +}); + +/** Create a worker response for execution result */ +const resultMessage = ( + result: StepExecutionResult +): WorkerResponseData => ({ + type: "result", + data: result, +}); + +/** Initialize the execution controller */ +const initController = () => { + postMessage(ackMessage("init")); +}; + +/** + * Reset the state of the controller and engine, to + * prepare for execution of a new program. + */ +const resetController = (controller: ExecutionController) => { + controller.resetState(); + postMessage(ackMessage("reset")); +}; + +/** + * Load program code into the engine. + * @param code Code content of the program + */ +const prepare = ( + controller: ExecutionController, + { code, input }: { code: string; input: string } +) => { + controller.prepare(code, input); + postMessage(ackMessage("prepare")); +}; + +/** + * Update debugging breakpoints + * @param points List of line numbers having breakpoints + */ +const updateBreakpoints = ( + controller: ExecutionController, + points: number[] +) => { + controller.updateBreakpoints(points); + postMessage(ackMessage("bp-update")); +}; + +/** + * Execute the entire program loaded on engine, + * and return result of execution. + */ +const execute = (controller: ExecutionController, interval: number) => { + controller.executeAll({ + interval, + onResult: (res) => postMessage(resultMessage(res)), + }); +}; + +/** Trigger pause in program execution */ +const pauseExecution = async (controller: ExecutionController) => { + await controller.pauseExecution(); + postMessage(ackMessage("pause")); +}; + +/** Run a single execution step */ +const executeStep = (controller: ExecutionController) => { + const result = controller.executeStep(); + postMessage(resultMessage(result)); +}; + +/** + * Create an execution controller worker script with the given engine. + * @param engine Language engine to create worker for + */ +export const setupWorker = (engine: LanguageEngine) => { + const controller = new ExecutionController(engine); + + addEventListener("message", async (ev: MessageEvent) => { + 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); + throw new Error("Invalid worker message type"); + }); +}; diff --git a/engines/worker.ts b/engines/worker.ts deleted file mode 100644 index 23062a9..0000000 --- a/engines/worker.ts +++ /dev/null @@ -1,99 +0,0 @@ -import BrainfuckLanguageEngine from "./brainfuck/engine"; -import ExecutionController from "./execution-controller"; -import { StepExecutionResult } from "./types"; -import { - WorkerAckType, - WorkerRequestData, - WorkerResponseData, -} from "./worker-constants"; - -let _controller: ExecutionController | null = null; - -/** Create a worker response for update acknowledgement */ -const ackMessage = (state: WorkerAckType): WorkerResponseData => ({ - type: "ack", - data: state, -}); - -/** Create a worker response for execution result */ -const resultMessage = ( - result: StepExecutionResult -): WorkerResponseData => ({ - type: "result", - data: result, -}); - -/** - * Initialize the execution controller. - */ -const initController = () => { - const engine = new BrainfuckLanguageEngine(); - _controller = new ExecutionController(engine); - postMessage(ackMessage("init")); -}; - -/** - * Reset the state of the controller and engine, to - * prepare for execution of a new program. - */ -const resetController = () => { - _controller!.resetState(); - postMessage(ackMessage("reset")); -}; - -/** - * Load program code into the engine. - * @param code Code content of the program - */ -const prepare = ({ code, input }: { code: string; input: string }) => { - _controller!.prepare(code, input); - 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")); -}; - -/** - * Execute the entire program loaded on engine, - * and return result of execution. - */ -const execute = (interval: number) => { - _controller!.executeAll({ - interval, - onResult: (res) => postMessage(resultMessage(res)), - }); -}; - -/** - * Trigger pause in program execution - */ -const pauseExecution = async () => { - await _controller!.pauseExecution(); - postMessage(ackMessage("pause")); -}; - -/** - * Run a single execution step - */ -const executeStep = () => { - const result = _controller!.executeStep(); - postMessage(resultMessage(result)); -}; - -addEventListener("message", async (ev: MessageEvent) => { - if (ev.data.type === "Init") return initController(); - 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 === "Pause") return await pauseExecution(); - if (ev.data.type === "ExecuteStep") return executeStep(); - if (ev.data.type === "UpdateBreakpoints") - return updateBreakpoints(ev.data.params.points); - throw new Error("Invalid worker message type"); -}); diff --git a/pages/ide/brainfuck.tsx b/pages/ide/brainfuck.tsx new file mode 100644 index 0000000..0f9545f --- /dev/null +++ b/pages/ide/brainfuck.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { NextPage } from "next"; +import Head from "next/head"; +import { Mainframe } from "../../ui/Mainframe"; +import { Header } from "../../ui/header"; +import LangProvider from "../../engines/brainfuck"; +const LANG_ID = "brainfuck"; +const LANG_NAME = "Brainfuck"; + +const IDE: NextPage = () => { + return ( + <> + + {LANG_NAME} | Esolang Park + +
+
+
+ +
+
+ + ); +}; + +export default IDE; diff --git a/pages/ide/sample-lang.tsx b/pages/ide/sample-lang.tsx new file mode 100644 index 0000000..b4fbbba --- /dev/null +++ b/pages/ide/sample-lang.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { NextPage } from "next"; +import Head from "next/head"; +import { Mainframe } from "../../ui/Mainframe"; +import { Header } from "../../ui/header"; +import LangProvider from "../../engines/sample-lang"; +const LANG_ID = "sample-lang"; +const LANG_NAME = "Sample"; + +const IDE: NextPage = () => { + return ( + <> + + {LANG_NAME} | Esolang Park + +
+
+
+ +
+
+ + ); +}; + +export default IDE; diff --git a/pages/index.tsx b/pages/index.tsx index cfd0af4..36c8dfa 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -3,6 +3,7 @@ import { NextPage } from "next"; import { Mainframe } from "../ui/Mainframe"; import Head from "next/head"; import { Header } from "../ui/header"; +import BrainfuckProvider from "../engines/brainfuck"; const Index: NextPage = () => { return ( @@ -13,7 +14,7 @@ const Index: NextPage = () => {
- +
diff --git a/ui/Mainframe.tsx b/ui/Mainframe.tsx index 5e6c779..c1e7e72 100644 --- a/ui/Mainframe.tsx +++ b/ui/Mainframe.tsx @@ -4,11 +4,15 @@ import { InputEditor, InputEditorRef } from "../ui/input-editor"; import { MainLayout } from "../ui/MainLayout"; import { useExecController } from "../ui/use-exec-controller"; import { LanguageProvider, StepExecutionResult } from "../engines/types"; -import BrainfuckProvider from "../engines/brainfuck"; import { OutputViewer, OutputViewerRef } from "../ui/output-viewer"; import { ExecutionControls } from "./execution-controls"; import { RendererRef, RendererWrapper } from "./renderer-wrapper"; +type Props = { + langName: string; + provider: LanguageProvider; +}; + /** * React component that contains and controls the entire IDE. * @@ -17,10 +21,10 @@ import { RendererRef, RendererWrapper } from "./renderer-wrapper"; * small execution intervals if rendered on every execution. All state management * is delegated to imperatively controlled child components. */ -export const Mainframe = () => { +export const Mainframe = ({ langName, provider }: Props) => { // Language provider and engine - const providerRef = React.useRef>(BrainfuckProvider); - const execController = useExecController(); + const providerRef = React.useRef(provider); + const execController = useExecController(langName); // Refs for controlling UI components const codeEditorRef = React.useRef(null); @@ -137,7 +141,7 @@ export const Mainframe = () => { renderRenderer={() => ( )} renderInput={() => } diff --git a/ui/use-exec-controller.ts b/ui/use-exec-controller.ts index c8f43ee..b116a2e 100644 --- a/ui/use-exec-controller.ts +++ b/ui/use-exec-controller.ts @@ -21,7 +21,7 @@ type WorkerState = * Also abstracts away the details of message-passing and exposes * an imperative API to the parent component. */ -export const useExecController = () => { +export const useExecController = (langName: string) => { const workerRef = React.useRef(null); const [workerState, setWorkerState] = React.useState("loading"); @@ -74,9 +74,7 @@ export const useExecController = () => { React.useEffect(() => { (async () => { if (workerRef.current) throw new Error("Tried to reinitialize worker"); - workerRef.current = new Worker( - new URL("../engines/worker.ts", import.meta.url) - ); + workerRef.current = new Worker(`../workers/${langName}.js`); const res = await requestWorker({ type: "Init" }); if (res.type === "ack" && res.data === "init") setWorkerState("empty"); else throwUnexpectedRes("init", res);