Rename directory "engines" to "languages"
This commit is contained in:
456
languages/befunge93/runtime.ts
Normal file
456
languages/befunge93/runtime.ts
Normal file
@@ -0,0 +1,456 @@
|
||||
import InputStream from "./input-stream";
|
||||
import {
|
||||
DocumentEdit,
|
||||
DocumentRange,
|
||||
LanguageEngine,
|
||||
StepExecutionResult,
|
||||
} from "../types";
|
||||
import { ParseError, RuntimeError } from "../worker-errors";
|
||||
import { Bfg93RS, Bfg93Op, Bfg93Direction } from "./constants";
|
||||
import { toSafePrintableChar } from "../engine-utils";
|
||||
|
||||
const ROWSIZE = 80; // Maximum size of a single grid row
|
||||
const COLSIZE = 25; // Maximum size of a single grid column
|
||||
|
||||
/** Program counter is coordinates in 2D grid. */
|
||||
type PC = {
|
||||
x: number; // 0-indexed, goes rightwards
|
||||
y: number; // 0-indexed, goes downwards
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines bounds of the used portion of the grid. So, if the code
|
||||
* only occupies top-left 30x20 square, all items in array `x` are < 30,
|
||||
* and all items in array `y` are < 20.
|
||||
*
|
||||
* - `bounds.x[10]`: highest index used on 11th row of grid
|
||||
* - `bounds.y[5]`: highest index used on 6th column of grid
|
||||
*/
|
||||
type CodeBounds = {
|
||||
x: number[];
|
||||
y: number[];
|
||||
};
|
||||
|
||||
// Default values for internal states
|
||||
// Factories are used to create new objects on reset
|
||||
const DEFAULT_AST = (): string[] => [];
|
||||
const DEFAULT_PC = () => ({ x: -1, y: -1 });
|
||||
const DEFAULT_STACK = (): number[] => [];
|
||||
const DEFAULT_DIRN = Bfg93Direction.RIGHT;
|
||||
const DEFAULT_STR_MODE = false;
|
||||
const DEFAULT_BOUNDS = (): CodeBounds => ({
|
||||
x: [],
|
||||
y: [],
|
||||
});
|
||||
|
||||
// List of characters representing valid Befunge-93 ops
|
||||
const OP_CHARS = Object.values(Bfg93Op);
|
||||
|
||||
export default class Befunge93LanguageEngine
|
||||
implements LanguageEngine<Bfg93RS>
|
||||
{
|
||||
private _ast: string[] = DEFAULT_AST();
|
||||
private _stack: number[] = DEFAULT_STACK();
|
||||
private _pc: PC = DEFAULT_PC();
|
||||
private _dirn: Bfg93Direction = DEFAULT_DIRN;
|
||||
private _strmode: boolean = DEFAULT_STR_MODE;
|
||||
private _bounds: CodeBounds = DEFAULT_BOUNDS();
|
||||
private _input: InputStream = new InputStream("");
|
||||
private _edits: DocumentEdit[] = [];
|
||||
|
||||
resetState() {
|
||||
this._ast = DEFAULT_AST();
|
||||
this._stack = DEFAULT_STACK();
|
||||
this._pc = DEFAULT_PC();
|
||||
this._dirn = DEFAULT_DIRN;
|
||||
this._strmode = DEFAULT_STR_MODE;
|
||||
this._bounds = DEFAULT_BOUNDS();
|
||||
this._input = new InputStream("");
|
||||
this._edits = [];
|
||||
}
|
||||
|
||||
validateCode(code: string) {
|
||||
this.parseCode(code);
|
||||
}
|
||||
|
||||
prepare(code: string, input: string) {
|
||||
this._ast = this.parseCode(code);
|
||||
this._edits = this.getGridPaddingEdits(code);
|
||||
this._input = new InputStream(input);
|
||||
}
|
||||
|
||||
executeStep(): StepExecutionResult<Bfg93RS> {
|
||||
// Execute and update program counter
|
||||
let output: string | undefined = undefined;
|
||||
let edits: DocumentEdit[] | undefined = undefined;
|
||||
let end: boolean = false;
|
||||
if (this._pc.x === -1 && this._pc.y === -1) {
|
||||
this._pc = { x: 0, y: 0 };
|
||||
edits = this._edits;
|
||||
} else {
|
||||
const result = this.processOp();
|
||||
output = result.output;
|
||||
edits = result.edit && [result.edit];
|
||||
end = !!result.end;
|
||||
}
|
||||
|
||||
// Prepare location of next step
|
||||
let nextStepLocation: DocumentRange | null = null;
|
||||
if (!end) nextStepLocation = this.toRange(this._pc.y, this._pc.x);
|
||||
|
||||
// Prepare and return execution result
|
||||
const rendererState: Bfg93RS = {
|
||||
stack: this._stack,
|
||||
direction: this._dirn,
|
||||
strMode: this._strmode,
|
||||
};
|
||||
return { rendererState, nextStepLocation, output, codeEdits: edits };
|
||||
}
|
||||
|
||||
private parseCode(code: string) {
|
||||
// A Befunge program can contain any character in the program, so the only
|
||||
// validation to do is ensure program is within 80x25 bounds.
|
||||
|
||||
// Validate that program is within the 80x25 bounds
|
||||
const lines = code.split("\n");
|
||||
if (lines.length > COLSIZE)
|
||||
throw new ParseError(`Code is longer than ${COLSIZE} lines`, {
|
||||
line: COLSIZE,
|
||||
});
|
||||
lines.forEach((line, idx) => {
|
||||
if (line.length > ROWSIZE)
|
||||
throw new ParseError(`Line is longer than ${ROWSIZE} characters`, {
|
||||
line: idx,
|
||||
charRange: { start: ROWSIZE },
|
||||
});
|
||||
});
|
||||
|
||||
// Global bounds for each axis
|
||||
const maxX = Math.max(...lines.map((line) => line.length - 1));
|
||||
const maxY = lines.length - 1;
|
||||
|
||||
// Define bounds for each line and column
|
||||
for (let i = 0; i < COLSIZE; ++i)
|
||||
this._bounds.x[i] = lines[i]?.length - 1 || -1;
|
||||
for (let j = 0; j < ROWSIZE; ++j) this._bounds.y[j] = j <= maxX ? maxY : -1;
|
||||
|
||||
// Pad the program to size 80x25 for execution
|
||||
const grid = lines.map((line) => line.padEnd(80, " "));
|
||||
grid.push(...new Array(25 - lines.length).fill(" ".repeat(80)));
|
||||
return grid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the instruction at the current program grid pointer.
|
||||
* Also updates stack and pointer states.
|
||||
* @returns String to append to output, if any
|
||||
*/
|
||||
private processOp(): { output?: string; end?: boolean; edit?: DocumentEdit } {
|
||||
const char = this.getGridCell(this._pc.x, this._pc.y);
|
||||
if (this._strmode && char !== '"') {
|
||||
// Push character to string and return;
|
||||
this._stack.push(char.charCodeAt(0));
|
||||
this.updatePointer();
|
||||
return {};
|
||||
}
|
||||
|
||||
let output: string | undefined = undefined;
|
||||
let edit: DocumentEdit | undefined = undefined;
|
||||
let end: boolean = false;
|
||||
|
||||
const op = this.charToOp(char);
|
||||
if (!op) throw new RuntimeError("Invalid instruction");
|
||||
switch (op) {
|
||||
case Bfg93Op.NOOP: {
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.ADD: {
|
||||
const a = this.popStack();
|
||||
const b = this.popStack();
|
||||
this.pushStack(a + b);
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.SUBTRACT: {
|
||||
const a = this.popStack();
|
||||
const b = this.popStack();
|
||||
this.pushStack(b - a);
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.MULTIPLY: {
|
||||
const a = this.popStack();
|
||||
const b = this.popStack();
|
||||
this.pushStack(a * b);
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.DIVIDE: {
|
||||
const a = this.popStack();
|
||||
const b = this.popStack();
|
||||
if (a === 0) throw new RuntimeError("cannot divide by zero");
|
||||
this.pushStack(Math.floor(b / a));
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.MODULO: {
|
||||
const a = this.popStack();
|
||||
const b = this.popStack();
|
||||
this.pushStack(b % a);
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.NOT: {
|
||||
const val = this.popStack();
|
||||
this.pushStack(val === 0 ? 1 : 0);
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.GREATER: {
|
||||
const a = this.popStack();
|
||||
const b = this.popStack();
|
||||
this.pushStack(b > a ? 1 : 0);
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.RIGHT: {
|
||||
this._dirn = Bfg93Direction.RIGHT;
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.LEFT: {
|
||||
this._dirn = Bfg93Direction.LEFT;
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.UP: {
|
||||
this._dirn = Bfg93Direction.UP;
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.DOWN: {
|
||||
this._dirn = Bfg93Direction.DOWN;
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.RANDOM: {
|
||||
const rand = Math.floor(Math.random() * 4);
|
||||
if (rand === 0) this._dirn = Bfg93Direction.RIGHT;
|
||||
else if (rand === 1) this._dirn = Bfg93Direction.LEFT;
|
||||
else if (rand === 2) this._dirn = Bfg93Direction.UP;
|
||||
else this._dirn = Bfg93Direction.DOWN;
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.H_IF: {
|
||||
const val = this.popStack();
|
||||
if (val === 0) this._dirn = Bfg93Direction.RIGHT;
|
||||
else this._dirn = Bfg93Direction.LEFT;
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.V_IF: {
|
||||
const val = this.popStack();
|
||||
if (val === 0) this._dirn = Bfg93Direction.DOWN;
|
||||
else this._dirn = Bfg93Direction.UP;
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.TOGGLE_STR: {
|
||||
this._strmode = !this._strmode;
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.DUPLICATE: {
|
||||
const val = this.popStack();
|
||||
this.pushStack(val);
|
||||
this.pushStack(val);
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.SWAP: {
|
||||
const top = this.popStack();
|
||||
const other = this.popStack();
|
||||
this.pushStack(top);
|
||||
this.pushStack(other);
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.POP_DELETE: {
|
||||
this.popStack();
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.POP_OUTINT: {
|
||||
const int = this.popStack();
|
||||
output = int.toString() + " ";
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.POP_OUTCHAR: {
|
||||
const charCode = this.popStack();
|
||||
output = String.fromCharCode(charCode);
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.BRIDGE: {
|
||||
this.updatePointer();
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.GET_DATA: {
|
||||
const y = this.popStack();
|
||||
const x = this.popStack();
|
||||
const char = this.getGridCell(x, y);
|
||||
this.pushStack(char.charCodeAt(0));
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.PUT_DATA: {
|
||||
const y = this.popStack();
|
||||
const x = this.popStack();
|
||||
const charCode = this.popStack();
|
||||
edit = this.setGridCell(x, y, charCode);
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.STDIN_INT: {
|
||||
this.pushStack(this._input.getNumber());
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.STDIN_CHAR: {
|
||||
const charCode = this._input.getChar();
|
||||
this.pushStack(charCode);
|
||||
break;
|
||||
}
|
||||
case Bfg93Op.END: {
|
||||
end = true;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this.pushStack(parseInt(op, 10));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update grid pointer and return
|
||||
this.updatePointer();
|
||||
return { output, end, edit };
|
||||
}
|
||||
|
||||
/** Push a number onto the stack */
|
||||
private pushStack(num: number): void {
|
||||
this._stack.push(num);
|
||||
}
|
||||
|
||||
/** Pop a number from stack. If empty stack, returns 0 */
|
||||
private popStack(): number {
|
||||
if (this._stack.length === 0) return 0;
|
||||
else return this._stack.pop()!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get character at position (x, y) of program grid.
|
||||
* Throws RuntimeError if (x, y) is out of bounds.
|
||||
*/
|
||||
private getGridCell(x: number, y: number): string {
|
||||
if (!this.isInGrid(x, y))
|
||||
throw new RuntimeError("Coordinates out of bounds");
|
||||
else return this._ast[y][x];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cell at (x, y) of program grid to character with given ASCII value.
|
||||
* Throws if (x, y) is out of bounds
|
||||
*/
|
||||
private setGridCell(x: number, y: number, asciiVal: number): DocumentEdit {
|
||||
if (!this.isInGrid(x, y))
|
||||
throw new RuntimeError("Coordinates out of bound");
|
||||
|
||||
// Change character at position (x, y)
|
||||
this._ast[y] =
|
||||
this._ast[y].slice(0, x) +
|
||||
String.fromCharCode(asciiVal) +
|
||||
this._ast[y].slice(x + 1);
|
||||
|
||||
// Update grid bounds
|
||||
this._bounds.x[y] = Math.max(this._bounds.x[y], x);
|
||||
this._bounds.y[x] = Math.max(this._bounds.y[x], y);
|
||||
|
||||
// Return code edit object
|
||||
return {
|
||||
text: toSafePrintableChar(asciiVal),
|
||||
range: { line: y, charRange: { start: x, end: x + 1 } },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update program grid pointer according to currently set direction.
|
||||
* Throws RuntimeError if pointer lands outside 80x25 grid.
|
||||
*/
|
||||
private updatePointer(): void {
|
||||
// Update pointer
|
||||
if (this._dirn === Bfg93Direction.RIGHT) this._pc.x += 1;
|
||||
else if (this._dirn === Bfg93Direction.LEFT) this._pc.x -= 1;
|
||||
else if (this._dirn === Bfg93Direction.UP) this._pc.y -= 1;
|
||||
else if (this._dirn === Bfg93Direction.DOWN) this._pc.y += 1;
|
||||
else throw new Error("Unknown direction");
|
||||
|
||||
// Check pointer position and wrap if necessary
|
||||
this.wrapPointer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the pointer around the program bounds. Note that program bounds are
|
||||
* not 80x25 - they are the bounds of the used parts of grid.
|
||||
*
|
||||
* Assumes that only one of x and y-coordinates is out of bounds.
|
||||
*/
|
||||
private wrapPointer(): void {
|
||||
if (this._strmode) {
|
||||
// String mode: just wrap the pointer around the 80x25 grid
|
||||
this._pc.x = (this._pc.x + ROWSIZE) % ROWSIZE;
|
||||
this._pc.y = (this._pc.y + COLSIZE) % COLSIZE;
|
||||
} else if (
|
||||
this._dirn === Bfg93Direction.LEFT ||
|
||||
this._dirn === Bfg93Direction.RIGHT
|
||||
) {
|
||||
// Wrap pointer around code bounds in horizontal direction (along x-axis)
|
||||
if (this._pc.x < 0) this._pc.x = this._bounds.x[this._pc.y];
|
||||
else if (this._pc.x > this._bounds.x[this._pc.y]) this._pc.x = 0;
|
||||
} else {
|
||||
// Wrap pointer around code bounds in vertical direction (along y-axis)
|
||||
if (this._pc.y < 0) this._pc.y = this._bounds.y[this._pc.x];
|
||||
else if (this._pc.y > this._bounds.y[this._pc.x]) this._pc.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate `DocumentEdit`s to apply on code to pad it up to 80x25 size.
|
||||
* @param code Code content, lines separated by '\n'
|
||||
* @returns Array of `DocumentEdit`s to apply on code
|
||||
*/
|
||||
private getGridPaddingEdits(code: string): DocumentEdit[] {
|
||||
const lines = code.split("\n");
|
||||
const edits: DocumentEdit[] = [];
|
||||
for (let i = 0; i < COLSIZE; ++i) {
|
||||
if (i < lines.length) {
|
||||
if (lines[i].length === ROWSIZE) continue;
|
||||
// Add padding to line upto full-length
|
||||
edits.push({
|
||||
range: {
|
||||
line: i,
|
||||
charRange: { start: lines[i].length, end: lines[i].length },
|
||||
},
|
||||
text: " ".repeat(ROWSIZE - lines[i].length),
|
||||
});
|
||||
} else {
|
||||
// Add full-length empty line
|
||||
edits.push({
|
||||
range: { line: i, charRange: { start: 0, end: 0 } },
|
||||
text: "\n" + " ".repeat(80),
|
||||
});
|
||||
}
|
||||
}
|
||||
return edits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast the given character to corresponding Befunge-93 op.
|
||||
* If character is invalid op, returns null.
|
||||
* @param char Character to cast to Befunge-93 op
|
||||
* @returns Corresponding Befunge-93 op, or null.
|
||||
*/
|
||||
private charToOp(char: string): Bfg93Op | null {
|
||||
if (char.length !== 1) throw new Error(`'${char}' not a character`);
|
||||
if (!OP_CHARS.includes(char as Bfg93Op)) return null;
|
||||
else return char as Bfg93Op;
|
||||
}
|
||||
|
||||
/** Convert 2D coordinates to DocumentRange */
|
||||
private toRange(line: number, char: number): DocumentRange {
|
||||
return { line, charRange: { start: char, end: char + 1 } };
|
||||
}
|
||||
|
||||
/** Check if given coordinates lies inside 80x25 grid */
|
||||
private isInGrid(x: number, y: number): boolean {
|
||||
return x >= 0 && x < ROWSIZE && y >= 0 && y < COLSIZE;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user