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,3 @@
# Befunge-93
- Interactive input is not supported yet, so currenty division-by-zero throws a runtime error.

View File

@ -0,0 +1,79 @@
import { MonacoTokensProvider } from "../types";
export type Bfg93RS = {
stack: number[];
direction: Bfg93Direction;
strMode: boolean;
};
/** Direction of program counter */
export enum Bfg93Direction {
UP = "up",
DOWN = "down",
LEFT = "left",
RIGHT = "right",
}
/** Allowed operations in Befunge */
export enum Bfg93Op {
NOOP = " ",
ADD = "+",
SUBTRACT = "-",
MULTIPLY = "*",
DIVIDE = "/",
MODULO = "%",
NOT = "!",
GREATER = "`",
RIGHT = ">",
LEFT = "<",
UP = "^",
DOWN = "v",
RANDOM = "?",
H_IF = "_",
V_IF = "|",
TOGGLE_STR = '"',
DUPLICATE = ":",
SWAP = "\\",
POP_DELETE = "$",
POP_OUTINT = ".",
POP_OUTCHAR = ",",
BRIDGE = "#",
GET_DATA = "g",
PUT_DATA = "p",
STDIN_INT = "&",
STDIN_CHAR = "~",
END = "@",
PUSH_0 = "0",
PUSH_1 = "1",
PUSH_2 = "2",
PUSH_3 = "3",
PUSH_4 = "4",
PUSH_5 = "5",
PUSH_6 = "6",
PUSH_7 = "7",
PUSH_8 = "8",
PUSH_9 = "9",
}
/** Sample program printing "Hello world" */
export const sampleProgram = [
`"!dlroW ,olleH">:v`,
` |,<`,
` @`,
].join("\n");
/** Tokens provider */
export const editorTokensProvider: MonacoTokensProvider = {
tokenizer: {
root: [
[/[\>\^<v\?]/, "red"],
[/[\+\-\*\/%!`]/, "orange"],
[/[|_]/, "blue"],
[/[":\\#]/, "green"],
[/[\$\.,]/, "violet"],
[/[gp@]/, "indigo"],
[/[&~0-9]/, "turquoise"],
],
},
defaultToken: "comment",
};

View File

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

View File

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

View File

@ -0,0 +1,46 @@
import { RuntimeError } from "../worker-errors";
/**
* A barebones input stream implementation for consuming integers and characters from a string.
*/
export default class InputStream {
private _text: string;
/** Create a new input stream loaded with the given input */
constructor(text: string) {
this._text = text;
}
/** Remove leading whitespace from the current input stream */
private exhaustLeadingWhitespace(): void {
const firstChar = this._text.trim()[0];
const posn = this._text.search(firstChar);
this._text = this._text.slice(posn);
}
/** Parse input stream for an integer */
getNumber(): number {
this.exhaustLeadingWhitespace();
// The extra whitespace differentiates whether string is empty or all numbers.
if (this._text === "") throw new RuntimeError("Unexpected end of input");
let posn = this._text.search(/[^0-9]/);
if (posn === 0)
throw new RuntimeError(`Unexpected input character: '${this._text[0]}'`);
if (posn === -1) posn = this._text.length;
// Consume and parse numeric part
const numStr = this._text.slice(0, posn);
this._text = this._text.slice(posn);
return parseInt(numStr, 10);
}
/**
* Parse input stream for the first character, and return its ASCII code.
* If end of input, returns -1.
*/
getChar(): number {
if (this._text.length === 0) return -1;
const char = this._text[0];
this._text = this._text.slice(1);
return char.charCodeAt(0);
}
}

View File

@ -0,0 +1,80 @@
import { Card, Colors, Icon, IconName } from "@blueprintjs/core";
import { RendererProps } from "../types";
import { Bfg93Direction, Bfg93RS } from "./constants";
/** Common border color for dark and light, using transparency */
export const BorderColor = Colors.GRAY3 + "55";
const styles = {
placeholderDiv: {
height: "100%",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
fontSize: "1.2em",
},
rootContainer: {
height: "100%",
display: "flex",
flexDirection: "column" as "column",
},
dirnContainer: {
borderBottom: "1px solid " + BorderColor,
padding: "5px 10px",
},
stackContainer: {
padding: 10,
height: "100%",
display: "flex",
flexWrap: "wrap" as "wrap",
alignContent: "flex-start",
overflowY: "auto" as "auto",
},
stackItem: {
// Sizing
width: "10%",
height: "40px",
margin: "5px 0.25%",
// Center-align values
display: "flex",
justifyContent: "center",
alignItems: "center",
},
};
const DirectionIcons: { [k: string]: IconName } = {
[Bfg93Direction.RIGHT]: "arrow-right",
[Bfg93Direction.LEFT]: "arrow-left",
[Bfg93Direction.UP]: "arrow-up",
[Bfg93Direction.DOWN]: "arrow-down",
};
const StackItem = ({ value }: { value: number }) => {
const cellStyle = { ...styles.stackItem };
return <Card style={cellStyle}>{value}</Card>;
};
export const Renderer = ({ state }: RendererProps<Bfg93RS>) => {
if (state == null)
return <div style={styles.placeholderDiv}>Run some code...</div>;
return (
<div style={styles.rootContainer}>
<div style={styles.dirnContainer}>
<span style={{ fontWeight: "bold", marginRight: 5 }}>Direction: </span>
<Icon icon={DirectionIcons[state.direction]} />
{/* <span style={{ marginLeft: 10 }} /> */}
<span style={{ marginLeft: 30, fontWeight: "bold", marginRight: 5 }}>
String mode:{" "}
</span>
{state.strMode ? "ON" : "OFF"}
</div>
<div style={styles.stackContainer}>
{state.stack.map((value, idx) => (
<StackItem key={idx} value={value} />
))}
</div>
</div>
);
};

View File

@ -0,0 +1,456 @@
import InputStream from "./input-stream";
import {
DocumentEdit,
DocumentRange,
LanguageEngine,
StepExecutionResult,
} from "../types";
import { ParseError, RuntimeError } from "../worker-errors";
import { Bfg93RS, Bfg93Op, Bfg93Direction } from "./constants";
import { toSafePrintableChar } from "../engine-utils";
const ROWSIZE = 80; // Maximum size of a single grid row
const COLSIZE = 25; // Maximum size of a single grid column
/** Program counter is coordinates in 2D grid. */
type PC = {
x: number; // 0-indexed, goes rightwards
y: number; // 0-indexed, goes downwards
};
/**
* Defines bounds of the used portion of the grid. So, if the code
* only occupies top-left 30x20 square, all items in array `x` are < 30,
* and all items in array `y` are < 20.
*
* - `bounds.x[10]`: highest index used on 11th row of grid
* - `bounds.y[5]`: highest index used on 6th column of grid
*/
type CodeBounds = {
x: number[];
y: number[];
};
// Default values for internal states
// Factories are used to create new objects on reset
const DEFAULT_AST = (): string[] => [];
const DEFAULT_PC = () => ({ x: -1, y: -1 });
const DEFAULT_STACK = (): number[] => [];
const DEFAULT_DIRN = Bfg93Direction.RIGHT;
const DEFAULT_STR_MODE = false;
const DEFAULT_BOUNDS = (): CodeBounds => ({
x: [],
y: [],
});
// List of characters representing valid Befunge-93 ops
const OP_CHARS = Object.values(Bfg93Op);
export default class Befunge93LanguageEngine
implements LanguageEngine<Bfg93RS>
{
private _ast: string[] = DEFAULT_AST();
private _stack: number[] = DEFAULT_STACK();
private _pc: PC = DEFAULT_PC();
private _dirn: Bfg93Direction = DEFAULT_DIRN;
private _strmode: boolean = DEFAULT_STR_MODE;
private _bounds: CodeBounds = DEFAULT_BOUNDS();
private _input: InputStream = new InputStream("");
private _edits: DocumentEdit[] = [];
resetState() {
this._ast = DEFAULT_AST();
this._stack = DEFAULT_STACK();
this._pc = DEFAULT_PC();
this._dirn = DEFAULT_DIRN;
this._strmode = DEFAULT_STR_MODE;
this._bounds = DEFAULT_BOUNDS();
this._input = new InputStream("");
this._edits = [];
}
validateCode(code: string) {
this.parseCode(code);
}
prepare(code: string, input: string) {
this._ast = this.parseCode(code);
this._edits = this.getGridPaddingEdits(code);
this._input = new InputStream(input);
}
executeStep(): StepExecutionResult<Bfg93RS> {
// Execute and update program counter
let output: string | undefined = undefined;
let edits: DocumentEdit[] | undefined = undefined;
let end: boolean = false;
if (this._pc.x === -1 && this._pc.y === -1) {
this._pc = { x: 0, y: 0 };
edits = this._edits;
} else {
const result = this.processOp();
output = result.output;
edits = result.edit && [result.edit];
end = !!result.end;
}
// Prepare location of next step
let nextStepLocation: DocumentRange | null = null;
if (!end) nextStepLocation = this.toRange(this._pc.y, this._pc.x);
// Prepare and return execution result
const rendererState: Bfg93RS = {
stack: this._stack,
direction: this._dirn,
strMode: this._strmode,
};
return { rendererState, nextStepLocation, output, codeEdits: edits };
}
private parseCode(code: string) {
// A Befunge program can contain any character in the program, so the only
// validation to do is ensure program is within 80x25 bounds.
// Validate that program is within the 80x25 bounds
const lines = code.split("\n");
if (lines.length > COLSIZE)
throw new ParseError(`Code is longer than ${COLSIZE} lines`, {
line: COLSIZE,
});
lines.forEach((line, idx) => {
if (line.length > ROWSIZE)
throw new ParseError(`Line is longer than ${ROWSIZE} characters`, {
line: idx,
charRange: { start: ROWSIZE },
});
});
// Global bounds for each axis
const maxX = Math.max(...lines.map((line) => line.length - 1));
const maxY = lines.length - 1;
// Define bounds for each line and column
for (let i = 0; i < COLSIZE; ++i)
this._bounds.x[i] = lines[i]?.length - 1 || -1;
for (let j = 0; j < ROWSIZE; ++j) this._bounds.y[j] = j <= maxX ? maxY : -1;
// Pad the program to size 80x25 for execution
const grid = lines.map((line) => line.padEnd(80, " "));
grid.push(...new Array(25 - lines.length).fill(" ".repeat(80)));
return grid;
}
/**
* Process the instruction at the current program grid pointer.
* Also updates stack and pointer states.
* @returns String to append to output, if any
*/
private processOp(): { output?: string; end?: boolean; edit?: DocumentEdit } {
const char = this.getGridCell(this._pc.x, this._pc.y);
if (this._strmode && char !== '"') {
// Push character to string and return;
this._stack.push(char.charCodeAt(0));
this.updatePointer();
return {};
}
let output: string | undefined = undefined;
let edit: DocumentEdit | undefined = undefined;
let end: boolean = false;
const op = this.charToOp(char);
if (!op) throw new RuntimeError("Invalid instruction");
switch (op) {
case Bfg93Op.NOOP: {
break;
}
case Bfg93Op.ADD: {
const a = this.popStack();
const b = this.popStack();
this.pushStack(a + b);
break;
}
case Bfg93Op.SUBTRACT: {
const a = this.popStack();
const b = this.popStack();
this.pushStack(b - a);
break;
}
case Bfg93Op.MULTIPLY: {
const a = this.popStack();
const b = this.popStack();
this.pushStack(a * b);
break;
}
case Bfg93Op.DIVIDE: {
const a = this.popStack();
const b = this.popStack();
if (a === 0) throw new RuntimeError("cannot divide by zero");
this.pushStack(Math.floor(b / a));
break;
}
case Bfg93Op.MODULO: {
const a = this.popStack();
const b = this.popStack();
this.pushStack(b % a);
break;
}
case Bfg93Op.NOT: {
const val = this.popStack();
this.pushStack(val === 0 ? 1 : 0);
break;
}
case Bfg93Op.GREATER: {
const a = this.popStack();
const b = this.popStack();
this.pushStack(b > a ? 1 : 0);
break;
}
case Bfg93Op.RIGHT: {
this._dirn = Bfg93Direction.RIGHT;
break;
}
case Bfg93Op.LEFT: {
this._dirn = Bfg93Direction.LEFT;
break;
}
case Bfg93Op.UP: {
this._dirn = Bfg93Direction.UP;
break;
}
case Bfg93Op.DOWN: {
this._dirn = Bfg93Direction.DOWN;
break;
}
case Bfg93Op.RANDOM: {
const rand = Math.floor(Math.random() * 4);
if (rand === 0) this._dirn = Bfg93Direction.RIGHT;
else if (rand === 1) this._dirn = Bfg93Direction.LEFT;
else if (rand === 2) this._dirn = Bfg93Direction.UP;
else this._dirn = Bfg93Direction.DOWN;
break;
}
case Bfg93Op.H_IF: {
const val = this.popStack();
if (val === 0) this._dirn = Bfg93Direction.RIGHT;
else this._dirn = Bfg93Direction.LEFT;
break;
}
case Bfg93Op.V_IF: {
const val = this.popStack();
if (val === 0) this._dirn = Bfg93Direction.DOWN;
else this._dirn = Bfg93Direction.UP;
break;
}
case Bfg93Op.TOGGLE_STR: {
this._strmode = !this._strmode;
break;
}
case Bfg93Op.DUPLICATE: {
const val = this.popStack();
this.pushStack(val);
this.pushStack(val);
break;
}
case Bfg93Op.SWAP: {
const top = this.popStack();
const other = this.popStack();
this.pushStack(top);
this.pushStack(other);
break;
}
case Bfg93Op.POP_DELETE: {
this.popStack();
break;
}
case Bfg93Op.POP_OUTINT: {
const int = this.popStack();
output = int.toString() + " ";
break;
}
case Bfg93Op.POP_OUTCHAR: {
const charCode = this.popStack();
output = String.fromCharCode(charCode);
break;
}
case Bfg93Op.BRIDGE: {
this.updatePointer();
break;
}
case Bfg93Op.GET_DATA: {
const y = this.popStack();
const x = this.popStack();
const char = this.getGridCell(x, y);
this.pushStack(char.charCodeAt(0));
break;
}
case Bfg93Op.PUT_DATA: {
const y = this.popStack();
const x = this.popStack();
const charCode = this.popStack();
edit = this.setGridCell(x, y, charCode);
break;
}
case Bfg93Op.STDIN_INT: {
this.pushStack(this._input.getNumber());
break;
}
case Bfg93Op.STDIN_CHAR: {
const charCode = this._input.getChar();
this.pushStack(charCode);
break;
}
case Bfg93Op.END: {
end = true;
break;
}
default: {
this.pushStack(parseInt(op, 10));
break;
}
}
// Update grid pointer and return
this.updatePointer();
return { output, end, edit };
}
/** Push a number onto the stack */
private pushStack(num: number): void {
this._stack.push(num);
}
/** Pop a number from stack. If empty stack, returns 0 */
private popStack(): number {
if (this._stack.length === 0) return 0;
else return this._stack.pop()!;
}
/**
* Get character at position (x, y) of program grid.
* Throws RuntimeError if (x, y) is out of bounds.
*/
private getGridCell(x: number, y: number): string {
if (!this.isInGrid(x, y))
throw new RuntimeError("Coordinates out of bounds");
else return this._ast[y][x];
}
/**
* Set cell at (x, y) of program grid to character with given ASCII value.
* Throws if (x, y) is out of bounds
*/
private setGridCell(x: number, y: number, asciiVal: number): DocumentEdit {
if (!this.isInGrid(x, y))
throw new RuntimeError("Coordinates out of bound");
// Change character at position (x, y)
this._ast[y] =
this._ast[y].slice(0, x) +
String.fromCharCode(asciiVal) +
this._ast[y].slice(x + 1);
// Update grid bounds
this._bounds.x[y] = Math.max(this._bounds.x[y], x);
this._bounds.y[x] = Math.max(this._bounds.y[x], y);
// Return code edit object
return {
text: toSafePrintableChar(asciiVal),
range: { line: y, charRange: { start: x, end: x + 1 } },
};
}
/**
* Update program grid pointer according to currently set direction.
* Throws RuntimeError if pointer lands outside 80x25 grid.
*/
private updatePointer(): void {
// Update pointer
if (this._dirn === Bfg93Direction.RIGHT) this._pc.x += 1;
else if (this._dirn === Bfg93Direction.LEFT) this._pc.x -= 1;
else if (this._dirn === Bfg93Direction.UP) this._pc.y -= 1;
else if (this._dirn === Bfg93Direction.DOWN) this._pc.y += 1;
else throw new Error("Unknown direction");
// Check pointer position and wrap if necessary
this.wrapPointer();
}
/**
* Wraps the pointer around the program bounds. Note that program bounds are
* not 80x25 - they are the bounds of the used parts of grid.
*
* Assumes that only one of x and y-coordinates is out of bounds.
*/
private wrapPointer(): void {
if (this._strmode) {
// String mode: just wrap the pointer around the 80x25 grid
this._pc.x = (this._pc.x + ROWSIZE) % ROWSIZE;
this._pc.y = (this._pc.y + COLSIZE) % COLSIZE;
} else if (
this._dirn === Bfg93Direction.LEFT ||
this._dirn === Bfg93Direction.RIGHT
) {
// Wrap pointer around code bounds in horizontal direction (along x-axis)
if (this._pc.x < 0) this._pc.x = this._bounds.x[this._pc.y];
else if (this._pc.x > this._bounds.x[this._pc.y]) this._pc.x = 0;
} else {
// Wrap pointer around code bounds in vertical direction (along y-axis)
if (this._pc.y < 0) this._pc.y = this._bounds.y[this._pc.x];
else if (this._pc.y > this._bounds.y[this._pc.x]) this._pc.y = 0;
}
}
/**
* Generate `DocumentEdit`s to apply on code to pad it up to 80x25 size.
* @param code Code content, lines separated by '\n'
* @returns Array of `DocumentEdit`s to apply on code
*/
private getGridPaddingEdits(code: string): DocumentEdit[] {
const lines = code.split("\n");
const edits: DocumentEdit[] = [];
for (let i = 0; i < COLSIZE; ++i) {
if (i < lines.length) {
if (lines[i].length === ROWSIZE) continue;
// Add padding to line upto full-length
edits.push({
range: {
line: i,
charRange: { start: lines[i].length, end: lines[i].length },
},
text: " ".repeat(ROWSIZE - lines[i].length),
});
} else {
// Add full-length empty line
edits.push({
range: { line: i, charRange: { start: 0, end: 0 } },
text: "\n" + " ".repeat(80),
});
}
}
return edits;
}
/**
* Cast the given character to corresponding Befunge-93 op.
* If character is invalid op, returns null.
* @param char Character to cast to Befunge-93 op
* @returns Corresponding Befunge-93 op, or null.
*/
private charToOp(char: string): Bfg93Op | null {
if (char.length !== 1) throw new Error(`'${char}' not a character`);
if (!OP_CHARS.includes(char as Bfg93Op)) return null;
else return char as Bfg93Op;
}
/** Convert 2D coordinates to DocumentRange */
private toRange(line: number, char: number): DocumentRange {
return { line, charRange: { start: char, end: char + 1 } };
}
/** Check if given coordinates lies inside 80x25 grid */
private isInGrid(x: number, y: number): boolean {
return x >= 0 && x < ROWSIZE && y >= 0 && y < COLSIZE;
}
}

View File

@ -0,0 +1,96 @@
import { readTestProgram, executeProgram } from "../../test-utils";
import { Bfg93Direction } from "../constants";
import Engine from "../runtime";
/**
* All test programs are picked up from https://esolangs.org/wiki/Befunge,
* except the modifications mentioned alongside each test.
*/
/** Relative path to directory of sample programs */
const DIRNAME = __dirname + "/samples";
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.charCodeAt(result.output.length - 1)).toBe(0);
expect(result.output.slice(0, -1)).toBe("Hello, World!");
expect(result.rendererState.direction).toBe(Bfg93Direction.DOWN);
expect(result.rendererState.stack.length).toBe(0);
});
// cat program
test("cat program", async () => {
const input = "abcd efgh\nijkl mnop\n";
const code = readTestProgram(DIRNAME, "cat");
const result = await executeProgram(new Engine(), code, input);
expect(result.output).toBe(input);
expect(result.rendererState.direction).toBe(Bfg93Direction.LEFT);
expect(result.rendererState.stack).toEqual([-1]);
});
// Random DNA printer
test("random DNA", async () => {
const code = readTestProgram(DIRNAME, "dna");
const result = await executeProgram(new Engine(), code);
// program prints "\r\n" at the end of output
expect(result.output.length).toBe(56 + 2);
expect(result.output.trim().search(/[^ATGC]/)).toBe(-1);
expect(result.rendererState.direction).toBe(Bfg93Direction.RIGHT);
expect(result.rendererState.stack).toEqual([0]);
});
// Factorial program
test("factorial", async () => {
const code = readTestProgram(DIRNAME, "factorial");
const result = await executeProgram(new Engine(), code, "5");
expect(result.output).toBe("120 ");
expect(result.rendererState.direction).toBe(Bfg93Direction.RIGHT);
expect(result.rendererState.stack.length).toBe(0);
});
// Sieve of Eratosthenes - prints prime nums upto 36
// (original prints up to 80, shortened here for testing purposes)
test("sieve of eratosthenes", async () => {
const code = readTestProgram(DIRNAME, "prime-sieve");
const result = await executeProgram(new Engine(), code);
const outputNums = result.output
.trim()
.split(" ")
.map((a) => parseInt(a, 10));
const primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31];
expect(outputNums).toEqual(primes);
expect(result.rendererState.direction).toBe(Bfg93Direction.DOWN);
expect(result.rendererState.stack).toEqual([37]);
});
// Quine 1 - simple single-line quine
test("simple singleline quine", async () => {
const code = readTestProgram(DIRNAME, "quine1");
const result = await executeProgram(new Engine(), code);
expect(result.output).toBe(code);
expect(result.rendererState.direction).toBe(Bfg93Direction.RIGHT);
expect(result.rendererState.stack).toEqual([44]);
});
// Quine 2 - multiline quine
test("multiline quine", async () => {
const code = readTestProgram(DIRNAME, "quine2");
const result = await executeProgram(new Engine(), code);
// Output has an extra space at the end - verified on tio.run
expect(result.output).toBe(code + " ");
expect(result.rendererState.direction).toBe(Bfg93Direction.LEFT);
expect(result.rendererState.stack).toEqual([0]);
});
// Quine 3 - quine without using "g"
test("quine without using 'g'", async () => {
const code = readTestProgram(DIRNAME, "quine3");
const result = await executeProgram(new Engine(), code);
expect(result.output).toBe(code);
expect(result.rendererState.direction).toBe(Bfg93Direction.LEFT);
expect(result.rendererState.stack).toEqual([0]);
});
});

View File

@ -0,0 +1 @@
~:1+!#@_,

View File

@ -0,0 +1,8 @@
7^DN>vA
v_#v? v
7^<""""
3 ACGT
90!""""
4*:>>>v
+8^-1,<
> ,+,@)

View File

@ -0,0 +1,2 @@
&>:1-:v v *_$.@
^ _$>\:^

View File

@ -0,0 +1,3 @@
"!dlroW ,olleH">:v
|,<
@

View File

@ -0,0 +1,4 @@
2>:3g" "-!v\ g30 <
|!`"$":+1_:.:03p>03g+:"$"`|
@ ^ p3\" ":<
2 234567890123456789012345678901234567890123456789012345678901234567890123456789

View File

@ -0,0 +1 @@
01->1# +# :# 0# g# ,# :# 5# 8# *# 4# +# -# _@

View File

@ -0,0 +1,2 @@
0 v
"<@_ #! #: #,<*2-1*92,*84,*25,+*92*4*55.0

View File

@ -0,0 +1 @@
<@,+2*48_,#! #:<,_$#-:#*8#4<8"

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

19
languages/chef/README.md Normal file
View File

@ -0,0 +1,19 @@
# Chef
- Ingredient names are case-sensitive and must not contain periods.
- Auxiliary recipe names are case-sensitive. If the recipe title is `Chocolate Sauce`, calling instruction must be `Serve with Chocolate Sauce` and not `Serve with chocolate sauce`.
- Each method instruction must end with a period.
- The method section can be spread across multiple lines.
- A single method instruction cannot roll over to the next line.
- The Chef language involves usage of present and past forms of verbs:
```
Blend the sugar
<other instructions>
Shake the mixture until blended
```
The Esolang Park interpreter cannot convert verbs between the two forms, so we adopt the following convention: the past form of the verb is the same as the present form of the verb. So the above example must be changed to the following for Esolang Park:
```
Blend the sugar
<other instructions>
Shake the mixture until blend
```

View File

@ -0,0 +1,63 @@
import { MonacoTokensProvider } from "../types";
/** Error thrown on malformed syntax. Caught and converted into ParseError higher up */
export class SyntaxError extends Error {
constructor(message: string) {
super(message);
this.name = "SyntaxError";
}
}
/** Check if an error is instance of SyntaxError */
export const isSyntaxError = (error: any): error is SyntaxError => {
return error instanceof SyntaxError || error.name === "SyntaxError";
};
/** Sample Hello World program for Chef */
export const sampleProgram = [
"Hello World Souffle.",
"",
'This recipe prints the immortal words "Hello world!", in a basically brute force way. It also makes a lot of food for one person.',
"",
"Ingredients.",
"72 g haricot beans",
"101 eggs",
"108 g lard",
"111 cups oil",
"32 zucchinis",
"119 ml water",
"114 g red salmon",
"100 g dijon mustard",
"33 potatoes",
"",
"Method.",
"Put potatoes into the mixing bowl.",
"Put dijon mustard into the mixing bowl.",
"Put lard into the mixing bowl.",
"Put red salmon into the mixing bowl.",
"Put oil into the mixing bowl.",
"Put water into the mixing bowl.",
"Put zucchinis into the mixing bowl.",
"Put oil into the mixing bowl.",
"Put lard into the mixing bowl.",
"Put lard into the mixing bowl.",
"Put eggs into the mixing bowl.",
"Put haricot beans into the mixing bowl.",
"Liquefy contents of the mixing bowl.",
"Pour contents of the mixing bowl into the baking dish.",
"",
"Serves 1.",
].join("\n");
export const editorTokensProvider: MonacoTokensProvider = {
tokenizer: {
root: [
[/Ingredients./, "red"],
[/Method./, "red"],
[/mixing bowl/, "green"],
[/baking dish/, "blue"],
[/\d(st|nd|rd|th)?/, "orange"],
],
},
defaultToken: "plain",
};

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

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

12
languages/chef/index.ts Normal file
View File

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

View File

@ -0,0 +1,31 @@
import { ChefArithmeticOp } from "../types";
/** Ingredient measures considered as dry */
export const DryMeasures = ["g", "kg", "pinch", "pinches"];
/** Ingredient measures considered as liquid */
export const LiquidMeasures = ["ml", "l", "dash", "dashes"];
/** Ingredient measures that may be dry or liquid */
export const UnknownMeasures = [
"cup",
"cups",
"teaspoon",
"teaspoons",
"tablespoon",
"tablespoons",
];
/** Types of measures - irrelevant to execution */
export const MeasureTypes = ["heaped", "level"];
/** A map from arithmetic instruction verbs to op codes */
export const ArithmeticCodes: { [k: string]: ChefArithmeticOp["code"] } = {
Add: "ADD",
Remove: "SUBTRACT",
Combine: "MULTIPLY",
Divide: "DIVIDE",
};
/** Placeholder value for loop jump addresses */
export const JumpAddressPlaceholder = -1;

View File

@ -0,0 +1,240 @@
import {
ArithmeticCodes,
DryMeasures,
JumpAddressPlaceholder,
LiquidMeasures,
MeasureTypes,
UnknownMeasures,
} from "./constants";
import { SyntaxError } from "../constants";
import * as R from "./regex";
import * as C from "../types";
/**
* Ideally, this would convert past form of verb to present form. Due to
* the requirement of an English dictionary for sufficient accuracy, we instead
* require the past form to be the same as present form in Esolang Park. Thus,
* this function is currently a no-op.
*
* @param verbed Past form of verb
* @returns Present imperative form of verb
*/
const toPresentTense = (verbed: string) => {
return verbed;
};
/** Parse a string as an ingredient measure */
const parseMeasure = (measure: string): C.StackItemType | undefined => {
if (DryMeasures.includes(measure)) return "dry";
if (LiquidMeasures.includes(measure)) return "liquid";
if (UnknownMeasures.includes(measure)) return "unknown";
};
/** Validate and parse string as integer. Empty string is treated as 1 */
const parseIndex = (str: string): number => {
if (!str || str.trim().length === 0) return 1;
const parsed = parseInt(str.trim(), 10);
if (Number.isNaN(parsed)) throw new SyntaxError("Not a number");
return parsed;
};
/** Parse a string as an ordinal identifier (1st, 2nd, etc) */
const parseOrdinal = (measure: string): number => {
if (!measure || measure.trim().length === 0) return 1;
const parsed = parseInt(measure.trim(), 10);
if (Number.isNaN(parsed))
throw new SyntaxError("Invalid dish/bowl identifier");
return parsed;
};
/** Parse a line as an arithmetic operation in Chef */
const parseArithmeticOp = (line: string): C.ChefArithmeticOp => {
const matches = assertMatch(line, R.ArithmeticOpRegex);
const code = ArithmeticCodes[matches[1]];
const bowlId = parseIndex(matches[4]);
// If mixing bowl segment is entirely missing...
if (!matches[3]) return { code, ing: matches[2], bowlId };
// Case-wise checks for each operation
if (
(matches[1] === "Add" && matches[3] === "to") ||
(matches[1] === "Remove" && matches[3] === "from") ||
(matches[1] === "Combine" && matches[3] === "into") ||
(matches[1] === "Divide" && matches[3] === "into")
)
return { code, ing: matches[2], bowlId };
throw new SyntaxError("Instruction has incorrect syntax");
};
/** Assert that a line matches the given regex and return matches */
const assertMatch = (line: string, regex: RegExp): RegExpMatchArray => {
const matches = line.match(regex);
if (!matches) throw new SyntaxError("Unknown instruction");
return matches;
};
/**
* Parse a line as the definition of an ingredient in Chef
* @param line Line to be parsed as ingredient definition
* @returns Ingredient definition in parsed form
*/
export const parseIngredientItem = (
line: string
): { name: C.IngredientName; item: C.IngredientItem } => {
const words = line.trim().split(/\s+/).reverse();
// Try to parse the first word as a number
const parsedValue = parseInt(words[words.length - 1], 10);
const quantity = Number.isNaN(parsedValue) ? undefined : parsedValue;
if (quantity != null) words.pop();
// Try to parse next word as measure type (heaped/level)
const measureType = words[words.length - 1];
const hasMeasureType = MeasureTypes.includes(measureType);
if (hasMeasureType) words.pop();
// Parse next word as measurement unit
const measure = parseMeasure(words[words.length - 1]);
if (hasMeasureType && !measure) throw new SyntaxError("Invalid measure");
if (measure) words.pop();
// Parse rest of word as name of ingredient
const ingredientName = words.reverse().join(" ");
// Return parsed ingredient item
return {
name: ingredientName,
item: { type: measure || "unknown", value: quantity },
};
};
/**
* Parse a line as a single instruction of a Chef recipe.
*
* Note that loop-closer and loop-opener addresses are inserted as -1 as this function
* does not have scope of the entire method and loop stack. These addresses must be modified
* by the caller by tracking loop statements.
*
* @param line Line containing instruction, ending just before period.
*/
export const parseMethodStep = (line: string): C.ChefOperation => {
if (line.startsWith("Take ")) {
// Take `ingredient` from refrigerator
const matches = assertMatch(line, R.TakeFromFridgeRegex);
return { code: "STDIN", ing: matches[1] };
//========================================================================
} else if (line.startsWith("Put ")) {
// Put `ingredient` into [nth] mixing bowl
const matches = assertMatch(line, R.PutInBowlRegex);
return { code: "PUSH", ing: matches[1], bowlId: parseOrdinal(matches[2]) };
//========================================================================
} else if (line.startsWith("Fold ")) {
// Fold `ingredient` into [nth] mixing bowl
const matches = assertMatch(line, R.FoldIntoBowlRegex);
return { code: "POP", ing: matches[1], bowlId: parseOrdinal(matches[2]) };
//========================================================================
} else if (line.startsWith("Add dry ingredients")) {
// Add dry ingredients [into [nth] mixing bowl]
const matches = assertMatch(line, R.AddDryIngsOpRegex);
return { code: "ADD-DRY", bowlId: parseIndex(matches[1]) };
//========================================================================
} else if (
["Add", "Remove", "Combine", "Divide"].includes(line.split(" ", 1)[0])
) {
// Add | Remove | Combine | Divide ...
return parseArithmeticOp(line);
//========================================================================
} else if (
line.startsWith("Liquefy contents of the ") ||
line.startsWith("Liquefy the contents of the ")
) {
// Liquefy contents of the [nth] mixing bowl
const matches = assertMatch(line, R.LiquefyBowlRegex);
return { code: "LIQ-BOWL", bowlId: parseIndex(matches[1]) };
//========================================================================
} else if (line.startsWith("Liquefy ")) {
// Liquefy `ingredient`
const matches = assertMatch(line, R.LiquefyIngRegex);
return { code: "LIQ-ING", ing: matches[1] };
//========================================================================
} else if (
line.startsWith("Stir ") &&
(line.endsWith("minute") || line.endsWith("minutes"))
) {
// Stir [the [nth] mixing bowl] for `number` minutes
const matches = assertMatch(line, R.StirBowlRegex);
return {
code: "ROLL-BOWL",
bowlId: parseIndex(matches[1]),
num: parseIndex(matches[2]),
};
//========================================================================
} else if (line.startsWith("Stir ")) {
// Stir ingredient into the [nth] mixing bowl
const matches = assertMatch(line, R.StirIngredientRegex);
return {
code: "ROLL-ING",
ing: matches[1],
bowlId: parseIndex(matches[2]),
};
//========================================================================
} else if (line.startsWith("Mix ")) {
// Mix [the [nth] mixing bowl] well
const matches = assertMatch(line, R.MixBowlRegex);
return { code: "RANDOM", bowlId: parseIndex(matches[1]) };
//========================================================================
} else if (line.startsWith("Clean ")) {
// Clean [nth] mixing bowl
const matches = assertMatch(line, R.CleanBowlRegex);
return { code: "CLEAR", bowlId: parseIndex(matches[1]) };
//========================================================================
} else if (line.startsWith("Pour ")) {
// Pour contents of [nth] mixing bowl into [pth] baking dish
const matches = assertMatch(line, R.PourBowlRegex);
return {
code: "COPY",
bowlId: parseIndex(matches[1]),
dishId: parseIndex(matches[2]),
};
//========================================================================
} else if (line === "Set aside") {
// Set aside
return { code: "LOOP-BREAK", closer: JumpAddressPlaceholder };
//========================================================================
} else if (line.startsWith("Serve with ")) {
// Serve with `auxiliary recipe`
const matches = assertMatch(line, R.ServeWithRegex);
return { code: "FNCALL", recipe: matches[1] };
//========================================================================
} else if (line.startsWith("Refrigerate")) {
// Refrigerate [for `number` hours]
const matches = assertMatch(line, R.RefrigerateRegex);
const num = matches[1] ? parseIndex(matches[1]) : undefined;
return { code: "END", num };
//========================================================================
} else if (line.includes(" until ")) {
// `Verb` [the `ingredient`] until `verbed`
const matches = assertMatch(line, R.LoopEnderRegex);
const ingredient = matches[1] || undefined;
const verb = toPresentTense(matches[2]);
return {
code: "LOOP-CLOSE",
ing: ingredient,
verb,
opener: JumpAddressPlaceholder,
};
//========================================================================
} else {
// `Verb` [the] `ingredient`
const matches = assertMatch(line, R.LoopOpenerRegex);
return {
code: "LOOP-OPEN",
verb: matches[1].toLowerCase(),
ing: matches[2],
closer: JumpAddressPlaceholder,
};
}
};

View File

@ -0,0 +1,347 @@
import * as T from "../types";
import { DocumentRange } from "../../types";
import { parseIngredientItem, parseMethodStep } from "./core";
import { ParseError } from "../../worker-errors";
import { isSyntaxError, SyntaxError } from "../constants";
/**
* We parse a Chef program by creating an array containing the lines of code (with line nos)
* in reverse order. This array represents a stack with the first line at the top.
*
* Each parse step then pops and parses statements from the stack. For instance,
* the parseTitle function pops one statement from the stack and parses it for the title.
* The rest of the stack is then parsed by firther steps.
*/
/** Reversed array of lines of code, used as a stack */
type CodeStack = { line: string; row: number }[];
/** Text and location of a single method instruction */
type MethodSegment = { str: string; location: DocumentRange };
/** Parse a Chef program */
export const parseProgram = (code: string): T.ChefProgram => {
// Convert code to a reverse stack
const stack: CodeStack = code
.split("\n")
.map((l, idx) => ({ line: l, row: idx }))
.reverse();
// Location of code's last char, used for errors
const lastCharPosition = stack[0]?.line.length - 1 || 0;
const lastCharRange: DocumentRange = {
line: stack.length - 1,
charRange: { start: lastCharPosition, end: lastCharPosition + 1 },
};
// Exhaust any empty lines at the start of the program
exhaustEmptyLines(stack);
// Parse the main recipe
const mainRecipe = parseRecipe(stack, lastCharRange);
exhaustEmptyLines(stack);
// Parse any auxiliary recipes
const auxRecipes: { [k: string]: T.ChefRecipe } = {};
while (stack.length && stack[stack.length - 1].line.trim() !== "") {
const recipe = parseRecipe(stack, lastCharRange);
auxRecipes[recipe.name] = recipe;
exhaustEmptyLines(stack);
}
const program = { main: mainRecipe, auxes: auxRecipes };
validateProgram(program);
return program;
};
/** Pop all empty lines from top of the stack */
const exhaustEmptyLines = (stack: CodeStack): void => {
while (stack.length && stack[stack.length - 1].line.trim() === "")
stack.pop();
};
/**
* Parse the stack for recipe title
* @param stack Code stack to parse next line of
* @param lastCharRange Error range used if stack is found to be empty
*/
const parseTitle = (stack: CodeStack, lastCharRange: DocumentRange): string => {
const { line, row } = popCodeStack(stack, true);
if (line === null)
throw new ParseError("Expected recipe title", lastCharRange);
if (!line) throw new ParseError("Expected recipe title", { line: row });
if (!line.endsWith("."))
throw new ParseError("Recipe title must end with period", { line: row });
return line.slice(0, -1);
};
/**
* Parse the stack for an empty line
* @param stack Code stack to parse next line of
* @param lastCharRange Error range used if stack is found to be empty
*/
const parseEmptyLine = (
stack: CodeStack,
lastCharRange: DocumentRange
): void => {
const { line, row } = popCodeStack(stack, true);
if (line === null) throw new ParseError("Expected blank line", lastCharRange);
if (line) throw new ParseError("Expected blank line", { line: row });
};
/** Parse the stack for method instructions section */
const parseRecipeComments = (stack: CodeStack): void => {
while (stack[stack.length - 1].line.trim() !== "") stack.pop();
};
/** Parse the stack for the header of ingredients section */
const parseIngredientsHeader = (stack: CodeStack): void => {
const { line, row } = popCodeStack(stack, true);
if (line !== "Ingredients.")
throw new ParseError("Expected ingredients header", { line: row });
};
/** Parse the stack for ingredient definition lines */
const parseIngredientsSection = (stack: CodeStack): T.IngredientBox => {
const box: T.IngredientBox = {};
while (stack[stack.length - 1].line.trim() !== "") {
const { line, row } = popCodeStack(stack);
const { name, item } = parseIngredientItem(line!);
box[name] = item;
}
return box;
};
/** Parse stack for cooking time statement. No data is returned. */
const parseCookingTime = (stack: CodeStack): void => {
const regex = /^Cooking time: \d+ (?:hours?|minutes?).$/;
const { line, row } = popCodeStack(stack, true);
if (!line!.match(regex))
throw new ParseError("Invalid cooking time statement", { line: row });
};
/** Parse stack for oven setting statement. No data is returned. */
const parseOvenSetting = (stack: CodeStack): void => {
const regex =
/^Pre-heat oven to \d+ degrees Celsius(?: \(gas mark [\d/]+\))?.$/;
const { line, row } = popCodeStack(stack, true);
if (!line!.match(regex))
throw new ParseError("Invalid oven setting statement", { line: row });
};
/** Parse the stack for the header of method section */
const parseMethodHeader = (stack: CodeStack): void => {
const { line, row } = popCodeStack(stack, true);
if (line !== "Method.")
throw new ParseError('Expected "Method."', { line: row });
};
/** Parse the stack for method instructions section */
const parseMethodSection = (stack: CodeStack): T.ChefOpWithLocation[] => {
const loopStack: { opener: number; verb: string }[] = [];
const pendingBreaks: number[] = [];
const segments = serializeMethodOps(stack);
const ops: T.ChefOpWithLocation[] = [];
segments.forEach((segment, index) => {
try {
processMethodSegment(segment, index, ops, loopStack, pendingBreaks);
} catch (error) {
if (isSyntaxError(error))
throw new ParseError(error.message, segment.location);
else throw error;
}
});
return ops;
};
/**
* Process a single method segment
* @param segment Method segment to process
* @param index Index of segment in the method section
* @param ops List of already processed method segments
* @param loopStack Stack of currently active loops
* @param pendingBreaks Loop-breaks in the currently active loop
*/
const processMethodSegment = (
segment: MethodSegment,
index: number,
ops: T.ChefOpWithLocation[],
loopStack: { opener: number; verb: string }[],
pendingBreaks: number[]
) => {
// Parse operation and push to result
const op = parseMethodStep(segment.str);
ops.push({ op, location: segment.location });
switch (op.code) {
case "LOOP-OPEN": {
loopStack.push({ opener: index, verb: op.verb });
// `closer` will be added while handling loop-closer
break;
}
case "LOOP-BREAK": {
pendingBreaks.push(index);
// `closer` will be added while handling loop-closer
break;
}
case "LOOP-CLOSE": {
// Validate match with innermost loop
const loop = loopStack.pop()!;
if (loop.verb !== op.verb)
throw new SyntaxError(
`Loop verb mismatch: expected '${loop.verb}', found '${op.verb}'`
);
op.opener = loop.opener;
// Add jump address to loop opener
const openerOp = ops[loop.opener].op;
if (openerOp.code !== "LOOP-OPEN") throw new Error("Bad jump address");
openerOp.closer = index;
// Add jump address to intermediate loop-breaks
while (pendingBreaks.length) {
const breaker = ops[pendingBreaks.pop()!].op;
if (breaker.code !== "LOOP-BREAK")
throw new Error("Memorized op not a breaker");
breaker.closer = index;
}
break;
}
}
};
/** Parse a method section and serialize to list of instructions and corresponding document ranges */
const serializeMethodOps = (stack: CodeStack): MethodSegment[] => {
const segments: MethodSegment[] = [];
while (stack.length && stack[stack.length - 1].line.trim() !== "") {
const item = stack.pop()!;
// Find all the periods in the line
const periodIdxs: number[] = [-1];
for (let i = 0; i < item.line.length; ++i) {
if (item.line[i] === ".") periodIdxs.push(i);
}
// Parse each period-separated segment
for (let i = 0; i < periodIdxs.length - 1; ++i) {
const start = periodIdxs[i] + 1;
const end = periodIdxs[i + 1];
const range = { line: item.row, charRange: { start, end } };
segments.push({
str: item.line.slice(start, end).trim(),
location: range,
});
}
}
return segments;
};
/** Parse the stack for a "Serves N" statement */
const parseServesLine = (stack: CodeStack): T.ChefRecipeServes => {
const { line, row } = popCodeStack(stack, true);
const match = line!.match(/^Serves (\d+).$/);
if (!match) throw new ParseError("Malformed serves statement", { line: row });
return { line: row, num: parseInt(match[1], 10) };
};
/** Parse the stack for a single Chef recipe */
const parseRecipe = (
stack: CodeStack,
lastCharRange: DocumentRange
): T.ChefRecipe => {
// Title of the recipe
const title = parseTitle(stack, lastCharRange);
parseEmptyLine(stack, lastCharRange);
// Check if exists and parse recipe comments
if (stack[stack.length - 1].line.trim() !== "Ingredients.") {
parseRecipeComments(stack);
parseEmptyLine(stack, lastCharRange);
}
// Parse ingredients
parseIngredientsHeader(stack);
const ingredientBox = parseIngredientsSection(stack);
parseEmptyLine(stack, lastCharRange);
// Check if exists and parse cooking time
if (stack[stack.length - 1].line.trim().startsWith("Cooking time: ")) {
parseCookingTime(stack);
parseEmptyLine(stack, lastCharRange);
}
// Check if exists and parse oven temperature
if (stack[stack.length - 1].line.trim().startsWith("Pre-heat oven ")) {
parseOvenSetting(stack);
parseEmptyLine(stack, lastCharRange);
}
// Parse method
parseMethodHeader(stack);
const method = parseMethodSection(stack);
exhaustEmptyLines(stack);
// Check if exists and parse recipe
const serves = stack[stack.length - 1]?.line.trim().startsWith("Serves ")
? parseServesLine(stack)
: undefined;
return { name: title, ingredients: ingredientBox, method, serves };
};
/**
* Validate the provided recipe.
* - Check that ingredient names used in method are valid.
* - Check that auxiliary recipe names used ar valid.
* @param recipe Recipe to validate
* @param auxes Map of auxiliary recipes, keyed by name
*/
const validateRecipe = (
recipe: T.ChefRecipe,
auxes: T.ChefProgram["auxes"]
): void => {
for (const line of recipe.method) {
const ingName = (line.op as any).ing;
if (ingName && !recipe.ingredients[ingName])
throw new ParseError(`Invalid ingredient: ${ingName}`, line.location);
if (line.op.code === "FNCALL" && !auxes[line.op.recipe])
throw new ParseError(
`Invalid recipe name: ${line.op.recipe}`,
line.location
);
}
};
/**
* Validate all recipes in the given parsed Chef program
* @param program Chef program to validate
*/
const validateProgram = (program: T.ChefProgram): void => {
validateRecipe(program.main, program.auxes);
for (const auxName in program.auxes)
validateRecipe(program.auxes[auxName], program.auxes);
};
/**
* Utility to pop a line off the code stack.
* @param stack Code stack to pop line off
* @param trim Pass true if result should contain trimmed line
* @returns Object containing `line` and `row` of popped line
*/
const popCodeStack = (
stack: CodeStack,
trim?: boolean
): { line: string | null; row: number } => {
const item = stack.pop();
if (!item) return { line: null, row: -1 };
const line = trim ? item.line.trim() : item.line;
return { line, row: item.row };
};

View File

@ -0,0 +1,160 @@
/**
* For each regular expression below:
* - Doc comments include the details of each capture group in the regex.
* - Regex comments provide a little overview of the regex, where
* [...] denotes optional clause, <...> denotes capture group.
*/
/**
* Regular expression for `Take ingredient from refrigerator` op
* Capture groups:
* 1. **Ingredient name**: string with letters and spaces
*/
export const TakeFromFridgeRegex =
/** <Ingredient> */
/^Take ([a-zA-Z ]+?) from(?: the)? refrigerator$/;
/**
* Regular expression for `Put ingredient into nth bowl` op
* Capture groups:
* 1. **Ingredient name**: string with letters and spaces
* 2. **Mixing bowl index** (optional): integer
*/
export const PutInBowlRegex =
/** <Ingredient> [ <Bowl identifier> ] */
/^Put(?: the)? ([a-zA-Z ]+?) into(?: the)?(?: (\d+)(?:nd|rd|th|st))? mixing bowl$/;
/**
* Regular expression for `Fold ingredient into nth bowl` op
* Capture groups:
* 1. **Ingredient name**: string with letters and spaces
* 2. **Mixing bowl index** (optional): integer
*/
export const FoldIntoBowlRegex =
/** <Ingredient> [ <Bowl identifier> ] */
/^Fold(?: the)? ([a-zA-Z ]+?) into(?: the)?(?: (\d+)(?:nd|rd|th|st))? mixing bowl$/;
/**
* Regular expression to match the four main arithmetic operations in Chef.
* Capture groups:
* 1. **Operation name**: `"Add" | "Remove" | "Combine" | "Divide"`
* 2. **Ingredient name**: string with letters and spaces
* 3. **Proverb** (optional): `"to" | "into" | "from"`
* 4. **Mixing bowl index** (optional): integer
*/
export const ArithmeticOpRegex =
/** <Operation name> <Ingredient> [ <Proverb> [ <Bowl identifier> ] ] */
/^(Add|Remove|Combine|Divide) ([a-zA-Z ]+?)(?: (to|into|from)(?: the)?(?: (\d+)(?:nd|rd|th|st))? mixing bowl)?$/;
/**
* Regular expression for the `Add dry ingredients ...` op.
* Capture groups:
* 1. **Mixing bowl index** (optional): integer
*/
export const AddDryIngsOpRegex =
/** [ [ <Bowl identifier> ] ] */
/^Add dry ingredients(?: to(?: the)?(?: (\d+)(?:nd|rd|th|st))? mixing bowl)?$/;
/**
* Regular expression for the `Liquefy contents` op
* Capture groups:
* 1. **Mixing bowl index** (optional): integer
*/
export const LiquefyBowlRegex =
/** [ <Bowl identifier> ] */
/^Liquefy(?: the)? contents of the(?: (\d+)(?:nd|rd|th|st))? mixing bowl$/;
/**
* Regular expression for the `Liquefy ingredient` op
* Capture groups:
* 1. **Ingredient name**: string with letters and spaces
*/
export const LiquefyIngRegex =
/** <Ingredient> */
/^Liquefy(?: the)? ([a-zA-Z ]+?)$/;
/**
* Regular expression to match the `Stir <bowl> for <n> minutes` op.
* Capture groups:
* 1. **Mixing bowl index** (optional): integer
* 2. **Number of mins**: integer
*/
export const StirBowlRegex =
/** [ [ <Bowl identifier> ]? ]? <Number> */
/^Stir(?: the(?: (\d+)(?:nd|rd|th|st))? mixing bowl)? for (\d+) minutes?$/;
/**
* Regular expression to match the `Stir <ingredient> into [nth] mixing bowl` op.
* Capture groups:
* 1. **Ingredient name**: string with letters and spaces
* 2. **Mixing bowl index** (optional): integer
*/
export const StirIngredientRegex =
/** <Ingredient> [ <Bowl identifier> ] */
/^Stir ([a-zA-Z ]+?) into the(?: (\d+)(?:nd|rd|th|st))? mixing bowl$/;
/**
* Regular expression to match the `Mix [the [nth] mixing bowl] well` op.
* Capture groups:
* 1. **Mixing bowl index** (optional): integer
*/
export const MixBowlRegex =
/** [ [ <Bowl identifier> ]? ]? */
/^Mix(?: the(?: (\d+)(?:nd|rd|th|st))? mixing bowl)? well$/;
/**
* Regular expression for the `Clean bowl` op
* Capture groups:
* 1. **Mixing bowl index** (optional): integer
*/
export const CleanBowlRegex =
/** [ <Bowl identifier> ] */
/^Clean(?: the)?(?: (\d+)(?:nd|rd|th|st))? mixing bowl$/;
/**
* Regular expression to match the `Pour ...` op.
* Capture groups:
* 1. **Mixing bowl index** (optional): integer
* 2. **Baking dish index** (optional): integer
*/
export const PourBowlRegex =
/** [ <Bowl identifier> ]? [ <Bowl identifier> ]? */
/^Pour contents of the(?: (\d+)(?:nd|rd|th|st))? mixing bowl into the(?: (\d+)(?:nd|rd|th|st))? baking dish$/;
/**
* Regular expression to match the `Serve with` op.
* Capture groups:
* 1. **Name of aux recipe**: string with alphanumerics and spaces
*/
export const ServeWithRegex =
/** <aux recipe> */
/^Serve with ([a-zA-Z0-9 ]+)$/;
/**
* Regular expression to match the `Refrigerate` op.
* Capture groups:
* 1. **Number of hours** (optional): integer
*/
export const RefrigerateRegex =
/** [ <num of hours> ] */
/^Refrigerate(?: for (\d+) hours?)?$/;
/**
* Regular expression to match the `Verb the ingredient` op.
* Capture groups:
* 1. **Verb**: string with letters
* 2. **Ingredient name**: string with letters and spaces
*/
export const LoopOpenerRegex =
/** <Verb> <Ingredient> */
/^([a-zA-Z]+?)(?: the)? ([a-zA-Z ]+)$/;
/**
* Regular expression to match the `Verb [the ing] until verbed` op.
* Capture groups:
* 1. **Ingredient name** (optional): string with letters and spaces
* 2. **Matched verb**: string with letters
*/
export const LoopEnderRegex =
/** Verb [ <Ingredient> ] <Verbed> */
/^(?:[a-zA-Z]+?)(?: the)?(?: ([a-zA-Z ]+?))? until ([a-zA-Z]+)$/;

View File

@ -0,0 +1,100 @@
import * as React from "react";
import { MixingBowl, StackItem } from "../types";
import { ItemTypeIcons } from "./utils";
const styles = {
cellContainer: {
display: "flex",
justifyContent: "space-between",
margin: "2px 0",
},
listContainer: {
width: "80%",
marginTop: 5,
marginLeft: "auto",
marginRight: "auto",
},
stackContainer: {
overflowY: "auto" as "auto",
},
columnContainer: {
height: "100%",
textAlign: "center" as "center",
margin: "0 10px",
display: "flex",
flexDirection: "column" as "column",
},
stackMarker: {
height: "0.7em",
borderRadius: 5,
},
stackHeader: {
fontWeight: "bold",
margin: "5px 0",
},
};
/** Displays a single item of a bowl or dish, along with type */
const StackItemCell = ({ item }: { item: StackItem }) => {
return (
<div style={styles.cellContainer}>
<span title={item.type}>{ItemTypeIcons[item.type]}</span>
<span title={"Character: " + String.fromCharCode(item.value)}>
{item.value.toString()}
</span>
</div>
);
};
/** Displays a list of bowl/dish items in reverse order */
const StackItemList = ({ items }: { items: StackItem[] }) => {
return (
<div style={styles.listContainer}>
{items.map((item, idx) => (
<StackItemCell key={idx} item={item} />
))}
</div>
);
};
/** Displays a mixing bowl in a vertical strip */
export const MixingBowlColumn = ({
bowl,
index,
}: {
bowl: MixingBowl;
index: number;
}) => {
return (
<div style={styles.columnContainer}>
<div style={styles.stackHeader}>Bowl {index + 1}</div>
<div style={styles.stackContainer}>
<div
style={{ ...styles.stackMarker, backgroundColor: "#137CBD" }}
></div>
<StackItemList items={bowl} />
</div>
</div>
);
};
/** Displays a baking dish in a vertical strip */
export const BakingDishColumn = ({
dish,
index,
}: {
dish: MixingBowl;
index: number;
}) => {
return (
<div style={styles.columnContainer}>
<div style={styles.stackHeader}>Dish {index + 1}</div>
<div style={styles.stackContainer}>
<div
style={{ ...styles.stackMarker, backgroundColor: "#0F9960" }}
></div>
<StackItemList items={dish} />
</div>
</div>
);
};

View File

@ -0,0 +1,50 @@
import { Breadcrumbs } from "@blueprintjs/core";
import * as React from "react";
import { RendererProps } from "../../types";
import { ChefRS } from "../types";
import { KitchenDisplay } from "./kitchen-display";
import { BorderColor } from "./utils";
const styles = {
placeholderDiv: {
height: "100%",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
fontSize: "1.2em",
},
rootContainer: {
height: "100%",
display: "flex",
flexDirection: "column" as "column",
},
callStackContainer: {
borderBottom: "1px solid " + BorderColor,
padding: "5px 10px",
},
kitchenContainer: {
flex: 1,
minHeight: 0,
},
};
export const Renderer = ({ state }: RendererProps<ChefRS>) => {
if (state == null)
return (
<div style={styles.placeholderDiv}>Run some code to see the kitchen!</div>
);
const crumbs = state.stack.map((name) => ({ text: name }));
return (
<div style={styles.rootContainer}>
<div style={styles.callStackContainer}>
<Breadcrumbs items={crumbs} />
</div>
<div style={styles.kitchenContainer}>
<KitchenDisplay state={state.currentKitchen} />
</div>
</div>
);
};

View File

@ -0,0 +1,56 @@
import { IngredientBox, IngredientItem } from "../types";
import { ItemTypeIcons } from "./utils";
const styles = {
paneHeader: {
fontSize: "1.1em",
fontWeight: "bold",
marginBottom: 15,
},
paneContainer: {
height: "100%",
padding: 10,
},
rowItemContainer: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
margin: "3px 0",
},
rowItemRight: {
display: "flex",
alignItems: "center",
},
};
/** Displays a single ingredient item's name, type and value */
const IngredientPaneRow = ({
name,
item,
}: {
name: string;
item: IngredientItem;
}) => {
return (
<div style={styles.rowItemContainer}>
<span>{name}</span>
<span title={item.type} style={styles.rowItemRight}>
{item.value == null ? "-" : item.value.toString()}
<span style={{ width: 10 }} />
{ItemTypeIcons[item.type]}
</span>
</div>
);
};
/** Displays list of ingredients under an "Ingredients" header */
export const IngredientsPane = ({ box }: { box: IngredientBox }) => {
return (
<div style={styles.paneContainer}>
<div style={styles.paneHeader}>Ingredients</div>
{Object.keys(box).map((name) => (
<IngredientPaneRow key={name} name={name} item={box[name]} />
))}
</div>
);
};

View File

@ -0,0 +1,57 @@
import { Colors } from "@blueprintjs/core";
import { ChefRS } from "../types";
import { BakingDishColumn, MixingBowlColumn } from "./bowl-dish-columns";
import { IngredientsPane } from "./ingredients-pane";
import { BorderColor } from "./utils";
const styles = {
ingredientsPane: {
width: 200,
flexShrink: 0,
overflowY: "auto" as "auto",
borderRight: "1px solid " + BorderColor,
},
stacksPane: {
padding: 5,
flexGrow: 1,
display: "flex",
height: "100%",
overflowX: "auto" as "auto",
},
stackColumn: {
width: 125,
flexShrink: 0,
},
};
export const KitchenDisplay = ({
state,
}: {
state: ChefRS["currentKitchen"];
}) => {
return (
<div style={{ display: "flex", height: "100%" }}>
<div style={styles.ingredientsPane}>
<IngredientsPane box={state!.ingredients} />
</div>
<div style={styles.stacksPane}>
{Object.keys(state!.bowls).map((bowlId) => (
<div key={bowlId} style={styles.stackColumn}>
<MixingBowlColumn
index={parseInt(bowlId, 10)}
bowl={state!.bowls[bowlId as any]}
/>
</div>
))}
{Object.keys(state!.dishes).map((dishId) => (
<div key={dishId} style={styles.stackColumn}>
<BakingDishColumn
index={parseInt(dishId, 10)}
dish={state!.dishes[dishId as any]}
/>
</div>
))}
</div>
</div>
);
};

View File

@ -0,0 +1,12 @@
import { Icon } from "@blueprintjs/core";
import { Colors } from "@blueprintjs/core";
/** Common border color for dark and light, using transparency */
export const BorderColor = Colors.GRAY3 + "55";
/** Map from item type to corresponding icon */
export const ItemTypeIcons: { [k: string]: React.ReactNode } = {
dry: <Icon icon="ring" size={12} color={Colors.RED3} />,
liquid: <Icon icon="tint" size={12} color={Colors.BLUE3} />,
unknown: <Icon icon="help" size={12} color={Colors.ORANGE3} />,
};

View File

@ -0,0 +1,277 @@
import {
DocumentRange,
LanguageEngine,
StepExecutionResult,
} from "../../types";
import { parseProgram } from "../parser";
import * as T from "../types";
import InputStream from "./input-stream";
import ChefKitchen from "./kitchen";
/** Type for an item in the call stack */
type CallStackItem = {
auxName?: string;
kitchen: ChefKitchen;
recipe: T.ChefRecipe;
pc: number;
};
// Default values for internal states
// Factories are used to create new objects on reset
const DEFAULT_CALL_STACK = (): CallStackItem[] => [];
const DEFAULT_INPUT = (): InputStream => new InputStream("");
const DEFAULT_AST = (): T.ChefProgram => ({
main: { ingredients: {}, name: "", method: [] },
auxes: {},
});
export default class ChefLanguageEngine implements LanguageEngine<T.ChefRS> {
private _ast: T.ChefProgram = DEFAULT_AST();
private _stack: CallStackItem[] = DEFAULT_CALL_STACK();
private _input: InputStream = DEFAULT_INPUT();
resetState() {
this._ast = DEFAULT_AST();
this._stack = DEFAULT_CALL_STACK();
this._input = DEFAULT_INPUT();
}
validateCode(code: string) {
parseProgram(code);
}
prepare(code: string, input: string) {
this._ast = parseProgram(code);
this._input = new InputStream(input);
const mainKitchen = new ChefKitchen(
this._input,
this._ast.main.ingredients
);
this._stack.push({ kitchen: mainKitchen, recipe: this._ast.main, pc: -1 });
}
executeStep(): StepExecutionResult<T.ChefRS> {
let output: string | undefined = undefined;
/**
* Execution happens only for method steps and the "Serves" line.
* `currFrame.pc === method.length` implies that execution is currently at the "Serves" line.
*/
// Process next operation
const currFrame = this.getCurrentFrame();
if (currFrame.pc === -1) {
// First execution step - dummy
currFrame.pc += 1;
} else if (currFrame.pc === currFrame.recipe.method.length) {
// Execution of the "Serves" statement
const serves = currFrame.recipe.serves!;
output = this.getKitchenOutput(currFrame.kitchen, serves.num);
currFrame.pc += 1;
} else {
// Execution of a method instruction
const { op } = currFrame.recipe.method[currFrame.pc];
output = this.processOp(op);
}
// Save for renderer state, in case program ends in this step
const mainFrame = this._stack[0];
{
// Check for end of recipe and pop call stack
const currFrame = this.getCurrentFrame();
const methodLength = currFrame.recipe.method.length;
if (currFrame.pc > methodLength) {
// "Serves" statement was just executed - now fold call stack
this.foldCallStack();
} else if (currFrame.pc === methodLength) {
// Check if "Serves" statement exists. If not, fold call stack.
if (!currFrame.recipe.serves) this.foldCallStack();
}
}
// Prepare location of next step
let nextStepLocation: DocumentRange | null = null;
if (this._stack.length !== 0) {
const currFrame = this.getCurrentFrame();
if (currFrame.pc === currFrame.recipe.method.length) {
// Next step is "Serves" statement
nextStepLocation = { line: currFrame.recipe.serves!.line + 1 };
} else {
// Next step is a regular method instruction
const nextOp = currFrame.recipe.method[currFrame.pc];
nextStepLocation = nextOp.location;
}
}
// Prepare call stack names list
const stackNames = this._stack.length
? this._stack.map((frame) => frame.auxName || "Main recipe")
: ["End of program"];
// Serialize current kitchen's state
const currentKitchen = this._stack.length
? this._stack[this._stack.length - 1].kitchen
: mainFrame.kitchen;
// Prepare and send execution result
return {
rendererState: {
stack: stackNames,
currentKitchen: currentKitchen.serialize(),
},
nextStepLocation,
output,
};
}
/**
* Process an operation. Also updates program counter state.
* Note that call stack popping must be done by caller when pc goes past end of recipe.
* @param op Operation to process
* @returns String representing operation output (stdout)
*/
private processOp(op: T.ChefOperation): string | undefined {
const currRecipe = this.getCurrentFrame();
let opOutput = "";
switch (op.code) {
case "LOOP-OPEN": {
// Check ingredient value and jump/continue
const ing = currRecipe.kitchen.getIngredient(op.ing, true);
if (ing.value === 0) currRecipe.pc = op.closer + 1;
else currRecipe.pc += 1;
break;
}
case "LOOP-BREAK": {
// Jump to one past the loop closer
currRecipe.pc = op.closer + 1;
break;
}
case "LOOP-CLOSE": {
// Decrement value of ingredient
if (op.ing) {
const ing = currRecipe.kitchen.getIngredient(op.ing, true);
ing.value = ing.value! - 1;
}
// Check value of loop-opener ingredient
const opener = currRecipe.recipe.method[op.opener].op;
if (opener.code !== "LOOP-OPEN") throw new Error("Bad jump address");
const ing = currRecipe.kitchen.getIngredient(opener.ing, true);
if (ing.value === 0) currRecipe.pc += 1;
else currRecipe.pc = op.opener;
break;
}
case "FNCALL": {
currRecipe.pc += 1;
this.forkToAuxRecipe(currRecipe.kitchen, op.recipe);
break;
}
case "END": {
// If `num` provided, get baking dishes output
if (op.num)
opOutput += this.getKitchenOutput(currRecipe.kitchen, op.num);
// Move pc to past end of recipe. Call stack popping is handled by `executeStep`
currRecipe.pc = currRecipe.recipe.method.length;
break;
}
default: {
// Simple kitchen operations
currRecipe.kitchen.processOperation(op);
currRecipe.pc += 1;
break;
}
}
if (opOutput) return opOutput;
}
/**
* Empty the first N dishes of given kitchen into text output.
* @param numDishes Number of dishes to empty as output
* @returns Concatenated output from N baking dishes
*/
private getKitchenOutput(kitchen: ChefKitchen, numDishes: number): string {
let output = "";
for (let i = 1; i <= numDishes; ++i)
output += kitchen.serializeAndClearDish(i);
return output;
}
/**
* Forks the bowls and dishes of the topmost recipe in the call stack
* into an auxiliary recipe, and push it to the call stack.
*/
private forkToAuxRecipe(kitchen: ChefKitchen, recipeName: string): void {
const { bowls, dishes } = kitchen.serialize();
const auxRecipe = this._ast.auxes[recipeName];
const ingredientsClone = this.deepCopy(auxRecipe.ingredients);
const auxKitchen = new ChefKitchen(
this._input,
ingredientsClone,
bowls,
dishes
);
this._stack.push({
auxName: recipeName,
recipe: auxRecipe,
kitchen: auxKitchen,
pc: 0,
});
}
/**
* Repeatedly, pops topmost frame from call stack and pours its first mixing bowl
* into the caller frame's first mixing bowl in the same order. This is done until
* a not-fully-executed frame appears at the top of the call stack.
*
* Consider the call stack as a long divided strip of paper with each cell denoting a frame.
* Then, visualize this as repeatedly paper-folding the completely-executed cell at end of
* the paper strip onto the adjacent cell, until a non-completed cell is reached.
*
* The first iteration is done regardless of whether the topmost frame is completed or not.
*
* If the call stack is empty after a popping, no pouring is done.
*/
private foldCallStack(): void {
while (true) {
// Pop topmost frame and fold first bowl into parent frame's kitchen
const poppedFrame = this._stack.pop()!;
if (this._stack.length === 0) break;
const parentFrame = this.getCurrentFrame();
const firstBowl = poppedFrame.kitchen.getBowl(1);
parentFrame.kitchen.getBowl(1).push(...firstBowl);
// Check if new topmost frame is completed or not
if (!this.isFrameCompleted(parentFrame)) break;
}
}
/** Check if a call stack frame is completely executed */
private isFrameCompleted(frame: CallStackItem): boolean {
if (frame.pc < frame.recipe.method.length) return false;
if (frame.pc > frame.recipe.method.length) return true;
return !frame.recipe.serves;
}
/** Get topmost frame in call stack. Throws if stack is empty. */
private getCurrentFrame(): CallStackItem {
if (this._stack.length === 0) throw new Error("Call stack is empty");
return this._stack[this._stack.length - 1];
}
/**
* A naive function to create a deep copy of an object.
* Uses JSON serialization, so non-simple values like Date won't work.
*/
private deepCopy<T extends {}>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
}

View File

@ -0,0 +1,35 @@
import { RuntimeError } from "../../worker-errors";
/**
* A barebones input stream implementation for consuming integers from a string.
*/
export default class InputStream {
private _text: string;
/** Create a new input stream loaded with the given input */
constructor(text: string) {
this._text = text;
}
/** Remove leading whitespace from the current input stream */
private exhaustLeadingWhitespace(): void {
const firstChar = this._text.trim()[0];
const posn = this._text.search(firstChar);
this._text = this._text.slice(posn);
}
/** Parse input stream for an integer */
getNumber(): number {
this.exhaustLeadingWhitespace();
// The extra whitespace differentiates whether string is empty or all numbers.
if (this._text === "") throw new RuntimeError("Unexpected end of input");
let posn = this._text.search(/[^0-9]/);
if (posn === 0)
throw new RuntimeError(`Unexpected input character: '${this._text[0]}'`);
if (posn === -1) posn = this._text.length;
// Consume and parse numeric part
const numStr = this._text.slice(0, posn);
this._text = this._text.slice(posn);
return parseInt(numStr, 10);
}
}

View File

@ -0,0 +1,251 @@
import {
BakingDish,
ChefKitchenOp,
IngredientBox,
IngredientItem,
MixingBowl,
StackItem,
} from "../types";
import InputStream from "./input-stream";
import { RuntimeError } from "../../worker-errors";
/** Type for a list maintained as an index map */
type IndexList<T> = { [k: string]: T };
/**
* Class for manipulating resources and utensils for a single Chef kitchen.
* Contains methods for modifying ingredients, bowls and dishes corresponding to Chef instructions.
*/
export default class ChefKitchen {
private _ingredients: IngredientBox;
private _bowls: IndexList<MixingBowl>;
private _dishes: IndexList<BakingDish>;
private _input: InputStream;
constructor(
inputStream: InputStream,
ingredients: IngredientBox,
bowls: IndexList<MixingBowl> = {},
dishes: IndexList<BakingDish> = {}
) {
this._ingredients = ingredients;
this._bowls = bowls;
this._dishes = dishes;
this._input = inputStream;
}
/** Serialize and create a deep copy of the kitchen's ingredients, bowls and dishes */
serialize(): {
ingredients: IngredientBox;
bowls: IndexList<MixingBowl>;
dishes: IndexList<BakingDish>;
} {
return {
ingredients: this.deepCopy(this._ingredients),
bowls: this.deepCopy(this._bowls),
dishes: this.deepCopy(this._dishes),
};
}
/** Get mixing bowl by 1-indexed identifier */
getBowl(bowlId: number): MixingBowl {
if (this._bowls[bowlId - 1] == null) this._bowls[bowlId - 1] = [];
return this._bowls[bowlId - 1];
}
/** Get baking dish by 1-indexed identifier */
getDish(dishId: number): BakingDish {
if (this._dishes[dishId - 1] == null) this._dishes[dishId - 1] = [];
return this._dishes[dishId - 1];
}
/** Serialize baking dish into string and clear the dish */
serializeAndClearDish(dishId: number): string {
const dish = this.getDish(dishId);
let output = "";
while (dish.length !== 0) {
const item = dish.pop()!;
if (item.type === "liquid") output += String.fromCharCode(item.value);
else output += " " + item.value.toString();
}
return output;
}
getIngredient(name: string, assertValue?: boolean): IngredientItem {
const item = this._ingredients[name];
if (!item) throw new RuntimeError(`Ingredient '${name}' does not exist`);
if (assertValue && item.value == null)
throw new RuntimeError(`Ingredient '${name}' is undefined`);
else return item;
}
/** Process a Chef kitchen operation on this kitchen */
processOperation(op: ChefKitchenOp): void {
if (op.code === "STDIN") this.stdinToIngredient(op.ing);
else if (op.code === "PUSH") this.pushToBowl(op.bowlId, op.ing);
else if (op.code === "POP") this.popFromBowl(op.bowlId, op.ing);
else if (op.code === "ADD") this.addValue(op.bowlId, op.ing);
else if (op.code === "SUBTRACT") this.subtractValue(op.bowlId, op.ing);
else if (op.code === "MULTIPLY") this.multiplyValue(op.bowlId, op.ing);
else if (op.code === "DIVIDE") this.divideValue(op.bowlId, op.ing);
else if (op.code === "ADD-DRY") this.addDryIngredients(op.bowlId);
else if (op.code === "LIQ-ING") this.liquefyIngredient(op.ing);
else if (op.code === "LIQ-BOWL") this.liquefyBowl(op.bowlId);
else if (op.code === "ROLL-BOWL") this.stirBowl(op.bowlId, op.num);
else if (op.code === "ROLL-ING") this.stirIngredient(op.bowlId, op.ing);
else if (op.code === "RANDOM") this.mixBowl(op.bowlId);
else if (op.code === "CLEAR") this.cleanBowl(op.bowlId);
else if (op.code === "COPY") this.pourIntoDish(op.bowlId, op.dishId);
else throw new Error(`Unknown kitchen opcode: '${op["code"]}''`);
}
/** Read a number from stdin into the value of an ingredient. */
stdinToIngredient(ingredient: string): void {
const value = this._input.getNumber();
this.getIngredient(ingredient).value = value;
}
/** Push value of an ingredient into a mixing bowl */
pushToBowl(bowlId: number, ingredient: string): void {
const item = this.getIngredient(ingredient, true);
this.getBowl(bowlId).push({ ...(item as StackItem) });
}
/** Pop value from a mixing bowl and store into an ingredient */
popFromBowl(bowlId: number, ingredient: string): void {
const bowl = this.getBowl(bowlId);
if (bowl.length === 0) throw new RuntimeError(`Bowl ${bowlId} is empty`);
const item = bowl.pop() as StackItem;
this.getIngredient(ingredient).type = item.type;
this.getIngredient(ingredient).value = item.value;
}
/**
* Add the value of an ingredient to the top of a mixing bowl,
* pushing the result onto the same bowl
*/
addValue(bowlId: number, ingredient: string): void {
const bowl = this.getBowl(bowlId);
if (bowl.length === 0) throw new RuntimeError(`Bowl ${bowlId} is empty`);
const bowlValue = bowl.pop()!.value;
const ingValue = this.getIngredient(ingredient, true).value as number;
bowl.push({ type: "unknown", value: ingValue + bowlValue });
}
/**
* Subtract the value of an ingredient from the top of a mixing bowl,
* pushing the result onto the same bowl
*/
subtractValue(bowlId: number, ingredient: string): void {
const bowl = this.getBowl(bowlId);
if (bowl.length === 0) throw new RuntimeError(`Bowl ${bowlId} is empty`);
const bowlValue = bowl.pop()!.value;
const ingValue = this.getIngredient(ingredient, true).value as number;
bowl.push({ type: "unknown", value: bowlValue - ingValue });
}
/**
* Multiply the value of an ingredient with the top of a mixing bowl,
* pushing the result onto the same bowl
*/
multiplyValue(bowlId: number, ingredient: string): void {
const bowl = this.getBowl(bowlId);
if (bowl.length === 0) throw new RuntimeError(`Bowl ${bowlId} is empty`);
const bowlValue = bowl.pop()!.value;
const ingValue = this.getIngredient(ingredient, true).value as number;
bowl.push({ type: "unknown", value: ingValue * bowlValue });
}
/**
* Divide the top of a mixing bowl by the value of an ingredient,
* pushing the result onto the same bowl
*/
divideValue(bowlId: number, ingredient: string): void {
const bowl = this.getBowl(bowlId);
if (bowl.length === 0) throw new RuntimeError(`Bowl ${bowlId} is empty`);
const bowlValue = bowl.pop()!.value;
const ingValue = this.getIngredient(ingredient, true).value as number;
bowl.push({ type: "unknown", value: bowlValue / ingValue });
}
/** Add values of all dry ingredients and push onto a mixing bowl */
addDryIngredients(bowlId: number): void {
const totalValue = Object.keys(this._ingredients).reduce((sum, name) => {
const ing = this._ingredients[name];
if (ing.type !== "dry") return sum;
if (ing.value == null)
throw new RuntimeError(`Ingredient ${name} is undefined`);
return sum + ing.value;
}, 0);
this.getBowl(bowlId).push({ type: "dry", value: totalValue });
}
/** Convert an ingredient into a liquid */
liquefyIngredient(name: string): void {
this.getIngredient(name).type = "liquid";
}
/** Convert all items in a bowl to liquids */
liquefyBowl(bowlId: number): void {
const bowl = this.getBowl(bowlId);
bowl.forEach((item) => (item.type = "liquid"));
}
/**
* Roll the top `num` elements of a bowl such that top item goes down `num` places.
* If bowl has less than `num` items, top item goes to bottom of bowl.
*/
stirBowl(bowlId: number, num: number): void {
const bowl = this.getBowl(bowlId);
const topIngredient = bowl.pop();
if (!topIngredient) return;
const posn = Math.max(bowl.length - num, 0);
bowl.splice(posn, 0, topIngredient);
}
/**
* Roll the top `num` elements of a bowl such that top item goes down `num` places ,
* where `num` is the value of the specified ingredient. If bowl has less than `num` items,
* top item goes to bottom of bowl.
*/
stirIngredient(bowlId: number, ingredient: string): void {
const ing = this.getIngredient(ingredient, true);
const num = ing.value as number;
this.stirBowl(bowlId, num);
}
/** Randomly shuffle the order of items in a mixing bowl */
mixBowl(bowlId: number): void {
const bowl = this.getBowl(bowlId);
// Fisher-Yates algorithm
let remaining = bowl.length;
while (remaining) {
const i = Math.floor(Math.random() * remaining--);
const temp = bowl[i];
bowl[i] = bowl[remaining];
bowl[remaining] = temp;
}
}
/** Remove all items from a mixing bowl */
cleanBowl(bowlId: number): void {
this._bowls[bowlId - 1] = [];
}
/** Copy the items of a mixing bowl to a baking dish in the same order */
pourIntoDish(bowlId: number, dishId: number): void {
const bowl = this.getBowl(bowlId);
const dish = this.getDish(dishId);
dish.push(...bowl);
}
/**
* A naive function to create a deep copy of an object.
* Uses JSON serialization, so non-simple values like Date won't work.
*/
private deepCopy<T extends {}>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
}

View File

@ -0,0 +1,25 @@
import { executeProgram, readTestProgram } from "../../test-utils";
import ChefRuntime from "../runtime";
/** Absolute path to directory of sample programs */
const DIRNAME = __dirname + "/samples";
describe("Test programs", () => {
test("Hello World Souffle", async () => {
const code = readTestProgram(DIRNAME, "hello-world-souffle");
const result = await executeProgram(new ChefRuntime(), code);
expect(result.output).toBe("Hello world!");
});
test("Fibonacci Du Fromage", async () => {
const code = readTestProgram(DIRNAME, "fibonacci-fromage");
const result = await executeProgram(new ChefRuntime(), code, "10");
expect(result.output).toBe(" 1 1 2 3 5 8 13 21 34 55");
});
test("Hello World Cake with Chocolate Sauce", async () => {
const code = readTestProgram(DIRNAME, "hello-world-cake");
const result = await executeProgram(new ChefRuntime(), code);
expect(result.output).toBe("Hello world!");
});
});

View File

@ -0,0 +1,389 @@
import { ChefOperation, StackItemType } from "../types";
import { JumpAddressPlaceholder } from "../parser/constants";
import { parseIngredientItem, parseMethodStep } from "../parser/core";
/** Test the result of parsing an ingredient definition string */
const testIngredientItem = (
str: string,
name: string,
value: number | undefined,
type: StackItemType
) => {
const result = parseIngredientItem(str);
expect(result.name).toBe(name);
expect(result.item.value).toBe(value);
expect(result.item.type).toBe(type);
};
/** Test the result of parsing a method operation string */
const testMethodOp = (str: string, op: ChefOperation) => {
const result = parseMethodStep(str);
expect(result).toEqual(op);
};
describe("Parsing ingredient definitions", () => {
test("dry ingredients", () => {
testIngredientItem("10 g sugar", "sugar", 10, "dry");
testIngredientItem("2 kg dry almonds", "dry almonds", 2, "dry");
testIngredientItem("1 pinch chilli powder", "chilli powder", 1, "dry");
testIngredientItem("3 pinches chilli powder", "chilli powder", 3, "dry");
});
test("liquid ingredients", () => {
testIngredientItem("10 ml essence", "essence", 10, "liquid");
testIngredientItem("2 l milk", "milk", 2, "liquid");
testIngredientItem("1 dash oil", "oil", 1, "liquid");
testIngredientItem("3 dashes oil", "oil", 3, "liquid");
});
test("dry-or-liquid ingredients", () => {
testIngredientItem("1 cup flour", "flour", 1, "unknown");
testIngredientItem("2 cups flour", "flour", 2, "unknown");
testIngredientItem("1 teaspoon salt", "salt", 1, "unknown");
testIngredientItem("2 teaspoons salt", "salt", 2, "unknown");
testIngredientItem("1 tablespoon ketchup", "ketchup", 1, "unknown");
testIngredientItem("2 tablespoons ketchup", "ketchup", 2, "unknown");
});
});
describe("Parsing method instructions", () => {
test("Take `ing` from refrigerator", () => {
testMethodOp("Take chilli powder from refrigerator", {
code: "STDIN",
ing: "chilli powder",
});
});
test("Put `ing` into [the] [`nth`] mixing bowl", () => {
testMethodOp("Put dry ice into the mixing bowl", {
code: "PUSH",
ing: "dry ice",
bowlId: 1,
});
testMethodOp("Put dry ice into the 21nd mixing bowl", {
code: "PUSH",
ing: "dry ice",
bowlId: 21,
});
testMethodOp("Put dry ice into mixing bowl", {
code: "PUSH",
ing: "dry ice",
bowlId: 1,
});
testMethodOp("Put dry ice into 21nd mixing bowl", {
code: "PUSH",
ing: "dry ice",
bowlId: 21,
});
});
test("Fold `ing` into [the] [`nth`] mixing bowl", () => {
testMethodOp("Fold dry ice into the mixing bowl", {
code: "POP",
ing: "dry ice",
bowlId: 1,
});
testMethodOp("Fold dry ice into the 21nd mixing bowl", {
code: "POP",
ing: "dry ice",
bowlId: 21,
});
testMethodOp("Fold dry ice into mixing bowl", {
code: "POP",
ing: "dry ice",
bowlId: 1,
});
testMethodOp("Fold dry ice into 21nd mixing bowl", {
code: "POP",
ing: "dry ice",
bowlId: 21,
});
});
test("Add `ing` [to [the] [`nth`] mixing bowl]", () => {
testMethodOp("Add black salt", {
code: "ADD",
ing: "black salt",
bowlId: 1,
});
testMethodOp("Add black salt to the mixing bowl", {
code: "ADD",
ing: "black salt",
bowlId: 1,
});
testMethodOp("Add black salt to the 100th mixing bowl", {
code: "ADD",
ing: "black salt",
bowlId: 100,
});
testMethodOp("Add black salt to mixing bowl", {
code: "ADD",
ing: "black salt",
bowlId: 1,
});
testMethodOp("Add black salt to 100th mixing bowl", {
code: "ADD",
ing: "black salt",
bowlId: 100,
});
});
test("Remove `ing` [from [the] [`nth`] mixing bowl]", () => {
testMethodOp("Remove black salt", {
code: "SUBTRACT",
ing: "black salt",
bowlId: 1,
});
testMethodOp("Remove black salt from the mixing bowl", {
code: "SUBTRACT",
ing: "black salt",
bowlId: 1,
});
testMethodOp("Remove black salt from the 100th mixing bowl", {
code: "SUBTRACT",
ing: "black salt",
bowlId: 100,
});
testMethodOp("Remove black salt from mixing bowl", {
code: "SUBTRACT",
ing: "black salt",
bowlId: 1,
});
testMethodOp("Remove black salt from 100th mixing bowl", {
code: "SUBTRACT",
ing: "black salt",
bowlId: 100,
});
});
test("Combine `ing` [into [the] [`nth`] mixing bowl]", () => {
testMethodOp("Combine black salt", {
code: "MULTIPLY",
ing: "black salt",
bowlId: 1,
});
testMethodOp("Combine black salt into the mixing bowl", {
code: "MULTIPLY",
ing: "black salt",
bowlId: 1,
});
testMethodOp("Combine black salt into the 2nd mixing bowl", {
code: "MULTIPLY",
ing: "black salt",
bowlId: 2,
});
testMethodOp("Combine black salt into mixing bowl", {
code: "MULTIPLY",
ing: "black salt",
bowlId: 1,
});
testMethodOp("Combine black salt into 2nd mixing bowl", {
code: "MULTIPLY",
ing: "black salt",
bowlId: 2,
});
});
test("Divide `ing` [into [the] [`nth`] mixing bowl]", () => {
testMethodOp("Divide black salt", {
code: "DIVIDE",
ing: "black salt",
bowlId: 1,
});
testMethodOp("Divide black salt into the mixing bowl", {
code: "DIVIDE",
ing: "black salt",
bowlId: 1,
});
testMethodOp("Divide black salt into the 23rd mixing bowl", {
code: "DIVIDE",
ing: "black salt",
bowlId: 23,
});
testMethodOp("Divide black salt into mixing bowl", {
code: "DIVIDE",
ing: "black salt",
bowlId: 1,
});
testMethodOp("Divide black salt into 23rd mixing bowl", {
code: "DIVIDE",
ing: "black salt",
bowlId: 23,
});
});
test("Add dry ingredients [to [the] [`nth`] mixing bowl]", () => {
testMethodOp("Add dry ingredients", {
code: "ADD-DRY",
bowlId: 1,
});
testMethodOp("Add dry ingredients to mixing bowl", {
code: "ADD-DRY",
bowlId: 1,
});
testMethodOp("Add dry ingredients to 100th mixing bowl", {
code: "ADD-DRY",
bowlId: 100,
});
testMethodOp("Add dry ingredients to mixing bowl", {
code: "ADD-DRY",
bowlId: 1,
});
testMethodOp("Add dry ingredients to 100th mixing bowl", {
code: "ADD-DRY",
bowlId: 100,
});
});
test("Liquefy `ingredient`", () => {
testMethodOp("Liquefy nitrogen gas", {
code: "LIQ-ING",
ing: "nitrogen gas",
});
testMethodOp("Liquefy the nitrogen gas", {
code: "LIQ-ING",
ing: "nitrogen gas",
});
testMethodOp("Liquefy themed leaves", {
code: "LIQ-ING",
ing: "themed leaves",
});
});
test("Liquefy [the] contents of the [`nth`] mixing bowl", () => {
testMethodOp("Liquefy the contents of the mixing bowl", {
code: "LIQ-BOWL",
bowlId: 1,
});
testMethodOp("Liquefy the contents of the 22nd mixing bowl", {
code: "LIQ-BOWL",
bowlId: 22,
});
testMethodOp("Liquefy contents of the mixing bowl", {
code: "LIQ-BOWL",
bowlId: 1,
});
testMethodOp("Liquefy contents of the 22nd mixing bowl", {
code: "LIQ-BOWL",
bowlId: 22,
});
});
test("Stir [the [`nth`] mixing bowl] for `num` minutes", () => {
testMethodOp("Stir for 5 minutes", {
code: "ROLL-BOWL",
bowlId: 1,
num: 5,
});
testMethodOp("Stir the mixing bowl for 22 minutes", {
code: "ROLL-BOWL",
bowlId: 1,
num: 22,
});
testMethodOp("Stir the 3rd mixing bowl for 0 minutes", {
code: "ROLL-BOWL",
bowlId: 3,
num: 0,
});
});
test("Stir `ing` into the [`nth`] mixing bowl", () => {
testMethodOp("Stir dry ice into the mixing bowl", {
code: "ROLL-ING",
bowlId: 1,
ing: "dry ice",
});
testMethodOp("Stir dry ice into the 2nd mixing bowl", {
code: "ROLL-ING",
bowlId: 2,
ing: "dry ice",
});
});
test("Mix [the [`nth`] mixing bowl] well", () => {
testMethodOp("Mix well", { code: "RANDOM", bowlId: 1 });
testMethodOp("Mix the mixing bowl well", { code: "RANDOM", bowlId: 1 });
testMethodOp("Mix the 21st mixing bowl well", {
code: "RANDOM",
bowlId: 21,
});
});
test("Clean [the] [`nth`] mixing bowl", () => {
testMethodOp("Clean the mixing bowl", { code: "CLEAR", bowlId: 1 });
testMethodOp("Clean the 21st mixing bowl", { code: "CLEAR", bowlId: 21 });
testMethodOp("Clean mixing bowl", { code: "CLEAR", bowlId: 1 });
testMethodOp("Clean 21st mixing bowl", { code: "CLEAR", bowlId: 21 });
});
test("Pour contents of the [`nth`] mixing bowl into the [`pth`] baking dish", () => {
testMethodOp("Pour contents of the mixing bowl into the baking dish", {
code: "COPY",
bowlId: 1,
dishId: 1,
});
testMethodOp(
"Pour contents of the 2nd mixing bowl into the 100th baking dish",
{
code: "COPY",
bowlId: 2,
dishId: 100,
}
);
testMethodOp(
"Pour contents of the mixing bowl into the 100th baking dish",
{
code: "COPY",
bowlId: 1,
dishId: 100,
}
);
testMethodOp("Pour contents of the 2nd mixing bowl into the baking dish", {
code: "COPY",
bowlId: 2,
dishId: 1,
});
});
test("`Verb` the `ingredient`", () => {
testMethodOp("Bake the dough", {
code: "LOOP-OPEN",
verb: "bake",
ing: "dough",
closer: JumpAddressPlaceholder,
});
});
test("`Verb` [the `ingredient`] until `verbed`", () => {
testMethodOp("Destroy until bake", {
code: "LOOP-CLOSE",
verb: "bake",
opener: JumpAddressPlaceholder,
});
testMethodOp("Destroy the tomato ketchup until bake", {
code: "LOOP-CLOSE",
verb: "bake",
ing: "tomato ketchup",
opener: JumpAddressPlaceholder,
});
});
test("Set aside", () => {
testMethodOp("Set aside", {
code: "LOOP-BREAK",
closer: JumpAddressPlaceholder,
});
});
test("Serve with `auxiliary-recipe`", () => {
testMethodOp("Serve with chocolate sauce", {
code: "FNCALL",
recipe: "chocolate sauce",
});
});
test("Refrigerate [for `num` hours]", () => {
testMethodOp("Refrigerate", { code: "END" });
testMethodOp("Refrigerate for 2 hours", { code: "END", num: 2 });
});
});

View File

@ -0,0 +1,159 @@
import { readTestProgram } from "../../test-utils";
import { parseProgram } from "../parser";
import { LoopCloseOp, LoopOpenOp } from "../types";
/** Absolute path to directory of sample programs */
const DIRNAME = __dirname + "/samples";
describe("Parsing entire programs", () => {
test("Hello World Souffle", () => {
const code = readTestProgram(DIRNAME, "hello-world-souffle");
const program = parseProgram(code);
expect(program.auxes).toEqual({});
expect(program.main.name).toBe("Hello World Souffle");
expect(program.main.serves).toEqual({ line: 19, num: 1 });
// Lightly check list of ingredients
const ingredients = program.main.ingredients;
expect(Object.keys(ingredients).length).toBe(9);
expect(ingredients["haricot beans"].type).toBe("dry");
expect(ingredients["haricot beans"].value).toBe(72);
expect(ingredients["eggs"].type).toBe("unknown");
expect(ingredients["eggs"].value).toBe(101);
expect(ingredients["oil"].type).toBe("unknown");
expect(ingredients["oil"].value).toBe(111);
expect(ingredients["water"].type).toBe("liquid");
expect(ingredients["water"].value).toBe(119);
// Check method operations
const method = program.main.method;
expect(method.length).toBe(14);
expect(method.slice(0, 12).every((m) => m.op.code === "PUSH")).toBe(true);
expect(method[12].op.code).toBe("LIQ-BOWL");
expect(method[12].location.line).toBe(17);
expect([403, 404]).toContain(method[12].location.charRange?.start);
expect([439, 440]).toContain(method[12].location.charRange?.end);
expect(method[13].op.code).toBe("COPY");
expect(method[13].location.line).toBe(17);
});
test("Fibonacci Du Fromage", () => {
const code = readTestProgram(DIRNAME, "fibonacci-fromage");
const program = parseProgram(code);
expect(program.main.name).toBe("Fibonacci Du Fromage");
expect(program.main.serves).toEqual({ line: 30, num: 1 });
// ====== MAIN RECIPE =======
// Check the list of ingredients
const mainIngredients = program.main.ingredients;
expect(Object.keys(mainIngredients).length).toBe(2);
expect(mainIngredients["numbers"]).toEqual({ type: "dry", value: 5 });
expect(mainIngredients["cheese"]).toEqual({ type: "dry", value: 1 });
// Check the method instructions
const mainMethod = program.main.method;
expect(mainMethod.length).toBe(19);
expect(mainMethod[0].op.code).toBe("STDIN");
expect(mainMethod[0].location.line).toBe(10);
expect(mainMethod[0].location.charRange?.start).toBe(0);
expect(mainMethod[0].location.charRange?.end).toBe(30);
expect(mainMethod[18].op.code).toBe("COPY");
expect(mainMethod[18].location.line).toBe(28);
expect(mainMethod[18].location.charRange?.start).toBe(0);
expect(mainMethod[18].location.charRange?.end).toBe(57);
// Check loop jump addresses in method
const mainOpener1 = mainMethod[8].op as LoopOpenOp;
const mainCloser1 = mainMethod[10].op as LoopCloseOp;
expect(mainOpener1.closer).toBe(10);
expect(mainCloser1.opener).toBe(8);
const mainOpener2 = mainMethod[14].op as LoopOpenOp;
const mainCloser2 = mainMethod[17].op as LoopCloseOp;
expect(mainOpener2.closer).toBe(17);
expect(mainCloser2.opener).toBe(14);
// ====== AUXILIARY RECIPE =========
expect(Object.keys(program.auxes)).toEqual(["salt and pepper"]);
const auxIngredients = program.auxes["salt and pepper"].ingredients;
// Check the list of ingredients
expect(Object.keys(auxIngredients).length).toBe(2);
expect(auxIngredients["salt"]).toEqual({ type: "dry", value: 1 });
expect(auxIngredients["pepper"]).toEqual({ type: "dry", value: 1 });
// Check the method instructions
const auxMethod = program.auxes["salt and pepper"].method;
expect(auxMethod.length).toBe(5);
expect(auxMethod[0].op.code).toBe("POP");
expect(auxMethod[0].location.line).toBe(39);
expect(auxMethod[0].location.charRange?.start).toBe(0);
expect(auxMethod[0].location.charRange?.end).toBe(26);
expect(auxMethod[4].op.code).toBe("ADD");
expect(auxMethod[4].location.line).toBe(43);
expect(auxMethod[4].location.charRange?.start).toBe(0);
expect(auxMethod[4].location.charRange?.end).toBe(10);
});
test("Hello World Cake with Chocolate Sauce", () => {
const code = readTestProgram(DIRNAME, "hello-world-cake");
const program = parseProgram(code);
expect(program.main.name).toBe("Hello World Cake with Chocolate sauce");
expect(program.main.serves).toBeUndefined();
// ====== MAIN RECIPE =======
// Lightly check the list of ingredients
const mainIngredients = program.main.ingredients;
expect(Object.keys(mainIngredients).length).toBe(9);
expect(mainIngredients["butter"]).toEqual({ type: "dry", value: 100 });
expect(mainIngredients["baking powder"]).toEqual({ type: "dry", value: 2 });
expect(mainIngredients["cake mixture"]).toEqual({ type: "dry", value: 0 });
// Check the method instructions
const mainMethod = program.main.method;
expect(mainMethod.length).toBe(15);
expect(mainMethod[0].op.code).toBe("PUSH");
expect(mainMethod[0].location.line).toBe(27);
expect(mainMethod[0].location.charRange?.start).toBe(0);
expect(mainMethod[0].location.charRange?.end).toBe(40);
expect(mainMethod[14].op.code).toBe("FNCALL");
expect(mainMethod[14].location.line).toBe(41);
expect(mainMethod[14].location.charRange?.start).toBe(0);
expect(mainMethod[14].location.charRange?.end).toBe(26);
// Check loop jump addresses in method
const mainOpener = mainMethod[12].op as LoopOpenOp;
const mainCloser = mainMethod[13].op as LoopCloseOp;
expect(mainOpener.closer).toBe(13);
expect(mainCloser.opener).toBe(12);
// ====== AUXILIARY RECIPE =========
expect(Object.keys(program.auxes)).toEqual(["chocolate sauce"]);
const auxIngredients = program.auxes["chocolate sauce"].ingredients;
// Check the list of ingredients
expect(Object.keys(auxIngredients).length).toBe(5);
expect(auxIngredients["sugar"]).toEqual({ type: "dry", value: 111 });
expect(auxIngredients["heated double cream"]).toEqual({
type: "liquid",
value: 108,
});
// Check the method instructions
const auxMethod = program.auxes["chocolate sauce"].method;
expect(auxMethod.length).toBe(13);
expect(auxMethod[0].op.code).toBe("CLEAR");
expect(auxMethod[0].location.line).toBe(53);
expect(auxMethod[0].location.charRange?.start).toBe(0);
expect(auxMethod[0].location.charRange?.end).toBe(21);
expect(auxMethod[12].op.code).toBe("END");
expect(auxMethod[12].location.line).toBe(65);
expect(auxMethod[12].location.charRange?.start).toBe(0);
expect(auxMethod[12].location.charRange?.end).toBe(22);
// Check loop jump addresses in method
const auxOpener = auxMethod[4].op as LoopOpenOp;
const auxCloser = auxMethod[5].op as LoopCloseOp;
expect(auxOpener.closer).toBe(5);
expect(auxCloser.opener).toBe(4);
});
});

View File

@ -0,0 +1,44 @@
Fibonacci Du Fromage.
==== Source: https://github.com/joostrijneveld/Chef-Interpreter/blob/master/ChefInterpreter/FibonacciDuFromage ====
An improvement on the Fibonacci with Caramel Sauce recipe. Much less for the sweettooths, much more correct.
Ingredients.
5 g numbers
1 g cheese
Method.
Take numbers from refrigerator.
Put cheese into mixing bowl.
Put cheese into mixing bowl.
Put numbers into 2nd mixing bowl.
Remove cheese from 2nd mixing bowl.
Remove cheese from 2nd mixing bowl.
Fold numbers into 2nd mixing bowl.
Put numbers into 2nd mixing bowl.
Calculate the numbers.
Serve with salt and pepper.
Ponder the numbers until calculate.
Add cheese to 2nd mixing bowl.
Add cheese to 2nd mixing bowl.
Fold numbers into 2nd mixing bowl.
Move the numbers.
Fold cheese into mixing bowl.
Put cheese into 2nd mixing bowl.
Transfer the numbers until move.
Pour contents of the 2nd mixing bowl into the baking dish.
Serves 1.
salt and pepper.
Ingredients.
1 g salt
1 g pepper
Method.
Fold salt into mixing bowl.
Fold pepper into mixing bowl.
Clean mixing bowl.
Put salt into mixing bowl.
Add pepper.

View File

@ -0,0 +1,66 @@
Hello World Cake with Chocolate sauce.
==== Source: Mike Worth, http://www.mike-worth.com/2013/03/31/baking-a-hello-world-cake/ ====
This prints hello world, while being tastier than Hello World Souffle. The main
chef makes a " world!" cake, which he puts in the baking dish. When he gets the
sous chef to make the "Hello" chocolate sauce, it gets put into the baking dish
and then the whole thing is printed when he refrigerates the sauce. When
actually cooking, I'm interpreting the chocolate sauce baking dish to be
separate from the cake one and Liquify to mean either melt or blend depending on
context.
Ingredients.
33 g chocolate chips
100 g butter
54 ml double cream
2 pinches baking powder
114 g sugar
111 ml beaten eggs
119 g flour
32 g cocoa powder
0 g cake mixture
Cooking time: 25 minutes.
Pre-heat oven to 180 degrees Celsius.
Method.
Put chocolate chips into the mixing bowl.
Put butter into the mixing bowl.
Put sugar into the mixing bowl.
Put beaten eggs into the mixing bowl.
Put flour into the mixing bowl.
Put baking powder into the mixing bowl.
Put cocoa powder into the mixing bowl.
Stir the mixing bowl for 1 minute.
Combine double cream into the mixing bowl.
Stir the mixing bowl for 4 minutes.
Liquefy the contents of the mixing bowl.
Pour contents of the mixing bowl into the baking dish.
bake the cake mixture.
Wait until bake.
Serve with chocolate sauce.
chocolate sauce.
Ingredients.
111 g sugar
108 ml hot water
108 ml heated double cream
101 g dark chocolate
72 g milk chocolate
Method.
Clean the mixing bowl.
Put sugar into the mixing bowl.
Put hot water into the mixing bowl.
Put heated double cream into the mixing bowl.
dissolve the sugar.
agitate the sugar until dissolve.
Liquefy the dark chocolate.
Put dark chocolate into the mixing bowl.
Liquefy the milk chocolate.
Put milk chocolate into the mixing bowl.
Liquefy contents of the mixing bowl.
Pour contents of the mixing bowl into the baking dish.
Refrigerate for 1 hour.

View File

@ -0,0 +1,20 @@
Hello World Souffle.
==== Source: David Morgan-Mar, https://www.dangermouse.net/esoteric/chef_hello.html ====
This recipe prints the immortal words "Hello world!", in a basically brute force way. It also makes a lot of food for one person.
Ingredients.
72 g haricot beans
101 eggs
108 g lard
111 cups oil
32 zucchinis
119 ml water
114 g red salmon
100 g dijon mustard
33 potatoes
Method.
Put potatoes into the mixing bowl. Put dijon mustard into the mixing bowl. Put lard into the mixing bowl. Put red salmon into the mixing bowl. Put oil into the mixing bowl. Put water into the mixing bowl. Put zucchinis into the mixing bowl. Put oil into the mixing bowl. Put lard into the mixing bowl. Put lard into the mixing bowl. Put eggs into the mixing bowl. Put haricot beans into the mixing bowl. Liquefy contents of the mixing bowl. Pour contents of the mixing bowl into the baking dish.
Serves 1.

215
languages/chef/types.ts Normal file
View File

@ -0,0 +1,215 @@
import { DocumentRange } from "../types";
/** Type alias for renderer state */
export type ChefRS = {
stack: string[];
currentKitchen: {
ingredients: IngredientBox;
bowls: { [k: number]: MixingBowl };
dishes: { [k: number]: BakingDish };
};
};
/********************************
******** UTILITY ALIASES *******
********************************/
/** The name of an ingredient */
export type IngredientName = string;
/** Identifier of a mixing bowl */
export type BowlId = number;
/** Indentifier of a baking dish */
export type DishId = number;
/********************************
****** RUNTIME CONSTRUCTS ******
********************************/
/** Type of an element in a Chef stack */
export type StackItemType = "dry" | "liquid" | "unknown";
/** An element of Chef's stack constructs */
export type StackItem = { value: number; type: StackItemType };
/** Details of an ingredient - kind and value */
export type IngredientItem = {
type: StackItemType;
value?: number;
};
/** Set of ingredients (global variables) in a Chef program */
export type IngredientBox = { [k: IngredientName]: IngredientItem };
/** A mixing bowl (stack construct) in Chef */
export type MixingBowl = StackItem[];
/** A baking dish (stack construct) in Chef */
export type BakingDish = StackItem[];
/********************************
***** PROGRAM INSTRUCTIONS *****
********************************/
/** TAKE: Take numeric input from STDIN and write in ingredient `ing` */
export type StdinOp = { code: "STDIN"; ing: IngredientName };
/** PUT: Push value of ingredient `ing` into `bowlId`'th mixing bowl */
export type PushOp = { code: "PUSH"; ing: IngredientName; bowlId: BowlId };
/** FOLD: Pop value from top of `bowlId`'th mixing bowl and put in ingredient `ing` */
export type PopOp = { code: "POP"; ing: IngredientName; bowlId: BowlId };
/** ADD: Add value of `ing` to top value of bowl `bowlId` and push result onto same bowl */
export type AddOp = { code: "ADD"; ing: IngredientName; bowlId: BowlId };
/** REMOVE: Subtract value of `ing` from top value of bowl `bowlId` and push result onto same bowl */
export type SubtractOp = {
code: "SUBTRACT";
ing: IngredientName;
bowlId: BowlId;
};
/** COMBINE: Multiply value of `ing` with top value of bowl `bowlId` and push result onto same bowl */
export type MultiplyOp = {
code: "MULTIPLY";
ing: IngredientName;
bowlId: BowlId;
};
/** DIVIDE: Divide top value of bowl `bowlId` by value of `ing` and push result onto same bowl */
export type DivideOp = { code: "DIVIDE"; ing: IngredientName; bowlId: BowlId };
/** ADD DRY: Add values of all dry ingredients and push result on bowl `bowlId` */
export type AddDryOp = { code: "ADD-DRY"; bowlId: BowlId };
/** LIQUEFY: Convert ingredient `ing` to a liquid */
export type LiquefyIngOp = { code: "LIQ-ING"; ing: IngredientName };
/** LIQUEFY CONTENTS: Convert each item in bowl `bowlId` to liquid */
export type LiquefyBowlOp = { code: "LIQ-BOWL"; bowlId: BowlId };
/** STIR BOWL: Rotates top `num` items of bowl `bowlId` topwards (top ingredient goes to ~`num` position) */
export type RollStackOp = { code: "ROLL-BOWL"; bowlId: BowlId; num: number };
/** STIR ING: Rotates top [value of `ing`] items of bowl `bowlId` topwards */
export type RollIngOp = {
code: "ROLL-ING";
bowlId: BowlId;
ing: IngredientName;
};
/** MIX: Randomizes the order of items in the bowl `bowlId` */
export type RandomizeOp = { code: "RANDOM"; bowlId: BowlId };
/** CLEAN: Remove all items from the bowl `bowlId` */
export type ClearOp = { code: "CLEAR"; bowlId: BowlId };
/** POUR: Copies all items from `bowlId`'th bowl onto `dishId`'th baking dish, in the same order */
export type CopyToDishOp = { code: "COPY"; bowlId: BowlId; dishId: DishId };
/** VERB: Loop-opener, execute inner steps until `ing` is zero - then continues past loop-closer. */
export type LoopOpenOp = {
code: "LOOP-OPEN";
verb: string;
ing: IngredientName;
/** Index of corresponding loop-closing op in current method */
closer: number;
};
/** VERB: Loop-closer - also decrement value of `ing` by 1 on execution, if provided */
export type LoopCloseOp = {
code: "LOOP-CLOSE";
verb: string;
ing?: IngredientName;
/** Index of corresponding loop-opener op in current method */
opener: number;
};
/** SET ASIDE: Break out of innermost loop and continue past loop-closer */
export type LoopBreakOp = {
code: "LOOP-BREAK";
/** Index of closing op of innermost loop in current method */
closer: number;
};
/** SERVE: Run auxiliary recipe and wait until completion */
export type FnCallOp = { code: "FNCALL"; recipe: string };
/** REFRIGERATE: End recipe execution. If provided, print first `num` baking dishes */
export type EndOp = { code: "END"; num?: number };
/** Four main arithmetic operations in Chef */
export type ChefArithmeticOp = AddOp | SubtractOp | MultiplyOp | DivideOp;
/** Kitchen manipulation operations in Chef */
export type ChefKitchenOp =
| StdinOp
| PushOp
| PopOp
| ChefArithmeticOp
| AddDryOp
| LiquefyIngOp
| RollStackOp
| RollIngOp
| RandomizeOp
| ClearOp
| CopyToDishOp
| LiquefyIngOp
| LiquefyBowlOp;
/** Flow control operations in Chef */
export type ChefFlowControlOp =
| LoopOpenOp
| LoopCloseOp
| LoopBreakOp
| FnCallOp
| EndOp;
/** A single operation of a Chef recipe */
export type ChefOperation = ChefKitchenOp | ChefFlowControlOp;
/** List of codes for flow control operations in Chef */
const flowControlOpTypes: ChefFlowControlOp["code"][] = [
"LOOP-OPEN",
"LOOP-CLOSE",
"LOOP-BREAK",
"FNCALL",
"END",
];
/** Check if a Chef op is a flow control operation */
export const isFlowControlOp = (op: ChefOperation): op is ChefFlowControlOp => {
return flowControlOpTypes.includes(op.code as any);
};
/********************************
******* PROGRAM SEMANTICS ******
********************************/
/** Details about serving of recipe */
export type ChefRecipeServes = {
line: number; // Line number of the "Serves" line
num: number; // Number of servings
};
/** Chef operation with its location in code */
export type ChefOpWithLocation = {
location: DocumentRange;
op: ChefOperation;
};
/** A single Chef recipe */
export type ChefRecipe = {
name: string;
ingredients: IngredientBox;
method: ChefOpWithLocation[];
serves?: ChefRecipeServes;
};
/** A parsed Chef program */
export type ChefProgram = {
main: ChefRecipe;
auxes: { [name: string]: ChefRecipe };
};

View File

@ -0,0 +1,8 @@
# Deadfish
## Allowed symbols
- `i`: Increment value by 1
- `d`: Decrement value by 1
- `s`: Square the value
- `o`: Output the value

View File

@ -0,0 +1,45 @@
import { MonacoTokensProvider } from "../types";
export type DFRS = {
value: number;
};
export enum DF_OP {
INCR = "i",
DECR = "d",
SQ = "s",
OUT = "o",
}
/** A single element of the program's AST */
export type DFAstStep = {
instr: DF_OP;
location: { line: number; char: number };
};
/** Sample program printing "Hello world" */
export const sampleProgram = [
"iisiiiisiiiiiiiio",
"iiiiiiiiiiiiiiiiiiiiiiiiiiiiio",
"iiiiiiioo",
"iiio",
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddo",
"dddddddddddddddddddddsddo",
"ddddddddo",
"iiio",
"ddddddo",
"ddddddddo",
].join("\n");
/** Tokens provider */
export const editorTokensProvider: MonacoTokensProvider = {
tokenizer: {
root: [
[/i/, "orange"],
[/d/, "red"],
[/s/, "blue"],
[/o/, "green"],
],
},
defaultToken: "comment",
};

View File

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

View File

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

View File

@ -0,0 +1,24 @@
import { RendererProps } from "../types";
import { DFRS } from "./constants";
const styles = {
container: {
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
text: {
fontSize: "4em",
},
};
export const Renderer = ({ state }: RendererProps<DFRS>) => {
const value = state == null ? 0 : state.value;
return (
<div style={styles.container}>
<h1 style={styles.text}>{value}</h1>
</div>
);
};

View File

@ -0,0 +1,88 @@
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;
}
validateCode(code: string) {
this.parseCode(code);
}
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, char: cIdx },
});
}
});
});
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,40 @@
import { readTestProgram, executeProgram } from "../../test-utils";
import Engine from "../runtime";
/**
* All test programs are picked up from https://esolangs.org/wiki/Deadfish.
*/
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

