Rename directory "engines" to "languages"
This commit is contained in:
31
languages/chef/parser/constants.ts
Normal file
31
languages/chef/parser/constants.ts
Normal 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;
|
240
languages/chef/parser/core.ts
Normal file
240
languages/chef/parser/core.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
};
|
347
languages/chef/parser/index.ts
Normal file
347
languages/chef/parser/index.ts
Normal 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 };
|
||||
};
|
160
languages/chef/parser/regex.ts
Normal file
160
languages/chef/parser/regex.ts
Normal 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]+)$/;
|
Reference in New Issue
Block a user