241 lines
9.3 KiB
TypeScript
241 lines
9.3 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
};
|