Compile Error Handling (#687)

This commit is contained in:
Marcin Kostrzewa 2020-04-28 14:03:33 +02:00 committed by GitHub
parent 92d8393495
commit 4ba26a3034
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 681 additions and 108 deletions

View File

@ -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:

View File

@ -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(

View File

@ -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<String> 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<Boolean> 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;
}
}

View File

@ -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()
)

View File

@ -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

View File

@ -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.")
}
}
/**

View File

@ -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);
}
}

View File

@ -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.
*

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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."
}
}
}

View File

@ -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
}

View File

@ -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 })
}

View File

@ -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))
}
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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)),

View File

@ -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."
}
}

View File

@ -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."
)
}
}