Rename directory "engines" to "languages"
This commit is contained in:
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,
|
||||
};
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user