Add Befunge-93
This commit is contained in:
parent
81a3563181
commit
45385a3266
3
engines/befunge93/README.md
Normal file
3
engines/befunge93/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Befunge-93
|
||||
|
||||
- Interactive input is not supported yet, so currenty division-by-zero throws a runtime error.
|
79
engines/befunge93/constants.ts
Normal file
79
engines/befunge93/constants.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { MonacoTokensProvider } from "../types";
|
||||
|
||||
export type Bfg93RS = {
|
||||
stack: number[];
|
||||
direction: Bfg93Direction;
|
||||
strMode: boolean;
|
||||
};
|
||||
|
||||
/** Direction of program counter */
|
||||
export enum Bfg93Direction {
|
||||
UP = "up",
|
||||
DOWN = "down",
|
||||
LEFT = "left",
|
||||
RIGHT = "right",
|
||||
}
|
||||
|
||||
/** Allowed operations in Befunge */
|
||||
export enum Bfg93Op {
|
||||
NOOP = " ",
|
||||
ADD = "+",
|
||||
SUBTRACT = "-",
|
||||
MULTIPLY = "*",
|
||||
DIVIDE = "/",
|
||||
MODULO = "%",
|
||||
NOT = "!",
|
||||
GREATER = "`",
|
||||
RIGHT = ">",
|
||||
LEFT = "<",
|
||||
UP = "^",
|
||||
DOWN = "v",
|
||||
RANDOM = "?",
|
||||
H_IF = "_",
|
||||
V_IF = "|",
|
||||
TOGGLE_STR = '"',
|
||||
DUPLICATE = ":",
|
||||
SWAP = "\\",
|
||||
POP_DELETE = "$",
|
||||
POP_OUTINT = ".",
|
||||
POP_OUTCHAR = ",",
|
||||
BRIDGE = "#",
|
||||
GET_DATA = "g",
|
||||
PUT_DATA = "p",
|
||||
STDIN_INT = "&",
|
||||
STDIN_CHAR = "~",
|
||||
END = "@",
|
||||
PUSH_0 = "0",
|
||||
PUSH_1 = "1",
|
||||
PUSH_2 = "2",
|
||||
PUSH_3 = "3",
|
||||
PUSH_4 = "4",
|
||||
PUSH_5 = "5",
|
||||
PUSH_6 = "6",
|
||||
PUSH_7 = "7",
|
||||
PUSH_8 = "8",
|
||||
PUSH_9 = "9",
|
||||
}
|
||||
|
||||
/** Sample program printing "Hello world" */
|
||||
export const sampleProgram = [
|
||||
`"!dlroW ,olleH">:v`,
|
||||
` |,<`,
|
||||
` @`,
|
||||
].join("\n");
|
||||
|
||||
/** Tokens provider */
|
||||
export const editorTokensProvider: MonacoTokensProvider = {
|
||||
tokenizer: {
|
||||
root: [
|
||||
[/[\>\^<v\?]/, "red"],
|
||||
[/[\+\-\*\/%!`]/, "orange"],
|
||||
[/[|_]/, "blue"],
|
||||
[/[":\\#]/, "green"],
|
||||
[/[\$\.,]/, "violet"],
|
||||
[/[gp@]/, "indigo"],
|
||||
[/[&~0-9]/, "turquoise"],
|
||||
],
|
||||
},
|
||||
defaultToken: "comment",
|
||||
};
|
4
engines/befunge93/engine.ts
Normal file
4
engines/befunge93/engine.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { setupWorker } from "../setup-worker";
|
||||
import Befunge93LanguageEngine from "./runtime";
|
||||
|
||||
setupWorker(new Befunge93LanguageEngine());
|
11
engines/befunge93/index.ts
Normal file
11
engines/befunge93/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Renderer } from "./renderer";
|
||||
import { LanguageProvider } from "../types";
|
||||
import { Bfg93RS, sampleProgram, editorTokensProvider } from "./constants";
|
||||
|
||||
const provider: LanguageProvider<Bfg93RS> = {
|
||||
Renderer,
|
||||
sampleProgram,
|
||||
editorTokensProvider,
|
||||
};
|
||||
|
||||
export default provider;
|
46
engines/befunge93/input-stream.ts
Normal file
46
engines/befunge93/input-stream.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { RuntimeError } from "../worker-errors";
|
||||
|
||||
/**
|
||||
* A barebones input stream implementation for consuming integers and characters from a string.
|
||||
*/
|
||||
export default class InputStream {
|
||||
private _text: string;
|
||||
|
||||
/** Create a new input stream loaded with the given input */
|
||||
constructor(text: string) {
|
||||
this._text = text;
|
||||
}
|
||||
|
||||
/** Remove leading whitespace from the current input stream */
|
||||
private exhaustLeadingWhitespace(): void {
|
||||
const firstChar = this._text.trim()[0];
|
||||
const posn = this._text.search(firstChar);
|
||||
this._text = this._text.slice(posn);
|
||||
}
|
||||
|
||||
/** Parse input stream for an integer */
|
||||
getNumber(): number {
|
||||
this.exhaustLeadingWhitespace();
|
||||
// The extra whitespace differentiates whether string is empty or all numbers.
|
||||
if (this._text === "") throw new RuntimeError("Unexpected end of input");
|
||||
let posn = this._text.search(/[^0-9]/);
|
||||
if (posn === 0)
|
||||
throw new RuntimeError(`Unexpected input character: '${this._text[0]}'`);
|
||||
if (posn === -1) posn = this._text.length;
|
||||
// Consume and parse numeric part
|
||||
const numStr = this._text.slice(0, posn);
|
||||
this._text = this._text.slice(posn);
|
||||
return parseInt(numStr, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse input stream for the first character, and return its ASCII code.
|
||||
* If end of input, returns -1.
|
||||
*/
|
||||
getChar(): number {
|
||||
if (this._text.length === 0) return -1;
|
||||
const char = this._text[0];
|
||||
this._text = this._text.slice(1);
|
||||
return char.charCodeAt(0);
|
||||
}
|
||||
}
|
80
engines/befunge93/renderer.tsx
Normal file
80
engines/befunge93/renderer.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { Card, Colors, Icon, IconName } from "@blueprintjs/core";
|
||||
import { RendererProps } from "../types";
|
||||
import { Bfg93Direction, Bfg93RS } from "./constants";
|
||||
|
||||
/** Common border color for dark and light, using transparency */
|
||||
export const BorderColor = Colors.GRAY3 + "55";
|
||||
|
||||
const styles = {
|
||||
placeholderDiv: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
fontSize: "1.2em",
|
||||
},
|
||||
rootContainer: {
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column" as "column",
|
||||
},
|
||||
dirnContainer: {
|
||||
borderBottom: "1px solid " + BorderColor,
|
||||
padding: "5px 10px",
|
||||
},
|
||||
stackContainer: {
|
||||
padding: 10,
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexWrap: "wrap" as "wrap",
|
||||
alignContent: "flex-start",
|
||||
overflowY: "auto" as "auto",
|
||||
},
|
||||
stackItem: {
|
||||
// Sizing
|
||||
width: "10%",
|
||||
height: "40px",
|
||||
margin: "5px 0.25%",
|
||||
// Center-align values
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
};
|
||||
|
||||
const DirectionIcons: { [k: string]: IconName } = {
|
||||
[Bfg93Direction.RIGHT]: "arrow-right",
|
||||
[Bfg93Direction.LEFT]: "arrow-left",
|
||||
[Bfg93Direction.UP]: "arrow-up",
|
||||
[Bfg93Direction.DOWN]: "arrow-down",
|
||||
};
|
||||
|
||||
const StackItem = ({ value }: { value: number }) => {
|
||||
const cellStyle = { ...styles.stackItem };
|
||||
return <Card style={cellStyle}>{value}</Card>;
|
||||
};
|
||||
|
||||
export const Renderer = ({ state }: RendererProps<Bfg93RS>) => {
|
||||
if (state == null)
|
||||
return <div style={styles.placeholderDiv}>Run some code...</div>;
|
||||
|
||||
return (
|
||||
<div style={styles.rootContainer}>
|
||||
<div style={styles.dirnContainer}>
|
||||
<span style={{ fontWeight: "bold", marginRight: 5 }}>Direction: </span>
|
||||
<Icon icon={DirectionIcons[state.direction]} />
|
||||
{/* <span style={{ marginLeft: 10 }} /> */}
|
||||
<span style={{ marginLeft: 30, fontWeight: "bold", marginRight: 5 }}>
|
||||
String mode:{" "}
|
||||
</span>
|
||||
{state.strMode ? "ON" : "OFF"}
|
||||
</div>
|
||||
<div style={styles.stackContainer}>
|
||||
{state.stack.map((value, idx) => (
|
||||
<StackItem key={idx} value={value} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
407
engines/befunge93/runtime.ts
Normal file
407
engines/befunge93/runtime.ts
Normal file
@ -0,0 +1,407 @@
|
||||
import InputStream from "./input-stream";
|
||||
import { DocumentRange, LanguageEngine, StepExecutionResult } from "../types";
|
||||
import { ParseError, RuntimeError } from "../worker-errors";
|
||||
import { Bfg93RS, Bfg93Op, Bfg93Direction } from "./constants";
|
||||
|
||||
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("");
|
||||
|
||||
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("");
|
||||
}
|
||||
|
||||
validateCode(code: string) {
|
||||
this.parseCode(code);
|
||||
}
|
||||
|
||||
prepare(code: string, input: string) {
|
||||
this._ast = this.parseCode(code);
|
||||
this._input = new InputStream(input);
|
||||
}
|
||||
|
||||
executeStep(): StepExecutionResult<Bfg93RS> {
|
||||
// Execute and update program counter
|
||||
let output: string | undefined = undefined;
|
||||
let end: boolean = false;
|
||||
if (this._pc.x === -1 && this._pc.y === -1) {
|
||||
this._pc = { x: 0, y: 0 };
|
||||
} else {
|
||||
const result = this.processOp();
|
||||
output = result.output;
|
||||
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 };
|
||||
}
|
||||
|
||||
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 } {
|
||||
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 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();
|
||||
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 };
|
||||
}
|
||||
|
||||
/** 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): void {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
96
engines/befunge93/tests/index.test.ts
Normal file
96
engines/befunge93/tests/index.test.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { readTestProgram, executeProgram } from "../../test-utils";
|
||||
import { Bfg93Direction } from "../constants";
|
||||
import Engine from "../runtime";
|
||||
|
||||
/**
|
||||
* All test programs are picked up from https://esolangs.org/wiki/Befunge,
|
||||
* except the modifications mentioned alongside each test.
|
||||
*/
|
||||
|
||||
/** Relative path to directory of sample programs */
|
||||
const DIRNAME = __dirname + "/samples";
|
||||
|
||||
describe("Test programs", () => {
|
||||
// Standard hello-world program
|
||||
test("hello world", async () => {
|
||||
const code = readTestProgram(DIRNAME, "helloworld");
|
||||
const result = await executeProgram(new Engine(), code);
|
||||
expect(result.output.charCodeAt(result.output.length - 1)).toBe(0);
|
||||
expect(result.output.slice(0, -1)).toBe("Hello, World!");
|
||||
expect(result.rendererState.direction).toBe(Bfg93Direction.DOWN);
|
||||
expect(result.rendererState.stack.length).toBe(0);
|
||||
});
|
||||
|
||||
// cat program
|
||||
test("cat program", async () => {
|
||||
const input = "abcd efgh\nijkl mnop\n";
|
||||
const code = readTestProgram(DIRNAME, "cat");
|
||||
const result = await executeProgram(new Engine(), code, input);
|
||||
expect(result.output).toBe(input);
|
||||
expect(result.rendererState.direction).toBe(Bfg93Direction.LEFT);
|
||||
expect(result.rendererState.stack).toEqual([-1]);
|
||||
});
|
||||
|
||||
// Random DNA printer
|
||||
test("random DNA", async () => {
|
||||
const code = readTestProgram(DIRNAME, "dna");
|
||||
const result = await executeProgram(new Engine(), code);
|
||||
// program prints "\r\n" at the end of output
|
||||
expect(result.output.length).toBe(56 + 2);
|
||||
expect(result.output.trim().search(/[^ATGC]/)).toBe(-1);
|
||||
expect(result.rendererState.direction).toBe(Bfg93Direction.RIGHT);
|
||||
expect(result.rendererState.stack).toEqual([0]);
|
||||
});
|
||||
|
||||
// Factorial program
|
||||
test("factorial", async () => {
|
||||
const code = readTestProgram(DIRNAME, "factorial");
|
||||
const result = await executeProgram(new Engine(), code, "5");
|
||||
expect(result.output).toBe("120 ");
|
||||
expect(result.rendererState.direction).toBe(Bfg93Direction.RIGHT);
|
||||
expect(result.rendererState.stack.length).toBe(0);
|
||||
});
|
||||
|
||||
// Sieve of Eratosthenes - prints prime nums upto 36
|
||||
// (original prints up to 80, shortened here for testing purposes)
|
||||
test("sieve of eratosthenes", async () => {
|
||||
const code = readTestProgram(DIRNAME, "prime-sieve");
|
||||
const result = await executeProgram(new Engine(), code);
|
||||
const outputNums = result.output
|
||||
.trim()
|
||||
.split(" ")
|
||||
.map((a) => parseInt(a, 10));
|
||||
const primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31];
|
||||
expect(outputNums).toEqual(primes);
|
||||
expect(result.rendererState.direction).toBe(Bfg93Direction.DOWN);
|
||||
expect(result.rendererState.stack).toEqual([37]);
|
||||
});
|
||||
|
||||
// Quine 1 - simple single-line quine
|
||||
test("simple singleline quine", async () => {
|
||||
const code = readTestProgram(DIRNAME, "quine1");
|
||||
const result = await executeProgram(new Engine(), code);
|
||||
expect(result.output).toBe(code);
|
||||
expect(result.rendererState.direction).toBe(Bfg93Direction.RIGHT);
|
||||
expect(result.rendererState.stack).toEqual([44]);
|
||||
});
|
||||
|
||||
// Quine 2 - multiline quine
|
||||
test("multiline quine", async () => {
|
||||
const code = readTestProgram(DIRNAME, "quine2");
|
||||
const result = await executeProgram(new Engine(), code);
|
||||
// Output has an extra space at the end - verified on tio.run
|
||||
expect(result.output).toBe(code + " ");
|
||||
expect(result.rendererState.direction).toBe(Bfg93Direction.LEFT);
|
||||
expect(result.rendererState.stack).toEqual([0]);
|
||||
});
|
||||
|
||||
// Quine 3 - quine without using "g"
|
||||
test("quine without using 'g'", async () => {
|
||||
const code = readTestProgram(DIRNAME, "quine3");
|
||||
const result = await executeProgram(new Engine(), code);
|
||||
expect(result.output).toBe(code);
|
||||
expect(result.rendererState.direction).toBe(Bfg93Direction.LEFT);
|
||||
expect(result.rendererState.stack).toEqual([0]);
|
||||
});
|
||||
});
|
1
engines/befunge93/tests/samples/cat.txt
Normal file
1
engines/befunge93/tests/samples/cat.txt
Normal file
@ -0,0 +1 @@
|
||||
~:1+!#@_,
|
8
engines/befunge93/tests/samples/dna.txt
Normal file
8
engines/befunge93/tests/samples/dna.txt
Normal file
@ -0,0 +1,8 @@
|
||||
7^DN>vA
|
||||
v_#v? v
|
||||
7^<""""
|
||||
3 ACGT
|
||||
90!""""
|
||||
4*:>>>v
|
||||
+8^-1,<
|
||||
> ,+,@)
|
2
engines/befunge93/tests/samples/factorial.txt
Normal file
2
engines/befunge93/tests/samples/factorial.txt
Normal file
@ -0,0 +1,2 @@
|
||||
&>:1-:v v *_$.@
|
||||
^ _$>\:^
|
3
engines/befunge93/tests/samples/helloworld.txt
Normal file
3
engines/befunge93/tests/samples/helloworld.txt
Normal file
@ -0,0 +1,3 @@
|
||||
"!dlroW ,olleH">:v
|
||||
|,<
|
||||
@
|
4
engines/befunge93/tests/samples/prime-sieve.txt
Normal file
4
engines/befunge93/tests/samples/prime-sieve.txt
Normal file
@ -0,0 +1,4 @@
|
||||
2>:3g" "-!v\ g30 <
|
||||
|!`"$":+1_:.:03p>03g+:"$"`|
|
||||
@ ^ p3\" ":<
|
||||
2 234567890123456789012345678901234567890123456789012345678901234567890123456789
|
1
engines/befunge93/tests/samples/quine1.txt
Normal file
1
engines/befunge93/tests/samples/quine1.txt
Normal file
@ -0,0 +1 @@
|
||||
01->1# +# :# 0# g# ,# :# 5# 8# *# 4# +# -# _@
|
2
engines/befunge93/tests/samples/quine2.txt
Normal file
2
engines/befunge93/tests/samples/quine2.txt
Normal file
@ -0,0 +1,2 @@
|
||||
0 v
|
||||
"<@_ #! #: #,<*2-1*92,*84,*25,+*92*4*55.0
|
1
engines/befunge93/tests/samples/quine3.txt
Normal file
1
engines/befunge93/tests/samples/quine3.txt
Normal file
@ -0,0 +1 @@
|
||||
<@,+2*48_,#! #:<,_$#-:#*8#4<8"
|
@ -153,7 +153,7 @@ class ExecutionController<RS> {
|
||||
if (isRuntimeError(error)) {
|
||||
this._isPaused = true;
|
||||
resolve({
|
||||
result: this._result!,
|
||||
result: { ...this._result!, output: undefined },
|
||||
error: serializeRuntimeError(error),
|
||||
});
|
||||
} else reject(error);
|
||||
|
26
pages/ide/befunge93.tsx
Normal file
26
pages/ide/befunge93.tsx
Normal 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/befunge93";
|
||||
const LANG_ID = "befunge93";
|
||||
const LANG_NAME = "Befunge-93";
|
||||
|
||||
const IDE: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{LANG_NAME} | Esolang Park</title>
|
||||
</Head>
|
||||
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
<Header langName={LANG_NAME} />
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
<Mainframe langName={LANG_ID} provider={LangProvider} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IDE;
|
Loading…
x
Reference in New Issue
Block a user