From 9b09c6fe9d0ff192984a301665a075bde2be87ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Mar=C5=A1=C3=A1lek?= Date: Fri, 22 Mar 2024 10:40:44 +0100 Subject: [PATCH] Errors (#380) * prepare Error classes * use new errors everywhere * update cli * refactor getOutput func * forward Invariant errors as warnings when recoverable * keepDistinctWarningsOnly when all results are errors too --- src/IR/assignments.ts | 6 +- src/IR/collections.ts | 3 +- src/IR/types.ts | 29 +++--- src/cli.ts | 154 ++++++++++++++++--------------- src/common/compile.test.ts | 17 ++-- src/common/compile.ts | 72 +++++++++------ src/common/emit.ts | 24 ----- src/common/errors.ts | 57 ++++++++++-- src/common/expandVariants.ts | 8 +- src/common/getType.test.ts | 4 +- src/common/getType.ts | 69 +++++++++----- src/common/objective.ts | 26 +++++- src/common/symbols.ts | 15 +-- src/frontend/parse.ts | 77 +++++++--------- src/interpreter/index.ts | 46 ++++++--- src/languages/clojure/emit.ts | 13 +-- src/languages/golfscript/emit.ts | 7 +- src/languages/janet/emit.ts | 24 +++-- src/languages/javascript/emit.ts | 13 +-- src/languages/lua/emit.ts | 9 +- src/languages/nim/emit.ts | 12 +-- src/languages/python/emit.ts | 16 ++-- src/languages/swift/emit.ts | 16 +--- src/languages/text/index.ts | 4 +- src/plugins/idents.ts | 10 +- src/plugins/loops.ts | 10 +- src/plugins/types.ts | 7 +- 27 files changed, 422 insertions(+), 326 deletions(-) diff --git a/src/IR/assignments.ts b/src/IR/assignments.ts index 9443c38b..3e7f54bb 100644 --- a/src/IR/assignments.ts +++ b/src/IR/assignments.ts @@ -1,4 +1,4 @@ -import { PolygolfError } from "../common/errors"; +import { UserError } from "../common/errors"; import { type BaseNode, id, @@ -138,9 +138,9 @@ export function varDeclarationWithAssignment( (!isAssignment(assignment) && assignment.variables.some((y) => !isIdent()(y))) ) { - throw new PolygolfError( + throw new UserError( "VarDeclarationWithAssignment needs assignments to variables.", - assignment.source, + assignment, ); } return { diff --git a/src/IR/collections.ts b/src/IR/collections.ts index eb6d6458..e41ed256 100644 --- a/src/IR/collections.ts +++ b/src/IR/collections.ts @@ -1,3 +1,4 @@ +import { InvariantError } from "../common/errors"; import { type BaseNode, type Node, @@ -62,7 +63,7 @@ export function isEqualToLiteral(x: Node, literal: Literal): boolean { isEqualToLiteral(x.value, literal.value) ); } - throw new Error("Unknown literal kind."); + throw new InvariantError(`Unknown literal kind. ${literal.kind}`); } export function array(value: readonly Node[]): Array { diff --git a/src/IR/types.ts b/src/IR/types.ts index 118ebc41..60d4bf7a 100644 --- a/src/IR/types.ts +++ b/src/IR/types.ts @@ -1,3 +1,4 @@ +import { InvariantError, UserError } from "../common/errors"; import { getType } from "../common/getType"; import { type Spine } from "../common/Spine"; import { @@ -358,7 +359,7 @@ export function intersection(a: Type, b: Type): Type { } else if (a.kind === b.kind) { return a; } - throw new Error("Empty intersection."); + throw new UserError("Empty intersection.", undefined); } export function union(a: Type, b: Type): Type { @@ -397,12 +398,16 @@ export function union(a: Type, b: Type): Type { } else if (a.kind === b.kind) { return a; } - throw new Error(`Cannot model union of ${toString(a)} and ${toString(b)}.`); + throw new UserError( + `Cannot model union of ${toString(a)} and ${toString(b)}.`, + undefined, + ); } catch (e) { - throw new Error( + throw new UserError( `Cannot model union of ${toString(a)} and ${toString(b)}.\n${ e instanceof Error ? e.message : "" }`, + undefined, ); } } @@ -461,7 +466,7 @@ export function neg(a: IntegerBound): IntegerBound { export function add(a: IntegerBound, b: IntegerBound): IntegerBound { if (leq(b, a)) [a, b] = [b, a]; if (a === "-oo" && b === "oo") - throw new Error("Indeterminate result of -oo + oo."); + throw new InvariantError("Indeterminate result of -oo + oo."); if (a === "-oo") return a; if (b === "oo") return b; return (a as bigint) + (b as bigint); @@ -472,7 +477,7 @@ export function sub(a: IntegerBound, b: IntegerBound): IntegerBound { export function mul(a: IntegerBound, b: IntegerBound): IntegerBound { if (leq(b, a)) [a, b] = [b, a]; if ((a === "-oo" && b === 0n) || (b === "oo" && a === 0n)) - throw new Error("Indeterminate result of 0 * oo."); + throw new InvariantError("Indeterminate result of 0 * oo."); if (a === "-oo") return lt(b, 0n) ? "oo" : "-oo"; if (b === "oo") return lt(a, 0n) ? "-oo" : "oo"; return (a as bigint) * (b as bigint); @@ -482,9 +487,9 @@ export function floorDiv(a: IntegerBound, b: IntegerBound): IntegerBound { return mul(res, b) !== a && lt(a, 0n) !== lt(b, 0n) ? sub(res, 1n) : res; } export function truncDiv(a: IntegerBound, b: IntegerBound): IntegerBound { - if (b === 0n) throw new Error("Indeterminate result of x / 0."); + if (b === 0n) throw new InvariantError("Indeterminate result of x / 0."); if (!isFiniteBound(a) && !isFiniteBound(b)) - throw new Error("Indeterminate result of +-oo / +-oo."); + throw new InvariantError("Indeterminate result of +-oo / +-oo."); if (!isFiniteBound(a)) { if (lt(a, 0n) === lt(b, 0n)) return "oo"; else return "-oo"; @@ -531,7 +536,7 @@ export function defaultValue(a: Type): Node { if (lt(0n, a.low)) return int(a.low as bigint); return int(0); } - throw new Error(`Unsupported default value for type ${toString(a)}`); + throw new InvariantError(`Unsupported default value for type ${toString(a)}`); } export function instantiateGenerics( @@ -542,7 +547,7 @@ export function instantiateGenerics( case "Array": { const lengthType = instantiate(type.length); if (lengthType.kind !== "TypeArg" && !isArrayIndexType(lengthType)) - throw new Error( + throw new InvariantError( "Array type's second argument must be a constant integer type.", ); return arrayType(instantiate(type.member), lengthType); @@ -555,7 +560,7 @@ export function instantiateGenerics( case "KeyValue": { const keyType = instantiate(type.key); if (keyType.kind !== "integer" && keyType.kind !== "text") - throw new Error( + throw new InvariantError( "KeyValue type's first argument must be an integer or text type.", ); return keyValueType(keyType, instantiate(type.value)); @@ -567,7 +572,7 @@ export function instantiateGenerics( keyType.kind !== "text" && keyType.kind !== "TypeArg" ) - throw new Error( + throw new InvariantError( "Table type's first argument must be an integer or text type.", ); return tableType(keyType, instantiate(type.value)); @@ -626,5 +631,5 @@ export function getLiteralOfType(type: Type, nonEmpty = false): Node { : [], ); } - throw new Error(`There's no literal of type '${type.kind}'.`); + throw new InvariantError(`There's no literal of type '${type.kind}'.`); } diff --git a/src/cli.ts b/src/cli.ts index 133d11f6..dc5585b6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,9 +4,12 @@ import yargs from "yargs"; import fs from "fs"; import path from "path"; import compile, { debugEmit } from "./common/compile"; -import { PolygolfError } from "./common/errors"; +import { + InvariantError, + NotImplementedError, + UserError, +} from "./common/errors"; import languages, { findLang } from "./languages/languages"; -import { EmitError } from "./common/emit"; const languageChoices = [ ...new Set(languages.flatMap((x) => [x.name.toLowerCase(), x.extension])), @@ -49,85 +52,92 @@ const options = yargs() .parseSync(process.argv.slice(2)); if (options.all === true && options.output !== undefined) { - throw new Error( + console.log( "All variants options is only allowed when the output file is not specified.", ); -} +} else { + const langs = + options.lang === undefined ? languages : [findLang(options.lang)!]; + let input = options.input; + if (!fs.existsSync(input)) input += ".polygolf"; + const code = fs.readFileSync(input, { encoding: "utf-8" }); + const printingMultipleLangs = + langs.length > 1 && options.output === undefined; + for (const result of compile( + code, + { + objective: options.chars === true ? "chars" : "bytes", + getAllVariants: options.all === true, + }, + ...langs, + )) { + if (printingMultipleLangs) console.log(result.language); + if (typeof result.result === "string") { + if (options.output !== undefined) { + fs.mkdirSync(path.dirname(options.output), { recursive: true }); + fs.writeFileSync( + options.output + + (langs.length > 1 || !options.output.includes(".") + ? "." + findLang(result.language)!.extension + : ""), + result.result, + ); + } else { + console.log(result.result); + } + if (result.warnings.length > 0) { + console.log("Warnings:"); + console.log(result.warnings.map((x) => x.message).join("\n")); + } + if (options.debug === true) { + console.log("History:"); + console.log( + result.history.map(([c, name]) => `${c} ${name}`).join("\n"), + ); -const langs = - options.lang === undefined ? languages : [findLang(options.lang)!]; -let input = options.input; -if (!fs.existsSync(input)) input += ".polygolf"; -const code = fs.readFileSync(input, { encoding: "utf-8" }); -const printingMultipleLangs = langs.length > 1 && options.output === undefined; -for (const result of compile( - code, - { - objective: options.chars === true ? "chars" : "bytes", - getAllVariants: options.all === true, - }, - ...langs, -)) { - if (printingMultipleLangs) console.log(result.language); - if (typeof result.result === "string") { - if (options.output !== undefined) { - fs.mkdirSync(path.dirname(options.output), { recursive: true }); - fs.writeFileSync( - options.output + - (langs.length > 1 || !options.output.includes(".") - ? "." + findLang(result.language)!.extension - : ""), - result.result, - ); + if (result.errors.length > 0) { + console.log("Errors:"); + console.log( + result.errors + .map( + (e) => + e.message + + (e instanceof NotImplementedError + ? "\n" + debugEmit(e.expr) + : ""), + ) + .join("\n"), + ); + } + } } else { - console.log(result.result); + if (!printingMultipleLangs && langs.length > 1) + console.log(result.language); + handleError(result.result); } - if (result.warnings.length > 0) { - console.log("Warnings:"); - console.log(result.warnings.map((x) => x.message).join("\n")); - } - if (options.debug === true) { - console.log("History:"); - console.log(result.history.map(([c, name]) => `${c} ${name}`).join("\n")); + console.log(""); + } - if (result.errors.length > 0) { - console.log("Errors:"); + function handleError(e: unknown) { + if (e instanceof UserError) { + console.log(e.message); + if (e.source != null) { + const startLine = e.source.line === 0 ? 0 : e.source.line - 2; console.log( - result.errors - .map( - (e) => - e.message + - (e instanceof EmitError ? "\n" + debugEmit(e.expr) : ""), - ) - .join("\n"), + code + .split(/\r?\n/) + .slice(startLine, e.source.line) + .map((x, i) => `${startLine + i + 1}`.padStart(3, " ") + " " + x) + .join("\n") + + "\n" + + " ".repeat(e.source.column + 3) + + "^", ); } + } else if (e instanceof InvariantError) { + console.log(e); + } else { + throw e; } - } else { - if (!printingMultipleLangs && langs.length > 1) - console.log(result.language); - handleError(result.result); - } - console.log(""); -} - -function handleError(e: unknown) { - if (e instanceof PolygolfError) { - console.log(e.message); - if (e.source != null) { - const startLine = e.source.line === 0 ? 0 : e.source.line - 2; - console.log( - code - .split(/\r?\n/) - .slice(startLine, e.source.line) - .map((x, i) => `${startLine + i + 1}`.padStart(3, " ") + " " + x) - .join("\n") + - "\n" + - " ".repeat(e.source.column + 3) + - "^", - ); - } - } else { - throw e; } } diff --git a/src/common/compile.test.ts b/src/common/compile.test.ts index fd9454d9..52ef56a4 100644 --- a/src/common/compile.test.ts +++ b/src/common/compile.test.ts @@ -1,9 +1,8 @@ import { isOp, isText, text } from "@/IR"; import { type Language, type Plugin, search, required } from "./Language"; -import { EmitError } from "./emit"; import compile from "./compile"; import { compilationOptionsFromKeywords } from "@/markdown-tests"; -import { PolygolfError } from "./errors"; +import { NotImplementedError, UserError } from "./errors"; const textLang: Language = { name: "text", @@ -14,18 +13,24 @@ const textLang: Language = { .map((x) => { if (isOp()(x) && x.args.length > 0 && isText()(x.args[0]!)) { if (x.args[0].value.endsWith("X")) { - context.addWarning(new PolygolfError("global warning"), true); context.addWarning( - new PolygolfError("local warning that should not be visible"), + new UserError("global warning", undefined), + true, + ); + context.addWarning( + new UserError( + "local warning that should not be visible", + undefined, + ), false, ); } context.addWarning( - new PolygolfError("local warning that should be visible"), + new UserError("local warning that should be visible", undefined), false, ); return [x.args[0].value]; - } else throw new EmitError(x); + } else throw new NotImplementedError(x); }) .join(""); }, diff --git a/src/common/compile.ts b/src/common/compile.ts index 6464c2e5..6690f9df 100644 --- a/src/common/compile.ts +++ b/src/common/compile.ts @@ -28,9 +28,9 @@ import { shorterBy, } from "./objective"; import { readsFromArgv, readsFromStdin } from "./symbols"; -import { PolygolfError } from "./errors"; +import { InvariantError, UserError } from "./errors"; import { charLength } from "./strings"; -import { getOutput } from "../interpreter"; +import { getOutputOrError } from "../interpreter"; export type OptimisationLevel = "nogolf" | "simple" | "full"; export interface CompilationOptions { @@ -114,7 +114,7 @@ export function applyAllToAllAndGetCounts( function getSingleOrUndefined(x: T | T[] | undefined): T | undefined { if (Array.isArray(x)) { if (x.length > 1) - throw new Error( + throw new InvariantError( `Programming error. Expected at most 1 item, but got ${stringify(x)}.`, ); return x[0]; @@ -173,7 +173,8 @@ function emit( if (language.noEmitter !== undefined) { try { return language.noEmitter.emit(program, context); - } catch { + } catch (e) { + if (e instanceof InvariantError) context.addWarning(e, true); return debugEmit(program); } } else { @@ -222,6 +223,7 @@ export default function compile( if (errorlessVariants.length === 0) { for (const variant of variants) { (variant as CompilationResult).warnings = parsed!.warnings; + keepDistinctWarningsOnly(variant as CompilationResult); } if (options.getAllVariants) { return variants as CompilationResult[]; @@ -241,25 +243,26 @@ export default function compile( const outputs = errorlessVariants.flatMap((x) => { const res = [compileVariant(x, options, language)]; if (!isOp("print[Text]")(x) || !isText()(x.args[0])) { - try { - const output = getOutput(x); - if (output !== "") { - const hardcoded = compileVariant( - op["print[Text]"](text(output)), - options, - language, - ); - if ( - isError(res[0].result) || - (!isError(hardcoded.result) && - options.level !== "nogolf" && - !options.skipPlugins.includes("hardcode") && - obj(hardcoded.result) < obj(res[0].result)) - ) { - res.push(hardcoded); - } + const output = getOutputOrError(x); + + if (typeof output === "string" && output !== "") { + const hardcoded = compileVariant( + op["print[Text]"](text(output)), + options, + language, + ); + if ( + isError(res[0].result) || + (!isError(hardcoded.result) && + options.level !== "nogolf" && + !options.skipPlugins.includes("hardcode") && + obj(hardcoded.result) < obj(res[0].result)) + ) { + res.push(hardcoded); } - } catch {} + } else if (output instanceof InvariantError) { + res[0].warnings.push(output); + } } return res; }); @@ -280,12 +283,9 @@ export default function compile( .get(language.readsFromStdinOnCodeDotGolf === true)! .map((x) => { if (!isOp("print[Text]")(x) || !isText()(x.args[0])) { - try { - const output = getOutput(x); - if (output !== "") { - return op["print[Text]"](text(output)); - } - } catch {} + const output = getOutputOrError(x); + if (typeof output === "string" && output !== "") + return op["print[Text]"](text(output)); } return undefined; }) @@ -309,11 +309,21 @@ export default function compile( for (const res of result) { res.warnings.push(...parsed!.warnings); + keepDistinctWarningsOnly(res); } return result; } +function keepDistinctWarningsOnly(result: CompilationResult) { + const warningMessagesSeen = new Set(); + result.warnings = result.warnings.filter((x) => { + if (warningMessagesSeen.has(x.message)) return false; + warningMessagesSeen.add(x.message); + return true; + }); +} + function getVariantsByInputMethod(variants: Node[]): Map { const variantsWithMethods = variants.map((variant) => { const spine = programToSpine(variant); @@ -324,7 +334,10 @@ function getVariantsByInputMethod(variants: Node[]): Map { }; }); if (variantsWithMethods.some((x) => x.readsFromArgv && x.readsFromStdin)) { - throw new PolygolfError("Program cannot read from both argv and stdin."); + throw new UserError( + "Program cannot read from both argv and stdin.", + undefined, + ); } return new Map( [true, false].map((preferStdin) => { @@ -512,6 +525,7 @@ export function compileVariantNoPacking( queue.enqueue(state); } catch (e) { if (isError(e)) { + if (e instanceof InvariantError) addWarning(e, true); errors.push(e); } } diff --git a/src/common/emit.ts b/src/common/emit.ts index 1403d632..f43cefba 100644 --- a/src/common/emit.ts +++ b/src/common/emit.ts @@ -2,14 +2,9 @@ import { type If, type IR, type Integer, - type Node, type ConditionalOp, isOfKind, - VirtualOpCodes, - argsOf, } from "../IR"; -import { debugEmit } from "./compile"; -import { PolygolfError } from "./errors"; import { $ } from "./fragments"; import type { TokenTree } from "./Language"; import type { Spine } from "./Spine"; @@ -79,25 +74,6 @@ export function containsMultiNode(exprs: readonly IR.Node[]): boolean { return false; } -export class EmitError extends PolygolfError { - expr: Node; - constructor(expr: Node, detail?: string) { - if (detail === undefined && "op" in expr && expr.op !== null) { - detail = [ - expr.op, - ...VirtualOpCodes.filter((x) => argsOf[x](expr) !== undefined), - ].join(", "); - } - detail = detail === undefined ? "" : ` (${detail})`; - const message = - `emit error - ${expr.kind}${detail} not supported.\n` + debugEmit(expr); - super(message, expr.source); - this.name = "EmitError"; - this.expr = expr; - Object.setPrototypeOf(this, EmitError.prototype); - } -} - export function shortest(x: string[]) { return x.reduce((x, y) => (x.length <= y.length ? x : y)); } diff --git a/src/common/errors.ts b/src/common/errors.ts index fb08eb2e..554920f1 100644 --- a/src/common/errors.ts +++ b/src/common/errors.ts @@ -1,11 +1,54 @@ -import { type SourcePointer } from "../IR"; +import { VirtualOpCodes, type Node, type SourcePointer, argsOf } from "../IR"; -export class PolygolfError extends Error { +/** + * This is an unrecoverable error. This should never be thrown unless there's a bug in Polygolf. + */ +export class InvariantError extends Error { + constructor(message: string, cause?: Error) { + super( + "A Polygolf Invariant was broken. This is a bug in Polygolf.\n" + message, + cause === undefined ? undefined : { cause }, + ); + this.name = "InvariantError"; + Object.setPrototypeOf(this, InvariantError.prototype); + } +} + +/** + * This is an error caused by a user of Polygolf. Parse errors, typecheck errors, etc. + */ +export class UserError extends Error { source?: SourcePointer; - constructor(message: string, source?: SourcePointer) { - super(message); - this.source = source; - this.name = "PolygolfError"; - Object.setPrototypeOf(this, PolygolfError.prototype); + constructor( + message: string, + source: SourcePointer | Node | undefined, + cause?: Error, + ) { + super(message, cause === undefined ? undefined : { cause }); + this.source = + source !== undefined && "kind" in source ? source.source : source; + this.name = "UserError"; + Object.setPrototypeOf(this, UserError.prototype); + } +} + +/** + * Particular feature is not implemented, but it might be in future. + */ +export class NotImplementedError extends UserError { + expr: Node; + constructor(expr: Node, detail?: string, cause?: Error) { + if (detail === undefined && "op" in expr && expr.op !== null) { + detail = [ + expr.op, + ...VirtualOpCodes.filter((x) => argsOf[x](expr) !== undefined), + ].join(", "); + } + detail = detail === undefined ? "" : ` (${detail})`; + const message = `emit error - ${expr.kind}${detail} not supported.\n`; + super(message, expr.source, cause); + this.name = "EmitError"; + this.expr = expr; + Object.setPrototypeOf(this, NotImplementedError.prototype); } } diff --git a/src/common/expandVariants.ts b/src/common/expandVariants.ts index 2cdf6fd2..2365a17b 100644 --- a/src/common/expandVariants.ts +++ b/src/common/expandVariants.ts @@ -1,4 +1,5 @@ import { type IR } from "../IR"; +import { InvariantError, UserError } from "./errors"; import { fromChildRemapFunc, getChild, @@ -14,13 +15,16 @@ import { export function expandVariants(program: IR.Node): IR.Node[] { const n = numVariants(program); if (n > 16) - throw new Error(`Variant count ${n} exceeds arbitrary limit. Giving up`); + throw new UserError( + `Variant count ${n} exceeds arbitrary limit. Giving up.`, + program, + ); return allVariantOptions(program).map((x) => structuredClone(x)); } export function getOnlyVariant(program: IR.Node): IR.Node { if (numVariants(program) > 1) { - throw new Error("Program contains multiple variants!"); + throw new InvariantError("Program contains multiple variants!"); } return allVariantOptions(program)[0]; } diff --git a/src/common/getType.test.ts b/src/common/getType.test.ts index 4af9d393..a5f1339e 100644 --- a/src/common/getType.test.ts +++ b/src/common/getType.test.ts @@ -34,7 +34,7 @@ import { lengthToArrayIndexType as length, isPhysicalOpCode, } from "IR"; -import { PolygolfError } from "./errors"; +import { UserError } from "./errors"; import { calcTypeAndResolveOpCode, getType } from "./getType"; const ascii = (x: number | IntegerType = int(0)) => text(x, true); @@ -159,7 +159,7 @@ describe("Assignment", () => { const aLHS = id("a"); const expr = assignment(aLHS, op.add(id("a"), e(int(1)))); expect(() => calcTypeAndResolveOpCode(aLHS, block([expr]))).toThrow( - PolygolfError, + UserError, ); }); }); diff --git a/src/common/getType.ts b/src/common/getType.ts index 91d074dd..09ed5a77 100644 --- a/src/common/getType.ts +++ b/src/common/getType.ts @@ -59,7 +59,7 @@ import { uniqueId, } from "../IR"; import { byteLength, charLength } from "./strings"; -import { PolygolfError } from "./errors"; +import { InvariantError, UserError } from "./errors"; import { type Spine } from "./Spine"; import { getIdentifierType, isIdentifierReadonly } from "./symbols"; import { stringify } from "./stringify"; @@ -79,7 +79,7 @@ export function getTypeAndResolveOpCode( const program = "kind" in context ? context : context.root.node; if (cachedType.has(expr)) return cachedType.get(expr)!; if (currentlyFinding.has(expr)) - throw new PolygolfError(`Node defined in terms of itself`, expr.source); + throw new UserError(`Node defined in terms of itself`, expr); currentlyFinding.add(expr); try { @@ -90,11 +90,13 @@ export function getTypeAndResolveOpCode( return t; } catch (e) { currentlyFinding.delete(expr); - /* - if (e instanceof Error && !(e instanceof PolygolfError)) { - throw new PolygolfError(e.message, expr.source); + if ( + e instanceof Error && + !(e instanceof InvariantError) && + !(e instanceof UserError) + ) { + throw new InvariantError(e.message, e); } - */ throw e; } } @@ -132,9 +134,9 @@ export function calcTypeAndResolveOpCode( isIdent()(expr.variable) && isIdentifierReadonly(expr.variable, program) ) { - throw new PolygolfError( + throw new UserError( `Type error. Cannot assign to readonly identifier ${expr.variable.name}.`, - expr.source, + expr, ); } const a = type(expr.variable); @@ -142,8 +144,9 @@ export function calcTypeAndResolveOpCode( if (isSubtype(b, a)) { return b; } - throw new Error( + throw new UserError( `Type error. Cannot assign ${toString(b)} to ${toString(a)}.`, + expr, ); } case "Op": @@ -151,17 +154,21 @@ export function calcTypeAndResolveOpCode( case "FunctionCall": { const fType = type(expr.func); if (fType.kind !== "Function") { - throw new Error(`Type error. Type ${toString(fType)} is not callable.`); + throw new UserError( + `Type error. Type ${toString(fType)} is not callable.`, + expr, + ); } if (expr.args.every((x, i) => isSubtype(type(x), fType.arguments[i]))) { return fType.result; } - throw new Error( + throw new UserError( `Type error. Function expected [${fType.arguments .map(toString) .join(", ")}] but got [${expr.args .map((x) => toString(type(x))) .join(", ")}].`, + expr, ); } case "Identifier": @@ -193,10 +200,11 @@ export function calcTypeAndResolveOpCode( const k = type(expr.key); const v = type(expr.value); if (k.kind === "integer" || k.kind === "text") return keyValueType(k, v); - throw new Error( + throw new UserError( `Type error. Operator 'key_value' error. Expected [-oo..oo | Text, T1] but got [${toString( k, )}, ${toString(v)}].`, + expr, ); } case "Table": { @@ -212,15 +220,16 @@ export function calcTypeAndResolveOpCode( ) : table(int(), voidType); } - throw new Error( + throw new UserError( "Programming error. Type of KeyValue nodes should always be KeyValue.", + expr, ); } case "ConditionalOp": { const conditionType = type(expr.condition); if (isSubtype(conditionType, booleanType)) return union(type(expr.consequent), type(expr.alternate)); - throw new Error( + throw new UserError( `Type error. Operator '${ expr.isSafe ? "conditional" : "unsafe_conditional" }' error. Expected [Boolean, T1, T1] but got [${toString( @@ -228,6 +237,7 @@ export function calcTypeAndResolveOpCode( )}, ${toString(type(expr.condition))}, ${toString( type(expr.alternate), )}].`, + expr, ); } case "ManyToManyAssignment": @@ -246,7 +256,7 @@ export function calcTypeAndResolveOpCode( return type(op[expr.behavesLike](expr.expr)); } } - throw new Error(`Type error. Unexpected node ${stringify(expr)}.`); + throw new InvariantError(`Type error. Unexpected node ${stringify(expr)}.`); } function getTypeBitNot(t: IntegerType): IntegerType { @@ -595,10 +605,11 @@ function _getOpCodeTypeFromTypes( int(0n, min(t.codepointLength.high, length.high)), t.isAscii, ); - throw new Error( + throw new UserError( `Type error. start index + length must be nonpositive, but got ${toString( startPlusLength, )}.`, + undefined, ); } case "slice[List]": @@ -609,10 +620,11 @@ function _getOpCodeTypeFromTypes( const startPlusLength = getArithmeticType("add", start, length); if (skipAdditionalChecks || isSubtype(startPlusLength, int(-Infinity, 0))) return got[0]; - throw new Error( + throw new UserError( `Type error. start index + length must be nonpositive, but got ${toString( startPlusLength, )}.`, + undefined, ); } case "ord[codepoint]": @@ -666,12 +678,13 @@ function getOpCodeType(expr: Op, program: Node): Type { const got = args.map((x) => getType(x, program)); if (!isTypeMatch(got, opCodeDefinitions[opCode].args)) { - throw new Error( + throw new UserError( `Type error. Operator '${ expr.op }' type error. Expected ${expectedTypesAsStrings(expr.op).join( ", ", )} but got [${got.map(toString).join(", ")}].`, + expr, ); } @@ -690,8 +703,14 @@ function getOpCodeType(expr: Op, program: Node): Type { case "size[Ascii]": return (got[0] as TextType).codepointLength; } - - return _getOpCodeTypeFromTypes(expr.op, got); + try { + return _getOpCodeTypeFromTypes(expr.op, got); + } catch (e) { + if (e instanceof UserError) { + e.source = expr.source; + } + throw e; + } } function resolveUnresolvedOpCode( @@ -721,7 +740,7 @@ function resolveUnresolvedOpCode( ); if (opCode === undefined) { - throw new Error( + throw new UserError( `Type error. Operator '${ expr.op }' type error. Expected ${arityMatchingOpCodes @@ -729,13 +748,15 @@ function resolveUnresolvedOpCode( .join(" or ")} but got [${expr.args .map((x) => toString(getType(x, program))) .join(", ")}].`, + expr, ); } if (expr.op === (".." as any) && legacyDotDot.includes(opCode)) { addWarning( - new PolygolfError( + new UserError( `Deprecated alias .. used. Use ${opCode} or + instead.`, + expr, ), ); } @@ -924,7 +945,7 @@ export function getArithmeticType( return int(); } } - throw new Error(`Type error. Unknown opcode. ${op ?? "null"}`); + throw new InvariantError(`Type error. Unknown opcode. ${op ?? "null"}`); } export function getCollectionTypes(expr: Node, program: Node): Type[] { @@ -939,7 +960,7 @@ export function getCollectionTypes(expr: Node, program: Node): Type[] { case "text": return [text(int(1, 1), exprType.isAscii)]; } - throw new Error("Type error. Node is not a collection."); + throw new InvariantError("Type error. Node is not a collection."); } function getIntegerTypeMod(a: IntegerType, b: IntegerType): IntegerType { diff --git a/src/common/objective.ts b/src/common/objective.ts index 5ff12994..87759966 100644 --- a/src/common/objective.ts +++ b/src/common/objective.ts @@ -1,4 +1,5 @@ import { type CompilationOptions, type CompilationResult } from "./compile"; +import { InvariantError } from "./errors"; import { byteLength, charLength } from "./strings"; export type Objective = "bytes" | "chars"; @@ -14,15 +15,32 @@ function isError(x: any): x is Error { return x instanceof Error; } +function withInvariantErrorWarningsFrom( + a: CompilationResult, + b: CompilationResult, +) { + const res = { + ...a, + warnings: [ + ...a.warnings, + ...b.warnings.filter((x) => x instanceof InvariantError), + ], + }; + if (typeof b.result !== "string" && b.result instanceof InvariantError) { + res.warnings.push(b.result); + } + return res; +} + export function shorterBy( obj: ObjectiveFunc, ): (a: CompilationResult, b: CompilationResult) => CompilationResult { return (a, b) => isError(a.result) - ? b + ? withInvariantErrorWarningsFrom(b, a) : isError(b.result) - ? a + ? withInvariantErrorWarningsFrom(a, b) : obj(a.result) < obj(b.result) - ? a - : b; + ? withInvariantErrorWarningsFrom(a, b) + : withInvariantErrorWarningsFrom(b, a); } diff --git a/src/common/symbols.ts b/src/common/symbols.ts index f745037a..8f1bd47e 100644 --- a/src/common/symbols.ts +++ b/src/common/symbols.ts @@ -10,7 +10,7 @@ import { type Type, toString, } from "../IR"; -import { PolygolfError } from "./errors"; +import { InvariantError, UserError } from "./errors"; import { $, getChildFragments, type PathFragment } from "./fragments"; import { getCollectionTypes, getType } from "./getType"; import { programToSpine, type Spine } from "./Spine"; @@ -20,9 +20,10 @@ class SymbolTable extends Map { getRequired(key: string) { const ret = this.get(key); if (ret === undefined) - throw new Error( + throw new UserError( `Symbol not found: ${key}. ` + `Defined symbols: ${[...this.keys()].join(", ")}`, + undefined, ); return ret; } @@ -54,7 +55,7 @@ export function symbolTableRoot(program: IR.Node): SymbolTable { (name, i) => i > 0 && sortedNames[i - 1] === name, ); if (duplicate !== undefined) - throw new Error(`Duplicate symbol: ${duplicate}`); + throw new InvariantError(`Duplicate symbol: ${duplicate}`); } symbolTableCache.set(program, table); return table; @@ -123,17 +124,17 @@ function getTypeFromBinding(name: string, spine: Spine): Type { node.variable.type !== undefined && !isSubtype(assignedType, node.variable.type) ) - throw new PolygolfError( + throw new UserError( `Value of type ${toString(assignedType)} cannot be assigned to ${ (node.variable as Identifier).name } of type ${toString(node.variable.type)}`, - node.source, + node, ); return node.variable.type ?? assignedType; } default: - throw new Error( - `Programming error: node of type ${node.kind} does not bind any symbol`, + throw new InvariantError( + `Node of type ${node.kind} does not bind any symbol`, ); } } diff --git a/src/frontend/parse.ts b/src/frontend/parse.ts index 263a196b..877ea9dd 100644 --- a/src/frontend/parse.ts +++ b/src/frontend/parse.ts @@ -1,4 +1,4 @@ -import { PolygolfError } from "../common/errors"; +import { InvariantError, UserError } from "../common/errors"; import { type Token } from "moo"; import nearley from "nearley"; import { @@ -108,7 +108,7 @@ export function sexpr( ? userName(alias.opCode) : alias.opCode; warnings.push( - new PolygolfError( + new UserError( `Deprecated alias used: ${callee}. Use ${alias.opCode} ${ alias.opCode === uName ? "" : `or ${uName} ` }${alias.asRhsOfAssignment ? "as RHS of an assignment " : ""}instead.`, @@ -128,27 +128,27 @@ export function sexpr( } function expectArity(low: number, high: number = low) { if (args.length < low || args.length > high) { - throw new PolygolfError( + throw new UserError( `Syntax error. Invalid argument count in application of ${callee}: ` + `Expected ${low}${low === high ? "" : ".." + String(high)} but got ${ args.length }.`, - calleeIdent.source, + calleeIdent, ); } } function assertIdentifier(e: Node): asserts e is Identifier { if (!isIdent()(e)) - throw new PolygolfError( + throw new UserError( `Syntax error. Application first argument must be identifier, but got ${args[0].kind}`, - e.source, + e, ); } function assertInteger(e: Node): asserts e is Integer { if (!isInt()(e)) - throw new PolygolfError( + throw new UserError( `Syntax error. Expected integer literal, but got ${e.kind}`, - e.source, + e, ); } function assertIdentifiers(e: readonly Node[]): asserts e is Identifier[] { @@ -157,17 +157,17 @@ export function sexpr( function assertKeyValues(e: readonly Node[]): asserts e is KeyValue[] { for (const x of e) { if (x.kind !== "KeyValue") - throw new PolygolfError( + throw new UserError( `Syntax error. Application ${callee} requires list of key-value pairs as argument`, - x.source, + x, ); } } function asString(e: Node): string { if (isText()(e)) return e.value; - throw new PolygolfError( + throw new UserError( `Syntax error. Expected string literal, but got ${e.kind}`, - e.source, + e, ); } function asArray(e: Node): readonly Node[] { @@ -176,9 +176,9 @@ export function sexpr( ? e.variants[0].children : [e.variants[0]]; } - throw new PolygolfError( + throw new UserError( `Syntax error. Expected single variant block, but got ${e.kind}`, - e.source, + e, ); } @@ -230,7 +230,7 @@ export function sexpr( colllection = op.range_excl(low, high, step); body = body0; warnings.push( - new PolygolfError( + new UserError( "Deprecated form of `for` used. Iterate over a range using `(low ..< high step)` instead.", calleeIdent.source, ), @@ -241,7 +241,7 @@ export function sexpr( colllection = op.range_excl(low, high, intNode(1n)); body = body0; warnings.push( - new PolygolfError( + new UserError( "Deprecated form of `for` used. Iterate over a range using `(low ..< high)` instead.", calleeIdent.source, ), @@ -376,9 +376,9 @@ export function sexpr( matchingOpCodes.push("concat[List]", "concat[Text]", "append"); } if (matchingOpCodes.length < 1) { - throw new PolygolfError( + throw new UserError( `Syntax error. Unrecognized builtin: ${callee}`, - calleeIdent.source, + calleeIdent, ); } @@ -390,7 +390,7 @@ export function sexpr( const expectedArities = normalizeRangeUnion( matchingOpCodes.map((opCode) => [minArity(opCode), maxArity(opCode)]), ); - throw new PolygolfError( + throw new UserError( `Syntax error. Invalid argument count in application of ${callee}: ` + `Expected ${expectedArities .map( @@ -398,7 +398,7 @@ export function sexpr( `${x}${y === x ? "" : ".." + (y === Infinity ? "oo" : y)}`, ) .join(", ")} but got ${args.length}.`, - calleeIdent.source, + calleeIdent, ); } @@ -428,33 +428,28 @@ export function userIdentifier(token: Token): Identifier { } export function typeSexpr(callee: Token, args: (Type | Integer)[]): Type { + const source = { line: callee.line, column: callee.col }; function expectArity(low: number, high: number = low) { if (args.length < low || args.length > high) { - throw new PolygolfError( + throw new UserError( `Syntax error. Invalid argument count in application of ${callee.value}: ` + `Expected ${low}${low === high ? "" : ".." + String(high)} but got ${ args.length }.`, - { line: callee.line, column: callee.col }, + source, ); } } function assertNumber(e: Type | Integer): asserts e is Integer { if (e.kind !== "Integer") - throw new PolygolfError(`Syntax error. Expected number, got type.`, { - line: callee.line, - column: callee.col, - }); + throw new UserError(`Syntax error. Expected number, got type.`, source); } function assertTypes(e: (Type | Integer)[]): asserts e is Type[] { e.forEach(assertType); } function assertType(e: Type | Integer): asserts e is Type { if (e.kind === "Integer") - throw new PolygolfError(`Syntax error. Expected type, got number.`, { - line: callee.line, - column: callee.col, - }); + throw new UserError(`Syntax error. Expected type, got number.`, source); } switch (callee.value) { case "Void": @@ -472,14 +467,11 @@ export function typeSexpr(callee: Token, args: (Type | Integer)[]): Type { return textType(Number(args[0].value), callee.value === "Ascii"); if (args[0].kind === "integer") return textType(args[0], callee.value === "Ascii"); - throw new PolygolfError( + throw new UserError( `Syntax error. Expected integer or integer type, got ${toString( args[0], )}.`, - { - line: callee.line, - column: callee.col, - }, + source, ); case "Bool": expectArity(0); @@ -499,7 +491,7 @@ export function typeSexpr(callee: Token, args: (Type | Integer)[]): Type { assertType(args[1]); if (args[0].kind === "integer") return tableType(args[0], args[1]); if (args[0].kind === "text") return tableType(args[0], args[1]); - throw new PolygolfError("Unexpected key type for table."); + throw new UserError("Unexpected key type for table.", source); case "Set": expectArity(1); assertType(args[0]); @@ -512,12 +504,9 @@ export function typeSexpr(callee: Token, args: (Type | Integer)[]): Type { args[args.length - 1], ); default: - throw new PolygolfError( + throw new UserError( `Syntax error. Unrecognized type: ${callee.value}`, - { - line: callee.line, - column: callee.col, - }, + source, ); } } @@ -585,7 +574,7 @@ export default function parse( if (expected.length > 0) { message += ` Expected one of ${expected.join(", ")}.`; } - throw new PolygolfError( + throw new UserError( message, token === undefined ? undefined @@ -600,11 +589,11 @@ export default function parse( } const results = parser.results; if (results.length > 1) { - throw new Error("Ambiguous parse of code"); // this is most likely an error in the grammar + throw new InvariantError("Ambiguous parse of code."); // this is most likely an error in the grammar } if (results.length === 0) { const lines = code.split("\n"); - throw new PolygolfError("Unexpected end of code", { + throw new UserError("Unexpected end of code", { line: lines.length + 1, column: (lines.at(-1)?.length ?? 0) + 1, }); diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 9aa2ce25..5194c4b2 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -7,7 +7,7 @@ import { isOfKind, } from "../IR"; import { readsFromInput } from "../common/symbols"; -import { PolygolfError } from "../common/errors"; +import { InvariantError, UserError } from "../common/errors"; import { compileVariant } from "../common/compile"; import javascriptLanguage from "../languages/javascript"; import { required } from "../common/Language"; @@ -36,17 +36,21 @@ const javascriptForInterpreting = { ], }; -const outputCache = new Map(); +const outputCache = new Map(); -export function getOutput(program: Node) { +export function getOutputOrError(program: Node) { if (!outputCache.has(program)) { try { outputCache.set(program, _getOutput(program)); } catch (e) { - outputCache.set(program, e); + outputCache.set(program, e as Error); } } - const res = outputCache.get(program); + return outputCache.get(program)!; +} + +export function getOutputOrThrow(program: Node) { + const res = getOutputOrError(program); if (typeof res === "string") return res; throw res; } @@ -54,7 +58,10 @@ export function getOutput(program: Node) { function _getOutput(program: Node): string { const spine = programToSpine(program); if (spine.someNode(readsFromInput)) - throw new PolygolfError("Program reads from input."); + throw new UserError( + "Program reads from input.", + spine.firstNode(readsFromInput), + ); const jsCode = compileVariant( program, { level: "nogolf" }, @@ -73,14 +80,29 @@ function _getOutput(program: Node): string { const start = Date.now(); function instrument() { if (Date.now() - start > 500) - throw new PolygolfError("Program took too long to interpret."); + throw new UserError("Program took too long to interpret.", undefined); } /* eslint-disable */ - new Function("print", "write", "instrument", jsCode.result)( - print, - write, - instrument, - ); + try { + new Function("print", "write", "instrument", jsCode.result)( + print, + write, + instrument, + ); + } catch (e) { + if (e instanceof Error) { + if (e instanceof SyntaxError) { + throw new InvariantError( + `Error while executing the following javascript:\n${jsCode.result}`, + e, + ); + } + if (!(e instanceof UserError)) { + throw new UserError("Error while executing code.", undefined); + } + } + throw e; + } /* eslint-enable */ return output; } diff --git a/src/languages/clojure/emit.ts b/src/languages/clojure/emit.ts index 9c384241..91f3e2f7 100644 --- a/src/languages/clojure/emit.ts +++ b/src/languages/clojure/emit.ts @@ -1,9 +1,4 @@ -import { - EmitError, - emitIntLiteral, - emitTextFactory, - getIfChain, -} from "../../common/emit"; +import { emitIntLiteral, emitTextFactory, getIfChain } from "../../common/emit"; import { isInt, isForEachRange, @@ -19,6 +14,7 @@ import { import { type Spine } from "../../common/Spine"; import type { CompilationContext } from "../../common/compile"; import { $ } from "../../common/fragments"; +import { NotImplementedError } from "../../common/errors"; const emitClojureText = emitTextFactory({ '"TEXT"': { "\\": `\\\\`, "\r": `\\r`, '"': `\\"` }, @@ -98,7 +94,8 @@ export class ClojureEmitter extends VisitorEmitter { case "FunctionCall": return list($.func, $.args.join()); case "RangeIndexCall": - if (!isInt(1n)(n.step)) throw new EmitError(n, "step not equal one"); + if (!isInt(1n)(n.step)) + throw new NotImplementedError(n, "step not equal one"); return isInt(0n)(n.low) ? list("take", $.high, $.collection) : list("subvec", list("vec", $.collection), $.low, $.high); @@ -118,7 +115,7 @@ export class ClojureEmitter extends VisitorEmitter { case "KeyValue": return [$.key, $.value]; default: - throw new EmitError(n); + throw new NotImplementedError(n); } } } diff --git a/src/languages/golfscript/emit.ts b/src/languages/golfscript/emit.ts index 0300e507..4e301fb7 100644 --- a/src/languages/golfscript/emit.ts +++ b/src/languages/golfscript/emit.ts @@ -3,11 +3,12 @@ import { VisitorEmitter, type EmitterVisitResult, } from "../../common/Language"; -import { EmitError, emitTextFactory } from "../../common/emit"; +import { emitTextFactory } from "../../common/emit"; import { integerType, isInt, isSubtype, isOp, type Node } from "../../IR"; import { getType } from "../../common/getType"; import type { Spine } from "../../common/Spine"; import { $ } from "../../common/fragments"; +import { NotImplementedError } from "../../common/errors"; const emitGolfscriptText = emitTextFactory({ '"TEXT"': { "\\": "\\\\", '"': `\\"` }, @@ -50,7 +51,7 @@ export class GolfscriptEmitter extends VisitorEmitter { isInt()(a.node) && a.node.value < 0n ? [(-a.node.value).toString(), "-"] : [a, "+"]; - } else throw new EmitError(n, "inclusive"); + } else throw new NotImplementedError(n, "inclusive"); } else { collection = $.collection; } @@ -117,7 +118,7 @@ export class GolfscriptEmitter extends VisitorEmitter { isInt(1n)(n.step) ? [] : [$.step, "%"], ]; default: - throw new EmitError(n); + throw new NotImplementedError(n); } } } diff --git a/src/languages/janet/emit.ts b/src/languages/janet/emit.ts index 7218a899..078f11c3 100644 --- a/src/languages/janet/emit.ts +++ b/src/languages/janet/emit.ts @@ -1,9 +1,4 @@ -import { - EmitError, - emitIntLiteral, - emitTextFactory, - getIfChain, -} from "../../common/emit"; +import { emitIntLiteral, emitTextFactory, getIfChain } from "../../common/emit"; import { isInt, isForEachRange, @@ -17,8 +12,9 @@ import { type EmitterVisitResult, } from "../../common/Language"; import { type Spine } from "../../common/Spine"; -import type { CompilationContext } from "../../common/compile"; +import { debugEmit, type CompilationContext } from "../../common/compile"; import { $ } from "../../common/fragments"; +import { InvariantError, NotImplementedError } from "../../common/errors"; const emitJanetText = emitTextFactory({ '"TEXT"': { "\\": `\\\\`, "\n": `\\n`, "\r": `\\r`, '"': `\\"` }, @@ -49,7 +45,9 @@ export class JanetEmitter extends VisitorEmitter { if (n.targetType === "array") { return ["@[;", "$GLUE$", $.expr, "]"]; } - throw new EmitError(n, "unsuported cast target type"); + throw new InvariantError( + `Unsuported cast target type '${n.targetType}'.`, + ); case "Block": { return prop === "consequent" || prop === "alternate" ? list("do", $.children.join()) @@ -95,9 +93,8 @@ export class JanetEmitter extends VisitorEmitter { case "VarDeclarationWithAssignment": { const assignment = n.assignment; if (assignment.kind !== "Assignment") { - throw new EmitError( - n, - `Declaration cannot contain ${assignment.kind}`, + throw new InvariantError( + `Declaration cannot contain ${assignment.kind}. ${debugEmit(n)}`, ); } return $.assignment; @@ -117,7 +114,8 @@ export class JanetEmitter extends VisitorEmitter { case "FunctionCall": return list($.func, $.args.join()); case "RangeIndexCall": - if (!isInt(1n)(n.step)) throw new EmitError(n, "step not equal one"); + if (!isInt(1n)(n.step)) + throw new NotImplementedError(n, "step not equal one"); return isInt(0n)(n.low) ? list("take", $.high, $.collection) : list("slice", $.collection, $.low, $.high); @@ -136,7 +134,7 @@ export class JanetEmitter extends VisitorEmitter { case "KeyValue": return [$.key, $.value]; default: - throw new EmitError(n); + throw new NotImplementedError(n); } } } diff --git a/src/languages/javascript/emit.ts b/src/languages/javascript/emit.ts index b5c6d4e2..851a8d44 100644 --- a/src/languages/javascript/emit.ts +++ b/src/languages/javascript/emit.ts @@ -3,11 +3,12 @@ import { PrecedenceVisitorEmitter, type Token, } from "../../common/Language"; -import { EmitError, emitIntLiteral, emitTextFactory } from "../../common/emit"; +import { emitIntLiteral, emitTextFactory } from "../../common/emit"; import { isText, isOfKind, type Node, uniqueId } from "../../IR"; import { type CompilationContext } from "../../common/compile"; import { $, type PathFragment } from "../../common/fragments"; import type { Spine } from "../../common/Spine"; +import { InvariantError, NotImplementedError } from "../../common/errors"; function escapeRegExp(string: string) { // https://stackoverflow.com/a/6969486/14611638 @@ -81,9 +82,7 @@ function binaryPrecedence(opname: string): number { return 3; } if (opname.endsWith("=")) return 0; - throw new Error( - `Programming error - unknown Javascript binary operator '${opname}.'`, - ); + throw new InvariantError(`Unknown Javascript binary operator '${opname}.'`); } export class JavascriptEmitter extends PrecedenceVisitorEmitter { @@ -150,7 +149,9 @@ export class JavascriptEmitter extends PrecedenceVisitorEmitter { if (n.targetType === "array") { return ["[...", $.expr, "]"]; } - throw new EmitError(n, "unsuported cast target type"); + throw new InvariantError( + `Unsuported cast target type ${n.targetType}.`, + ); case "VarDeclarationWithAssignment": return ["let", $.assignment]; case "Block": @@ -244,7 +245,7 @@ export class JavascriptEmitter extends PrecedenceVisitorEmitter { ]; default: - throw new EmitError(n); + throw new NotImplementedError(n); } } } diff --git a/src/languages/lua/emit.ts b/src/languages/lua/emit.ts index 60737c70..fe764f18 100644 --- a/src/languages/lua/emit.ts +++ b/src/languages/lua/emit.ts @@ -1,5 +1,5 @@ import { type CompilationContext } from "../../common/compile"; -import { EmitError, emitIntLiteral, emitTextFactory } from "../../common/emit"; +import { emitIntLiteral, emitTextFactory } from "../../common/emit"; import { isInt, isOp, type Node } from "../../IR"; import { defaultDetokenizer, @@ -7,6 +7,7 @@ import { } from "../../common/Language"; import type { Spine } from "../../common/Spine"; import { $, type PathFragment } from "../../common/fragments"; +import { InvariantError, NotImplementedError } from "../../common/errors"; const emitLuaText = emitTextFactory( { @@ -57,9 +58,7 @@ function binaryPrecedence(opname: string): number { case "or": return 1; } - throw new Error( - `Programming error - unknown Lua binary operator '${opname}.'`, - ); + throw new InvariantError(`Unknown Lua binary operator '${opname}.'`); } export class LuaEmitter extends PrecedenceVisitorEmitter { @@ -158,6 +157,6 @@ export class LuaEmitter extends PrecedenceVisitorEmitter { case "KeyValue": return [$.key, "=", $.value]; } - throw new EmitError(e); + throw new NotImplementedError(e); } } diff --git a/src/languages/nim/emit.ts b/src/languages/nim/emit.ts index 18cf968f..7d3fc366 100644 --- a/src/languages/nim/emit.ts +++ b/src/languages/nim/emit.ts @@ -5,7 +5,6 @@ import { import { emitTextFactory, joinTrees, - EmitError, emitIntLiteral, getIfChain, } from "../../common/emit"; @@ -13,6 +12,7 @@ import { type Array, isInt, type Node, type Text, type If } from "../../IR"; import { type CompilationContext } from "../../common/compile"; import { type Spine } from "../../common/Spine"; import { $, type PathFragment } from "../../common/fragments"; +import { InvariantError, NotImplementedError } from "../../common/errors"; function escape(x: number, i: number, arr: number[]) { if (x < 100 && (i === arr.length - 1 || arr[i + 1] < 48 || arr[i + 1] > 57)) @@ -78,9 +78,7 @@ function binaryPrecedence(opname: string): number { return 1; } if (opname.endsWith("=")) return 0; - throw new Error( - `Programming error - unknown Nim binary operator '${opname}.'`, - ); + throw new InvariantError(`Unknown Nim binary operator '${opname}.'`); } export class NimEmitter extends PrecedenceVisitorEmitter { @@ -201,7 +199,7 @@ export class NimEmitter extends PrecedenceVisitorEmitter { } case "Variants": case "ForCLike": - throw new EmitError(n); + throw new InvariantError(""); case "Assignment": return [$.variable, "=", $.expr]; case "ManyToManyAssignment": @@ -264,10 +262,10 @@ export class NimEmitter extends PrecedenceVisitorEmitter { case "IndexCall": return [$.collection, "$GLUE$", "[", $.index, "]"]; case "RangeIndexCall": - if (!isInt(1n)(n.step)) throw new EmitError(n, "step"); + if (!isInt(1n)(n.step)) throw new NotImplementedError(n, "step"); return [$.collection, "$GLUE$", "[", $.low, "..<", $.high, "]"]; default: - throw new EmitError(n); + throw new NotImplementedError(n); } } } diff --git a/src/languages/python/emit.ts b/src/languages/python/emit.ts index 1fcdc978..c070d377 100644 --- a/src/languages/python/emit.ts +++ b/src/languages/python/emit.ts @@ -6,7 +6,6 @@ import { } from "../../common/Language"; import { containsMultiNode, - EmitError, emitIntLiteral, emitTextFactory, getIfChain, @@ -16,6 +15,7 @@ import { isInt, isText, id, type Node, type If } from "../../IR"; import { type CompilationContext } from "../../common/compile"; import { $, type PathFragment } from "../../common/fragments"; import { type Spine } from "../../common/Spine"; +import { InvariantError, NotImplementedError } from "../../common/errors"; export const emitPythonText = emitTextFactory( { @@ -64,9 +64,7 @@ function binaryPrecedence(opname: string): number { return 1; } if (opname.endsWith("=")) return 0; - throw new Error( - `Programming error - unknown Python binary operator '${opname}'.`, - ); + throw new InvariantError(`Unknown Python binary operator '${opname}'.`); } function unaryPrecedence(opname: string): number { @@ -77,9 +75,7 @@ function unaryPrecedence(opname: string): number { case "not": return 3; } - throw new Error( - `Programming error - unknown Python unary operator '${opname}.'`, - ); + throw new InvariantError(`Unknown Python unary operator '${opname}.'`); } export class PythonEmitter extends PrecedenceVisitorEmitter { @@ -132,7 +128,9 @@ export class PythonEmitter extends PrecedenceVisitorEmitter { if (n.targetType === "set") { return ["{*", $.expr, "}"]; } - throw new EmitError(n, "unsuported cast target type"); + throw new InvariantError( + `Unsuported cast target type ${n.targetType}.`, + ); case "Block": return $.children.join( spine.isRoot || containsMultiNode(n.children) ? "\n" : ";", @@ -231,7 +229,7 @@ export class PythonEmitter extends PrecedenceVisitorEmitter { ]; } default: - throw new EmitError(n); + throw new NotImplementedError(n); } } } diff --git a/src/languages/swift/emit.ts b/src/languages/swift/emit.ts index 739e5bac..968d17c3 100644 --- a/src/languages/swift/emit.ts +++ b/src/languages/swift/emit.ts @@ -1,14 +1,10 @@ import { PrecedenceVisitorEmitter, type Token } from "../../common/Language"; -import { - EmitError, - emitIntLiteral, - emitTextFactory, - joinTrees, -} from "../../common/emit"; +import { emitIntLiteral, emitTextFactory, joinTrees } from "../../common/emit"; import { type Node } from "../../IR"; import { type CompilationContext } from "../../common/compile"; import { $, type PathFragment } from "../../common/fragments"; import type { Spine } from "../../common/Spine"; +import { InvariantError, NotImplementedError } from "../../common/errors"; const unicode01to09repls = { "\u{1}": `\\u{1}`, @@ -95,9 +91,7 @@ function binaryPrecedence(opname: string): number { return 1; } if (opname.endsWith("=")) return 0; - throw new Error( - `Programming error - unknown Swift binary operator '${opname}.'`, - ); + throw new InvariantError(`Unknown Swift binary operator '${opname}.'`); } function unaryPrecedence(opname: string): number { @@ -164,7 +158,7 @@ export class SwiftEmitter extends PrecedenceVisitorEmitter { ]; case "Variants": case "ForCLike": - throw new EmitError(n); + throw new InvariantError(""); case "Assignment": return [$.variable, "=", $.expr]; case "NamedArg": @@ -209,7 +203,7 @@ export class SwiftEmitter extends PrecedenceVisitorEmitter { return [$.collection, "[", $.low, "..<", $.high, "]"]; default: - throw new EmitError(n); + throw new NotImplementedError(n); } } diff --git a/src/languages/text/index.ts b/src/languages/text/index.ts index ee7310e9..7e36314e 100644 --- a/src/languages/text/index.ts +++ b/src/languages/text/index.ts @@ -1,4 +1,4 @@ -import { getOutput } from "../../interpreter"; +import { getOutputOrThrow } from "../../interpreter"; import type { Language } from "../../common/Language"; const textLanguage: Language = { @@ -7,7 +7,7 @@ const textLanguage: Language = { phases: [], emitter: { emit(x) { - return getOutput(x).trimEnd(); + return getOutputOrThrow(x).trimEnd(); }, }, }; diff --git a/src/plugins/idents.ts b/src/plugins/idents.ts index d327141b..a5cfd112 100644 --- a/src/plugins/idents.ts +++ b/src/plugins/idents.ts @@ -17,7 +17,7 @@ import { toString, } from "../IR"; import { getType } from "../common/getType"; -import { PolygolfError } from "../common/errors"; +import { InvariantError, NotImplementedError } from "../common/errors"; import { $ } from "../common/fragments"; import type { CompilationContext } from "@/common/compile"; @@ -78,8 +78,8 @@ export function renameIdents( if (isUserIdent()(node)) { const outputName = identMap.get(node.name); if (outputName === undefined) { - throw new Error( - `Programming error. Incomplete identMap. Defined: ${JSON.stringify([ + throw new InvariantError( + `Incomplete identMap. Defined: ${JSON.stringify([ ...identMap.keys(), ])}, missing ${JSON.stringify(node.name)}`, ); @@ -170,9 +170,9 @@ export function clone( const type = getType(node, spine); const res = mapping(node, type, spine); if (res === undefined) { - throw new PolygolfError( + throw new NotImplementedError( + node, `Could not clone an identifier of type ${toString(type)}`, - node.source, ); } context.skipChildren(); diff --git a/src/plugins/loops.ts b/src/plugins/loops.ts index 21a154dd..e04fda97 100644 --- a/src/plugins/loops.ts +++ b/src/plugins/loops.ts @@ -31,7 +31,7 @@ import { isText, uniqueId, } from "../IR"; -import { PolygolfError } from "../common/errors"; +import { InvariantError, UserError } from "../common/errors"; import { mapOps } from "./ops"; import { $ } from "../common/fragments"; import { byteLength, charLength } from "../common/strings"; @@ -49,7 +49,7 @@ export function forRangeToWhile(node: Node, spine: Spine) { const low = getType(start, spine); const high = getType(end, spine); if (low.kind !== "integer" || high.kind !== "integer") { - throw new Error(`Unexpected type (${low.kind},${high.kind})`); + throw new InvariantError(`Unexpected type (${low.kind},${high.kind})`); } const increment = assignment(node.variable, op.add(node.variable, step)); return block([ @@ -71,7 +71,7 @@ export function forRangeToForCLike(node: Node, spine: Spine) { const low = getType(start, spine); const high = getType(end, spine); if (low.kind !== "integer" || high.kind !== "integer") { - throw new Error(`Unexpected type (${low.kind},${high.kind})`); + throw new InvariantError(`Unexpected type (${low.kind},${high.kind})`); } const variable = node.variable ?? uniqueId(); return forCLike( @@ -192,7 +192,7 @@ export function assertForArgvTopLevel(node: Node, spine: Spine) { for (const kind of spine.compactMap((x) => x.kind)) { if (kind === "ForArgv") { if (forArgvSeen) - throw new PolygolfError( + throw new UserError( "Only a single for_argv node allowed.", node.source, ); @@ -207,7 +207,7 @@ export function assertForArgvTopLevel(node: Node, spine: Spine) { (spine.parent?.node.kind === "Block" && spine.parent.isRoot) ) ) { - throw new PolygolfError( + throw new UserError( "Node for_argv only allowed at the top level.", node.source, ); diff --git a/src/plugins/types.ts b/src/plugins/types.ts index b7b2f5b0..256c806d 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -1,5 +1,5 @@ import type { PluginVisitor, Spine } from "../common/Spine"; -import { PolygolfError } from "../common/errors"; +import { UserError } from "../common/errors"; import { getType } from "../common/getType"; import { int64Type, @@ -27,7 +27,7 @@ export function assertInt64(node: Node, spine: Spine) { return; // stuff like builtin identifiers etc. throw } if (isSubtype(type, integerType()) && !isSubtype(type, int64Type)) { - throw new PolygolfError( + throw new UserError( `Integer value that doesn't provably fit into a int64 type encountered.`, node.source, ); @@ -64,8 +64,9 @@ function needsBigint( const op = isOp()(parent) ? parent.op : "Assignment"; const res = (allowed as any)[op]; if (res === undefined) { - throw new PolygolfError( + throw new UserError( `Operation that is not supported on bigints encountered. (${op})`, + parent, ); } if (res === "bigint" && node.targetType !== "bigint") {