Adapt bf and chef to error handling
This commit is contained in:
parent
0efd4c79ef
commit
22ee70948a
@ -1,4 +1,5 @@
|
||||
import { DocumentRange, LanguageEngine, StepExecutionResult } from "../types";
|
||||
import { RuntimeError } from "../worker-errors";
|
||||
import { BFAstStep, BFInstruction, BFRS, BF_OP } from "./common";
|
||||
|
||||
// Default values for internal states
|
||||
@ -172,7 +173,7 @@ export default class BrainfuckLanguageEngine implements LanguageEngine<BFRS> {
|
||||
|
||||
/** Move the tape pointer one cell to the left */
|
||||
private decrementPtr(): void {
|
||||
if (this._ptr <= 0) throw new Error("Ptr out of bounds");
|
||||
if (this._ptr <= 0) throw new RuntimeError("Tape pointer out of bounds");
|
||||
this._ptr -= 1;
|
||||
this.getCell(this._ptr); // Init cell if required
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
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);
|
||||
@ -7,6 +8,11 @@ export class SyntaxError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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.",
|
||||
@ -50,7 +56,7 @@ export const editorTokensProvider: MonacoTokensProvider = {
|
||||
[/Method./, "red"],
|
||||
[/mixing bowl/, "green"],
|
||||
[/baking dish/, "blue"],
|
||||
[/\d/, "sepia"],
|
||||
[/\d(st|nd|rd|th)?/, "sepia"],
|
||||
],
|
||||
},
|
||||
defaultToken: "plain",
|
||||
|
@ -66,7 +66,7 @@ const parseArithmeticOp = (line: string): C.ChefArithmeticOp => {
|
||||
)
|
||||
return { code, ing: matches[2], bowlId };
|
||||
|
||||
throw new Error("Malformed instruction");
|
||||
throw new SyntaxError("Malformed instruction");
|
||||
};
|
||||
|
||||
/** Assert that a line matches the given regex and return matches */
|
||||
@ -98,7 +98,7 @@ export const parseIngredientItem = (
|
||||
|
||||
// Parse next word as measurement unit
|
||||
const measure = parseMeasure(words[words.length - 1]);
|
||||
if (hasMeasureType && !measure) throw new Error("Invalid measure");
|
||||
if (hasMeasureType && !measure) throw new SyntaxError("Invalid measure");
|
||||
if (measure) words.pop();
|
||||
|
||||
// Parse rest of word as name of ingredient
|
||||
|
@ -1,8 +1,8 @@
|
||||
import * as T from "../types";
|
||||
import { DocumentRange } from "../../types";
|
||||
import { parseIngredientItem, parseMethodStep } from "./core";
|
||||
import { ParseError, UnexpectedError } from "../../errors";
|
||||
import { SyntaxError } from "../constants";
|
||||
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)
|
||||
@ -106,8 +106,7 @@ const parseIngredientsSection = (stack: CodeStack): T.IngredientBox => {
|
||||
const box: T.IngredientBox = {};
|
||||
while (stack[stack.length - 1].line.trim() !== "") {
|
||||
const { line, row } = popCodeStack(stack);
|
||||
if (line === null) throw new UnexpectedError();
|
||||
const { name, item } = parseIngredientItem(line);
|
||||
const { name, item } = parseIngredientItem(line!);
|
||||
box[name] = item;
|
||||
}
|
||||
return box;
|
||||
@ -117,8 +116,7 @@ const parseIngredientsSection = (stack: CodeStack): T.IngredientBox => {
|
||||
const parseCookingTime = (stack: CodeStack): void => {
|
||||
const regex = /^Cooking time: \d+ (?:hours?|minutes?).$/;
|
||||
const { line, row } = popCodeStack(stack, true);
|
||||
if (!line) throw new UnexpectedError();
|
||||
if (!line.match(regex))
|
||||
if (!line!.match(regex))
|
||||
throw new ParseError("Malformed cooking time statement", { line: row });
|
||||
};
|
||||
|
||||
@ -127,8 +125,7 @@ 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) throw new UnexpectedError();
|
||||
if (!line.match(regex))
|
||||
if (!line!.match(regex))
|
||||
throw new ParseError("Malformed oven setting", { line: row });
|
||||
};
|
||||
|
||||
@ -150,7 +147,7 @@ const parseMethodSection = (stack: CodeStack): T.ChefOpWithLocation[] => {
|
||||
try {
|
||||
processMethodSegment(segment, index, ops, loopStack, pendingBreaks);
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError)
|
||||
if (isSyntaxError(error))
|
||||
throw new ParseError(error.message, segment.location);
|
||||
else throw error;
|
||||
}
|
||||
@ -193,25 +190,24 @@ const processMethodSegment = (
|
||||
|
||||
case "LOOP-CLOSE": {
|
||||
// Validate match with innermost loop
|
||||
const loop = loopStack.pop();
|
||||
if (!loop) throw new Error("Loop-closer found at top-level");
|
||||
const loop = loopStack.pop()!;
|
||||
if (loop.verb !== op.verb)
|
||||
throw new Error(
|
||||
`Loop verb mismatch: expected ${loop.verb}, found ${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 UnexpectedError();
|
||||
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("Something weird occured");
|
||||
throw new Error("Memorized op not a breaker");
|
||||
breaker.closer = index;
|
||||
}
|
||||
|
||||
@ -225,9 +221,7 @@ const serializeMethodOps = (stack: CodeStack): MethodSegment[] => {
|
||||
const segments: MethodSegment[] = [];
|
||||
|
||||
while (stack.length && stack[stack.length - 1].line.trim() !== "") {
|
||||
// Pop next line from code stack
|
||||
const item = stack.pop();
|
||||
if (!item?.line.trim()) throw new UnexpectedError();
|
||||
const item = stack.pop()!;
|
||||
|
||||
// Find all the periods in the line
|
||||
const periodIdxs: number[] = [-1];
|
||||
@ -253,8 +247,7 @@ const serializeMethodOps = (stack: CodeStack): MethodSegment[] => {
|
||||
/** Parse the stack for a "Serves N" statement */
|
||||
const parseServesLine = (stack: CodeStack): T.ChefRecipeServes => {
|
||||
const { line, row } = popCodeStack(stack, true);
|
||||
if (!line) throw new UnexpectedError();
|
||||
const match = line.match(/^Serves (\d+).$/);
|
||||
const match = line!.match(/^Serves (\d+).$/);
|
||||
if (!match) throw new ParseError("Malformed serves statement", { line: row });
|
||||
return { line: row, num: parseInt(match[1], 10) };
|
||||
};
|
||||
@ -318,9 +311,12 @@ const validateRecipe = (
|
||||
for (const line of recipe.method) {
|
||||
const ingName = (line.op as any).ing;
|
||||
if (ingName && !recipe.ingredients[ingName])
|
||||
throw new Error(`Invalid ingredient: ${ingName}`);
|
||||
throw new ParseError(`Invalid ingredient: ${ingName}`, line.location);
|
||||
if (line.op.code === "FNCALL" && !auxes[line.op.recipe])
|
||||
throw new Error(`Invalid recipe name: ${line.op.recipe}`);
|
||||
throw new ParseError(
|
||||
`Invalid recipe name: ${line.op.recipe}`,
|
||||
line.location
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -3,7 +3,6 @@ import {
|
||||
LanguageEngine,
|
||||
StepExecutionResult,
|
||||
} from "../../types";
|
||||
import { UnexpectedError } from "../../errors";
|
||||
import { parseProgram } from "../parser";
|
||||
import * as T from "../types";
|
||||
import InputStream from "./input-stream";
|
||||
@ -62,8 +61,7 @@ export default class ChefLanguageEngine implements LanguageEngine<T.ChefRS> {
|
||||
currFrame.pc += 1;
|
||||
} else if (currFrame.pc === currFrame.recipe.method.length) {
|
||||
// Execution of the "Serves" statement
|
||||
const serves = currFrame.recipe.serves;
|
||||
if (!serves) throw new UnexpectedError();
|
||||
const serves = currFrame.recipe.serves!;
|
||||
output = this.getKitchenOutput(currFrame.kitchen, serves.num);
|
||||
currFrame.pc += 1;
|
||||
} else {
|
||||
@ -157,7 +155,7 @@ export default class ChefLanguageEngine implements LanguageEngine<T.ChefRS> {
|
||||
|
||||
// Check value of loop-opener ingredient
|
||||
const opener = currRecipe.recipe.method[op.opener].op;
|
||||
if (opener.code !== "LOOP-OPEN") throw new UnexpectedError();
|
||||
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;
|
||||
@ -261,7 +259,7 @@ export default class ChefLanguageEngine implements LanguageEngine<T.ChefRS> {
|
||||
|
||||
/** Get topmost frame in call stack. Throws if stack is empty. */
|
||||
private getCurrentFrame(): CallStackItem {
|
||||
if (this._stack.length === 0) throw new UnexpectedError();
|
||||
if (this._stack.length === 0) throw new Error("Call stack is empty");
|
||||
return this._stack[this._stack.length - 1];
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { RuntimeError } from "../../worker-errors";
|
||||
|
||||
/**
|
||||
* A barebones input stream implementation for consuming integers from a string.
|
||||
*/
|
||||
@ -20,10 +22,10 @@ export default class InputStream {
|
||||
getNumber(): number {
|
||||
this.exhaustLeadingWhitespace();
|
||||
// The extra whitespace differentiates whether string is empty or all numbers.
|
||||
if (this._text === "") throw new Error("Unexpected end of input");
|
||||
if (this._text === "") throw new RuntimeError("Unexpected end of input");
|
||||
let posn = this._text.search(/[^0-9]/);
|
||||
if (posn === 0)
|
||||
throw new Error(`Unexpected input character: '${this._text[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);
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
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 };
|
||||
@ -72,9 +73,9 @@ export default class ChefKitchen {
|
||||
|
||||
getIngredient(name: string, assertValue?: boolean): IngredientItem {
|
||||
const item = this._ingredients[name];
|
||||
if (!item) throw new Error(`Ingredient '${name}' does not exist`);
|
||||
if (!item) throw new RuntimeError(`Ingredient '${name}' does not exist`);
|
||||
if (assertValue && item.value == null)
|
||||
throw new Error(`Ingredient '${name}' is undefined`);
|
||||
throw new RuntimeError(`Ingredient '${name}' is undefined`);
|
||||
else return item;
|
||||
}
|
||||
|
||||
@ -113,7 +114,7 @@ export default class ChefKitchen {
|
||||
/** 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 Error(`Bowl ${bowlId} is empty`);
|
||||
if (bowl.length === 0) throw new RuntimeError(`Bowl ${bowlId} is empty`);
|
||||
|
||||
const item = bowl.pop() as StackItem;
|
||||
this.getIngredient(ingredient).type = item.type;
|
||||
@ -126,7 +127,7 @@ export default class ChefKitchen {
|
||||
*/
|
||||
addValue(bowlId: number, ingredient: string): void {
|
||||
const bowl = this.getBowl(bowlId);
|
||||
if (bowl.length === 0) throw new Error(`Bowl ${bowlId} is empty`);
|
||||
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 });
|
||||
@ -138,7 +139,7 @@ export default class ChefKitchen {
|
||||
*/
|
||||
subtractValue(bowlId: number, ingredient: string): void {
|
||||
const bowl = this.getBowl(bowlId);
|
||||
if (bowl.length === 0) throw new Error(`Bowl ${bowlId} is empty`);
|
||||
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 });
|
||||
@ -150,7 +151,7 @@ export default class ChefKitchen {
|
||||
*/
|
||||
multiplyValue(bowlId: number, ingredient: string): void {
|
||||
const bowl = this.getBowl(bowlId);
|
||||
if (bowl.length === 0) throw new Error(`Bowl ${bowlId} is empty`);
|
||||
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 });
|
||||
@ -162,7 +163,7 @@ export default class ChefKitchen {
|
||||
*/
|
||||
divideValue(bowlId: number, ingredient: string): void {
|
||||
const bowl = this.getBowl(bowlId);
|
||||
if (bowl.length === 0) throw new Error(`Bowl ${bowlId} is empty`);
|
||||
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 });
|
||||
@ -173,7 +174,8 @@ export default class ChefKitchen {
|
||||
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 Error(`Ingredient ${name} is undefined`);
|
||||
if (ing.value == null)
|
||||
throw new RuntimeError(`Ingredient ${name} is undefined`);
|
||||
return sum + ing.value;
|
||||
}, 0);
|
||||
this.getBowl(bowlId).push({ type: "dry", value: totalValue });
|
||||
|
@ -7,7 +7,6 @@ const formatParseError = (error: WorkerParseError): string => {
|
||||
const line = error.range.line + 1;
|
||||
const start = error.range.charRange?.start;
|
||||
const end = error.range.charRange?.end;
|
||||
console.log(line, start, end);
|
||||
let cols: string | null = null;
|
||||
if (start != null && end != null) cols = `col ${start + 1}-${end + 1}`;
|
||||
else if (start != null) cols = `col ${start + 1}`;
|
||||
|
Loading…
x
Reference in New Issue
Block a user