Add unit tests for brainfuck and deadfish

This commit is contained in:
Nilay Majorwar
2022-01-18 15:17:20 +05:30
parent d50e737682
commit eb9d5d861c
17 changed files with 458 additions and 275 deletions

View File

@ -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());

View 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;
}
}

View File

@ -0,0 +1 @@
diissisdo

View File

@ -0,0 +1,3 @@
iisiiiisiiiiiiiioiiiiiiiiiiiiiiiiiiiiiiiiiiiiioiiiiiiiooiiio
dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddo
dddddddddddddddddddddsddoddddddddoiiioddddddoddddddddo

View 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);
});
});

View File

@ -0,0 +1 @@
iissso

View File

@ -0,0 +1 @@
iissisdddddddddddddddddddddddddddddddddo