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