From b26e2e1f348268bd5b794886309074976f1de11b Mon Sep 17 00:00:00 2001 From: verytactical <186486509+verytactical@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:33:47 +0400 Subject: [PATCH] refactor: typed errors (#1210) * refactor: split parser errors * refactor: improve error messages and update snapshots --- knip.json | 2 + src/error/display-to-json.ts | 67 ++++++++ src/error/display-to-string.ts | 24 +++ src/error/display.ts | 19 +++ src/errors.ts | 45 +----- .../__snapshots__/grammar.spec.ts.snap | 152 +++++++++--------- src/grammar/checkConstAttributes.ts | 32 ---- src/grammar/checkFunctionAttributes.ts | 35 ---- src/grammar/checkVariableName.ts | 11 -- src/grammar/grammar.ts | 99 ++++++++---- src/grammar/parser-error.ts | 73 +++++++++ src/grammar/src-info.ts | 9 ++ src/index.ts | 2 - src/interpreter.ts | 4 +- 14 files changed, 340 insertions(+), 234 deletions(-) create mode 100644 src/error/display-to-json.ts create mode 100644 src/error/display-to-string.ts create mode 100644 src/error/display.ts delete mode 100644 src/grammar/checkConstAttributes.ts delete mode 100644 src/grammar/checkFunctionAttributes.ts delete mode 100644 src/grammar/checkVariableName.ts create mode 100644 src/grammar/parser-error.ts diff --git a/knip.json b/knip.json index b6828108d..99d24972e 100644 --- a/knip.json +++ b/knip.json @@ -5,6 +5,8 @@ "ignore": [ "src/grammar/ast.ts", "src/prettyPrinter.ts", + "src/error/display-to-json.ts", + "src/grammar/src-info.ts", ".github/workflows/tact*.yml" ], "ignoreDependencies": ["@tact-lang/ton-abi"] diff --git a/src/error/display-to-json.ts b/src/error/display-to-json.ts new file mode 100644 index 000000000..6c7e55daa --- /dev/null +++ b/src/error/display-to-json.ts @@ -0,0 +1,67 @@ +/** + * Render error message to JSON for tests + */ + +import { throwInternalCompilerError } from "../errors"; +import { SrcInfo } from "../grammar"; +import { srcInfoEqual } from "../grammar/src-info"; +import { ErrorDisplay } from "./display"; + +export type ErrorJson = ErrorSub | ErrorText | ErrorLink | ErrorAt; + +export type ErrorText = { kind: "text"; text: string }; +export type ErrorSub = { kind: "sub"; parts: string[]; subst: ErrorJson[] }; +export type ErrorLink = { kind: "link"; text: string; loc: SrcInfo }; +export type ErrorAt = { kind: "at"; body: ErrorJson; loc: SrcInfo }; + +export const errorJsonEqual = (left: ErrorJson, right: ErrorJson): boolean => { + switch (left.kind) { + case "link": { + return ( + left.kind === right.kind && + left.text === right.text && + srcInfoEqual(left.loc, right.loc) + ); + } + case "at": { + return ( + left.kind === right.kind && + errorJsonEqual(left.body, right.body) && + srcInfoEqual(left.loc, right.loc) + ); + } + case "text": { + return left.kind === right.kind && left.text === right.text; + } + case "sub": { + if (left.kind !== right.kind) { + return false; + } + if (left.parts.length !== right.parts.length) { + return false; + } + if (left.parts.some((part, index) => part != right.parts[index])) { + return false; + } + if (left.subst.length !== right.subst.length) { + return false; + } + return left.subst.every((leftChild, index) => { + const rightChild = right.subst[index]; + if (typeof rightChild === "undefined") { + throwInternalCompilerError( + "Impossible: by this moment array lengths must match", + ); + } + return errorJsonEqual(leftChild, rightChild); + }); + } + } +}; + +export const displayToJson: ErrorDisplay = { + text: (text) => ({ kind: "text", text }), + sub: (parts, ...subst) => ({ kind: "sub", parts: [...parts], subst }), + link: (text, loc) => ({ kind: "link", text, loc }), + at: (loc, body) => ({ kind: "at", body, loc }), +}; diff --git a/src/error/display-to-string.ts b/src/error/display-to-string.ts new file mode 100644 index 000000000..e93d1a06a --- /dev/null +++ b/src/error/display-to-string.ts @@ -0,0 +1,24 @@ +/** + * Render error message to string for compiler CLI + */ + +import { ErrorDisplay } from "./display"; +import { locationStr } from "../errors"; + +export const displayToString: ErrorDisplay = { + text: (text) => text, + sub: (parts, ...subst) => { + const [head, ...tail] = parts; + if (!head) { + return ""; + } + return tail.reduce((acc, part, index) => { + const sub = subst[index]; + return acc + sub + part; + }, head); + }, + link: (text, _loc) => text, + at: (loc, body) => { + return `${locationStr(loc)}${body}\n${loc.interval.getLineAndColumnMessage()}`; + }, +}; diff --git a/src/error/display.ts b/src/error/display.ts new file mode 100644 index 000000000..6f7cbf473 --- /dev/null +++ b/src/error/display.ts @@ -0,0 +1,19 @@ +/** + * Describes DSL for displaying errors + */ + +import { SrcInfo } from "../grammar"; + +export interface ErrorDisplay { + // Specify main error location + at: (loc: SrcInfo, body: T) => T; + + // Regular string + text: (text: string) => T; + + // Text with substitutions + sub: (text: TemplateStringsArray, ...subst: T[]) => T; + + // Reference some code location + link: (text: string, loc: SrcInfo) => T; +} diff --git a/src/errors.ts b/src/errors.ts index 3bceaeffa..0b2cac041 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,9 +1,7 @@ -import { MatchResult } from "ohm-js"; import path from "path"; import { cwd } from "process"; import { AstFuncId, AstId, AstTypeId } from "./grammar/ast"; -import { ItemOrigin, SrcInfo } from "./grammar"; -import { getSrcInfoFromOhm } from "./grammar/src-info"; +import { SrcInfo } from "./grammar"; export class TactError extends Error { readonly loc?: SrcInfo; @@ -13,19 +11,8 @@ export class TactError extends Error { } } -export class TactParseError extends TactError { - constructor(message: string, loc: SrcInfo) { - super(message, loc); - } -} - -export class TactSyntaxError extends TactError { - constructor(message: string, loc: SrcInfo) { - super(message, loc); - } -} - -/// This will be split at least into two categories: typechecking and codegen errors +// Any regular compilation error shown to user: +// parsing, typechecking, code generation export class TactCompilationError extends TactError { constructor(message: string, loc?: SrcInfo) { super(message, loc); @@ -46,7 +33,7 @@ export class TactConstEvalError extends TactCompilationError { } } -function locationStr(sourceInfo: SrcInfo): string { +export function locationStr(sourceInfo: SrcInfo): string { if (sourceInfo.file) { const loc = sourceInfo.interval.getLineAndColumn() as { lineNum: number; @@ -59,28 +46,6 @@ function locationStr(sourceInfo: SrcInfo): string { } } -export function throwParseError( - matchResult: MatchResult, - path: string, - origin: ItemOrigin, -): never { - const interval = matchResult.getInterval(); - const source = getSrcInfoFromOhm(interval, path, origin); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const message = `Parse error: expected ${(matchResult as any).getExpectedText()}\n`; - throw new TactParseError( - `${locationStr(source)}${message}\n${interval.getLineAndColumnMessage()}`, - source, - ); -} - -export function throwSyntaxError(message: string, source: SrcInfo): never { - throw new TactSyntaxError( - `Syntax error: ${locationStr(source)}${message}\n${source.interval.getLineAndColumnMessage()}`, - source, - ); -} - export function throwCompilationError( message: string, source?: SrcInfo, @@ -128,8 +93,6 @@ export function idTextErr( export type TactErrorCollection = | Error - | TactParseError - | TactSyntaxError | TactCompilationError | TactInternalCompilerError | TactConstEvalError; diff --git a/src/grammar/__snapshots__/grammar.spec.ts.snap b/src/grammar/__snapshots__/grammar.spec.ts.snap index fe2cecf11..7a65aa252 100644 --- a/src/grammar/__snapshots__/grammar.spec.ts.snap +++ b/src/grammar/__snapshots__/grammar.spec.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`grammar should fail abstract-const-without-modifier 1`] = ` -"Syntax error: :2:5: Abstract constant doesn't have abstract modifier +":2:5: Abstract constant doesn't have abstract modifier Line 2, col 5: 1 | trait t { > 2 | const c: Int; @@ -11,7 +11,7 @@ Line 2, col 5: `; exports[`grammar should fail const-abstract-abstract 1`] = ` -"Syntax error: :4:10: Duplicate constant attribute "abstract" +":4:10: Duplicate constant attribute "abstract" Line 4, col 10: 3 | > 4 | abstract abstract const Foo: Int = 42; @@ -21,7 +21,7 @@ Line 4, col 10: `; exports[`grammar should fail const-override 1`] = ` -"Syntax error: :4:1: Module-level constants do not support attributes +":4:1: Module-level constants do not support attributes Line 4, col 1: 3 | > 4 | override const Foo: Int = 42; @@ -31,7 +31,7 @@ Line 4, col 1: `; exports[`grammar should fail const-override-override 1`] = ` -"Syntax error: :4:10: Duplicate constant attribute "override" +":4:10: Duplicate constant attribute "override" Line 4, col 10: 3 | > 4 | override override const Foo: Int = 42; @@ -41,7 +41,7 @@ Line 4, col 10: `; exports[`grammar should fail const-override-virtual 1`] = ` -"Syntax error: :4:1: Module-level constants do not support attributes +":4:1: Module-level constants do not support attributes Line 4, col 1: 3 | > 4 | override virtual const Foo: Int = 42; @@ -51,7 +51,7 @@ Line 4, col 1: `; exports[`grammar should fail const-virtual 1`] = ` -"Syntax error: :4:1: Module-level constants do not support attributes +":4:1: Module-level constants do not support attributes Line 4, col 1: 3 | > 4 | virtual const Foo: Int = 42; @@ -61,7 +61,7 @@ Line 4, col 1: `; exports[`grammar should fail const-virtual-override 1`] = ` -"Syntax error: :4:1: Module-level constants do not support attributes +":4:1: Module-level constants do not support attributes Line 4, col 1: 3 | > 4 | virtual override const Foo: Int = 42; @@ -71,7 +71,7 @@ Line 4, col 1: `; exports[`grammar should fail const-virtual-virtual 1`] = ` -"Syntax error: :4:9: Duplicate constant attribute "virtual" +":4:9: Duplicate constant attribute "virtual" Line 4, col 9: 3 | > 4 | virtual virtual const Foo: Int = 42; @@ -81,7 +81,7 @@ Line 4, col 9: `; exports[`grammar should fail contract-const-abstract 1`] = ` -":5:26: Parse error: expected "=" +":5:26: Expected "=" Line 5, col 26: 4 | contract TestContract { @@ -92,7 +92,7 @@ Line 5, col 26: `; exports[`grammar should fail contract-const-abstract-abstract 1`] = ` -"Syntax error: :5:12: Duplicate constant attribute "abstract" +":5:12: Duplicate constant attribute "abstract" Line 5, col 12: 4 | contract TestContract { > 5 | abstract abstract const Foo: Int = 42; @@ -102,7 +102,7 @@ Line 5, col 12: `; exports[`grammar should fail contract-const-abstract-with-initializer 1`] = ` -"Syntax error: :5:3: Non-abstract constant has abstract modifier +":5:3: Non-abstract constant has abstract modifier Line 5, col 3: 4 | contract TestContract { > 5 | abstract const Foo: Int = 42; @@ -112,7 +112,7 @@ Line 5, col 3: `; exports[`grammar should fail contract-const-override-override 1`] = ` -"Syntax error: :5:12: Duplicate constant attribute "override" +":5:12: Duplicate constant attribute "override" Line 5, col 12: 4 | contract TestContract { > 5 | override override const Foo: Int = 42; @@ -122,7 +122,7 @@ Line 5, col 12: `; exports[`grammar should fail contract-const-virtual-virtual 1`] = ` -"Syntax error: :5:11: Duplicate constant attribute "virtual" +":5:11: Duplicate constant attribute "virtual" Line 5, col 11: 4 | contract TestContract { > 5 | virtual virtual const Foo: Int = 42; @@ -132,7 +132,7 @@ Line 5, col 11: `; exports[`grammar should fail contract-empty-traits-list-with-keyword 1`] = ` -":1:20: Parse error: expected "_", "A".."Z", or "a".."z" +":1:20: Expected "_", "A".."Z", or "a".."z" Line 1, col 20: > 1 | contract Name with {} @@ -142,7 +142,7 @@ Line 1, col 20: `; exports[`grammar should fail contract-getter-parens-no-method-id 1`] = ` -":2:9: Parse error: expected "\\"", "initOf", "null", "_", "A".."Z", "a".."z", "false", "true", "0", "1".."9", "0O", "0o", "0B", "0b", "0X", "0x", "(", "~", "!", "+", or "-" +":2:9: Expected "\\"", "initOf", "null", "_", "A".."Z", "a".."z", "false", "true", "0", "1".."9", "0O", "0o", "0B", "0b", "0X", "0x", "(", "~", "!", "+", or "-" Line 2, col 9: 1 | contract Test { @@ -153,7 +153,7 @@ Line 2, col 9: `; exports[`grammar should fail contract-init-trailing-comma-empty-params 1`] = ` -"Syntax error: :2:10: Empty parameter list should not have a dangling comma. +":2:10: Empty parameter list should not have a dangling comma Line 2, col 10: 1 | contract Name { > 2 | init(,) {} @@ -163,7 +163,7 @@ Line 2, col 10: `; exports[`grammar should fail contract-trailing-comma-empty-traits-list 1`] = ` -":1:19: Parse error: expected "_", "A".."Z", or "a".."z" +":1:19: Expected "_", "A".."Z", or "a".."z" Line 1, col 19: > 1 | contract Name with, {} @@ -173,7 +173,7 @@ Line 1, col 19: `; exports[`grammar should fail contract-with-imports 1`] = ` -":6:1: Parse error: expected end of input, "trait", "contract", "message", "struct", "const", "@name", "asm", "fun", or "primitive" +":6:1: Expected end of input, "trait", "contract", "message", "struct", "const", "@name", "asm", "fun", or "primitive" Line 6, col 1: 5 | // all imports must be at the very top of the file @@ -184,7 +184,7 @@ Line 6, col 1: `; exports[`grammar should fail destructuring-duplicate-source-id 1`] = ` -"Syntax error: :15:19: Duplicate destructuring field: 'a' +":15:19: Duplicate field destructuring: "a" Line 15, col 19: 14 | let s = S{ a: 1, b: 2, c: 3 }; > 15 | let S { a: x, a: y } = s; @@ -194,7 +194,7 @@ Line 15, col 19: `; exports[`grammar should fail expr-fun-call-trailing-comma-no-args 1`] = ` -"Syntax error: :6:14: Empty argument list should not have a dangling comma. +":6:14: Empty parameter list should not have a dangling comma Line 6, col 14: 5 | fun b(): Int { > 6 | return a(,); @@ -204,7 +204,7 @@ Line 6, col 14: `; exports[`grammar should fail expr-method-call-trailing-comma-no-args 1`] = ` -"Syntax error: :2:24: Empty argument list should not have a dangling comma. +":2:24: Empty parameter list should not have a dangling comma Line 2, col 24: 1 | fun another() { > 2 | return 42.toString(,); @@ -214,7 +214,7 @@ Line 2, col 24: `; exports[`grammar should fail funcid-native-fun-arith-operator 1`] = ` -":1:7: Parse error: expected not a funcInvalidId or "\`" +":1:7: Expected not a funcInvalidId or "\`" Line 1, col 7: > 1 | @name(/) @@ -224,7 +224,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-assign-operator 1`] = ` -":1:7: Parse error: expected not a funcInvalidId or "\`" +":1:7: Expected not a funcInvalidId or "\`" Line 1, col 7: > 1 | @name(^>>=) @@ -234,7 +234,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-bitwise-operator 1`] = ` -":1:8: Parse error: expected not (a whiteSpace or "(" or ")" or "[" or "]" or "," or "." or ";" or "~") or "\`" +":1:8: Expected not (a whiteSpace or "(" or ")" or "[" or "]" or "," or "." or ";" or "~") or "\`" Line 1, col 8: > 1 | @name(~) @@ -244,7 +244,7 @@ Line 1, col 8: `; exports[`grammar should fail funcid-native-fun-comma 1`] = ` -":1:19: Parse error: expected ")" +":1:19: Expected ")" Line 1, col 19: > 1 | @name(send_message,then_terminate) @@ -254,7 +254,7 @@ Line 1, col 19: `; exports[`grammar should fail funcid-native-fun-comparison-operator 1`] = ` -":1:7: Parse error: expected not a funcInvalidId or "\`" +":1:7: Expected not a funcInvalidId or "\`" Line 1, col 7: > 1 | @name(<=>) @@ -264,7 +264,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-control-keyword 1`] = ` -":1:7: Parse error: expected not a funcInvalidId or "\`" +":1:7: Expected not a funcInvalidId or "\`" Line 1, col 7: > 1 | @name(elseifnot) @@ -274,7 +274,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-delimiter 1`] = ` -":1:7: Parse error: expected not a funcInvalidId or "\`" +":1:7: Expected not a funcInvalidId or "\`" Line 1, col 7: > 1 | @name([) @@ -284,7 +284,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-directive 1`] = ` -":1:7: Parse error: expected not a funcInvalidId or "\`" +":1:7: Expected not a funcInvalidId or "\`" Line 1, col 7: > 1 | @name(#include) @@ -294,7 +294,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-dot 1`] = ` -":1:10: Parse error: expected ")" +":1:10: Expected ")" Line 1, col 10: > 1 | @name(msg.sender) @@ -304,7 +304,7 @@ Line 1, col 10: `; exports[`grammar should fail funcid-native-fun-keyword 1`] = ` -":1:7: Parse error: expected not a funcInvalidId or "\`" +":1:7: Expected not a funcInvalidId or "\`" Line 1, col 7: > 1 | @name(global) @@ -314,7 +314,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-multiline-comments 1`] = ` -":1:7: Parse error: expected not ("\\"" or "{-") +":1:7: Expected not ("\\"" or "{-") Line 1, col 7: > 1 | @name({-aaa-}) @@ -324,7 +324,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-number 1`] = ` -":1:7: Parse error: expected not a funcInvalidId or "\`" +":1:7: Expected not a funcInvalidId or "\`" Line 1, col 7: > 1 | @name(123) @@ -334,7 +334,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-number-decimal 1`] = ` -":1:7: Parse error: expected not a funcInvalidId or "\`" +":1:7: Expected not a funcInvalidId or "\`" Line 1, col 7: > 1 | @name(0) @@ -344,7 +344,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-number-hexadecimal 1`] = ` -":1:7: Parse error: expected not a funcInvalidId or "\`" +":1:7: Expected not a funcInvalidId or "\`" Line 1, col 7: > 1 | @name(0x0) @@ -354,7 +354,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-number-hexadecimal-2 1`] = ` -":1:7: Parse error: expected not a funcInvalidId or "\`" +":1:7: Expected not a funcInvalidId or "\`" Line 1, col 7: > 1 | @name(0xDEADBEEF) @@ -364,7 +364,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-number-neg-decimal 1`] = ` -":1:7: Parse error: expected not a funcInvalidId or "\`" +":1:7: Expected not a funcInvalidId or "\`" Line 1, col 7: > 1 | @name(-1) @@ -374,7 +374,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-number-neg-hexadecimal 1`] = ` -":1:7: Parse error: expected not a funcInvalidId or "\`" +":1:7: Expected not a funcInvalidId or "\`" Line 1, col 7: > 1 | @name(-0x0) @@ -384,7 +384,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-only-underscore 1`] = ` -":1:7: Parse error: expected not a funcInvalidId or "\`" +":1:7: Expected not a funcInvalidId or "\`" Line 1, col 7: > 1 | @name(_) @@ -394,7 +394,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-parens 1`] = ` -":1:11: Parse error: expected ")" +":1:11: Expected ")" Line 1, col 11: > 1 | @name(take(first)Entry) @@ -404,7 +404,7 @@ Line 1, col 11: `; exports[`grammar should fail funcid-native-fun-semicolons 1`] = ` -":1:9: Parse error: expected ")" +":1:9: Expected ")" Line 1, col 9: > 1 | @name(pa;;in"\`aaa\`") @@ -414,7 +414,7 @@ Line 1, col 9: `; exports[`grammar should fail funcid-native-fun-space 1`] = ` -":1:11: Parse error: expected ")" +":1:11: Expected ")" Line 1, col 11: > 1 | @name(foo foo) @@ -424,7 +424,7 @@ Line 1, col 11: `; exports[`grammar should fail funcid-native-fun-square-brackets 1`] = ` -":1:11: Parse error: expected ")" +":1:11: Expected ")" Line 1, col 11: > 1 | @name(take[some]entry) @@ -434,7 +434,7 @@ Line 1, col 11: `; exports[`grammar should fail funcid-native-fun-string 1`] = ` -":1:7: Parse error: expected not ("\\"" or "{-") +":1:7: Expected not ("\\"" or "{-") Line 1, col 7: > 1 | @name("not_a_string) @@ -444,7 +444,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-type-keyword 1`] = ` -":1:7: Parse error: expected not a funcInvalidId or "\`" +":1:7: Expected not a funcInvalidId or "\`" Line 1, col 7: > 1 | @name(->) @@ -454,7 +454,7 @@ Line 1, col 7: `; exports[`grammar should fail funcid-native-fun-unclosed-parens 1`] = ` -":1:9: Parse error: expected ")" +":1:9: Expected ")" Line 1, col 9: > 1 | @name(aa(bb) @@ -464,7 +464,7 @@ Line 1, col 9: `; exports[`grammar should fail ident-cannot-be-if-reserved-word 1`] = ` -":2:9: Parse error: expected "A".."Z" or not a reservedWord +":2:9: Expected "A".."Z" or not a reservedWord Line 2, col 9: 1 | fun hello(): Int { @@ -542,7 +542,7 @@ Line 2, col 9: `; exports[`grammar should fail ident-struct-cannot-start-with-__gen 1`] = ` -":1:8: Parse error: expected "A".."Z" +":1:8: Expected "A".."Z" Line 1, col 8: > 1 | struct __genA { @@ -552,7 +552,7 @@ Line 1, col 8: `; exports[`grammar should fail item-fun-non-void-trailing-comma-no-params 1`] = ` -"Syntax error: :1:14: Empty parameter list should not have a dangling comma. +":1:14: Empty parameter list should not have a dangling comma Line 1, col 14: > 1 | fun function(,) : Int { ^ @@ -561,7 +561,7 @@ Line 1, col 14: `; exports[`grammar should fail item-fun-void-trailing-comma-no-params 1`] = ` -"Syntax error: :1:14: Empty parameter list should not have a dangling comma. +":1:14: Empty parameter list should not have a dangling comma Line 1, col 14: > 1 | fun function(,) {} ^ @@ -570,7 +570,7 @@ Line 1, col 14: `; exports[`grammar should fail item-fun-without-body 1`] = ` -":1:20: Parse error: expected "{" +":1:20: Expected "{" Line 1, col 20: > 1 | fun testFunc(): Int; @@ -579,7 +579,7 @@ Line 1, col 20: `; exports[`grammar should fail item-native-fun-not-void-decl-trailing-comma-no-params 1`] = ` -"Syntax error: :2:31: Empty parameter list should not have a dangling comma. +":2:31: Empty parameter list should not have a dangling comma Line 2, col 31: 1 | @name(native_name_2) > 2 | native testNativeFuncWithType(,): Int; @@ -588,7 +588,7 @@ Line 2, col 31: `; exports[`grammar should fail item-native-fun-void-decl-trailing-comma-no-params 1`] = ` -"Syntax error: :2:23: Empty parameter list should not have a dangling comma. +":2:23: Empty parameter list should not have a dangling comma Line 2, col 23: 1 | @name(native_name_1) > 2 | native testNativeFunc(,); @@ -597,7 +597,7 @@ Line 2, col 23: `; exports[`grammar should fail items-asm-fun-1 1`] = ` -":1:5: Parse error: expected ")" +":1:5: Expected ")" Line 1, col 5: > 1 | asm(1 0) extends fun loadCoins(self: Slice): Int { @@ -607,7 +607,7 @@ Line 1, col 5: `; exports[`grammar should fail items-asm-fun-2 1`] = ` -":1:9: Parse error: expected ")", "->", "_", "A".."Z", or "a".."z" +":1:9: Expected ")", "->", "_", "A".."Z", or "a".."z" Line 1, col 9: > 1 | asm(c b 42) extends fun storeDict(b: Builder, c: Cell) { @@ -617,7 +617,7 @@ Line 1, col 9: `; exports[`grammar should fail items-asm-fun-3 1`] = ` -":1:14: Parse error: expected "0" or "1".."9" +":1:14: Expected "0" or "1".."9" Line 1, col 14: > 1 | asm(s len -> len 1 0) extends fun loadInt(self: Slice, len: Int): Int { @@ -627,7 +627,7 @@ Line 1, col 14: `; exports[`grammar should fail items-asm-fun-4 1`] = ` -":1:7: Parse error: expected "0" or "1".."9" +":1:7: Expected "0" or "1".."9" Line 1, col 7: > 1 | asm(->) extends fun loadInt(self: Slice, len: Int): Int { @@ -637,7 +637,7 @@ Line 1, col 7: `; exports[`grammar should fail items-asm-fun-5 1`] = ` -":3:5: Parse error: expected end of input, "trait", "contract", "message", "struct", "const", "@name", "asm", "fun", or "primitive" +":3:5: Expected end of input, "trait", "contract", "message", "struct", "const", "@name", "asm", "fun", or "primitive" Line 3, col 5: 2 | { INC } : } @@ -648,7 +648,7 @@ Line 3, col 5: `; exports[`grammar should fail items-asm-fun-6 1`] = ` -"Syntax error: :2:5: The binary bitstring has more than 128 digits +":2:5: Bitstring has more than 128 digits Line 2, col 5: 1 | asm fun giganticBinary() { > 2 | b{000000001111111100000000111111110000000011111111000000001111111100000000111111110000000011111111000000001111111100000000111111110} @@ -658,7 +658,7 @@ Line 2, col 5: `; exports[`grammar should fail literal-dec-trailing-underscore 1`] = ` -":2:16: Parse error: expected a digit +":2:16: Expected a digit Line 2, col 16: 1 | fun test_fun(): Int { @@ -669,7 +669,7 @@ Line 2, col 16: `; exports[`grammar should fail literal-double-underscore 1`] = ` -":2:20: Parse error: expected a digit +":2:20: Expected a digit Line 2, col 20: 1 | fun test_fun(): Int { @@ -680,7 +680,7 @@ Line 2, col 20: `; exports[`grammar should fail literal-hex-trailing-underscore 1`] = ` -":2:18: Parse error: expected a hexadecimal digit +":2:18: Expected a hexadecimal digit Line 2, col 18: 1 | fun test_fun(): Int { @@ -691,7 +691,7 @@ Line 2, col 18: `; exports[`grammar should fail literal-no-underscore-after-0b 1`] = ` -":2:14: Parse error: expected "1" or "0" +":2:14: Expected "1" or "0" Line 2, col 14: 1 | fun test_fun(): Int { @@ -702,7 +702,7 @@ Line 2, col 14: `; exports[`grammar should fail literal-no-underscores-if-leading-zero 1`] = ` -":2:15: Parse error: expected "}" or ";" +":2:15: Expected "}" or ";" Line 2, col 15: 1 | fun test_fun(): Int { @@ -713,7 +713,7 @@ Line 2, col 15: `; exports[`grammar should fail literal-non-binary-digits 1`] = ` -":2:15: Parse error: expected "}" or ";" +":2:15: Expected "}" or ";" Line 2, col 15: 1 | fun test_fun(): Int { @@ -724,7 +724,7 @@ Line 2, col 15: `; exports[`grammar should fail literal-underscore-after-leading-zero 1`] = ` -":2:13: Parse error: expected "}" or ";" +":2:13: Expected "}" or ";" Line 2, col 13: 1 | fun test_fun(): Int { @@ -735,7 +735,7 @@ Line 2, col 13: `; exports[`grammar should fail struct-double-semicolon 1`] = ` -":2:19: Parse error: expected "}" +":2:19: Expected "}" Line 2, col 19: 1 | // too many semicolons @@ -746,7 +746,7 @@ Line 2, col 19: `; exports[`grammar should fail struct-missing-semicolon-between-fields 1`] = ` -":2:19: Parse error: expected "}", ";", "=", "as", or "?" +":2:19: Expected "}", ";", "=", "as", or "?" Line 2, col 19: 1 | // missing ; between fields @@ -757,7 +757,7 @@ Line 2, col 19: `; exports[`grammar should fail struct-missing-semicolon-between-fields-with-initializer 1`] = ` -":2:24: Parse error: expected "}", ";", ".", "!!", "%", "/", "*", ">>", "<<", "-", "+", "==", "!=", "<=", "<", ">=", ">", "^", "&", "&&", "|", "?", or "||" +":2:24: Expected "}", ";", ".", "!!", "%", "/", "*", ">>", "<<", "-", "+", "==", "!=", "<=", "<", ">=", ">", "^", "&", "&&", "|", "?", or "||" Line 2, col 24: 1 | // missing ; between fields @@ -768,7 +768,7 @@ Line 2, col 24: `; exports[`grammar should fail trait-const-abstract-with-initializer 1`] = ` -"Syntax error: :5:3: Non-abstract constant has abstract modifier +":5:3: Non-abstract constant has abstract modifier Line 5, col 3: 4 | trait TestContract { > 5 | abstract const Foo: Int = 42; @@ -778,7 +778,7 @@ Line 5, col 3: `; exports[`grammar should fail trait-empty-traits-list-with-keyword 1`] = ` -":1:17: Parse error: expected "_", "A".."Z", or "a".."z" +":1:17: Expected "_", "A".."Z", or "a".."z" Line 1, col 17: > 1 | trait Name with {} @@ -788,7 +788,7 @@ Line 1, col 17: `; exports[`grammar should fail trait-fun-non-void-trailing-comma-no-params 1`] = ` -"Syntax error: :2:39: Empty parameter list should not have a dangling comma. +":2:39: Empty parameter list should not have a dangling comma Line 2, col 39: 1 | trait Test { > 2 | abstract fun testAbstractWithType(,): Int; @@ -798,7 +798,7 @@ Line 2, col 39: `; exports[`grammar should fail trait-fun-void-trailing-comma-no-params 1`] = ` -"Syntax error: :2:31: Empty parameter list should not have a dangling comma. +":2:31: Empty parameter list should not have a dangling comma Line 2, col 31: 1 | trait Test { > 2 | abstract fun testAbstract(,); @@ -808,7 +808,7 @@ Line 2, col 31: `; exports[`grammar should fail trait-trailing-comma-empty-traits-list 1`] = ` -":1:16: Parse error: expected "_", "A".."Z", or "a".."z" +":1:16: Expected "_", "A".."Z", or "a".."z" Line 1, col 16: > 1 | trait Name with, {} @@ -818,7 +818,7 @@ Line 1, col 16: `; exports[`grammar should fail type-ident-msg-should-be-capitalized 1`] = ` -":1:14: Parse error: expected "A".."Z" +":1:14: Expected "A".."Z" Line 1, col 14: > 1 | message(123) foo { @@ -828,7 +828,7 @@ Line 1, col 14: `; exports[`grammar should fail type-ident-struct-should-be-capitalized 1`] = ` -":1:8: Parse error: expected "A".."Z" +":1:8: Expected "A".."Z" Line 1, col 8: > 1 | struct lowercaseIdForType { diff --git a/src/grammar/checkConstAttributes.ts b/src/grammar/checkConstAttributes.ts deleted file mode 100644 index b7fdee28b..000000000 --- a/src/grammar/checkConstAttributes.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { AstConstantAttribute } from "./ast"; -import { throwSyntaxError } from "../errors"; -import { SrcInfo } from "./src-info"; - -export function checkConstAttributes( - isAbstract: boolean, - attributes: AstConstantAttribute[], - loc: SrcInfo, -) { - const k: Set = new Set(); - for (const a of attributes) { - if (k.has(a.type)) { - throwSyntaxError(`Duplicate constant attribute "${a.type}"`, a.loc); - } - k.add(a.type); - } - if (isAbstract) { - if (!k.has("abstract")) { - throwSyntaxError( - `Abstract constant doesn't have abstract modifier`, - loc, - ); - } - } else { - if (k.has("abstract")) { - throwSyntaxError( - `Non-abstract constant has abstract modifier`, - loc, - ); - } - } -} diff --git a/src/grammar/checkFunctionAttributes.ts b/src/grammar/checkFunctionAttributes.ts deleted file mode 100644 index 3b163fdfa..000000000 --- a/src/grammar/checkFunctionAttributes.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { AstFunctionAttribute } from "./ast"; -import { throwCompilationError } from "../errors"; -import { SrcInfo } from "./src-info"; - -export function checkFunctionAttributes( - isAbstract: boolean, - attrs: AstFunctionAttribute[], - loc: SrcInfo, -) { - const k: Set = new Set(); - for (const a of attrs) { - if (k.has(a.type)) { - throwCompilationError( - `Duplicate function attribute "${a.type}"`, - a.loc, - ); - } - k.add(a.type); - } - if (isAbstract) { - if (!k.has("abstract")) { - throwCompilationError( - `Abstract function doesn't have abstract modifier`, - loc, - ); - } - } else { - if (k.has("abstract")) { - throwCompilationError( - `Non abstract function have abstract modifier`, - loc, - ); - } - } -} diff --git a/src/grammar/checkVariableName.ts b/src/grammar/checkVariableName.ts deleted file mode 100644 index faef19733..000000000 --- a/src/grammar/checkVariableName.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { throwCompilationError } from "../errors"; -import { SrcInfo } from "./src-info"; - -export function checkVariableName(name: string, loc: SrcInfo) { - if (name.startsWith("__gen")) { - throwCompilationError(`Variable name cannot start with "__gen"`, loc); - } - if (name.startsWith("__tact")) { - throwCompilationError(`Variable name cannot start with "__tact"`, loc); - } -} diff --git a/src/grammar/grammar.ts b/src/grammar/grammar.ts index 09acefac8..29c65dfe4 100644 --- a/src/grammar/grammar.ts +++ b/src/grammar/grammar.ts @@ -18,22 +18,22 @@ import { AstId, FactoryAst, } from "./ast"; -import { throwParseError, throwSyntaxError } from "../errors"; -import { checkVariableName } from "./checkVariableName"; -import { checkFunctionAttributes } from "./checkFunctionAttributes"; -import { checkConstAttributes } from "./checkConstAttributes"; import { getSrcInfoFromOhm, ItemOrigin, SrcInfo } from "./src-info"; +import { parserErrorSchema, ParserErrors } from "./parser-error"; +import { displayToString } from "../error/display-to-string"; type Context = { origin: ItemOrigin | null; currentFile: string | null; createNode: FactoryAst["createNode"] | null; + errorTypes: ParserErrors | null; }; const defaultContext: Context = Object.freeze({ createNode: null, currentFile: null, origin: null, + errorTypes: null, }); let context: Context = defaultContext; @@ -63,6 +63,14 @@ const createNode: FactoryAst["createNode"] = (...args) => { return context.createNode(...args); }; +const err = () => { + if (context.errorTypes === null) { + throwInternalCompilerError("Parser context was not initialized"); + } + + return context.errorTypes; +}; + // helper to unwrap optional grammar elements (marked with "?") // ohm-js represents those essentially as lists (IterationNodes) function unwrapOptNode( @@ -73,6 +81,42 @@ function unwrapOptNode( return optNode !== undefined ? f(optNode) : null; } +function checkVariableName(name: string, loc: SrcInfo) { + if (name.startsWith("__gen")) { + err().reservedVarPrefix("__gen")(loc); + } + if (name.startsWith("__tact")) { + err().reservedVarPrefix("__tact")(loc); + } +} + +const checkAttributes = + (kind: "constant" | "function") => + ( + isAbstract: boolean, + attributes: (AstConstantAttribute | AstFunctionAttribute)[], + loc: SrcInfo, + ) => { + const { duplicate, tooAbstract, notAbstract } = err()[kind]; + const k: Set = new Set(); + for (const a of attributes) { + if (k.has(a.type)) { + duplicate(a.type)(a.loc); + } + k.add(a.type); + } + if (isAbstract && !k.has("abstract")) { + notAbstract()(loc); + } + if (!isAbstract && k.has("abstract")) { + tooAbstract()(loc); + } + }; + +const checkConstAttributes = checkAttributes("constant"); + +const checkFunctionAttributes = checkAttributes("function"); + const semantics = tactGrammar.createSemantics(); semantics.addOperation("astOfModule", { @@ -89,10 +133,7 @@ semantics.addOperation("astOfImport", { Import(_importKwd, path, _semicolon) { const pathAST = path.astOfExpression() as AstString; if (pathAST.value.includes("\\")) { - throwSyntaxError( - 'Import path can\'t contain "\\"', - createRef(path), - ); + err().importWithBackslash()(createRef(path)); } return createNode({ kind: "import", @@ -225,8 +266,7 @@ semantics.addOperation("astOfModuleItem", { ModuleConstant(constant) { const astConstDef: AstConstantDef = constant.astOfItem(); if (astConstDef.attributes.length !== 0) { - throwSyntaxError( - `Module-level constants do not support attributes`, + err().topLevelConstantWithAttribute()( astConstDef.attributes[0]!.loc, ); } @@ -540,20 +580,14 @@ semantics.addOperation("astOfAsmInstruction", { const length = digits.numChildren; const underscore = unwrapOptNode(optUnderscore, (t) => t.sourceString); if (length > 128) { - throwSyntaxError( - "The hex bitstring has more than 128 digits", - createRef(this), - ); + err().literalTooLong()(createRef(this)); } return `${prefix.sourceString}${digits.sourceString}${underscore ?? ""}}`; }, AsmInstruction_binLiteral(_prefix, digits, _rbrace, _ws) { const length = digits.numChildren; if (length > 128) { - throwSyntaxError( - "The binary bitstring has more than 128 digits", - createRef(this), - ); + err().literalTooLong()(createRef(this)); } return `b{${digits.sourceString}}`; }, @@ -655,10 +689,7 @@ semantics.addOperation("astsOfList", { params.source.contents === "" && optTrailingComma.sourceString === "," ) { - throwSyntaxError( - "Empty parameter list should not have a dangling comma.", - createRef(optTrailingComma), - ); + err().extraneousComma()(createRef(optTrailingComma)); } return params.asIteration().children.map((p) => p.astOfDeclaration()); }, @@ -667,10 +698,7 @@ semantics.addOperation("astsOfList", { args.source.contents === "" && optTrailingComma.sourceString === "," ) { - throwSyntaxError( - "Empty argument list should not have a dangling comma.", - createRef(optTrailingComma), - ); + err().extraneousComma()(createRef(optTrailingComma)); } return args.asIteration().children.map((arg) => arg.astOfExpression()); }, @@ -1008,8 +1036,7 @@ semantics.addOperation("astOfStatement", { .children.reduce((map, item) => { const destructItem = item.astOfExpression(); if (map.has(destructItem.field.text)) { - throwSyntaxError( - `Duplicate destructuring field: '${destructItem.field.text}'`, + err().duplicateField(destructItem.field.text)( destructItem.loc, ); } @@ -1427,10 +1454,7 @@ semantics.addOperation("astOfExpression", { structFields.source.contents === "" && optTrailingComma.sourceString === "," ) { - throwSyntaxError( - "Empty parameter list should not have a dangling comma.", - createRef(optTrailingComma), - ); + err().extraneousComma()(createRef(optTrailingComma)); } return createNode({ @@ -1470,17 +1494,20 @@ semantics.addOperation("astOfExpression", { }); export const getParser = (ast: FactoryAst) => { + const errorTypes = parserErrorSchema(displayToString); + function parse(src: string, path: string, origin: ItemOrigin): AstModule { return withContext( { currentFile: path, origin, createNode: ast.createNode, + errorTypes, }, () => { const matchResult = tactGrammar.match(src); if (matchResult.failed()) { - throwParseError(matchResult, path, origin); + errorTypes.generic(matchResult, path, origin); } return semantics(matchResult).astOfModule(); }, @@ -1493,11 +1520,12 @@ export const getParser = (ast: FactoryAst) => { currentFile: null, origin: "user", createNode: ast.createNode, + errorTypes, }, () => { const matchResult = tactGrammar.match(sourceCode, "Expression"); if (matchResult.failed()) { - throwParseError(matchResult, "", "user"); + errorTypes.generic(matchResult, "", "user"); } return semantics(matchResult).astOfExpression(); }, @@ -1514,11 +1542,12 @@ export const getParser = (ast: FactoryAst) => { currentFile: path, origin, createNode: ast.createNode, + errorTypes, }, () => { const matchResult = tactGrammar.match(src, "JustImports"); if (matchResult.failed()) { - throwParseError(matchResult, path, origin); + errorTypes.generic(matchResult, path, origin); } return semantics(matchResult).astOfJustImports(); }, diff --git a/src/grammar/parser-error.ts b/src/grammar/parser-error.ts new file mode 100644 index 000000000..3b258f1fb --- /dev/null +++ b/src/grammar/parser-error.ts @@ -0,0 +1,73 @@ +import { MatchResult } from "ohm-js"; +import { ErrorDisplay } from "../error/display"; +import { TactCompilationError } from "../errors"; +import { getSrcInfoFromOhm, ItemOrigin, SrcInfo } from "./src-info"; + +const attributeSchema = + (name: string) => + ({ text, sub }: ErrorDisplay, handle: (t: T) => U) => ({ + duplicate: (attr: string) => { + return handle( + sub`Duplicate ${text(name)} attribute "${text(attr)}"`, + ); + }, + notAbstract: () => { + return handle( + sub`Abstract ${text(name)} doesn't have abstract modifier`, + ); + }, + tooAbstract: () => { + return handle( + sub`Non-abstract ${text(name)} has abstract modifier`, + ); + }, + }); + +const syntaxErrorSchema = ( + display: ErrorDisplay, + handle: (t: T) => U, +) => { + const { sub, text } = display; + + return { + constant: attributeSchema("constant")(display, handle), + function: attributeSchema("function")(display, handle), + topLevelConstantWithAttribute: () => { + return handle( + sub`Module-level constants do not support attributes`, + ); + }, + literalTooLong: () => { + return handle(sub`Bitstring has more than 128 digits`); + }, + extraneousComma: () => { + return handle( + sub`Empty parameter list should not have a dangling comma`, + ); + }, + duplicateField: (name: string) => { + return handle(text(`Duplicate field destructuring: "${name}"`)); + }, + importWithBackslash: () => { + return handle(sub`Import path can't contain "\\"`); + }, + reservedVarPrefix: (prefix: string) => { + return handle(text(`Variable name cannot start with "${prefix}"`)); + }, + }; +}; + +export const parserErrorSchema = (display: ErrorDisplay) => ({ + ...syntaxErrorSchema(display, (message) => (source: SrcInfo) => { + throw new TactCompilationError(display.at(source, message), source); + }), + generic: (matchResult: MatchResult, path: string, origin: ItemOrigin) => { + const interval = matchResult.getInterval(); + const source = getSrcInfoFromOhm(interval, path, origin); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = `Expected ${(matchResult as any).getExpectedText()}\n`; + throw new TactCompilationError(display.at(source, message), source); + }, +}); + +export type ParserErrors = ReturnType; diff --git a/src/grammar/src-info.ts b/src/grammar/src-info.ts index 4ca54f771..8485c4e3c 100644 --- a/src/grammar/src-info.ts +++ b/src/grammar/src-info.ts @@ -46,6 +46,15 @@ export interface SrcInfo { toJSON: () => object; } +export const srcInfoEqual = (left: SrcInfo, right: SrcInfo): boolean => { + return ( + left.file === right.file && + left.interval.contents === right.interval.contents && + left.interval.startIdx === right.interval.startIdx && + left.interval.endIdx === right.interval.endIdx + ); +}; + const isEndline = (s: string) => s === "\n"; const repeat = (s: string, n: number): string => new Array(n + 1).join(s); diff --git a/src/index.ts b/src/index.ts index da47c4368..15ec85cf7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,6 @@ export { enableFeatures, build } from "./pipeline/build"; export { precompile } from "./pipeline/precompile"; export { TactError, - TactParseError, - TactSyntaxError, TactCompilationError, TactInternalCompilerError, TactConstEvalError, diff --git a/src/interpreter.ts b/src/interpreter.ts index 66424fe2a..e81a1e1ab 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -4,8 +4,8 @@ import * as crc32 from "crc-32"; import { evalConstantExpression } from "./constEval"; import { CompilerContext } from "./context"; import { + TactCompilationError, TactConstEvalError, - TactParseError, idTextErr, throwConstEvalError, throwInternalCompilerError, @@ -614,7 +614,7 @@ export function parseAndEvalExpression( return { kind: "ok", value: constEvalResult }; } catch (error) { if ( - error instanceof TactParseError || + error instanceof TactCompilationError || error instanceof TactConstEvalError ) return { kind: "error", message: error.message };