-
Notifications
You must be signed in to change notification settings - Fork 8
Architecture
The core design of Esolang Park is quite simple. It consists of two broad parts, which run in different threads and communicate with each other by passing messages. These two parts are the React frontend (which displays things) and the execution worker (which does the actual program execution).
If you're interested in adding a language to Esolang Park, you should read the Language Providers guide after this page. The guide contains more implementation-oriented details, while this page contains the overall functioning of the IDE and how it interacts with the language provider.
The React frontend contains 5 main components:
-
CodeEditor: wrapper around Monaco editor, along with some code for breakpoints, highlights and stuff. -
InputEditor: a simple textarea, exposes single function for getting input value. -
OutputViewer: readonly text for showing program output and errors. -
RendererWrapper: wrapper around the language-specific renderer. -
Mainframe: root component, manages updates to above components and communicates with the web worker.
For performance reasons, the first four "child" components expose an imperative API instead of doing it the declarative React way. This allows the app to render execution updates quick enough - for instance, the CodeEditor component is not re-rendered at all during program execution.
Components of the language provider (apart from the language engine) are directly imported into the frontend for syntax highlighting, the Renderer, and for loading a language-specific sample program in the code editor.
Actual execution of the user's program occurs in a web worker (a separate OS-level thread), which communicates with the main thread (the React frontend) via message-passing. The web worker consists of only three parts:
-
setupWorker: worker entrypoint script, sets up communication with the main thread. -
ExecutionController: encapsulates all logic related to the execution loop. -
LanguageEngine: language-specific class that contains the language's parser and interpreter.
The below diagram summarises the workings of the above parts:

The goal of the execution process is to abstract the execution loop away and keep the language interpreters as API-slim as possible. To achieve this, the execution process is split into two parts: the ExecutionController and the language engine.
The ExecutionController class is responsible for handling the execution loop, including play-pause and breakpoint handling. It exposes the following API to the worker entrypoint script:
-
updateBreakpoints(breakpoints): updates ExecutionController about addition or removal of a breakpoint -
prepare(code, input): parses code and loads the parsed AST along with user input -
execute(): executes (or resumes execution of) the loaded program until paused or a breakpoint is reached -
pauseExecution(): pauses the ongoing execution -
resetState(): reset the ExecutionController and language engine for a new cycle of execution
The language engine (an interface implemented for each esolang) implements everything execution-related that is specific to the esolang. The bulk of adding a new language lies in implementing this interface. It exposes the following API to the ExecutionController:
-
prepare(code, input): parses code and loads the parsed AST along with user input -
resetState(): reset all internal state for a new cycle of execution -
executeStep(): performs a single near-atomic step of execution, and returns updated runtime state
The return value of executeStep is important, and contains the following fields:
-
rendererState: T: passed as props to theRenderercomponent in React frontend -
output?: string: appended to output in theOutputViewercomponent in React frontend -
codeEdits?: DocumentEdit[]: for self-modifying esolangs - edits to apply on the code being executed (resetted after execution) -
nextStepLocation: DocumentRange | null: section of code that's going to be run on the next step. Highlighted in theCodeEditor.nullindicates end of execution.
Each iteration of the execution loop in the ExecutionController, roughly this happens:
- ExecutionController asks the language engine to process one step.
- If
nextStepLocationis null, resolve any pause calls and send result. - Check for pending pause calls. If yes, resolve them and send result with signal "paused".
- If
nextStepLocationhas a breakpoint set, send result with signal "paused". - Sleep for
executionIntervalmilliseconds.
The execution loop also includes error handling. Errors thrown by the language engine are caught and serialized at the ExecutionController layer, and sent to the main thread as part of the result message.
The executeStep function is the crux of the language engine - it is responsible for taking the code execution process from one step to the next. This allows the execution loop to be abstracted away from the language engine and still support complete flexibility in language semantics (well, almost). Language engines for both Chef - simple line-by-line syntax - and Befunge - a 2D self-modifying instruction grid - implement the same one interface above.
The following sequence diagram illustrates the execution process. The code is quite readable (hopefully), so feel free to look into the codebase for more details.

Due to being a web app, native-like performance cannot be a goal at all. For Esolang Park, the bottleneck in performance turns out to be the React frontend. It is the time taken by the React frontend to render execution updates that dictates the fastest possible execution speed. If the React frontend is unable to keep up with the execution speed set by user, the main thread won't be able to handle worker responses and the message queue piles up. The one big symptom of this is the pause and stop buttons working after a delay of seconds (proportional to queue pileup).
This performance limit necessitates a suboptimal execution interval. The >5ms execution interval makes debugging medium to long running programs a pain if the point you want to debug comes late in the program. For quick checks, an offline native interpreter or even a lightweight online interpreter fares much better than Esolang Park.
An idea to resolve the above problem is to allow non-visual execution (quick runs). Execution can happen at 0ms intervals, while only a spinner spins on the frontend and stdout is synced. User can still pause execution or set breakpoints, on which the debugger goes back to visual mode.