Rename directory "engines" to "languages"

This commit is contained in:
Nilay Majorwar
2022-01-30 20:47:33 +05:30
parent 0bf7c0de3a
commit e3be5a8a83
82 changed files with 27 additions and 21 deletions

View File

@ -0,0 +1,21 @@
# Brainfuck
## Allowed symbols
- `>`: Move the pointer to the right
- `<`: Move the pointer to the left
- `+`: Increment the memory cell at the pointer
- `-`: Decrement the memory cell at the pointer
- `.`: Output the character signified by the cell at the pointer
- `,`: Input a character and store it in the cell at the pointer
- `[`: Jump past the matching `]` if the cell at the pointer is 0
- `]`: Jump back to the matching `[` if the cell at the pointer is nonzero
## Memory specifications
> These parameters will be configurable when engine configuration is added to the project
- For Turing-completeness, the number of cells is kept unbounded.
- Cell size is 8 bits, and allows values in the range `[-128, 127]`.
- Value `10` is designated for newlines.
- The value `0` is returned on reaching `EOF`.

View File

@ -0,0 +1,90 @@
import { MonacoTokensProvider } from "../types";
export type BFRS = {
tape: { [k: number]: number };
pointer: number;
};
export enum BF_OP {
LEFT = "<",
RIGHT = ">",
INCR = "+",
DECR = "-",
OUT = ".",
IN = ",",
LOOPIN = "[",
LOOPOUT = "]",
}
/** A single instruction of the program */
export type BFInstruction = {
/** Type of instruction */
type: BF_OP;
/** Used for location of opposite end of loops */
param?: number;
};
/** A single element of the program's AST */
export type BFAstStep = {
instr: BFInstruction;
location: { line: number; char: number };
};
/** Sample program printing "Hello World!" */
export const sampleProgram = [
"+++++ +++ 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",
].join("\n");
/** Tokens provider */
export const editorTokensProvider: MonacoTokensProvider = {
tokenizer: {
root: [
[/[-\+]/, "plain"],
[/[<>]/, "green"],
[/[\[\]]/, "violet"],
[/[\,\.]/, "orange"],
],
},
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;
};

View File

@ -0,0 +1,4 @@
import { setupWorker } from "../setup-worker";
import BrainfuckLanguageEngine from "./runtime";
setupWorker(new BrainfuckLanguageEngine());

View File

@ -0,0 +1,11 @@
import { Renderer } from "./renderer";
import { LanguageProvider } from "../types";
import { BFRS, sampleProgram, editorTokensProvider } from "./common";
const provider: LanguageProvider<BFRS> = {
Renderer,
sampleProgram,
editorTokensProvider,
};
export default provider;

View File

@ -0,0 +1,57 @@
import { Card, Colors } from "@blueprintjs/core";
import { CSSProperties } from "react";
import { useDarkMode } from "../../ui/providers/dark-mode-provider";
import { RendererProps } from "../types";
import { BFRS, serializeTapeMap } from "./common";
// Colors used as background of active cells
const darkActiveBG = Colors.DARK_GRAY2;
const lightActiveBG = Colors.LIGHT_GRAY3;
const styles: { [k: string]: CSSProperties } = {
container: {
padding: 10,
height: "100%",
display: "flex",
flexWrap: "wrap",
alignContent: "flex-start",
overflowY: "auto",
},
cell: {
// Sizing
width: "12%",
height: "50px",
margin: "5px 0.25%",
// Center-align values
display: "flex",
justifyContent: "center",
alignItems: "center",
},
activeCell: {
background: "#CED9E0",
color: "#182026",
},
};
/** Component for displaying a single tape cell */
const Cell = ({ value, active }: { value: number; active: boolean }) => {
const { isDark } = useDarkMode();
const cellStyle = { ...styles.cell };
const activeBg = isDark ? darkActiveBG : lightActiveBG;
if (active) {
cellStyle.backgroundColor = activeBg;
cellStyle.fontWeight = "bold";
}
return <Card style={cellStyle}>{value}</Card>;
};
/** Renderer for Brainfuck */
export const Renderer = ({ state }: RendererProps<BFRS>) => {
return (
<div style={styles.container}>
{serializeTapeMap(state?.tape || {}).map((num, i) => (
<Cell value={num} key={i} active={(state?.pointer || 0) === i} />
))}
</div>
);
};

View File

@ -0,0 +1,199 @@
import { DocumentRange, LanguageEngine, StepExecutionResult } from "../types";
import { ParseError, RuntimeError } from "../worker-errors";
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;
}
validateCode(code: string) {
this.parseCode(code);
}
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: number | undefined = 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) {
// Check and add jump target to loop-opener
jumpTarget = loopStack.pop();
if (jumpTarget == null)
throw new ParseError("Unmatched ']'", {
line: lIdx,
charRange: { start: cIdx, end: cIdx + 1 },
});
// 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, char: cIdx },
});
});
});
// Ensure that we ended with an empty loop stack
if (loopStack.length !== 0) {
const opener = loopStack[loopStack.length - 1];
const location = ast[opener].location;
throw new ParseError("Unmatched '['", {
line: location.line,
charRange: { start: location.char, end: location.char + 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 RuntimeError("Tape pointer out of bounds");
this._ptr -= 1;
this.getCell(this._ptr); // Init cell if required
}
}

View File

@ -0,0 +1 @@
,[.,]

View 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"
+++++++++++[>+++>+++++++++>+++++++++>+<<<<-]>-.>-.+++++++.+++++++++++.<.
>>.++.+++++++..<-.>>-

View File

@ -0,0 +1,2 @@
>++++++++[-<+++++++++>]<.>>+>-[+]++>++>+++[>[->+++<<+++>]<<]>-----.>->
+++..+++.>-.<<+[>[+>+]>>]<--------------.>>.+++.------.--------.>+.>+.

View 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

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