Implement basic execution system and UI
This is a rather large commit that includes all of the following: - React UI with code editor, runtime renderer and input-output panes - Language providers for a sample language and Brainfuck - Implementation of code execution in a web worker - All-at-once unabortable execution of program fully functional
This commit is contained in:
21
engines/brainfuck/README.md
Normal file
21
engines/brainfuck/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Brainfuck
|
||||
|
||||
## Allowed symbols
|
||||
|
||||
- `>`: Move the pointer to the right
|
||||
- `<`: Move the pointer to the left
|
||||
- `+`: Increment the memory cell at the pointer
|
||||
- `-`: Decrement the memory cell at the pointer
|
||||
- `.`: Output the character signified by the cell at the pointer
|
||||
- `,`: Input a character and store it in the cell at the pointer
|
||||
- `[`: Jump past the matching `]` if the cell at the pointer is 0
|
||||
- `]`: Jump back to the matching `[` if the cell at the pointer is nonzero
|
||||
|
||||
## Memory specifications
|
||||
|
||||
> These parameters will be configurable when engine configuration is added to the project
|
||||
|
||||
- For Turing-completeness, the number of cells is kept unbounded.
|
||||
- Cell size is 8 bits, and allows values in the range `[-128, 127]`.
|
||||
- Value `10` is designated for newlines.
|
||||
- The value `0` is returned on reaching `EOF`.
|
81
engines/brainfuck/constants.ts
Normal file
81
engines/brainfuck/constants.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { MonacoTokensProvider } from "../types";
|
||||
|
||||
export type BFRS = {
|
||||
tape: { [k: number]: number };
|
||||
pointer: number;
|
||||
};
|
||||
|
||||
export enum BF_OP {
|
||||
LEFT = "<",
|
||||
RIGHT = ">",
|
||||
INCR = "+",
|
||||
DECR = "-",
|
||||
OUT = ".",
|
||||
IN = ",",
|
||||
LOOPIN = "[",
|
||||
LOOPOUT = "]",
|
||||
}
|
||||
|
||||
/** A single instruction of the program */
|
||||
export type BFInstruction = {
|
||||
/** Type of instruction */
|
||||
type: BF_OP;
|
||||
/** Used for location of opposite end of loops */
|
||||
param?: number;
|
||||
};
|
||||
|
||||
/** A single element of the program's AST */
|
||||
export type BFAstStep = {
|
||||
instr: BFInstruction;
|
||||
location: { line: number; char: number };
|
||||
};
|
||||
|
||||
/** Sample program printing "Hello World!" */
|
||||
export const sampleProgram = [
|
||||
"+++++ +++ Set Cell #0 to 8",
|
||||
"[",
|
||||
" >++++ Add 4 to Cell #1; this will always set Cell #1 to 4",
|
||||
" [ as the cell will be cleared by the loop",
|
||||
" >++ Add 4*2 to Cell #2",
|
||||
" >+++ Add 4*3 to Cell #3",
|
||||
" >+++ Add 4*3 to Cell #4",
|
||||
" >+ Add 4 to Cell #5",
|
||||
" <<<<- Decrement the loop counter in Cell #1",
|
||||
" ] Loop till Cell #1 is zero",
|
||||
" >+ Add 1 to Cell #2",
|
||||
" >+ Add 1 to Cell #3",
|
||||
" >- Subtract 1 from Cell #4",
|
||||
" >>+ Add 1 to Cell #6",
|
||||
" [<] Move back to the first zero cell you find; this will",
|
||||
" be Cell #1 which was cleared by the previous loop",
|
||||
" <- Decrement the loop Counter in Cell #0",
|
||||
"] Loop till Cell #0 is zero",
|
||||
"",
|
||||
"The result of this is:",
|
||||
"Cell No : 0 1 2 3 4 5 6",
|
||||
"Contents: 0 0 72 104 88 32 8",
|
||||
"Pointer : ^",
|
||||
"",
|
||||
">>. Cell #2 has value 72 which is 'H'",
|
||||
">---. Subtract 3 from Cell #3 to get 101 which is 'e'",
|
||||
"+++++ ++..+++. Likewise for 'llo' from Cell #3",
|
||||
">>. Cell #5 is 32 for the space",
|
||||
"<-. Subtract 1 from Cell #4 for 87 to give a 'W'",
|
||||
"<. Cell #3 was set to 'o' from the end of 'Hello'",
|
||||
"+++.----- -.----- ---. Cell #3 for 'rl' and 'd'",
|
||||
">>+. Add 1 to Cell #5 gives us an exclamation point",
|
||||
">++. And finally a newline from Cell #6",
|
||||
].join("\n");
|
||||
|
||||
/** Tokens provider */
|
||||
export const editorTokensProvider: MonacoTokensProvider = {
|
||||
tokenizer: {
|
||||
root: [
|
||||
[/[-\+]/, "operator"],
|
||||
[/[<>]/, "annotation"],
|
||||
[/[\[\]]/, "keyword"],
|
||||
[/[\,\.]/, "type.identifier"],
|
||||
],
|
||||
},
|
||||
defaultToken: "comment",
|
||||
};
|
181
engines/brainfuck/engine.ts
Normal file
181
engines/brainfuck/engine.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import { DocumentRange, LanguageEngine, StepExecutionResult } from "../types";
|
||||
import { BFAstStep, BFInstruction, BFRS, BF_OP } from "./constants";
|
||||
|
||||
// Default values for internal states
|
||||
// Factories are used to create new objects on reset
|
||||
const DEFAULT_AST = (): BFAstStep[] => [];
|
||||
const DEFAULT_PTR = 0;
|
||||
const DEFAULT_PC = -1;
|
||||
const DEFAULT_TAPE = (): { [k: number]: number } => ({});
|
||||
const DEFAULT_INPUT: string = "";
|
||||
|
||||
// Instruction characters
|
||||
const OP_CHARS = Object.values(BF_OP);
|
||||
|
||||
class BrainfuckLanguageEngine implements LanguageEngine<BFRS> {
|
||||
private _ast: BFAstStep[] = DEFAULT_AST();
|
||||
private _ptr: number = DEFAULT_PTR;
|
||||
private _tape: { [k: number]: number } = DEFAULT_TAPE();
|
||||
private _input: string = DEFAULT_INPUT;
|
||||
private _pc: number = DEFAULT_PC;
|
||||
|
||||
resetState() {
|
||||
this._ast = DEFAULT_AST();
|
||||
this._ptr = DEFAULT_PTR;
|
||||
this._tape = DEFAULT_TAPE();
|
||||
this._input = DEFAULT_INPUT;
|
||||
this._pc = DEFAULT_PC;
|
||||
}
|
||||
|
||||
prepare(code: string, input: string) {
|
||||
this._input = input;
|
||||
this._ast = this.parseCode(code);
|
||||
}
|
||||
|
||||
executeStep(): StepExecutionResult<BFRS> {
|
||||
// Execute and update program counter
|
||||
let output: string | undefined = undefined;
|
||||
if (this._pc !== -1) {
|
||||
const astStep = this._ast[this._pc];
|
||||
const opResult = this.processOp(astStep.instr);
|
||||
this._pc = opResult?.newPc == null ? this._pc + 1 : opResult.newPc;
|
||||
output = opResult?.output;
|
||||
} else this._pc += 1;
|
||||
|
||||
// Prepare location of next step
|
||||
let nextStepLocation: DocumentRange | null = null;
|
||||
if (this._pc < this._ast.length) {
|
||||
const { line, char } = this._ast[this._pc].location;
|
||||
const charRange = { start: char, end: char + 1 };
|
||||
nextStepLocation = { line, charRange };
|
||||
}
|
||||
|
||||
// Prepare and return execution result
|
||||
const rendererState = { tape: this._tape, pointer: this._ptr };
|
||||
return { rendererState, nextStepLocation, output };
|
||||
}
|
||||
|
||||
private parseCode(code: string) {
|
||||
const ast: BFAstStep[] = [];
|
||||
|
||||
// Stack to maintain loop counts. Element of stack denotes
|
||||
// program counter for loop-opening instruction.
|
||||
const loopStack: number[] = [];
|
||||
|
||||
// For each line...
|
||||
code.split("\n").forEach((line, lIdx) => {
|
||||
// For each character of this line...
|
||||
line.split("").forEach((char, cIdx) => {
|
||||
// Ignore if the character is not an operation
|
||||
if (!OP_CHARS.includes(char as BF_OP)) return;
|
||||
|
||||
// Update loop-tracking stack if it's a loop-char
|
||||
let jumpTarget = undefined;
|
||||
if (char === BF_OP.LOOPIN) {
|
||||
// Push loop start into stack
|
||||
// Opposite end location will be added at loop close
|
||||
loopStack.push(ast.length);
|
||||
} else if (char === BF_OP.LOOPOUT) {
|
||||
// Get location of loop-opener
|
||||
jumpTarget = loopStack.pop()!;
|
||||
// Add closing end location to loop-opener
|
||||
ast[jumpTarget].instr.param = ast.length;
|
||||
}
|
||||
|
||||
// Add instruction to AST
|
||||
ast.push({
|
||||
instr: { type: char as BF_OP, param: jumpTarget },
|
||||
location: { line: lIdx + 1, char: cIdx + 1 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the given instruction and return the updated program counter and
|
||||
* any output to send to stdout.
|
||||
*
|
||||
* If program counter is not returned, counter should be incremented by 1.
|
||||
*
|
||||
* @param instr Instruction to apply
|
||||
* @returns Optional fields for new program counter and step output
|
||||
*/
|
||||
private processOp(
|
||||
instr: BFInstruction
|
||||
): { newPc?: number; output?: string } | void {
|
||||
// Pointer-shift operations
|
||||
if (instr.type === BF_OP.LEFT) this.decrementPtr();
|
||||
else if (instr.type === BF_OP.RIGHT) this.incrementPtr();
|
||||
// Current cell modifiers
|
||||
else if (instr.type === BF_OP.INCR) this.incrementCell(this._ptr);
|
||||
else if (instr.type === BF_OP.DECR) this.decrementCell(this._ptr);
|
||||
// Input and output
|
||||
else if (instr.type === BF_OP.OUT) return { output: this.outputChar() };
|
||||
else if (instr.type === BF_OP.IN) this.inputChar();
|
||||
// Looping
|
||||
else if (instr.type === BF_OP.LOOPIN) {
|
||||
// Conditionally jump past loop-closer
|
||||
if (this.getCell(this._ptr) !== 0) return;
|
||||
return { newPc: instr.param! + 1 };
|
||||
} else if (instr.type === BF_OP.LOOPOUT) {
|
||||
// Conditionally jump to loop-opener
|
||||
if (this.getCell(this._ptr) === 0) return;
|
||||
return { newPc: instr.param };
|
||||
} else throw new Error("Unexpected instruction type");
|
||||
}
|
||||
|
||||
/** Output character from current cell */
|
||||
private outputChar(): string {
|
||||
const code = this._tape[this._ptr];
|
||||
return String.fromCharCode(code);
|
||||
}
|
||||
|
||||
/** Input character into current cell */
|
||||
private inputChar(): void {
|
||||
if (this._input.length === 0) {
|
||||
// EOF is treated as a zero
|
||||
this._tape[this._ptr] = 0;
|
||||
} else {
|
||||
// Pop first char of input and set to cell
|
||||
this._tape[this._ptr] = this._input.charCodeAt(0);
|
||||
this._input = this._input.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
/** Get value of tape cell. Initializes cell if first use */
|
||||
private getCell(cellId: number): number {
|
||||
if (!this._tape[cellId]) this._tape[cellId] = 0;
|
||||
return this._tape[cellId];
|
||||
}
|
||||
|
||||
/** Increment tape cell at specified location */
|
||||
private incrementCell(cellId: number): void {
|
||||
if (!this._tape[cellId]) this._tape[cellId] = 0;
|
||||
this._tape[cellId] += 1;
|
||||
if (this._tape[cellId] === 128) this._tape[cellId] = -128;
|
||||
}
|
||||
|
||||
/** Decrement tape cell at specified location */
|
||||
private decrementCell(cellId: number): void {
|
||||
if (!this._tape[cellId]) this._tape[cellId] = 0;
|
||||
this._tape[cellId] -= 1;
|
||||
if (this._tape[cellId] === -129) this._tape[cellId] = 127;
|
||||
}
|
||||
|
||||
/** Move the tape pointer one cell to the right */
|
||||
private incrementPtr(): void {
|
||||
this._ptr += 1;
|
||||
this.getCell(this._ptr); // Init cell if required
|
||||
}
|
||||
|
||||
/** Move the tape pointer one cell to the left */
|
||||
private decrementPtr(): void {
|
||||
if (this._ptr <= 0) throw new Error("Ptr out of bounds");
|
||||
this._ptr -= 1;
|
||||
this.getCell(this._ptr); // Init cell if required
|
||||
}
|
||||
}
|
||||
|
||||
export default BrainfuckLanguageEngine;
|
11
engines/brainfuck/index.ts
Normal file
11
engines/brainfuck/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Renderer } from "./renderer";
|
||||
import { LanguageProvider } from "../types";
|
||||
import { BFRS, sampleProgram, editorTokensProvider } from "./constants";
|
||||
|
||||
const provider: LanguageProvider<BFRS> = {
|
||||
Renderer,
|
||||
sampleProgram,
|
||||
editorTokensProvider,
|
||||
};
|
||||
|
||||
export default provider;
|
60
engines/brainfuck/renderer.tsx
Normal file
60
engines/brainfuck/renderer.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { CSSProperties } from "react";
|
||||
import { RendererProps } from "../types";
|
||||
import { BFRS } from "./constants";
|
||||
|
||||
const styles: { [k: string]: CSSProperties } = {
|
||||
container: {
|
||||
padding: 10,
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
alignContent: "flex-start",
|
||||
overflowY: "auto",
|
||||
},
|
||||
cell: {
|
||||
// Sizing
|
||||
width: "12%",
|
||||
height: "50px",
|
||||
margin: "5px 0.25%",
|
||||
padding: 12,
|
||||
// Center-align values
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
// Border and colors
|
||||
border: "1px solid gray",
|
||||
borderRadius: 5,
|
||||
background: "#394B59",
|
||||
color: "#E1E8ED",
|
||||
},
|
||||
activeCell: {
|
||||
background: "#CED9E0",
|
||||
color: "#182026",
|
||||
},
|
||||
};
|
||||
|
||||
/** Component for displaying a single tape cell */
|
||||
const Cell = ({ value, active }: { value: number; active: boolean }) => {
|
||||
const cellStyle = { ...styles.cell, ...(active ? styles.activeCell : {}) };
|
||||
return <div style={cellStyle}>{value}</div>;
|
||||
};
|
||||
|
||||
/** Renderer for Brainfuck */
|
||||
export const Renderer = ({ state }: RendererProps<BFRS>) => {
|
||||
/** Serialize tape from object format into linear array */
|
||||
const serializeTapeObj = (tape: BFRS["tape"]) => {
|
||||
const cellIdxs = Object.keys(tape).map((s) => parseInt(s, 10));
|
||||
const maxCellIdx = Math.max(15, ...cellIdxs);
|
||||
const linearTape = Array(maxCellIdx + 1).fill(0);
|
||||
cellIdxs.forEach((i) => (linearTape[i] = tape[i] || 0));
|
||||
return linearTape;
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{serializeTapeObj(state?.tape || {}).map((num, i) => (
|
||||
<Cell value={num} key={i} active={(state?.pointer || 0) === i} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
60
engines/execution-controller.ts
Normal file
60
engines/execution-controller.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { LanguageEngine, StepExecutionResult } from "./types";
|
||||
|
||||
type ExecuteAllArgs<RS> = {
|
||||
/** Interval between two execution steps, in milliseconds */
|
||||
interval?: number;
|
||||
/**
|
||||
* Pass to run in streaming-response mode.
|
||||
* Callback is called with exeuction result on every execution step.
|
||||
*/
|
||||
onResult?: (result: StepExecutionResult<RS>) => void;
|
||||
};
|
||||
|
||||
class ExecutionController<RS> {
|
||||
private _engine: LanguageEngine<RS>;
|
||||
private _result: StepExecutionResult<RS> | null;
|
||||
|
||||
/**
|
||||
* Create a new ExecutionController.
|
||||
* @param engine Language engine to use for execution
|
||||
*/
|
||||
constructor(engine: LanguageEngine<RS>) {
|
||||
this._engine = engine;
|
||||
this._engine.resetState();
|
||||
this._result = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset execution state in controller and engine.
|
||||
* Clears out state from the current execution cycle.
|
||||
*/
|
||||
resetState() {
|
||||
this._engine.resetState();
|
||||
this._result = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load code and user input into the engine to prepare for execution.
|
||||
* @param code Code content, lines separated by `\n`
|
||||
* @param input User input, lines separated by '\n'
|
||||
*/
|
||||
prepare(code: string, input: string) {
|
||||
this._engine.prepare(code, input);
|
||||
}
|
||||
|
||||
async executeAll({ interval, onResult }: ExecuteAllArgs<RS>) {
|
||||
while (true) {
|
||||
this._result = this._engine.executeStep();
|
||||
onResult && onResult(this._result);
|
||||
if (!this._result.nextStepLocation) break;
|
||||
if (interval) await this.sleep(interval);
|
||||
}
|
||||
return this._result;
|
||||
}
|
||||
|
||||
private async sleep(millis: number) {
|
||||
return new Promise<void>((resolve) => setTimeout(resolve, millis));
|
||||
}
|
||||
}
|
||||
|
||||
export default ExecutionController;
|
10
engines/sample-lang/constants.ts
Normal file
10
engines/sample-lang/constants.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/** Type for state passed to renderer */
|
||||
export type RS = { value: number };
|
||||
|
||||
/** Sample program */
|
||||
export const sampleProgram = [
|
||||
"ADD 10",
|
||||
"SUBTRACT 4",
|
||||
"MULTIPLY 3",
|
||||
"DIVIDE 2",
|
||||
].join("\n");
|
123
engines/sample-lang/engine.ts
Normal file
123
engines/sample-lang/engine.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { LanguageEngine, StepExecutionResult } from "../types";
|
||||
import { RS } from "./constants";
|
||||
|
||||
// Default values for internal engine parameters
|
||||
const DEFAULT_AST: ASTStep[] = [];
|
||||
const DEFAULT_VALUE = 0;
|
||||
const DEFAULT_PC = -1;
|
||||
const DEFAULT_INPUT: number[] = [];
|
||||
const DEFAULT_INPUT_PC = 0;
|
||||
|
||||
/** Valid op keywords */
|
||||
enum OP_KEYWORD {
|
||||
ADD = "ADD",
|
||||
SUBTRACT = "SUBTRACT",
|
||||
MULTIPLY = "MULTIPLY",
|
||||
DIVIDE = "DIVIDE",
|
||||
}
|
||||
|
||||
/** Keyword used as value for using user input */
|
||||
const inputKeyword = "input";
|
||||
|
||||
type ASTStep = {
|
||||
/** Line number the step is located on */
|
||||
index: number;
|
||||
|
||||
/** Keyword and value of the step */
|
||||
step: { keyword: OP_KEYWORD; value: number | typeof inputKeyword };
|
||||
};
|
||||
|
||||
class SampleLanguageEngine implements LanguageEngine<RS> {
|
||||
private _ast: ASTStep[] = DEFAULT_AST;
|
||||
private _value: number = DEFAULT_VALUE;
|
||||
private _pc: number = DEFAULT_PC;
|
||||
private _input: number[] = DEFAULT_INPUT;
|
||||
private _inputPc: number = DEFAULT_INPUT_PC;
|
||||
|
||||
prepare(code: string, input: string): void {
|
||||
// Parse and load code
|
||||
const lines = code.split("\n").map((l) => l.trim());
|
||||
this._ast = lines.map((line, index) => {
|
||||
const astStep = this.parseLine(line);
|
||||
return { index: index + 1, step: astStep };
|
||||
});
|
||||
// Parse and load input
|
||||
const inputWords = input.split(/\s+/); // Split on whitespace
|
||||
this._input = inputWords.map((w) => parseInt(w, 10));
|
||||
}
|
||||
|
||||
executeStep(): StepExecutionResult<RS> {
|
||||
if (this._pc === -1) {
|
||||
// Initial dummy step
|
||||
this._pc += 1;
|
||||
return {
|
||||
rendererState: { value: this._value },
|
||||
nextStepLocation: { line: 1 },
|
||||
};
|
||||
}
|
||||
|
||||
// Execute step
|
||||
if (this._pc !== -1) {
|
||||
const step = this._ast[this._pc];
|
||||
this.processOp(step.step);
|
||||
}
|
||||
const rendererState = { value: this._value };
|
||||
|
||||
// Increment pc and return
|
||||
this._pc += 1;
|
||||
if (this._pc >= this._ast.length) {
|
||||
// Program execution is complete
|
||||
return {
|
||||
rendererState,
|
||||
nextStepLocation: null,
|
||||
output: this._value.toString(),
|
||||
};
|
||||
} else {
|
||||
// Add location of next line to be executed
|
||||
const lineNum = this._ast[this._pc].index;
|
||||
return { rendererState, nextStepLocation: { line: lineNum } };
|
||||
}
|
||||
}
|
||||
|
||||
resetState(): void {
|
||||
this._ast = DEFAULT_AST;
|
||||
this._pc = DEFAULT_PC;
|
||||
this._value = DEFAULT_VALUE;
|
||||
this._input = DEFAULT_INPUT;
|
||||
this._inputPc = DEFAULT_INPUT_PC;
|
||||
}
|
||||
|
||||
private processOp(step: ASTStep["step"]) {
|
||||
// Handle user input
|
||||
let value = 0;
|
||||
if (step.value === "input") value = this._input[this._inputPc++];
|
||||
else value = step.value;
|
||||
|
||||
// Modify runtime value according to instruction
|
||||
if (step.keyword === OP_KEYWORD.ADD) this._value += value;
|
||||
else if (step.keyword === OP_KEYWORD.SUBTRACT) this._value -= value;
|
||||
else if (step.keyword === OP_KEYWORD.MULTIPLY) this._value *= value;
|
||||
else if (step.keyword === OP_KEYWORD.DIVIDE) this._value /= value;
|
||||
}
|
||||
|
||||
private parseLine = (line: string): ASTStep["step"] => {
|
||||
// Check that line has two words
|
||||
const words = line.split(" ");
|
||||
if (words.length !== 2) throw new Error("Invalid line");
|
||||
|
||||
// Check that keyword is valid
|
||||
const [keyword, value] = words;
|
||||
if (!(keyword in OP_KEYWORD)) throw new Error("Invalid keyword");
|
||||
|
||||
// Check that value is valid
|
||||
const valueAsNum = parseInt(value, 10);
|
||||
const isInvalidValue = value !== inputKeyword && Number.isNaN(valueAsNum);
|
||||
if (isInvalidValue) throw new Error("Invalid value");
|
||||
|
||||
// Return as an AST step
|
||||
const validValue = value === inputKeyword ? inputKeyword : valueAsNum;
|
||||
return { keyword: keyword as OP_KEYWORD, value: validValue };
|
||||
};
|
||||
}
|
||||
|
||||
export default SampleLanguageEngine;
|
10
engines/sample-lang/index.ts
Normal file
10
engines/sample-lang/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Renderer } from "./renderer";
|
||||
import { LanguageProvider } from "../types";
|
||||
import { RS, sampleProgram } from "./constants";
|
||||
|
||||
const provider: LanguageProvider<RS> = {
|
||||
Renderer,
|
||||
sampleProgram,
|
||||
};
|
||||
|
||||
export default provider;
|
24
engines/sample-lang/renderer.tsx
Normal file
24
engines/sample-lang/renderer.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { RendererProps } from "../types";
|
||||
import { RS } from "./constants";
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
text: {
|
||||
fontSize: "4em",
|
||||
},
|
||||
};
|
||||
|
||||
export const Renderer = ({ state }: RendererProps<RS>) => {
|
||||
const value = state == null ? 0 : state.value;
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<h1 style={styles.text}>{value}</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
71
engines/types.ts
Normal file
71
engines/types.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import monaco from "monaco-editor";
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* Type alias for defining range of characters to highlight in a single line.
|
||||
* - Missing `start` means highlight starting from start of the line.
|
||||
* - Missing `end` means highlight ending at the end of the line.
|
||||
*/
|
||||
export type CharRange = { start?: number; end?: number };
|
||||
|
||||
/** Type denoting a range of text in document spanning within a line */
|
||||
export type DocumentRange = {
|
||||
line: number;
|
||||
charRange?: CharRange;
|
||||
};
|
||||
|
||||
/** Source code token provider for the language, specific to Monaco */
|
||||
export type MonacoTokensProvider = monaco.languages.IMonarchLanguage;
|
||||
|
||||
/** Type alias for props passed to renderer */
|
||||
export type RendererProps<RS> = { state: RS | null };
|
||||
|
||||
/**
|
||||
* Type alias for the result of engine executing a single step.
|
||||
*/
|
||||
export type StepExecutionResult<RS> = {
|
||||
/** New props to be passed to the renderer */
|
||||
rendererState: RS;
|
||||
|
||||
/** String to write to program output */
|
||||
output?: string;
|
||||
|
||||
/**
|
||||
* Used to highlight next line to be executed in the editor.
|
||||
* Passing `null` indicates reaching the end of program.
|
||||
*/
|
||||
nextStepLocation: DocumentRange | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Language engine is responsible for providing
|
||||
* execution and debugging API to the platform.
|
||||
*/
|
||||
export interface LanguageEngine<RS> {
|
||||
/** Load code and user input into the engine and prepare for execution */
|
||||
prepare: (code: string, input: string) => void;
|
||||
|
||||
/** Perform a single step of code execution */
|
||||
executeStep: () => StepExecutionResult<RS>;
|
||||
|
||||
/** Reset all state to prepare for new cycle */
|
||||
resetState: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Language provider provides all language-specific
|
||||
* functionality to the platform.
|
||||
*/
|
||||
export interface LanguageProvider<RS> {
|
||||
/** Monaco-specific tokenizer for syntax highlighting */
|
||||
editorTokensProvider?: MonacoTokensProvider;
|
||||
|
||||
/** Monaco-specific autocomplete provider */
|
||||
autocompleteProvider?: any;
|
||||
|
||||
/** Sample code sample for the language */
|
||||
sampleProgram: string;
|
||||
|
||||
/** React component for visualising runtime state */
|
||||
Renderer: React.FC<RendererProps<RS>>;
|
||||
}
|
23
engines/worker-constants.ts
Normal file
23
engines/worker-constants.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { StepExecutionResult } from "./types";
|
||||
|
||||
export type WorkerRequestData =
|
||||
| {
|
||||
type: "Init";
|
||||
params?: null;
|
||||
}
|
||||
| {
|
||||
type: "Reset";
|
||||
params?: null;
|
||||
}
|
||||
| {
|
||||
type: "Prepare";
|
||||
params: { code: string; input: string };
|
||||
}
|
||||
| {
|
||||
type: "Execute";
|
||||
params: { interval?: number };
|
||||
};
|
||||
|
||||
export type WorkerResponseData<RS> =
|
||||
| { type: "state"; data: "empty" | "ready" }
|
||||
| { type: "result"; data: StepExecutionResult<RS> };
|
71
engines/worker.ts
Normal file
71
engines/worker.ts
Normal file
@ -0,0 +1,71 @@
|
||||
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";
|
||||
|
||||
let _controller: ExecutionController<any> | null = null;
|
||||
|
||||
/** Create a worker response for state update */
|
||||
const stateMessage = <RS>(
|
||||
state: "empty" | "ready"
|
||||
): WorkerResponseData<RS> => ({
|
||||
type: "state",
|
||||
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 SampleLanguageEngine();
|
||||
const engine = new BrainfuckLanguageEngine();
|
||||
_controller = new ExecutionController(engine);
|
||||
postMessage(stateMessage("empty"));
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the state of the controller and engine, to
|
||||
* prepare for execution of a new program.
|
||||
*/
|
||||
const resetController = () => {
|
||||
_controller!.resetState();
|
||||
postMessage(stateMessage("empty"));
|
||||
};
|
||||
|
||||
/**
|
||||
* 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(stateMessage("ready"));
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute the entire program loaded on engine,
|
||||
* and return result of execution.
|
||||
*/
|
||||
const execute = (interval?: number) => {
|
||||
console.info(`Executing at interval ${interval}`);
|
||||
_controller!.executeAll({
|
||||
interval,
|
||||
onResult: (res) => postMessage(resultMessage(res)),
|
||||
});
|
||||
};
|
||||
|
||||
addEventListener("message", (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);
|
||||
throw new Error("Invalid worker message type");
|
||||
});
|
Reference in New Issue
Block a user