20
languages/engine-utils.ts Normal file
View File

@ -0,0 +1,20 @@
/**
* For given ASCII code, returns character that is safe to insert into code.
*
* This is useful for self-modifying programs that may insert non-printable characters into
* the source code at runtime. Characters like `\n`, `\r` and `Tab` distort the grid visually
* in the code editor. This function replaces such characters with safely printable alts. Other
* control characters will be safely rendered by the code editor.
*
* @param asciiVal ASCII value to get safe character for
* @returns Character safe to print without distorting code
*/
export const toSafePrintableChar = (asciiVal: number): string => {
// "\n" -> "⤶"
if (asciiVal === 10) return "\u21b5";
// "\r" -> "␍"
else if (asciiVal === 13) return "\u240d";
// Tab -> "⇆"
else if (asciiVal === 9) return "\u21c6";
else return String.fromCharCode(asciiVal);
};

View File

@ -0,0 +1,204 @@
import { LanguageEngine, StepExecutionResult } from "./types";
import {
isParseError,
isRuntimeError,
serializeParseError,
serializeRuntimeError,
WorkerRuntimeError,
} from "./worker-errors";
type ExecuteAllArgs<RS> = {
/** Interval between two execution steps, in milliseconds */
interval: number;
/**
* Pass to run in streaming-response mode.
* Callback is called with exeuction result on every execution step.
*/
onResult: (result: StepExecutionResult<RS>) => void;
};
class ExecutionController<RS> {
private _engine: LanguageEngine<RS>;
private _breakpoints: number[] = [];
private _result: StepExecutionResult<RS> | null;
private _resolvePause: (() => void) | null = null;
private _isPaused: boolean = false;
/**
* Create a new ExecutionController.
* @param engine Language engine to use for execution
*/
constructor(engine: LanguageEngine<RS>) {
this._engine = engine;
this._engine.resetState();
this._result = null;
}
/**
* Reset execution state in controller and engine.
* Clears out state from the current execution cycle.
*/
resetState() {
this._engine.resetState();
this._result = null;
}
/**
* Load code and user input into the engine to prepare for execution.
* @param code Code content, lines separated by `\n`
* @param input User input, lines separated by '\n'
*/
prepare(code: string, input: string) {
this._engine.prepare(code, input);
}
/**
* Update debugging breakpoints
* @param points Array of line numbers having breakpoints
*/
updateBreakpoints(points: number[]) {
this._breakpoints = points;
}
/**
* Validate the syntax of the given code
* @param code Code content, lines separated by '\n'
*/
validateCode(code: string) {
try {
this._engine.validateCode(code);
} catch (error) {
if (isParseError(error)) return serializeParseError(error);
else throw error;
}
}
/**
* Pause the ongoing execution.
* - If already paused, returns immediately
* - Queues up with any existing pause calls
* @returns Promise that resolves when execution is paused
*/
async pauseExecution(): Promise<void> {
// If already paused, return immediately
if (this._isPaused) return;
// If there's another "pause" call waiting, chain up with older resolver.
// This kinda creates a linked list of resolvers, with latest resolver at head.
if (this._resolvePause) {
console.log("Chaining pause calls");
return new Promise((resolve) => {
// Keep a reference to the existing resolver
const oldResolve = this._resolvePause;
// Replace resolver with new chained resolver
this._resolvePause = () => {
oldResolve && oldResolve();
resolve();
};
});
}
// Else, create a callback to be called by the execution loop
// when it finishes current execution step.
return new Promise(
(resolve) =>
(this._resolvePause = () => {
this._resolvePause = null;
this._isPaused = true;
resolve();
})
);
}
/**
* Run a single step of execution
* @returns Result of execution
*/
executeStep(): {
result: StepExecutionResult<RS>;
error?: WorkerRuntimeError;
} {
try {
this._result = this._engine.executeStep();
this._result.signal = "paused";
return { result: this._result };
} catch (error) {
if (isRuntimeError(error))
return { result: this._result!, error: serializeRuntimeError(error) };
else throw error;
}
}
/**
* Execute the loaded program until stopped. Throws if an error other than RuntimeError is encountered.
* @param param0.interval Interval between two execution steps
* @param param0.onResult Callback called with result on each execution step
* @returns Promise that resolves with result of last execution step and RuntimeError if any
*/
executeAll({ interval, onResult }: ExecuteAllArgs<RS>): Promise<{
result: StepExecutionResult<RS>;
error?: WorkerRuntimeError;
}> {
// Clear paused state
this._isPaused = false;
return new Promise(async (resolve, reject) => {
while (true) {
try {
const doBreak = this.runExecLoopIteration();
onResult(this._result!);
if (doBreak) break;
await this.sleep(interval);
} catch (error) {
if (isRuntimeError(error)) {
this._isPaused = true;
resolve({
result: { ...this._result!, output: undefined },
error: serializeRuntimeError(error),
});
} else reject(error);
break;
}
}
resolve({ result: this._result! });
});
}
/**
* Runs a single iteration of execution loop, and sets
* `this._result` to the execution result.
* @returns Boolean - if true, break execution loop.
*/
private runExecLoopIteration(): boolean {
// Run an execution step in the engine
this._result = this._engine.executeStep();
// Check end of program
if (!this._result.nextStepLocation) {
this._resolvePause && this._resolvePause(); // In case pause happens on same cycle
return true;
}
// Check if execution has been paused
if (this._resolvePause) {
this._result.signal = "paused";
this._resolvePause && this._resolvePause();
return true;
}
// Check if next line has breakpoint
if (this._breakpoints.includes(this._result.nextStepLocation!.line)) {
this._result.signal = "paused";
return true;
}
return false;
}
/** Sleep for `ms` milliseconds */
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
export default ExecutionController;

145
languages/setup-worker.ts Normal file
View File

@ -0,0 +1,145 @@
import ExecutionController from "./execution-controller";
import { LanguageEngine, StepExecutionResult } from "./types";
import * as E from "./worker-errors";
import * as C from "./worker-constants";
/** Create a worker response for acknowledgement */
const ackMessage = <RS, A extends C.WorkerAckType>(
ackType: A,
error?: C.WorkerAckError[A]
): C.WorkerResponseData<RS, A> => ({
type: "ack",
data: ackType,
error,
});
/** Create a worker response for code validation result */
const validationMessage = <RS, A extends C.WorkerAckType>(
error?: E.WorkerParseError
): C.WorkerResponseData<RS, A> => ({ type: "validate", error });
/** Create a worker response for execution result */
const resultMessage = <RS, A extends C.WorkerAckType>(
result: StepExecutionResult<RS>,
error?: E.WorkerRuntimeError
): C.WorkerResponseData<RS, A> => ({
type: "result",
data: result,
error,
});
/** Create a worker response for unexpected errors */
const errorMessage = <RS, A extends C.WorkerAckType>(
error: E.WorkerError
): C.WorkerResponseData<RS, A> => ({ type: "error", error });
/** Initialize the execution controller */
const initController = () => {
postMessage(ackMessage("init"));
};
/**
* Reset the state of the controller and engine, to
* prepare for execution of a new program.
*/
const resetController = <RS>(controller: ExecutionController<RS>) => {
controller.resetState();
postMessage(ackMessage("reset"));
};
/**
* Load program code into the engine.
* @param code Code content of the program
*/
const prepare = <RS>(
controller: ExecutionController<RS>,
{ code, input }: { code: string; input: string }
) => {
try {
controller.prepare(code, input);
postMessage(ackMessage("prepare"));
} catch (error) {
if (E.isParseError(error))
postMessage(ackMessage("prepare", E.serializeParseError(error)));
else throw error;
}
};
/**
* Update debugging breakpoints
* @param points List of line numbers having breakpoints
*/
const updateBreakpoints = <RS>(
controller: ExecutionController<RS>,
points: number[]
) => {
controller.updateBreakpoints(points);
postMessage(ackMessage("bp-update"));
};
/** Validate the user's program syntax */
const validateCode = <RS>(
controller: ExecutionController<RS>,
code: string
) => {
const error = controller.validateCode(code);
postMessage(validationMessage(error));
};
/**
* Execute the entire program loaded on engine,
* and return result of execution.
*/
const execute = async <RS>(
controller: ExecutionController<RS>,
interval: number
) => {
const { result, error } = await controller.executeAll({
interval,
onResult: (res) => postMessage(resultMessage(res)),
});
if (error) postMessage(resultMessage(result, error));
};
/** Trigger pause in program execution */
const pauseExecution = async <RS>(controller: ExecutionController<RS>) => {
await controller.pauseExecution();
postMessage(ackMessage("pause"));
};
/** Run a single execution step */
const executeStep = <RS>(controller: ExecutionController<RS>) => {
const { result, error } = controller.executeStep();
postMessage(resultMessage(result, error));
};
/**
* Create an execution controller worker script with the given engine.
* @param engine Language engine to create worker for
*/
export const setupWorker = <RS>(engine: LanguageEngine<RS>) => {
const controller = new ExecutionController(engine);
addEventListener("message", async (ev: MessageEvent<C.WorkerRequestData>) => {
try {
if (ev.data.type === "Init") return initController();
if (ev.data.type === "Reset") return resetController(controller);
if (ev.data.type === "Prepare")
return prepare(controller, ev.data.params);
if (ev.data.type === "ValidateCode")
return validateCode(controller, ev.data.params.code);
if (ev.data.type === "Execute")
return await execute(controller, ev.data.params.interval);
if (ev.data.type === "Pause") return await pauseExecution(controller);
if (ev.data.type === "ExecuteStep") return executeStep(controller);
if (ev.data.type === "UpdateBreakpoints")
return updateBreakpoints(controller, ev.data.params.points);
} catch (error) {
// Error here indicates an implementation bug
const err = error as Error;
postMessage(errorMessage(E.serializeError(err)));
return;
}
throw new Error("Invalid worker message type");
});
};

49
languages/test-utils.ts Normal file
View File

@ -0,0 +1,49 @@
import { readFileSync } from "fs";
import { resolve } from "path";
import ExecutionController from "./execution-controller";
import { LanguageEngine } from "./types";
/**
* Read the entire contents of a test program.
* @param dirname Absolute path to directory containing program
* @param name Name of TXT file, without extension
* @returns Contents of file, as a \n-delimited string
*/
export const readTestProgram = (dirname: string, name: string): string => {
const path = resolve(dirname, name + ".txt");
return readFileSync(path, { encoding: "utf8" }).toString();
};
/**
* Run code on language engine and return final result.
* If error thrown, promise rejects with the error.
* @param engine Engine to use for execution
* @param code Source code to execute
* @param input STDIN input for execution
* @returns Final execution result object
*/
export const executeProgram = async <T>(
engine: LanguageEngine<T>,
code: string,
input: string = ""
): Promise<{ output: string; rendererState: T }> => {
const controller = new ExecutionController(engine);
controller.prepare(code, input);
return new Promise(async (resolve, reject) => {
try {
let output = "";
const { error } = await controller.executeAll({
interval: 0,
onResult: (res) => {
if (res.output) output += res.output;
if (res.nextStepLocation == null) {
resolve({ output, rendererState: res.rendererState });
}
},
});
if (error) reject(error);
} catch (error) {
reject(error);
}
});
};

88
languages/types.ts Normal file
View File

@ -0,0 +1,88 @@
import monaco from "monaco-editor";
import React from "react";
/**
* Type alias for defining range of characters to highlight in a single line.
* - Missing `start` means highlight starting from start of the line.
* - Missing `end` means highlight ending at the end of the line.
*/
export type CharRange = { start?: number; end?: number };
/** Type denoting a range of text in document spanning within a line */
export type DocumentRange = {
line: number;
charRange?: CharRange;
};
/** Type denoting a document edit */
export type DocumentEdit = {
/** Range to replace with the given text. Keep empty to insert text */
range: DocumentRange;
/** Text to replace the given range with */
text: string;
};
/** Source code token provider for the language, specific to Monaco */
export type MonacoTokensProvider = monaco.languages.IMonarchLanguage;
/** Type alias for props passed to renderer */
export type RendererProps<RS> = { state: RS | null };
/**
* Type alias for the result of engine executing a single step.
*/
export type StepExecutionResult<RS> = {
/** New props to be passed to the renderer */
rendererState: RS;
/** String to write to program output */
output?: string;
/** Self-modifying programs: edit to apply on code */
codeEdits?: DocumentEdit[];
/**
* Used to highlight next line to be executed in the editor.
* Passing `null` indicates reaching the end of program.
*/
nextStepLocation: DocumentRange | null;
/** Signal if execution has been paused/stopped */
signal?: "paused";
};
/**
* Language engine is responsible for providing
* execution and debugging API to the platform.
*/
export interface LanguageEngine<RS> {
/** Validate the syntax of the given code. Throw ParseError if any */
validateCode: (code: string) => void;
/** Load code and user input into the engine and prepare for execution */
prepare: (code: string, input: string) => void;
/** Perform a single step of code execution */
executeStep: () => StepExecutionResult<RS>;
/** Reset all state to prepare for new cycle */
resetState: () => void;
}
/**
* Language provider provides all language-specific
* functionality to the platform.
*/
export interface LanguageProvider<RS> {
/** Monaco-specific tokenizer for syntax highlighting */
editorTokensProvider?: MonacoTokensProvider;
/** Monaco-specific autocomplete provider */
autocompleteProvider?: any;
/** Sample code sample for the language */
sampleProgram: string;
/** React component for visualising runtime state */
Renderer: React.FC<RendererProps<RS>>;
}

View File

@ -0,0 +1,76 @@
import { StepExecutionResult } from "./types";
import * as E from "./worker-errors";
/** Types of requests the worker handles */
export type WorkerRequestData =
| {
type: "Init";
params?: null;
}
| {
type: "Reset";
params?: null;
}
| {
type: "Prepare";
params: { code: string; input: string };
}
| {
type: "UpdateBreakpoints";
params: { points: number[] };
}
| {
type: "ValidateCode";
params: { code: string };
}
| {
type: "Execute";
params: { interval: number };
}
| {
type: "ExecuteStep";
params?: null;
}
| {
type: "Pause";
params?: null;
};
/** Kinds of acknowledgement responses the worker can send */
export type WorkerAckType =
| "init" // on initialization
| "reset" // on state reset
| "bp-update" // on updating breakpoints
| "prepare" // on preparing for execution
| "pause"; // on pausing execution
/** Errors associated with each response ack type */
export type WorkerAckError = {
init: undefined;
reset: undefined;
"bp-update": undefined;
prepare: E.WorkerParseError;
pause: undefined;
};
/** Types of responses the worker can send */
export type WorkerResponseData<RS, A extends WorkerAckType> =
/** Ack for one-off requests, optionally containing error occured (if any) */
| {
type: "ack";
data: A;
error?: WorkerAckError[A];
}
/** Result of code validation, containing parsing error (if any) */
| {
type: "validate";
error?: E.WorkerParseError;
}
/** Response containing step execution result, and runtime error (if any) */
| {
type: "result";
data: StepExecutionResult<RS>;
error?: E.WorkerRuntimeError;
}
/** Response indicating a bug in worker/engine logic */
| { type: "error"; error: E.WorkerError };

View File

@ -0,0 +1,83 @@
import { DocumentRange } from "./types";
/**
* Special error class, to be thrown when encountering a
* syntax error while parsing a program.
*/
export class ParseError extends Error {
/** Location of syntax error in the program */
range: DocumentRange;
/**
* Create an instance of ParseError
* @param message Error message
* @param range Location of syntactically incorrect code
*/
constructor(message: string, range: DocumentRange) {
super(message);
this.name = "ParseError";
this.range = range;
}
}
/**
* Special error class, to be thrown when encountering an error
* at runtime that is indicative of a bug in the user's program.
*/
export class RuntimeError extends Error {
/**
* Create an instance of RuntimeError
* @param message Error message
*/
constructor(message: string) {
super(message);
this.name = "RuntimeError";
}
}
/** Check if an error object is instance of a ParseError */
export const isParseError = (error: any): error is ParseError => {
return error instanceof ParseError || error.name === "ParseError";
};
/** Check if an error object is instance of a RuntimeError */
export const isRuntimeError = (error: any): error is RuntimeError => {
return error instanceof RuntimeError || error.name === "RuntimeError";
};
/** Error sent by worker in case of parsing error */
export type WorkerParseError = {
name: "ParseError";
message: string;
range: DocumentRange;
};
/** Error sent by worker in case error at runtime */
export type WorkerRuntimeError = {
name: "RuntimeError";
message: string;
};
/** Error sent by worker indicating an implementation bug */
export type WorkerError = {
name: string;
message: string;
stack?: string;
};
/** Serialize a RuntimeError instance into a plain object */
export const serializeRuntimeError = (
error: RuntimeError
): WorkerRuntimeError => {
return { name: "RuntimeError", message: error.message };
};
/** Serialize a ParseError instance into a plain object */
export const serializeParseError = (error: ParseError): WorkerParseError => {
return { name: "ParseError", message: error.message, range: error.range };
};
/** Serialize an arbitrary error into a plain object */
export const serializeError = (error: Error): WorkerError => {
return { name: error.name, message: error.message, stack: error.stack };
};