Skip to content

Commit

Permalink
Improve compiler's diagnostic messages (#6931)
Browse files Browse the repository at this point in the history
Improve and colorize compiler's messages. Heavily inspired by `gcc`.
  • Loading branch information
Akirathan authored Jun 8, 2023
1 parent 9f39fb3 commit 372bc8f
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 81 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,7 @@
- [Add project creation time to project metadata][6780]
- [Upgrade GraalVM to 22.3.1 JDK17][6750]
- [Ascribed types are checked during runtime][6790]
- [Improve and colorize compiler's diagnostic messages][6931]

[3227]: https://github.com/enso-org/enso/pull/3227
[3248]: https://github.com/enso-org/enso/pull/3248
Expand Down Expand Up @@ -929,6 +930,7 @@
[6755]: https://github.com/enso-org/enso/pull/6755
[6780]: https://github.com/enso-org/enso/pull/6780
[6790]: https://github.com/enso-org/enso/pull/6790
[6931]: https://github.com/enso-org/enso/pull/6931

# Enso 2.0.0-alpha.18 (2021-10-12)

Expand Down
4 changes: 3 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ val typesafeConfigVersion = "1.4.2"
val junitVersion = "4.13.2"
val junitIfVersion = "0.11"
val netbeansApiVersion = "RELEASE140"
val fansiVersion = "0.4.0"

// ============================================================================
// === Internal Libraries =====================================================
Expand Down Expand Up @@ -1339,7 +1340,8 @@ lazy val runtime = (project in file("engine/runtime"))
"org.graalvm.truffle" % "truffle-api" % graalVersion % Benchmark,
"org.typelevel" %% "cats-core" % catsVersion,
"junit" % "junit" % junitVersion % Test,
"com.novocode" % "junit-interface" % junitIfVersion % Test exclude ("junit", "junit-dep")
"com.novocode" % "junit-interface" % junitIfVersion % Test exclude ("junit", "junit-dep"),
"com.lihaoyi" %% "fansi" % fansiVersion % "provided"
),
Compile / compile / compileInputs := (Compile / compile / compileInputs)
.dependsOn(CopyTruffleJAR.preCompileTask)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.enso.compiler.Compiler;
import org.enso.compiler.context.InlineContext;
import org.enso.compiler.data.CompilerConfig;
Expand Down Expand Up @@ -265,10 +264,7 @@ protected ExecutableNode parse(InlineParsingRequest request) throws Exception {
throw new InlineParsingException("Unhandled entity: " + e.entity(), e);
} catch (CompilationAbortedException e) {
assert outputRedirect.toString().lines().count() > 1 : "Expected a header line from the compiler";
String compilerErrOutput = outputRedirect.toString()
.lines()
.skip(1)
.collect(Collectors.joining(";"));
String compilerErrOutput = outputRedirect.toString();
throw new InlineParsingException(compilerErrOutput, e);
} finally {
silentCompiler.shutdown(false);
Expand Down
209 changes: 175 additions & 34 deletions engine/runtime/src/main/scala/org/enso/compiler/Compiler.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.enso.compiler

import com.oracle.truffle.api.TruffleLogger
import com.oracle.truffle.api.source.Source
import com.oracle.truffle.api.source.{Source, SourceSection}
import org.enso.compiler.codegen.{IrToTruffle, RuntimeStubsGenerator}
import org.enso.compiler.context.{FreshNameSupply, InlineContext, ModuleContext}
import org.enso.compiler.core.IR
Expand All @@ -21,22 +21,21 @@ import org.enso.interpreter.runtime.scope.{LocalScope, ModuleScope}
import org.enso.interpreter.runtime.{EnsoContext, Module}
import org.enso.pkg.QualifiedName
import org.enso.polyglot.{LanguageInfo, RuntimeOptions}
import org.enso.syntax.text.Parser.IDMap
import org.enso.syntax.text.Parser
import org.enso.syntax.text.Parser.IDMap
import org.enso.syntax2.Tree

import java.io.{PrintStream, StringReader}
import java.util.concurrent.{
CompletableFuture,
ExecutorService,
Future,
LinkedBlockingDeque,
ThreadPoolExecutor,
TimeUnit
}
import java.util.logging.Level

import scala.jdk.OptionConverters._
import java.util.concurrent.Future

/** This class encapsulates the static transformation processes that take place
* on source code, including parsing, desugaring, type-checking, static
Expand Down Expand Up @@ -966,7 +965,6 @@ class Compiler(
diagnostics
.foldLeft(false) { case (result, (mod, diags)) =>
if (diags.nonEmpty) {
output.println(s"In module ${mod.getName}:")
reportDiagnostics(diags, mod.getSource) || result
} else {
result
Expand All @@ -985,41 +983,184 @@ class Compiler(
diagnostics: List[IR.Diagnostic],
source: Source
): Boolean = {
val errors = diagnostics.collect { case e: IR.Error => e }
val warnings = diagnostics.collect { case w: IR.Warning => w }
diagnostics.foreach(diag =>
output.println(new DiagnosticFormatter(diag, source).format())
)
diagnostics.exists(_.isInstanceOf[IR.Error])
}

if (warnings.nonEmpty) {
output.println("Compiler encountered warnings:")
warnings.foreach { warning =>
output.println(formatDiagnostic(warning, source))
/** Formatter of IR diagnostics. Heavily inspired by GCC. Can format one-line as well as multiline
* diagnostics. The output is colorized if the output stream supports ANSI colors.
* Also prints the offending lines from the source along with line number - the same way as
* GCC does.
* @param diagnostic the diagnostic to pretty print
* @param source the original source code
*/
private class DiagnosticFormatter(
private val diagnostic: IR.Diagnostic,
private val source: Source
) {
private val maxLineNum = 99999
private val blankLinePrefix = " | "
private val maxSourceLinesToPrint = 3
private val linePrefixSize = blankLinePrefix.length
private val outSupportsAnsiColors: Boolean = outSupportsColors
private val (textAttrs: fansi.Attrs, subject: String) = diagnostic match {
case _: IR.Error => (fansi.Color.Red ++ fansi.Bold.On, "error: ")
case _: IR.Warning => (fansi.Color.Yellow ++ fansi.Bold.On, "warning: ")
case _ => throw new IllegalStateException("Unexpected diagnostic type")
}
private val sourceSection: Option[SourceSection] =
diagnostic.location match {
case Some(location) =>
Some(source.createSection(location.start, location.length))
case None => None
}
private val shouldPrintLineNumber = sourceSection match {
case Some(section) =>
section.getStartLine <= maxLineNum && section.getEndLine <= maxLineNum
case None => false
}

if (errors.nonEmpty) {
output.println("Compiler encountered errors:")
errors.foreach { error =>
output.println(formatDiagnostic(error, source))
def format(): String = {
sourceSection match {
case Some(section) =>
val isOneLine = section.getStartLine == section.getEndLine
val srcPath: String =
if (source.getPath == null && source.getName == null) {
"<Unknown source>"
} else if (source.getPath != null) {
source.getPath
} else {
source.getName
}
if (isOneLine) {
val lineNumber = section.getStartLine
val startColumn = section.getStartColumn
val endColumn = section.getEndColumn
var str = fansi.Str()
str ++= fansi
.Str(srcPath + ":" + lineNumber + ":" + startColumn + ": ")
.overlay(fansi.Bold.On)
str ++= fansi.Str(subject).overlay(textAttrs)
str ++= diagnostic.formattedMessage
str ++= "\n"
str ++= oneLineFromSourceColored(lineNumber, startColumn, endColumn)
str ++= "\n"
str ++= underline(startColumn, endColumn)
if (outSupportsAnsiColors) {
str.render.stripLineEnd
} else {
str.plainText.stripLineEnd
}
} else {
var str = fansi.Str()
str ++= fansi
.Str(
srcPath + ":[" + section.getStartLine + ":" + section.getStartColumn + "-" + section.getEndLine + ":" + section.getEndColumn + "]: "
)
.overlay(fansi.Bold.On)
str ++= fansi.Str(subject).overlay(textAttrs)
str ++= diagnostic.formattedMessage
str ++= "\n"
val printAllSourceLines =
section.getEndLine - section.getStartLine <= maxSourceLinesToPrint
val endLine =
if (printAllSourceLines) section.getEndLine
else section.getStartLine + maxSourceLinesToPrint
for (lineNum <- section.getStartLine to endLine) {
str ++= oneLineFromSource(lineNum)
str ++= "\n"
}
if (!printAllSourceLines) {
val restLineCount =
section.getEndLine - section.getStartLine - maxSourceLinesToPrint
str ++= blankLinePrefix + "... and " + restLineCount + " more lines ..."
str ++= "\n"
}
if (outSupportsAnsiColors) {
str.render.stripLineEnd
} else {
str.plainText.stripLineEnd
}
}
case None =>
// We dont have location information, so we just print the message
var str = fansi.Str()
str ++= fansi
.Str(fileLocationFromSection(diagnostic.location, source))
.overlay(fansi.Bold.On)
str ++= ": "
str ++= fansi.Str(subject).overlay(textAttrs)
str ++= diagnostic.formattedMessage
if (outSupportsAnsiColors) {
str.render.stripLineEnd
} else {
str.plainText.stripLineEnd
}
}
true
} else {
false
}
}

/** Pretty prints compiler diagnostics.
*
* @param diagnostic the diagnostic to pretty print
* @param source the original source code
* @return the result of pretty printing `diagnostic`
*/
private def formatDiagnostic(
diagnostic: IR.Diagnostic,
source: Source
): String = {
fileLocationFromSection(
diagnostic.location,
source
) + ": " + diagnostic.formattedMessage
/** @see https://github.com/termstandard/colors/
* @see https://no-color.org/
* @return
*/
private def outSupportsColors: Boolean = {
if (System.console() == null) {
// Non-interactive output is always without color support
return false
}
if (System.getenv("NO_COLOR") != null) {
return false
}
if (config.outputRedirect.isDefined) {
return false
}
if (System.getenv("COLORTERM") != null) {
return true
}
if (System.getenv("TERM") != null) {
val termEnv = System.getenv("TERM").toLowerCase
return termEnv.split("-").contains("color") || termEnv
.split("-")
.contains("256color")
}
return false
}

private def oneLineFromSource(lineNum: Int): String = {
val line = source.createSection(lineNum).getCharacters.toString
linePrefix(lineNum) + line
}

private def oneLineFromSourceColored(
lineNum: Int,
startCol: Int,
endCol: Int
): String = {
val line = source.createSection(lineNum).getCharacters.toString
linePrefix(lineNum) + fansi
.Str(line)
.overlay(textAttrs, startCol - 1, endCol)
}

private def linePrefix(lineNum: Int): String = {
if (shouldPrintLineNumber) {
val pipeSymbol = " | "
val prefixWhitespaces =
linePrefixSize - lineNum.toString.length - pipeSymbol.length
" " * prefixWhitespaces + lineNum + pipeSymbol
} else {
blankLinePrefix
}
}

private def underline(startColumn: Int, endColumn: Int): String = {
val sectionLen = endColumn - startColumn
blankLinePrefix +
" " * (startColumn - 1) +
fansi.Str("^" + ("~" * sectionLen)).overlay(textAttrs)
}
}

private def fileLocationFromSection(
Expand All @@ -1038,7 +1179,7 @@ class Compiler(
"[" + locStr + "]"
}
.getOrElse("")
source.getName + srcLocation
source.getPath + ":" + srcLocation
}

/** Generates code for the truffle interpreter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import org.apache.commons.lang3.SystemUtils
import org.enso.interpreter.test.InterpreterException

class CacheInvalidationTest extends ModifiedTest {
private def isDiagnosticLine(line: String): Boolean = {
line.contains(" | ")
}

"IR caching" should "should propagate invalidation" in {
assume(!SystemUtils.IS_OS_WINDOWS)
evalTestProjectIteration("Test_Caching_Invalidation", iteration = 1)
Expand All @@ -18,7 +22,7 @@ class CacheInvalidationTest extends ModifiedTest {
"Test_Caching_Invalidation",
iteration = 3
) should have message "Compilation aborted due to errors."
val outLines3 = consumeOut
outLines3(2) should endWith("The name `foo` could not be found.")
val outLines3 = consumeOut.filterNot(isDiagnosticLine)
outLines3.head should endWith("The name `foo` could not be found.")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class InterpreterContext(
.err(err)
.option(RuntimeOptions.LOG_LEVEL, "WARNING")
.option(RuntimeOptions.DISABLE_IR_CACHES, "true")
.environment("NO_COLOR", "true")
.logHandler(System.err)
.in(in)
.option(
Expand Down
Loading

0 comments on commit 372bc8f

Please sign in to comment.