Fix workers

This commit is contained in:
2025-01-16 15:47:35 -08:00
parent 65ddbdb22f
commit 16c08e380b
13 changed files with 4 additions and 1 deletions

23
languages/bf/README.md Normal file
View File

@ -0,0 +1,23 @@
# Brainfuck
Brainfuck is perhaps the most popular esoteric programming language, created by Urban Müller in 1993.
It is Turing-complete and has 8 instructions which operate on a linear array of cells storing integer values.
The [esolangs wiki page](https://esolangs.org/wiki/Brainfuck) contains the language specification and some
sample programs.
Note that brainfuck has minor variants which primarily differ in the workings of the cell array. You may find
many brainfuck programs which don't work correctly on Esolang Park.
## Notes for the user
- The cell array is semi-infinite. There is no cell to the left of the initial cell, and trying to go left
anyway will result in a runtime error. The right side of the cell array is unbounded.
- Cell size is 8 bits, and allows values in the range `[-128, 127]`. Values loop over to the other side on reaching the bounds.
- The usual ASCII value `10` is designated for newlines.
- The value `0` is returned in input (`,`) operations on reaching `EOF`.
## Possible improvements
- The renderer currently uses Blueprint's `Card` component. This leads to performance issues when the stack grows large.
Usage of this component should be replaced by a simple custom component to drastically improve performance. Look at the
`SimpleTag` component used in the Shakespeare renderer for an example.

93
languages/bf/common.ts Normal file
View File

@ -0,0 +1,93 @@
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: [
[/[-\+]/, ""],
[/[<>]/, "tag"],
[/[\[\]]/, "keyword"],
[/[\,\.]/, "identifier"],
],
},
defaultToken: "comment",
};
/** Serialize tape from object format into linear array */
export const serializeTapeMap = (
tape: BFRS["tape"],
minCells: number = 0
): number[] => {
const cellIdxs = Object.keys(tape).map((s) => parseInt(s, 10));
const maxCellIdx = Math.max(minCells - 1, ...cellIdxs);
const linearTape: number[] = Array(maxCellIdx + 1).fill(0);
cellIdxs.forEach((i) => (linearTape[i] = tape[i] || 0));
return linearTape;
};

4
languages/bf/engine.ts Normal file
View File

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

11
languages/bf/index.ts Normal file
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;

58
languages/bf/renderer.tsx Normal file
View File

@ -0,0 +1,58 @@
import * as React from "react";
import { RendererProps } from "../types";
import { Box } from "../ui-utils";
import { BFRS, serializeTapeMap } from "./common";
/** Number of cells shown in a single row */
const ROWSIZE = 8;
// Parameters for cell sizing, balanced to span the full row width
// Constraint: `(width% + 2 * margin%) * ROWSIZE = 100%`
const CELL_WIDTH = "12%";
const CELL_MARGIN = "5px 0.25%";
const styles: { [k: string]: React.CSSProperties } = {
container: {
padding: 10,
height: "100%",
display: "flex",
flexWrap: "wrap",
alignContent: "flex-start",
overflowY: "auto",
},
cell: {
// Sizing
width: CELL_WIDTH,
margin: CELL_MARGIN,
height: "50px",
// Center-align values
display: "flex",
justifyContent: "center",
alignItems: "center",
},
};
/** Component for displaying a single tape cell */
const Cell = React.memo(
({ value, active }: { value: number; active: boolean }) => {
return (
<Box
intent={active ? "active" : "plain"}
style={{ ...styles.cell, fontWeight: active ? "bold" : undefined }}
>
{value}
</Box>
);
}
);
/** Renderer for Brainfuck */
export const Renderer = ({ state }: RendererProps<BFRS>) => {
return (
<div style={styles.container}>
{serializeTapeMap(state?.tape || {}, 2 * ROWSIZE).map((num, i) => (
<Cell value={num} key={i} active={(state?.pointer || 0) === i} />
))}
</div>
);
};

200
languages/bf/runtime.ts Normal file
View File

@ -0,0 +1,200 @@
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;
nextStepLocation = { startLine: line, startCol: char, endCol: char + 1 };
}
// 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 ']'", {
startLine: lIdx,
startCol: cIdx,
endCol: 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 '['", {
startLine: location.line,
startCol: location.char,
endCol: 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]);
});
});