Add unit tests for brainfuck and deadfish
This commit is contained in:
parent
d50e737682
commit
eb9d5d861c
@ -79,3 +79,12 @@ export const editorTokensProvider: MonacoTokensProvider = {
|
||||
},
|
||||
defaultToken: "comment",
|
||||
};
|
||||
|
||||
/** Serialize tape from object format into linear array */
|
||||
export const serializeTapeMap = (tape: BFRS["tape"]): number[] => {
|
||||
const cellIdxs = Object.keys(tape).map((s) => parseInt(s, 10));
|
||||
const maxCellIdx = Math.max(15, ...cellIdxs);
|
||||
const linearTape: number[] = Array(maxCellIdx + 1).fill(0);
|
||||
cellIdxs.forEach((i) => (linearTape[i] = tape[i] || 0));
|
||||
return linearTape;
|
||||
};
|
@ -1,182 +1,4 @@
|
||||
import { setupWorker } from "../setup-worker";
|
||||
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
|
||||
}
|
||||
}
|
||||
import BrainfuckLanguageEngine from "./runtime";
|
||||
|
||||
setupWorker(new BrainfuckLanguageEngine());
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Renderer } from "./renderer";
|
||||
import { LanguageProvider } from "../types";
|
||||
import { BFRS, sampleProgram, editorTokensProvider } from "./constants";
|
||||
import { BFRS, sampleProgram, editorTokensProvider } from "./common";
|
||||
|
||||
const provider: LanguageProvider<BFRS> = {
|
||||
Renderer,
|
||||
|
@ -2,7 +2,7 @@ import { Card, Colors } from "@blueprintjs/core";
|
||||
import { CSSProperties } from "react";
|
||||
import { useDarkMode } from "../../ui/providers/dark-mode-provider";
|
||||
import { RendererProps } from "../types";
|
||||
import { BFRS } from "./constants";
|
||||
import { BFRS, serializeTapeMap } from "./common";
|
||||
|
||||
// Colors used as background of active cells
|
||||
const darkActiveBG = Colors.DARK_GRAY2;
|
||||
@ -47,18 +47,9 @@ const Cell = ({ value, active }: { value: number; active: boolean }) => {
|
||||
|
||||
/** 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) => (
|
||||
{serializeTapeMap(state?.tape || {}).map((num, i) => (
|
||||
<Cell value={num} key={i} active={(state?.pointer || 0) === i} />
|
||||
))}
|
||||
</div>
|
||||
|
179
engines/brainfuck/runtime.ts
Normal file
179
engines/brainfuck/runtime.ts
Normal file
@ -0,0 +1,179 @@
|
||||
import { DocumentRange, LanguageEngine, StepExecutionResult } from "../types";
|
||||
import { BFAstStep, BFInstruction, BFRS, BF_OP } from "./common";
|
||||
|
||||
// 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);
|
||||
|
||||
export default 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
|
||||
}
|
||||
}
|
1
engines/brainfuck/tests/cat.txt
Normal file
1
engines/brainfuck/tests/cat.txt
Normal file
@ -0,0 +1 @@
|
||||
,[.,]
|
19
engines/brainfuck/tests/cellsize.txt
Normal file
19
engines/brainfuck/tests/cellsize.txt
Normal file
@ -0,0 +1,19 @@
|
||||
Calculate the value 256 and test if it's zero
|
||||
If the interpreter errors on overflow this is where it'll happen
|
||||
++++++++[>++++++++<-]>[<++++>-]
|
||||
+<[>-<
|
||||
Not zero so multiply by 256 again to get 65536
|
||||
[>++++<-]>[<++++++++>-]<[>++++++++<-]
|
||||
+>[>
|
||||
# Print "32"
|
||||
++++++++++[>+++++<-]>+.-.[-]<
|
||||
<[-]<->] <[>>
|
||||
# Print "16"
|
||||
+++++++[>+++++++<-]>.+++++.[-]<
|
||||
<<-]] >[>
|
||||
# Print "8"
|
||||
++++++++[>+++++++<-]>.[-]<
|
||||
<-]<
|
||||
# Print " bit cells"
|
||||
+++++++++++[>+++>+++++++++>+++++++++>+<<<<-]>-.>-.+++++++.+++++++++++.<.
|
||||
>>.++.+++++++..<-.>>-
|
2
engines/brainfuck/tests/helloworld-subzero.txt
Normal file
2
engines/brainfuck/tests/helloworld-subzero.txt
Normal file
@ -0,0 +1,2 @@
|
||||
>++++++++[-<+++++++++>]<.>>+>-[+]++>++>+++[>[->+++<<+++>]<<]>-----.>->
|
||||
+++..+++.>-.<<+[>[+>+]>>]<--------------.>>.+++.------.--------.>+.>+.
|
33
engines/brainfuck/tests/helloworld.txt
Normal file
33
engines/brainfuck/tests/helloworld.txt
Normal file
@ -0,0 +1,33 @@
|
||||
+++++ +++ 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
|
63
engines/brainfuck/tests/index.test.ts
Normal file
63
engines/brainfuck/tests/index.test.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { readTestProgram, executeProgram } from "../../test-utils";
|
||||
import { BFRS, serializeTapeMap } from "../common";
|
||||
import Engine from "../runtime";
|
||||
|
||||
/**
|
||||
* All test programs are picked up from https://esolangs.org/wiki/Brainfuck.
|
||||
* - Cell cleanup code at end of cell size program is not included.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if actual cell array matches expected cell array.
|
||||
* Expected cell array must exclude trailing zeros.
|
||||
* @param cellsMap Map of cell index to value, as provided in execution result.
|
||||
* @param expected Array of expected cell values, without trailing zeros.
|
||||
*/
|
||||
const expectCellsToBe = (cellsMap: BFRS["tape"], expected: number[]) => {
|
||||
const cells = serializeTapeMap(cellsMap);
|
||||
expect(cells.length).toBeGreaterThanOrEqual(expected.length);
|
||||
cells.forEach((value, idx) => {
|
||||
if (idx < expected.length) expect(value).toBe(expected[idx]);
|
||||
else expect(value).toBe(0);
|
||||
});
|
||||
};
|
||||
|
||||
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).toBe("Hello World!\n");
|
||||
expect(result.rendererState.pointer).toBe(6);
|
||||
const expectedCells = [0, 0, 72, 100, 87, 33, 10];
|
||||
expectCellsToBe(result.rendererState.tape, expectedCells);
|
||||
});
|
||||
|
||||
// Hello-world program using subzero cell values
|
||||
test("hello world with subzero cell values", async () => {
|
||||
const code = readTestProgram(__dirname, "helloworld-subzero");
|
||||
const result = await executeProgram(new Engine(), code);
|
||||
expect(result.output).toBe("Hello World!\n");
|
||||
expect(result.rendererState.pointer).toBe(6);
|
||||
const expectedCells = [72, 0, 87, 0, 100, 33, 10];
|
||||
expectCellsToBe(result.rendererState.tape, expectedCells);
|
||||
});
|
||||
|
||||
// cat program
|
||||
test("cat program", async () => {
|
||||
const code = readTestProgram(__dirname, "cat");
|
||||
const result = await executeProgram(new Engine(), code, "foo \n bar");
|
||||
expect(result.output).toBe("foo \n bar");
|
||||
expect(result.rendererState.pointer).toBe(0);
|
||||
expectCellsToBe(result.rendererState.tape, []);
|
||||
});
|
||||
|
||||
// Program to calculate cell size
|
||||
test("cell size calculator", async () => {
|
||||
const code = readTestProgram(__dirname, "cellsize");
|
||||
const result = await executeProgram(new Engine(), code);
|
||||
expect(result.output).toBe("8 bit cells");
|
||||
expect(result.rendererState.pointer).toBe(4);
|
||||
expectCellsToBe(result.rendererState.tape, [0, 32, 115, 108, 10]);
|
||||
});
|
||||
});
|
@ -1,87 +1,4 @@
|
||||
import { setupWorker } from "../setup-worker";
|
||||
import { DocumentRange, LanguageEngine, StepExecutionResult } from "../types";
|
||||
import { DFAstStep, DFRS, DF_OP } from "./constants";
|
||||
|
||||
// Default values for internal states
|
||||
// Factories are used to create new objects on reset
|
||||
const DEFAULT_AST = (): DFAstStep[] => [];
|
||||
const DEFAULT_PC = -1;
|
||||
const DEFAULT_VALUE = 0;
|
||||
|
||||
// Instruction characters
|
||||
const OP_CHARS = Object.values(DF_OP);
|
||||
|
||||
class DeadfishLanguageEngine implements LanguageEngine<DFRS> {
|
||||
private _ast: DFAstStep[] = DEFAULT_AST();
|
||||
private _value: number = DEFAULT_VALUE;
|
||||
private _pc: number = DEFAULT_PC;
|
||||
|
||||
resetState() {
|
||||
this._ast = DEFAULT_AST();
|
||||
this._value = DEFAULT_VALUE;
|
||||
this._pc = DEFAULT_PC;
|
||||
}
|
||||
|
||||
prepare(code: string, _input: string) {
|
||||
this._ast = this.parseCode(code);
|
||||
}
|
||||
|
||||
executeStep(): StepExecutionResult<DFRS> {
|
||||
// Execute and update program counter
|
||||
let output: string | undefined = undefined;
|
||||
if (this._pc !== -1) {
|
||||
const astStep = this._ast[this._pc];
|
||||
output = this.processOp(astStep.instr);
|
||||
}
|
||||
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 = { value: this._value };
|
||||
return { rendererState, nextStepLocation, output };
|
||||
}
|
||||
|
||||
private parseCode(code: string) {
|
||||
const ast: DFAstStep[] = [];
|
||||
|
||||
// For each line...
|
||||
code.split("\n").forEach((line, lIdx) => {
|
||||
// For each character of this line...
|
||||
line.split("").forEach((char, cIdx) => {
|
||||
if (OP_CHARS.includes(char as DF_OP)) {
|
||||
ast.push({
|
||||
instr: char as DF_OP,
|
||||
location: { line: lIdx + 1, char: cIdx + 1 },
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the given instruction and return string to push to output if any.
|
||||
*
|
||||
* @param instr Instruction to apply
|
||||
* @returns String to append to output, if any
|
||||
*/
|
||||
private processOp(instr: DF_OP): string | undefined {
|
||||
if (instr === DF_OP.INCR) ++this._value;
|
||||
else if (instr === DF_OP.DECR) --this._value;
|
||||
else if (instr === DF_OP.SQ) this._value = this._value * this._value;
|
||||
else if (instr === DF_OP.OUT) return this._value.toString();
|
||||
else throw new Error("Invalid instruction");
|
||||
|
||||
if (this._value === -1 || this._value === 256) this._value = 0;
|
||||
}
|
||||
}
|
||||
import DeadfishLanguageEngine from "./runtime";
|
||||
|
||||
setupWorker(new DeadfishLanguageEngine());
|
||||
|
84
engines/deadfish/runtime.ts
Normal file
84
engines/deadfish/runtime.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { DocumentRange, LanguageEngine, StepExecutionResult } from "../types";
|
||||
import { DFAstStep, DFRS, DF_OP } from "./constants";
|
||||
|
||||
// Default values for internal states
|
||||
// Factories are used to create new objects on reset
|
||||
const DEFAULT_AST = (): DFAstStep[] => [];
|
||||
const DEFAULT_PC = -1;
|
||||
const DEFAULT_VALUE = 0;
|
||||
|
||||
// Instruction characters
|
||||
const OP_CHARS = Object.values(DF_OP);
|
||||
|
||||
export default class DeadfishLanguageEngine implements LanguageEngine<DFRS> {
|
||||
private _ast: DFAstStep[] = DEFAULT_AST();
|
||||
private _value: number = DEFAULT_VALUE;
|
||||
private _pc: number = DEFAULT_PC;
|
||||
|
||||
resetState() {
|
||||
this._ast = DEFAULT_AST();
|
||||
this._value = DEFAULT_VALUE;
|
||||
this._pc = DEFAULT_PC;
|
||||
}
|
||||
|
||||
prepare(code: string, _input: string) {
|
||||
this._ast = this.parseCode(code);
|
||||
}
|
||||
|
||||
executeStep(): StepExecutionResult<DFRS> {
|
||||
// Execute and update program counter
|
||||
let output: string | undefined = undefined;
|
||||
if (this._pc !== -1) {
|
||||
const astStep = this._ast[this._pc];
|
||||
output = this.processOp(astStep.instr);
|
||||
}
|
||||
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 = { value: this._value };
|
||||
return { rendererState, nextStepLocation, output };
|
||||
}
|
||||
|
||||
private parseCode(code: string) {
|
||||
const ast: DFAstStep[] = [];
|
||||
|
||||
// For each line...
|
||||
code.split("\n").forEach((line, lIdx) => {
|
||||
// For each character of this line...
|
||||
line.split("").forEach((char, cIdx) => {
|
||||
if (OP_CHARS.includes(char as DF_OP)) {
|
||||
ast.push({
|
||||
instr: char as DF_OP,
|
||||
location: { line: lIdx + 1, char: cIdx + 1 },
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the given instruction and return string to push to output if any.
|
||||
*
|
||||
* @param instr Instruction to apply
|
||||
* @returns String to append to output, if any
|
||||
*/
|
||||
private processOp(instr: DF_OP): string | undefined {
|
||||
if (instr === DF_OP.INCR) ++this._value;
|
||||
else if (instr === DF_OP.DECR) --this._value;
|
||||
else if (instr === DF_OP.SQ) this._value = this._value * this._value;
|
||||
else if (instr === DF_OP.OUT) return this._value.toString();
|
||||
else throw new Error("Invalid instruction");
|
||||
|
||||
if (this._value === -1 || this._value === 256) this._value = 0;
|
||||
}
|
||||
}
|
1
engines/deadfish/tests/288.txt
Normal file
1
engines/deadfish/tests/288.txt
Normal file
@ -0,0 +1 @@
|
||||
diissisdo
|
3
engines/deadfish/tests/helloworld.txt
Normal file
3
engines/deadfish/tests/helloworld.txt
Normal file
@ -0,0 +1,3 @@
|
||||
iisiiiisiiiiiiiioiiiiiiiiiiiiiiiiiiiiiiiiiiiiioiiiiiiiooiiio
|
||||
dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddo
|
||||
dddddddddddddddddddddsddoddddddddoiiioddddddoddddddddo
|
57
engines/deadfish/tests/index.test.ts
Normal file
57
engines/deadfish/tests/index.test.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { readTestProgram, executeProgram } from "../../test-utils";
|
||||
// import { BFRS, serializeTapeMap } from "../constants";
|
||||
import Engine from "../runtime";
|
||||
|
||||
/**
|
||||
* All test programs are picked up from https://esolangs.org/wiki/Brainfuck.
|
||||
* - Cell cleanup code at end of cell size program is not included.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if actual cell array matches expected cell array.
|
||||
* Expected cell array must exclude trailing zeros.
|
||||
* @param cellsMap Map of cell index to value, as provided in execution result.
|
||||
* @param expected Array of expected cell values, without trailing zeros.
|
||||
*/
|
||||
// const expectCellsToBe = (cellsMap: BFRS["tape"], expected: number[]) => {
|
||||
// const cells = serializeTapeMap(cellsMap);
|
||||
// expect(cells.length).toBeGreaterThanOrEqual(expected.length);
|
||||
// cells.forEach((value, idx) => {
|
||||
// if (idx < expected.length) expect(value).toBe(expected[idx]);
|
||||
// else expect(value).toBe(0);
|
||||
// });
|
||||
// };
|
||||
|
||||
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).toBe("7210110810811132119111114108100");
|
||||
expect(result.rendererState.value).toBe(100);
|
||||
});
|
||||
|
||||
// Test program 1, output 0
|
||||
test("output zero (1)", async () => {
|
||||
const code = readTestProgram(__dirname, "zero1");
|
||||
const result = await executeProgram(new Engine(), code);
|
||||
expect(result.output).toBe("0");
|
||||
expect(result.rendererState.value).toBe(0);
|
||||
});
|
||||
|
||||
// Test program 2, output 0
|
||||
test("output zero (2)", async () => {
|
||||
const code = readTestProgram(__dirname, "zero2");
|
||||
const result = await executeProgram(new Engine(), code);
|
||||
expect(result.output).toBe("0");
|
||||
expect(result.rendererState.value).toBe(0);
|
||||
});
|
||||
|
||||
// Test program 3, output 288
|
||||
test("output 288", async () => {
|
||||
const code = readTestProgram(__dirname, "288");
|
||||
const result = await executeProgram(new Engine(), code);
|
||||
expect(result.output).toBe("288");
|
||||
expect(result.rendererState.value).toBe(288);
|
||||
});
|
||||
});
|
1
engines/deadfish/tests/zero1.txt
Normal file
1
engines/deadfish/tests/zero1.txt
Normal file
@ -0,0 +1 @@
|
||||
iissso
|
1
engines/deadfish/tests/zero2.txt
Normal file
1
engines/deadfish/tests/zero2.txt
Normal file
@ -0,0 +1 @@
|
||||
iissisdddddddddddddddddddddddddddddddddo
|
Loading…
x
Reference in New Issue
Block a user