Refactor to make language selection dynamic

This commit is contained in:
Nilay Majorwar 2021-12-18 17:15:22 +05:30
parent 2322ebe55d
commit bb9e85f422
9 changed files with 172 additions and 111 deletions

View File

@ -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());

View File

@ -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
View 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");
});
};

View File

@ -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
View 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
View 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;

View File

@ -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>
</>

View File

@ -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} />}

View File

@ -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);