Refactor to make language selection dynamic
This commit is contained in:
		| @ -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<BFRS> { | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default BrainfuckLanguageEngine; | ||||
| setupWorker(new BrainfuckLanguageEngine()); | ||||
|  | ||||
| @ -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<RS> { | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export default SampleLanguageEngine; | ||||
| setupWorker(new SampleLanguageEngine()); | ||||
|  | ||||
							
								
								
									
										103
									
								
								engines/setup-worker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								engines/setup-worker.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 = <RS>(state: WorkerAckType): WorkerResponseData<RS> => ({ | ||||
|   type: "ack", | ||||
|   data: state, | ||||
| }); | ||||
|  | ||||
| /** Create a worker response for execution result */ | ||||
| const resultMessage = <RS>( | ||||
|   result: StepExecutionResult<RS> | ||||
| ): WorkerResponseData<RS> => ({ | ||||
|   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 = <RS>(controller: ExecutionController<RS>) => { | ||||
|   controller.resetState(); | ||||
|   postMessage(ackMessage("reset")); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Load program code into the engine. | ||||
|  * @param code Code content of the program | ||||
|  */ | ||||
| const prepare = <RS>( | ||||
|   controller: ExecutionController<RS>, | ||||
|   { 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 = <RS>( | ||||
|   controller: ExecutionController<RS>, | ||||
|   points: number[] | ||||
| ) => { | ||||
|   controller.updateBreakpoints(points); | ||||
|   postMessage(ackMessage("bp-update")); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Execute the entire program loaded on engine, | ||||
|  * and return result of execution. | ||||
|  */ | ||||
| const execute = <RS>(controller: ExecutionController<RS>, interval: number) => { | ||||
|   controller.executeAll({ | ||||
|     interval, | ||||
|     onResult: (res) => postMessage(resultMessage(res)), | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| /** Trigger pause in program execution */ | ||||
| const pauseExecution = async <RS>(controller: ExecutionController<RS>) => { | ||||
|   await controller.pauseExecution(); | ||||
|   postMessage(ackMessage("pause")); | ||||
| }; | ||||
|  | ||||
| /** Run a single execution step */ | ||||
| const executeStep = <RS>(controller: ExecutionController<RS>) => { | ||||
|   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 = <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); | ||||
|     throw new Error("Invalid worker message type"); | ||||
|   }); | ||||
| }; | ||||
| @ -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<any> | null = null; | ||||
|  | ||||
| /** Create a worker response for update acknowledgement */ | ||||
| const ackMessage = <RS>(state: WorkerAckType): WorkerResponseData<RS> => ({ | ||||
|   type: "ack", | ||||
|   data: state, | ||||
| }); | ||||
|  | ||||
| /** Create a worker response for execution result */ | ||||
| const resultMessage = <RS>( | ||||
|   result: StepExecutionResult<RS> | ||||
| ): WorkerResponseData<RS> => ({ | ||||
|   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<WorkerRequestData>) => { | ||||
|   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"); | ||||
| }); | ||||
							
								
								
									
										26
									
								
								pages/ide/brainfuck.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								pages/ide/brainfuck.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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 ( | ||||
|     <> | ||||
|       <Head> | ||||
|         <title>{LANG_NAME} | Esolang Park</title> | ||||
|       </Head> | ||||
|       <div style={{ height: "100%", display: "flex", flexDirection: "column" }}> | ||||
|         <Header /> | ||||
|         <div style={{ flexGrow: 1 }}> | ||||
|           <Mainframe langName={LANG_ID} provider={LangProvider} /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default IDE; | ||||
							
								
								
									
										26
									
								
								pages/ide/sample-lang.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								pages/ide/sample-lang.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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 ( | ||||
|     <> | ||||
|       <Head> | ||||
|         <title>{LANG_NAME} | Esolang Park</title> | ||||
|       </Head> | ||||
|       <div style={{ height: "100%", display: "flex", flexDirection: "column" }}> | ||||
|         <Header /> | ||||
|         <div style={{ flexGrow: 1 }}> | ||||
|           <Mainframe langName={LANG_ID} provider={LangProvider} /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default IDE; | ||||
| @ -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 = () => { | ||||
|       <div style={{ height: "100%", display: "flex", flexDirection: "column" }}> | ||||
|         <Header /> | ||||
|         <div style={{ flexGrow: 1 }}> | ||||
|           <Mainframe /> | ||||
|           <Mainframe langName="brainfuck" provider={BrainfuckProvider} /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </> | ||||
|  | ||||
| @ -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<RS> = { | ||||
|   langName: string; | ||||
|   provider: LanguageProvider<RS>; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * 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 = <RS extends {}>({ langName, provider }: Props<RS>) => { | ||||
|   // Language provider and engine | ||||
|   const providerRef = React.useRef<LanguageProvider<any>>(BrainfuckProvider); | ||||
|   const execController = useExecController(); | ||||
|   const providerRef = React.useRef(provider); | ||||
|   const execController = useExecController(langName); | ||||
|  | ||||
|   // Refs for controlling UI components | ||||
|   const codeEditorRef = React.useRef<CodeEditorRef>(null); | ||||
| @ -137,7 +141,7 @@ export const Mainframe = () => { | ||||
|       renderRenderer={() => ( | ||||
|         <RendererWrapper | ||||
|           ref={rendererRef} | ||||
|           renderer={providerRef.current.Renderer} | ||||
|           renderer={providerRef.current.Renderer as any} | ||||
|         /> | ||||
|       )} | ||||
|       renderInput={() => <InputEditor ref={inputEditorRef} />} | ||||
|  | ||||
| @ -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 = <RS>() => { | ||||
| export const useExecController = <RS>(langName: string) => { | ||||
|   const workerRef = React.useRef<Worker | null>(null); | ||||
|   const [workerState, setWorkerState] = React.useState<WorkerState>("loading"); | ||||
|  | ||||
| @ -74,9 +74,7 @@ export const useExecController = <RS>() => { | ||||
|   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); | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Nilay Majorwar
					Nilay Majorwar