From 4ba26a3034cb23f7320ca9f804947ac5be380ba0 Mon Sep 17 00:00:00 2001 From: Marcin Kostrzewa Date: Tue, 28 Apr 2020 14:03:33 +0200 Subject: [PATCH] Compile Error Handling (#687) --- .../pr.md => PULL_REQUEST_TEMPLATE.md} | 0 .github/workflows/scala.yml | 4 +- .../enso/languageserver/boot/MainModule.scala | 2 +- .../org/enso/polyglot/RuntimeOptions.java | 27 +-- .../enso/polyglot/ModuleManagementTest.scala | 2 +- .../org/enso/runner/ContextFactory.scala | 6 +- .../src/main/scala/org/enso/runner/Main.scala | 22 ++- .../node/expression/constant/ErrorNode.java | 35 ++++ .../enso/interpreter/runtime/Builtins.java | 23 +++ .../org/enso/interpreter/runtime/Context.java | 10 + .../runtime/error/PanicException.java | 1 + .../scala/org/enso/compiler/Compiler.scala | 75 +++++++- .../org/enso/compiler/codegen/AstToIR.scala | 107 ++++++----- .../enso/compiler/codegen/IRToTruffle.scala | 39 +++- .../scala/org/enso/compiler/core/IR.scala | 181 +++++++++++++++++- .../CompilationAbortedException.scala | 14 ++ .../compiler/pass/analyse/GatherErrors.scala | 44 +++++ .../enso/compiler/pass/analyse/TailCall.scala | 2 +- .../test/pass/analyse/GatherErrorsTest.scala | 89 +++++++++ .../interpreter/test/InterpreterTest.scala | 2 +- .../enso/interpreter/test/PackageTest.scala | 17 +- .../enso/interpreter/test/ValueEquality.scala | 7 +- .../test/instrument/RuntimeServerTest.scala | 6 +- .../test/semantic/CompileErrorsTest.scala | 40 ++++ .../semantic/StrictCompileErrorsTest.scala | 34 ++++ 25 files changed, 681 insertions(+), 108 deletions(-) rename .github/{PULL_REQUEST_TEMPLATE/pr.md => PULL_REQUEST_TEMPLATE.md} (100%) create mode 100644 engine/runtime/src/main/java/org/enso/interpreter/node/expression/constant/ErrorNode.java create mode 100644 engine/runtime/src/main/scala/org/enso/compiler/exception/CompilationAbortedException.scala create mode 100644 engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/GatherErrors.scala create mode 100644 engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/GatherErrorsTest.scala create mode 100644 engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/CompileErrorsTest.scala create mode 100644 engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/StrictCompileErrorsTest.scala diff --git a/.github/PULL_REQUEST_TEMPLATE/pr.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE/pr.md rename to .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/workflows/scala.yml b/.github/workflows/scala.yml index e72be08de1..f44bb3f4cc 100644 --- a/.github/workflows/scala.yml +++ b/.github/workflows/scala.yml @@ -40,7 +40,7 @@ jobs: - name: Setup conda uses: s-weigand/setup-conda@v1 with: - update-conda: true + update-conda: false conda-channels: anaconda, conda-forge - name: Setup Conda Environment on Windows if: runner.os == 'Windows' @@ -51,7 +51,7 @@ jobs: if: runner.os == 'Windows' run: conda activate enso - name: Install FlatBuffers Compiler - run: conda install flatbuffers=1.12.0 + run: conda install --freeze-installed flatbuffers=1.12.0 - name: Setup GraalVM Environment uses: DeLaGuardo/setup-graalvm@2.0 with: diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala index ada7a7b415..d3af8a3688 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala @@ -108,7 +108,7 @@ class MainModule(serverConfig: LanguageServerConfig) { .allowAllAccess(true) .allowExperimentalOptions(true) .option(RuntimeServerInfo.ENABLE_OPTION, "true") - .option(RuntimeOptions.getPackagesPathOption, serverConfig.contentRootPath) + .option(RuntimeOptions.PACKAGES_PATH, serverConfig.contentRootPath) .serverTransport((uri: URI, peerEndpoint: MessageEndpoint) => { if (uri.toString == RuntimeServerInfo.URI) { val connection = new RuntimeConnector.Endpoint( diff --git a/engine/polyglot-api/src/main/java/org/enso/polyglot/RuntimeOptions.java b/engine/polyglot-api/src/main/java/org/enso/polyglot/RuntimeOptions.java index 139582925e..236c1e8eb3 100644 --- a/engine/polyglot-api/src/main/java/org/enso/polyglot/RuntimeOptions.java +++ b/engine/polyglot-api/src/main/java/org/enso/polyglot/RuntimeOptions.java @@ -1,20 +1,25 @@ package org.enso.polyglot; -import org.enso.polyglot.LanguageInfo; import org.graalvm.options.OptionDescriptor; import org.graalvm.options.OptionDescriptors; import org.graalvm.options.OptionKey; -import java.util.Collections; +import java.util.Arrays; /** Class representing runtime options supported by the Enso engine. */ public class RuntimeOptions { - private static final String PACKAGES_PATH = optionName("packagesPath"); + public static final String PACKAGES_PATH = optionName("packagesPath"); public static final OptionKey PACKAGES_PATH_KEY = new OptionKey<>(""); private static final OptionDescriptor PACKAGES_PATH_DESCRIPTOR = - OptionDescriptor.newBuilder(PACKAGES_PATH_KEY, getPackagesPathOption()).build(); + OptionDescriptor.newBuilder(PACKAGES_PATH_KEY, PACKAGES_PATH).build(); + + public static final String STRICT_ERRORS = optionName("strictErrors"); + public static final OptionKey STRICT_ERRORS_KEY = new OptionKey<>(false); + private static final OptionDescriptor STRICT_ERRORS_DESCRIPTOR = + OptionDescriptor.newBuilder(STRICT_ERRORS_KEY, STRICT_ERRORS).build(); + public static final OptionDescriptors OPTION_DESCRIPTORS = - OptionDescriptors.create(Collections.singletonList(PACKAGES_PATH_DESCRIPTOR)); + OptionDescriptors.create(Arrays.asList(PACKAGES_PATH_DESCRIPTOR, STRICT_ERRORS_DESCRIPTOR)); /** * Canonicalizes the option name by prefixing it with the language name. @@ -25,16 +30,4 @@ public class RuntimeOptions { private static String optionName(String name) { return LanguageInfo.ID + "." + name; } - - - - /** - * An option to pass a $PATH-like list of locations of all known modules that can be imported in - * the current run. - * - * @return the name of this option - */ - public static String getPackagesPathOption() { - return PACKAGES_PATH; - } } diff --git a/engine/polyglot-api/src/test/scala/org/enso/polyglot/ModuleManagementTest.scala b/engine/polyglot-api/src/test/scala/org/enso/polyglot/ModuleManagementTest.scala index aa604a2413..fde9eb83c9 100644 --- a/engine/polyglot-api/src/test/scala/org/enso/polyglot/ModuleManagementTest.scala +++ b/engine/polyglot-api/src/test/scala/org/enso/polyglot/ModuleManagementTest.scala @@ -17,7 +17,7 @@ class ModuleManagementTest extends AnyFlatSpec with Matchers { .newBuilder(LanguageInfo.ID) .allowExperimentalOptions(true) .allowAllAccess(true) - .option(RuntimeOptions.getPackagesPathOption, pkg.root.getAbsolutePath) + .option(RuntimeOptions.PACKAGES_PATH, pkg.root.getAbsolutePath) .build() ) diff --git a/engine/runner/src/main/scala/org/enso/runner/ContextFactory.scala b/engine/runner/src/main/scala/org/enso/runner/ContextFactory.scala index 4ead5c620d..6295f82450 100644 --- a/engine/runner/src/main/scala/org/enso/runner/ContextFactory.scala +++ b/engine/runner/src/main/scala/org/enso/runner/ContextFactory.scala @@ -25,13 +25,15 @@ class ContextFactory { packagesPath: String = "", in: InputStream, out: OutputStream, - repl: Repl + repl: Repl, + strictErrors: Boolean = false ): PolyglotContext = { val context = Context .newBuilder(LanguageInfo.ID) .allowExperimentalOptions(true) .allowAllAccess(true) - .option(RuntimeOptions.getPackagesPathOption, packagesPath) + .option(RuntimeOptions.PACKAGES_PATH, packagesPath) + .option(RuntimeOptions.STRICT_ERRORS, strictErrors.toString) .out(out) .in(in) .build diff --git a/engine/runner/src/main/scala/org/enso/runner/Main.scala b/engine/runner/src/main/scala/org/enso/runner/Main.scala index c01512d871..2fe6371e7f 100644 --- a/engine/runner/src/main/scala/org/enso/runner/Main.scala +++ b/engine/runner/src/main/scala/org/enso/runner/Main.scala @@ -10,7 +10,7 @@ import org.enso.languageserver.boot import org.enso.languageserver.boot.LanguageServerConfig import org.enso.pkg.Package import org.enso.polyglot.{LanguageInfo, Module, PolyglotContext} -import org.graalvm.polyglot.Value +import org.graalvm.polyglot.{PolyglotException, Value} import scala.annotation.unused import scala.util.Try @@ -172,7 +172,8 @@ object Main { packagePath, System.in, System.out, - Repl(TerminalIO()) + Repl(TerminalIO()), + strictErrors = true ) if (projectMode) { val pkg = Package.fromDirectory(file) @@ -196,18 +197,25 @@ object Main { ): Unit = { val topScope = context.getTopScope val mainModule = topScope.getModule(mainModuleName) - runMain(mainModule): Unit + runMain(mainModule) } private def runSingleFile(context: PolyglotContext, file: File): Unit = { val mainModule = context.evalModule(file) - runMain(mainModule): Unit + runMain(mainModule) } private def runMain(mainModule: Module): Value = { - val mainCons = mainModule.getAssociatedConstructor - val mainFun = mainModule.getMethod(mainCons, "main") - mainFun.execute(mainCons.newInstance()) + try { + val mainCons = mainModule.getAssociatedConstructor + val mainFun = mainModule.getMethod(mainCons, "main") + mainFun.execute(mainCons.newInstance()) + } catch { + case e: PolyglotException => + System.err.println(e.getMessage) + exitFail() + throw new RuntimeException("Impossible to reach here.") + } } /** diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/constant/ErrorNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/constant/ErrorNode.java new file mode 100644 index 0000000000..8ca22defc3 --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/constant/ErrorNode.java @@ -0,0 +1,35 @@ +package org.enso.interpreter.node.expression.constant; + +import com.oracle.truffle.api.frame.VirtualFrame; +import org.enso.interpreter.node.ExpressionNode; +import org.enso.interpreter.runtime.error.PanicException; + +/** Throws a runtime panic containing a statically-known payload. */ +public class ErrorNode extends ExpressionNode { + private final Object payload; + + private ErrorNode(Object payload) { + this.payload = payload; + } + + /** + * Executes the node, by throwing a new panic containing the specified payload. + * + * @param frame the stack frame for execution + * @return never returns + */ + @Override + public Object executeGeneric(VirtualFrame frame) { + throw new PanicException(payload, this); + } + + /** + * Create a new instance of this node. + * + * @param payload the payload carried by exceptions thrown in the course of this node's execution. + * @return a new instance of this node. + */ + public static ErrorNode build(Object payload) { + return new ErrorNode(payload); + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/Builtins.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/Builtins.java index 2647b219b9..369dc235ac 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/Builtins.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/Builtins.java @@ -44,6 +44,8 @@ public class Builtins { private final AtomConstructor function; private final AtomConstructor text; private final AtomConstructor debug; + private final AtomConstructor syntaxError; + private final AtomConstructor compileError; /** * Creates an instance with builtin methods installed. @@ -59,6 +61,14 @@ public class Builtins { function = new AtomConstructor("Function", scope).initializeFields(); text = new AtomConstructor("Text", scope).initializeFields(); debug = new AtomConstructor("Debug", scope).initializeFields(); + syntaxError = + new AtomConstructor("Syntax_Error", scope) + .initializeFields( + new ArgumentDefinition(0, "message", ArgumentDefinition.ExecutionMode.EXECUTE)); + compileError = + new AtomConstructor("Compile_Error", scope) + .initializeFields( + new ArgumentDefinition(0, "message", ArgumentDefinition.ExecutionMode.EXECUTE)); AtomConstructor nil = new AtomConstructor("Nil", scope).initializeFields(); AtomConstructor cons = @@ -85,6 +95,9 @@ public class Builtins { scope.registerConstructor(state); scope.registerConstructor(debug); + scope.registerConstructor(syntaxError); + scope.registerConstructor(compileError); + scope.registerMethod(io, "println", PrintNode.makeFunction(language)); scope.registerMethod(panic, "throw", PanicNode.makeFunction(language)); @@ -166,6 +179,16 @@ public class Builtins { return debug; } + /** @return the builtin {@code Syntax_Error} atom constructor. */ + public AtomConstructor syntaxError() { + return syntaxError; + } + + /** @return the builtin {@code Compile_Error} atom constructor. */ + public AtomConstructor compileError() { + return compileError; + } + /** * Returns the builtin module scope. * diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/Context.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/Context.java index 16f76160e6..72daca83c8 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/Context.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/Context.java @@ -18,6 +18,7 @@ import org.enso.interpreter.runtime.scope.TopLevelScope; import org.enso.interpreter.util.ScalaConversions; import org.enso.pkg.Package; import org.enso.pkg.QualifiedName; +import org.enso.polyglot.RuntimeOptions; /** * The language context is the internal state of the language that is associated with each thread in @@ -194,4 +195,13 @@ public class Context { public AtomConstructor getUnit() { return getBuiltins().unit(); } + + /** + * Checks whether the strict errors option was set for this context. + * + * @return true if the strict errors option is enabled, false otherwise. + */ + public boolean isStrictErrors() { + return getEnvironment().getOptions().get(RuntimeOptions.STRICT_ERRORS_KEY); + } } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/PanicException.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/PanicException.java index a77f80879f..e0688a8efa 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/PanicException.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/PanicException.java @@ -15,6 +15,7 @@ public class PanicException extends RuntimeException implements TruffleException * @param location the node throwing this exception, for use in guest stack traces */ public PanicException(Object payload, Node location) { + super(payload.toString()); this.payload = payload; this.location = location; } diff --git a/engine/runtime/src/main/scala/org/enso/compiler/Compiler.scala b/engine/runtime/src/main/scala/org/enso/compiler/Compiler.scala index 013ac2055c..1c10ecb18e 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/Compiler.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/Compiler.scala @@ -7,9 +7,11 @@ import com.oracle.truffle.api.source.Source import org.enso.compiler.codegen.{AstToIR, IRToTruffle} import org.enso.compiler.core.IR import org.enso.compiler.core.IR.{Expression, Module} -import org.enso.compiler.exception.CompilerError +import org.enso.compiler.exception.{CompilationAbortedException, CompilerError} import org.enso.compiler.pass.IRPass + import org.enso.compiler.pass.analyse._ + import org.enso.compiler.pass.desugar.{ GenerateMethodBodies, LiftSpecialOperators, @@ -68,6 +70,7 @@ class Compiler( val parsedAST = parse(source) val expr = generateIR(parsedAST) val compilerOutput = runCompilerPhases(expr) + runErrorHandling(compilerOutput, source) truffleCodegen(compilerOutput, source, scope) } @@ -132,6 +135,7 @@ class Compiler( generateIRInline(parsed).flatMap { ir => val compilerOutput = runCompilerPhasesInline(ir, inlineContext) + runErrorHandlingInline(compilerOutput, source, inlineContext) Some(truffleCodegenInline(compilerOutput, source, inlineContext)) } } @@ -210,6 +214,75 @@ class Compiler( ) } + /** + * Runs the strict error handling mechanism (if enabled in the language + * context) for the inline compiler flow. + * + * @param ir the IR after compilation passes. + * @param source the original source code. + * @param inlineContext the inline compilation context. + */ + def runErrorHandlingInline( + ir: IR.Expression, + source: Source, + inlineContext: InlineContext + ): Unit = if (context.isStrictErrors) { + val errors = GatherErrors + .runExpression(ir, inlineContext) + .unsafeGetMetadata[GatherErrors.Errors]( + "No errors metadata right after the gathering pass." + ) + .errors + reportErrors(errors, source) + } + + /** + * Runs the strict error handling mechanism (if enabled in the language + * context) for the module-level compiler flow. + * + * @param ir the IR after compilation passes. + * @param source the original source code. + */ + def runErrorHandling(ir: IR.Module, source: Source): Unit = + if (context.isStrictErrors) { + val errors = GatherErrors + .runModule(ir) + .unsafeGetMetadata[GatherErrors.Errors]( + "No errors metadata right after the gathering pass." + ) + .errors + reportErrors(errors, source) + } + + /** + * Reports compilation errors to the standard output and throws an exception + * breaking the execution flow. + * + * @param errors all the errors found in the program IR. + * @param source the original source code. + */ + def reportErrors(errors: List[IR.Error], source: Source): Unit = + if (errors.nonEmpty) { + context.getOut.println("Compiler encountered errors:") + errors.foreach { err => + val srcLocation = err.location + .map { loc => + val section = source + .createSection(loc.location.start, loc.location.length) + val locStr = + "" + section.getStartLine + ":" + + section.getStartColumn + "-" + + section.getEndLine + ":" + + section.getEndColumn + "[" + locStr + "]" + } + .getOrElse("") + val fullMsg = source.getName + srcLocation + ": " + err.message + context.getOut.println(fullMsg) + } + throw new CompilationAbortedException + } + /** Generates code for the truffle interpreter. * * @param ir the program to translate diff --git a/engine/runtime/src/main/scala/org/enso/compiler/codegen/AstToIR.scala b/engine/runtime/src/main/scala/org/enso/compiler/codegen/AstToIR.scala index 523258b5ab..687ad5b6da 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/codegen/AstToIR.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/codegen/AstToIR.scala @@ -31,9 +31,8 @@ object AstToIR { def translate(inputAST: AST): Module = { inputAST match { case AST.Module.any(inputAST) => translateModule(inputAST) - case _ => { + case _ => throw new UnhandledEntity(inputAST, "translate") - } } } @@ -111,7 +110,7 @@ object AstToIR { inputAST match { case AST.Def(consName, args, body) => if (body.isDefined) { - throw new RuntimeException("Cannot support complex type defs yet!!!!") + throw new UnhandledEntity(inputAST, "translateModuleSymbol") } else { Module.Scope.Definition .Atom( @@ -206,9 +205,8 @@ object AstToIR { case AST.App.any(inputAST) => translateApplicationLike(inputAST) case AST.Mixfix.any(inputAST) => translateApplicationLike(inputAST) case AST.Literal.any(inputAST) => translateLiteral(inputAST) - case AST.Group.any(inputAST) => - translateGroup(inputAST, translateExpression) - case AST.Ident.any(inputAST) => translateIdent(inputAST) + case AST.Group.any(inputAST) => translateGroup(inputAST) + case AST.Ident.any(inputAST) => translateIdent(inputAST) case AstView.Block(lines, retLine) => Expression.Block( lines.map(translateExpression), @@ -218,8 +216,9 @@ object AstToIR { case AST.Comment.any(inputAST) => translateComment(inputAST) case AST.Invalid.any(inputAST) => translateInvalid(inputAST) case AST.Foreign(_, _, _) => - throw new RuntimeException( - "Enso does not yet support foreign language blocks" + Error.Syntax( + inputAST, + Error.Syntax.UnsupportedSyntax("foreign blocks") ) case _ => throw new UnhandledEntity(inputAST, "translateExpression") @@ -246,10 +245,13 @@ object AstToIR { literal match { case AST.Literal.Number(base, number) => { if (base.isDefined && base.get != "10") { - throw new RuntimeException("Only base 10 is currently supported") + Error.Syntax( + literal, + Error.Syntax.UnsupportedSyntax("non-base-10 number literals") + ) + } else { + Literal.Number(number, getIdentifiedLocation(literal)) } - - Literal.Number(number, getIdentifiedLocation(literal)) } case AST.Literal.Text.any(literal) => literal.shape match { @@ -272,9 +274,15 @@ object AstToIR { Literal.Text(fullString, getIdentifiedLocation(literal)) case AST.Literal.Text.Block.Fmt(_, _, _) => - throw new RuntimeException("Format strings not yet supported") + Error.Syntax( + literal, + Error.Syntax.UnsupportedSyntax("format strings") + ) case AST.Literal.Text.Line.Fmt(_) => - throw new RuntimeException("Format strings not yet supported") + Error.Syntax( + literal, + Error.Syntax.UnsupportedSyntax("format strings") + ) case _ => throw new UnhandledEntity(literal.shape, "translateLiteral") } @@ -399,24 +407,20 @@ object AstToIR { getIdentifiedLocation(callable) ) case AST.App.Prefix(_, _) => - throw new RuntimeException( - "Enso does not support arbitrary prefix expressions" - ) + throw new UnhandledEntity(callable, "translateCallable") case AST.App.Section.any(_) => - throw new RuntimeException( - "Enso does not yet support operator sections" + Error.Syntax( + callable, + Error.Syntax.UnsupportedSyntax("operator sections") ) case AST.Mixfix(nameSegments, args) => val realNameSegments = nameSegments.collect { - case AST.Ident.Var.any(v) => v - } - - if (realNameSegments.length != nameSegments.length) { - throw new RuntimeException("Badly named mixfix function.") + case AST.Ident.Var.any(v) => v.name + case AST.Ident.Cons.any(v) => v.name.toLowerCase } val functionName = - AST.Ident.Var(realNameSegments.map(_.name).mkString("_")) + AST.Ident.Var(realNameSegments.mkString("_")) Application.Prefix( translateExpression(functionName), @@ -447,12 +451,19 @@ object AstToIR { case AST.Ident.Cons(name) => Name.Literal(name, getIdentifiedLocation(identifier)) case AST.Ident.Blank(_) => - throw new RuntimeException("Blanks not yet properly supported") + Error.Syntax( + identifier, + Error.Syntax.UnsupportedSyntax("blanks") + ) case AST.Ident.Opr.any(_) => - throw new RuntimeException("Operators not generically supported yet") + Error.Syntax( + identifier, + Error.Syntax.UnsupportedSyntax("operator sections") + ) case AST.Ident.Mod(_) => - throw new RuntimeException( - "Enso does not support arbitrary module identifiers yet" + Error.Syntax( + identifier, + Error.Syntax.UnsupportedSyntax("module identifiers") ) case _ => throw new UnhandledEntity(identifier, "translateIdent") @@ -533,17 +544,12 @@ object AstToIR { * It is currently an error to have an empty group. * * @param group the group to translate - * @param translator the function to apply to the group's contents - * @tparam T the result type of translating the expression contained in - * `group` * @return the [[Core]] representation of the contents of `group` */ - def translateGroup[T](group: AST.Group, translator: AST => T): T = { + def translateGroup(group: AST.Group): Expression = { group.body match { - case Some(ast) => translator(ast) - case None => { - throw new RuntimeException("Empty group") - } + case Some(ast) => translateExpression(ast) + case None => Error.Syntax(group, Error.Syntax.EmptyParentheses) } } @@ -569,25 +575,27 @@ object AstToIR { def translateInvalid(invalid: AST.Invalid): Expression = { invalid match { case AST.Invalid.Unexpected(_, _) => - throw new RuntimeException( - "Enso does not yet support unexpected blocks properly" + Error.Syntax( + invalid, + Error.Syntax.UnexpectedExpression ) case AST.Invalid.Unrecognized(_) => - throw new RuntimeException( - "Enso does not yet support unrecognised tokens properly" + Error.Syntax( + invalid, + Error.Syntax.UnrecognizedToken ) case AST.Ident.InvalidSuffix(_, _) => - throw new RuntimeException( - "Enso does not yet support invalid suffixes properly" + Error.Syntax( + invalid, + Error.Syntax.InvalidSuffix ) case AST.Literal.Text.Unclosed(_) => - throw new RuntimeException( - "Enso does not yet support unclosed text literals properly" + Error.Syntax( + invalid, + Error.Syntax.UnclosedTextLiteral ) case _ => - throw new RuntimeException( - "Fatal: Unhandled entity in processInvalid = " + invalid - ) + throw new UnhandledEntity(invalid, "translateInvalid") } } @@ -603,8 +611,9 @@ object AstToIR { def translateComment(comment: AST): Expression = { comment match { case AST.Comment(_) => - throw new RuntimeException( - "Enso does not yet support comments properly" + Error.Syntax( + comment, + Error.Syntax.UnsupportedSyntax("comments") ) case AST.Documented(doc, _, ast) => Comment.Documentation( diff --git a/engine/runtime/src/main/scala/org/enso/compiler/codegen/IRToTruffle.scala b/engine/runtime/src/main/scala/org/enso/compiler/codegen/IRToTruffle.scala index 6385fda4c2..54a1080f59 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/codegen/IRToTruffle.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/codegen/IRToTruffle.scala @@ -3,7 +3,7 @@ package org.enso.compiler.codegen import com.oracle.truffle.api.Truffle import com.oracle.truffle.api.source.{Source, SourceSection} import org.enso.compiler.core.IR -import org.enso.compiler.core.IR.IdentifiedLocation +import org.enso.compiler.core.IR.{Error, IdentifiedLocation} import org.enso.compiler.exception.{CompilerError, UnhandledEntity} import org.enso.compiler.pass.analyse.AliasAnalysis.Graph.{Scope => AliasScope} import org.enso.compiler.pass.analyse.AliasAnalysis.{Graph => AliasGraph} @@ -23,7 +23,8 @@ import org.enso.interpreter.node.callable.{ApplicationNode, InvokeCallableNode} import org.enso.interpreter.node.controlflow._ import org.enso.interpreter.node.expression.constant.{ ConstructorNode, - DynamicSymbolNode + DynamicSymbolNode, + ErrorNode } import org.enso.interpreter.node.expression.literal.{ IntegerLiteralNode, @@ -338,11 +339,7 @@ class IRToTruffle( case binding: IR.Expression.Binding => processBinding(binding) case caseExpr: IR.Case => processCase(caseExpr) case comment: IR.Comment => processComment(comment) - case err: IR.Error => - throw new CompilerError( - s"No errors should remain by the point of truffle codegen, but " + - s"found $err." - ) + case err: IR.Error => processError(err) case IR.Foreign.Definition(_, _, _, _) => throw new CompilerError( s"Foreign expressions not yet implemented: $ir." @@ -555,6 +552,34 @@ class IRToTruffle( setLocation(TextLiteralNode.build(text), location) } + /** + * Generates a runtime implementation for compile error nodes. + * + * @param error the IR representing a compile error. + * @return a runtime node representing the error. + */ + def processError(error: IR.Error): RuntimeExpression = { + val payload: AnyRef = error match { + case IR.Empty(_, _) => + throw new CompilerError("Unexpected Empty IR during codegen.") + case Error.InvalidIR(_, _) => + throw new CompilerError("Unexpected Invalid IR during codegen.") + case err: Error.Syntax => + context.getBuiltins + .syntaxError() + .newInstance(err.message) + case err: Error.Redefined.Binding => + context.getBuiltins + .compileError() + .newInstance(err.message) + case err: Error.Redefined.Argument => + context.getBuiltins + .compileError() + .newInstance(err.message) + } + setLocation(ErrorNode.build(payload), error.location) + } + /** Generates code for an Enso function body. * * @param arguments the arguments to the function diff --git a/engine/runtime/src/main/scala/org/enso/compiler/core/IR.scala b/engine/runtime/src/main/scala/org/enso/compiler/core/IR.scala index a5e10c76df..72ca4af593 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/core/IR.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/core/IR.scala @@ -73,6 +73,18 @@ sealed trait IR { */ def mapExpressions(fn: Expression => Expression): IR + /** Gets the list of all children IR nodes of this node. + * + * @return this node's children. + */ + def children: List[IR] + + /** Lists all the nodes in the preorder walk of the tree of this node. + * + * @return all the descendants of this node. + */ + def preorder: List[IR] = this :: children.flatMap(_.preorder) + /** Pretty prints the IR. * * @return a pretty-printed representation of the IR @@ -183,6 +195,11 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List() + + override def message: String = + "Empty IR: Please report this as a compiler bug." } // === Module =============================================================== @@ -238,6 +255,8 @@ object IR { ) } + override def children: List[IR] = imports ++ bindings + def transformExpressions( fn: PartialFunction[Expression, Expression] ): Module = { @@ -316,6 +335,8 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List() } /** A representation of top-level definitions. */ @@ -383,6 +404,8 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = name :: arguments } /** The definition of a method for a given constructor [[typeName]]. @@ -453,6 +476,8 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List(typeName, methodName, body) } } } @@ -547,6 +572,8 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = expressions :+ returnValue } /** A binding expression of the form `name = expr` @@ -604,6 +631,8 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List(name, expression) } } @@ -662,6 +691,8 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List() } /** A textual Enso literal. @@ -711,6 +742,8 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List() } } @@ -772,6 +805,8 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List() } /** A representation of the name `this`, used to refer to the current type. @@ -817,6 +852,8 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List() } /** A representation of the name `here`, used to refer to the current @@ -862,6 +899,8 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List() } } @@ -933,6 +972,8 @@ object IR { |id = $id |) |""".stripMargin + + override def children: List[IR] = List(typed, signature) } object Ascription extends Info { override val name: String = ":" @@ -993,6 +1034,9 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List(typed, context) + } object Context extends Info { override val name: String = "in" @@ -1069,6 +1113,9 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List(label, memberType, value) + } object Member extends Info { override val name: String = "_ : _ = _" @@ -1130,6 +1177,9 @@ object IR { |passData = ${this.showPassData}, |id = $id |""".toSingleLine + + override def children: List[IR] = List(left, right) + } object Subsumption extends Info { override val name: String = "<:" @@ -1189,6 +1239,9 @@ object IR { |passData = ${this.showPassData}, |id = $id |""".toSingleLine + + override def children: List[IR] = List(left, right) + } object Equality extends Info { override val name: String = "~" @@ -1248,6 +1301,9 @@ object IR { |passData = ${this.showPassData}, |id = $id |""".toSingleLine + + override def children: List[IR] = List(left, right) + } object Concat extends Info { override val name: String = "," @@ -1307,6 +1363,9 @@ object IR { |passData = ${this.showPassData}, |id = $id |""".toSingleLine + + override def children: List[IR] = List(left, right) + } object Union extends Info { override val name: String = "|" @@ -1368,6 +1427,9 @@ object IR { |passData = ${this.showPassData}, |id = $id |""".toSingleLine + + override def children: List[IR] = List(left, right) + } object Intersection extends Info { override val name: String = "&" @@ -1429,6 +1491,9 @@ object IR { |passData = ${this.showPassData}, |id = $id |""".toSingleLine + + override def children: List[IR] = List(left, right) + } object Subtraction extends Info { override val name: String = "\\" @@ -1527,6 +1592,9 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = arguments :+ body + } } @@ -1608,6 +1676,9 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = name :: defaultValue.toList + } // TODO [AA] Add support for `_` ignored arguments. @@ -1685,6 +1756,9 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = function :: arguments + } /** A representation of a term that is explicitly forced. @@ -1737,6 +1811,9 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List(target) + } /** Operator applications in Enso. */ @@ -1806,6 +1883,9 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List(left, operator, right) + } } @@ -1896,6 +1976,9 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = name.toList :+ value + } // TODO [AA] Add support for the `_` lambda shorthand argument (can be @@ -1975,6 +2058,10 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = + scrutinee :: branches.toList ++ fallback.toList + } /** A branch in a case statement. @@ -2032,6 +2119,9 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List(pattern, expression) + } /** The different types of patterns that can occur in a match. */ @@ -2113,6 +2203,9 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List(commented) + } } @@ -2179,6 +2272,9 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List() + } } @@ -2187,7 +2283,13 @@ object IR { /** A trait for all errors in Enso's IR. */ sealed trait Error extends Expression { override def mapExpressions(fn: Expression => Expression): Error - override def addMetadata(newData: Metadata): Error + + override def addMetadata(newData: Metadata): Error + + /** + * @return a human-readable description of this error condition. + */ + def message: String } object Error { @@ -2209,13 +2311,15 @@ object IR { /** A representation of an Enso syntax error. * * @param ast the erroneous AST + * @param reason the cause of this error * @param passData the pass metadata associated with this node */ sealed case class Syntax( ast: AST, + reason: Syntax.Reason, override val passData: ISet[Metadata] = ISet() ) extends Error - with Kind.Static + with Kind.Interactive with IRKind.Primitive { override protected var id: Identifier = randomId @@ -2228,10 +2332,11 @@ object IR { */ def copy( ast: AST = ast, + reason: Syntax.Reason = reason, passData: ISet[Metadata] = passData, id: Identifier = id ): Syntax = { - val res = Syntax(ast, passData) + val res = Syntax(ast, reason, passData) res.id = id res } @@ -2254,6 +2359,50 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List() + + override def message: String = reason.explanation + } + + object Syntax { + + /** + * A common type for all syntax errors expected by the language. + */ + sealed trait Reason { + + /** + * @return a human-readable description of the error. + */ + def explanation: String + } + + case class UnsupportedSyntax(syntaxName: String) extends Reason { + override def explanation: String = + s"Syntax is not supported yet: $syntaxName." + } + + case object EmptyParentheses extends Reason { + override def explanation: String = "Parentheses can't be empty." + } + + case object UnexpectedExpression extends Reason { + override def explanation: String = "Unexpected expression." + } + + case object UnrecognizedToken extends Reason { + override def explanation: String = "Unrecognized token." + } + + case object InvalidSuffix extends Reason { + override def explanation: String = "Invalid suffix." + } + + case object UnclosedTextLiteral extends Reason { + override def explanation: String = "Unclosed text literal." + } + } /** A representation of an invalid piece of IR. @@ -2304,6 +2453,12 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List(ir) + + override def message: String = + "InvalidIR: Please report this as a compiler bug." + } /** Errors pertaining to the redefinition of language constructs that are @@ -2318,7 +2473,7 @@ object IR { * @param passData the pass metadata for the error */ sealed case class Argument( - invalidArgDef: IR.DefinitionArgument, + invalidArgDef: IR.DefinitionArgument.Specified, override val passData: ISet[Metadata] = ISet() ) extends Redefined with Kind.Static @@ -2334,9 +2489,9 @@ object IR { * @return a copy of `this`, updated with the specified values */ def copy( - invalidArgDef: IR.DefinitionArgument = invalidArgDef, - passData: ISet[Metadata] = passData, - id: Identifier = id + invalidArgDef: IR.DefinitionArgument.Specified = invalidArgDef, + passData: ISet[Metadata] = passData, + id: Identifier = id ): Argument = { val res = Argument(invalidArgDef, passData) res.id = id @@ -2364,6 +2519,12 @@ object IR { |id = $id |) |""".toSingleLine + + override def children: List[IR] = List(invalidArgDef) + + override def message: String = + s"Argument ${invalidArgDef.name.name} is being redefined." + } /** An error representing the redefinition of a binding in a given scope. @@ -2418,6 +2579,12 @@ object IR { |id = $id |) |""".stripMargin + + override def children: List[IR] = List(invalidBinding) + + override def message: String = + s"Variable ${invalidBinding.name.name} is being redefined." + } } } diff --git a/engine/runtime/src/main/scala/org/enso/compiler/exception/CompilationAbortedException.scala b/engine/runtime/src/main/scala/org/enso/compiler/exception/CompilationAbortedException.scala new file mode 100644 index 0000000000..74bc6ed470 --- /dev/null +++ b/engine/runtime/src/main/scala/org/enso/compiler/exception/CompilationAbortedException.scala @@ -0,0 +1,14 @@ +package org.enso.compiler.exception + +import com.oracle.truffle.api.TruffleException +import com.oracle.truffle.api.nodes.Node + +/** + * An exception thrown to break out of the compilation flow after reporting + * all the encountered errors. + */ +class CompilationAbortedException extends Exception with TruffleException { + override def getMessage: String = "Compilation aborted due to errors." + + override def getLocation: Node = null +} diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/GatherErrors.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/GatherErrors.scala new file mode 100644 index 0000000000..39190fcdf5 --- /dev/null +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/GatherErrors.scala @@ -0,0 +1,44 @@ +package org.enso.compiler.pass.analyse + +import org.enso.compiler.InlineContext +import org.enso.compiler.core.IR +import org.enso.compiler.pass.IRPass + +/** + * A pass that traverses the given root IR and accumulates all the encountered + * error nodes in the root. + */ +case object GatherErrors extends IRPass { + + case class Errors(errors: List[IR.Error]) extends IR.Metadata { + /** The name of the metadata as a string. */ + override val metadataName: String = "GatherErrors.Errors" + } + + override type Metadata = Errors + + /** Executes the pass on the provided `ir`, and attaches all the encountered + * errors to its metadata storage. + * + * @param ir the IR to process + * @return `ir` with all the errors accumulated in pass metadata. + */ + override def runModule(ir: IR.Module): IR.Module = + ir.addMetadata(gatherErrors(ir)) + + /** Executes the pass on the provided `ir`, and attaches all the encountered + * errors to its metadata storage. + * + * @param ir the IR to process + * @param inlineContext a context object that contains the information needed + * for inline evaluation + * @return `ir` with all the errors accumulated in pass metadata. + */ + override def runExpression( + ir: IR.Expression, + inlineContext: InlineContext + ): IR.Expression = ir.addMetadata(gatherErrors(ir)) + + private def gatherErrors(ir: IR): Errors = + Errors(ir.preorder.collect { case err: IR.Error => err }) +} diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/TailCall.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/TailCall.scala index 8f974014d9..8546ea6d71 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/TailCall.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/TailCall.scala @@ -106,7 +106,7 @@ case object TailCall extends IRPass { expression = analyseExpression(expression, isInTailPosition) ) .addMetadata(TailPosition.fromBool(isInTailPosition)) - case err: IR.Error => err + case err: IR.Error => err.addMetadata(TailPosition.fromBool(isInTailPosition)) } } diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/GatherErrorsTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/GatherErrorsTest.scala new file mode 100644 index 0000000000..b3aa35692d --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/GatherErrorsTest.scala @@ -0,0 +1,89 @@ +package org.enso.compiler.test.pass.analyse + +import org.enso.compiler.InlineContext +import org.enso.compiler.core.IR +import org.enso.compiler.core.IR.CallArgument +import org.enso.compiler.pass.analyse.GatherErrors +import org.enso.compiler.test.CompilerTest +import org.enso.syntax.text.AST + +class GatherErrorsTest extends CompilerTest { + "Error Gathering" should { + val error1 = IR.Error.Redefined.Argument( + IR.DefinitionArgument + .Specified(IR.Name.Literal("foo", None), None, false, None) + ) + val error2 = IR.Error.Syntax( + AST.Invalid.Unrecognized("@@"), + IR.Error.Syntax.UnrecognizedToken + ) + val plusOp = IR.Name.Literal("+", None) + val plusApp = IR.Application.Prefix( + plusOp, + List( + CallArgument.Specified(None, error1, None), + CallArgument.Specified(None, error2, None) + ), + false, + None + ) + val lam = IR.Function.Lambda( + List( + IR.DefinitionArgument + .Specified(IR.Name.Literal("bar", None), None, false, None) + ), + plusApp, + None + ) + + "work with expression flow" in { + val result = GatherErrors.runExpression(lam, new InlineContext()) + val errors = result + .unsafeGetMetadata[GatherErrors.Errors]("Impossible") + .errors + + errors.toSet shouldEqual Set(error1, error2) + } + + "work with module flow" in { + val error3 = IR.Error.Syntax( + AST.Invalid.Unexpected("whoa, that was not expected", List()), + IR.Error.Syntax.UnexpectedExpression + ) + + val error4 = IR.Error.Syntax( + AST.Invalid.Unexpected("whoa, that was also not expected", List()), + IR.Error.Syntax.UnexpectedExpression + ) + + val typeName = IR.Name.Literal("Foo", None) + val method1Name = IR.Name.Literal("bar", None) + val method2Name = IR.Name.Literal("baz", None) + val fooName = IR.Name.Literal("foo", None) + + val module = IR.Module( + List(), + List( + IR.Module.Scope.Definition.Atom( + typeName, + List( + IR.DefinitionArgument + .Specified(fooName, Some(error3), false, None) + ), + None + ), + IR.Module.Scope.Definition.Method(typeName, method1Name, lam, None), + IR.Module.Scope.Definition.Method(typeName, method2Name, error4, None) + ), + None + ) + + val result = GatherErrors.runModule(module) + val errors = result + .unsafeGetMetadata[GatherErrors.Errors]("Impossible") + .errors + + errors.toSet shouldEqual Set(error1, error2, error3, error4) + } + } +} diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala index d6f85df19d..7a2eb9c4e1 100644 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala @@ -79,7 +79,7 @@ trait InterpreterRunner { .allowExperimentalOptions(true) .out(output) .build() - val executionContext = new PolyglotContext(ctx) + lazy val executionContext = new PolyglotContext(ctx) def withLocationsInstrumenter(test: LocationsInstrumenter => Unit): Unit = { val instrument = ctx.getEngine.getInstruments diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/PackageTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/PackageTest.scala index f674744ce7..98b7287684 100644 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/PackageTest.scala +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/PackageTest.scala @@ -1,6 +1,6 @@ package org.enso.interpreter.test -import java.io.File +import java.io.{ByteArrayOutputStream, File} import org.enso.pkg.Package import org.enso.polyglot.{LanguageInfo, PolyglotContext, RuntimeOptions} @@ -20,16 +20,19 @@ trait PackageTest extends AnyFlatSpec with Matchers with ValueEquality { .newBuilder(LanguageInfo.ID) .allowExperimentalOptions(true) .allowAllAccess(true) - .option(RuntimeOptions.getPackagesPathOption, pkgPath.getAbsolutePath) + .option(RuntimeOptions.PACKAGES_PATH, pkgPath.getAbsolutePath) + .option(RuntimeOptions.STRICT_ERRORS, "true") .out(System.out) .in(System.in) .build() context.initialize(LanguageInfo.ID) val executionContext = new PolyglotContext(context) - val topScope = executionContext.getTopScope - val mainModuleScope = topScope.getModule(mainModule.toString) - val assocCons = mainModuleScope.getAssociatedConstructor - val mainFun = mainModuleScope.getMethod(assocCons, "main") - InterpreterException.rethrowPolyglot(mainFun.execute(assocCons)) + InterpreterException.rethrowPolyglot { + val topScope = executionContext.getTopScope + val mainModuleScope = topScope.getModule(mainModule.toString) + val assocCons = mainModuleScope.getAssociatedConstructor + val mainFun = mainModuleScope.getMethod(assocCons, "main") + mainFun.execute(assocCons) + } } } diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/ValueEquality.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/ValueEquality.scala index 42766e35c6..c57fbba1b6 100644 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/ValueEquality.scala +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/ValueEquality.scala @@ -6,8 +6,9 @@ import org.scalactic.Equality trait ValueEquality { implicit val valueEquality: Equality[Value] = (a: Value, b: Any) => b match { - case _: Long => a.isNumber && a.fitsInLong && a.asLong == b - case _: Int => a.isNumber && a.fitsInInt && a.asInt == b - case _ => false + case _: Long => a.isNumber && a.fitsInLong && a.asLong == b + case _: Int => a.isNumber && a.fitsInInt && a.asInt == b + case _: String => a.isString && a.asString == b + case _ => false } } diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala index b76474d3f5..a23e62b6e3 100644 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala @@ -42,7 +42,7 @@ class RuntimeServerTest .newBuilder(LanguageInfo.ID) .allowExperimentalOptions(true) .allowAllAccess(true) - .option(RuntimeOptions.getPackagesPathOption, pkg.root.getAbsolutePath) + .option(RuntimeOptions.PACKAGES_PATH, pkg.root.getAbsolutePath) .option(RuntimeServerInfo.ENABLE_OPTION, "true") .out(out) .serverTransport { (uri, peer) => @@ -383,7 +383,9 @@ class RuntimeServerTest ) // recompute - context.send(Api.Request(requestId, Api.RecomputeContextRequest(contextId, None))) + context.send( + Api.Request(requestId, Api.RecomputeContextRequest(contextId, None)) + ) Set.fill(5)(context.receive) shouldEqual Set( Some(Api.Response(requestId, Api.RecomputeContextResponse(contextId))), Some(Main.update.idMainX(contextId)), diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/CompileErrorsTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/CompileErrorsTest.scala new file mode 100644 index 0000000000..105ada421b --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/CompileErrorsTest.scala @@ -0,0 +1,40 @@ +package org.enso.interpreter.test.semantic + +import org.enso.interpreter.test.InterpreterTest + +class CompileErrorsTest extends InterpreterTest { + "ast-processing errors" should "be surfaced in the language" in { + val code = + """ + |main = + | x = Panic.recover () + | x.catch err-> + | case err of + | Syntax_Error msg -> "Oopsie, it's a syntax error: " + msg + |""".stripMargin + eval(code) shouldEqual "Oopsie, it's a syntax error: Parentheses can't be empty." + } + + "parsing errors" should "be surfaced in the language" in { + val code = + """ + |main = + | x = Panic.recover @ + | x.catch to_text + |""".stripMargin + eval(code) shouldEqual "Syntax_Error Unrecognized token." + } + + "redefinition errors" should "be surfaced in the language" in { + val code = + """ + |foo = + | x = 1 + | x = 2 + | + |main = Panic.recover here.foo . catch to_text + |""".stripMargin + eval(code) shouldEqual "Compile_Error Variable x is being redefined." + } + +} diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/StrictCompileErrorsTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/StrictCompileErrorsTest.scala new file mode 100644 index 0000000000..5b23049599 --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/StrictCompileErrorsTest.scala @@ -0,0 +1,34 @@ +package org.enso.interpreter.test.semantic + +import org.enso.interpreter.test.{ + InterpreterException, + InterpreterTest, +} +import org.enso.polyglot.{LanguageInfo, RuntimeOptions} +import org.graalvm.polyglot.Context + +class StrictCompileErrorsTest extends InterpreterTest { + override val ctx: Context = Context + .newBuilder(LanguageInfo.ID) + .allowExperimentalOptions(true) + .option(RuntimeOptions.STRICT_ERRORS, "true") + .out(output) + .build() + + "Compile errors in batch mode" should "be reported and abort execution" in { + val code = + """main = + | x = () + | x = 5 + | y = @ + |""".stripMargin.lines.mkString("\n") + + the[InterpreterException] thrownBy eval(code) should have message "Compilation aborted due to errors." + val _ :: errors = consumeOut + errors.toSet shouldEqual Set( + "Test[2:9-2:10]: Parentheses can't be empty.", + "Test[3:5-3:9]: Variable x is being redefined.", + "Test[4:9-4:9]: Unrecognized token." + ) + } +}