Add unit tests for brainfuck and deadfish
This commit is contained in:
@ -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
|
Reference in New Issue
Block a user