
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
182 lines
6.0 KiB
TypeScript
182 lines
6.0 KiB
TypeScript
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;
|