Skip to content

Commit 5ccfd52

Browse files
committed
address some feedback, borrow a lot of scala#16685
1 parent ec2e8dc commit 5ccfd52

File tree

11 files changed

+175
-82
lines changed

11 files changed

+175
-82
lines changed

compiler/src/dotty/tools/dotc/CompilationUnit.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import core.Decorators._
1616
import config.{SourceVersion, Feature}
1717
import StdNames.nme
1818
import scala.annotation.internal.sharable
19+
import scala.util.control.ControlThrowable
1920
import transform.MacroAnnotations
2021

2122
class CompilationUnit protected (val source: SourceFile) {
@@ -105,7 +106,7 @@ class CompilationUnit protected (val source: SourceFile) {
105106

106107
object CompilationUnit {
107108

108-
class SuspendException extends Exception
109+
class SuspendException extends ControlThrowable
109110

110111
/** Make a compilation unit for top class `clsd` with the contents of the `unpickled` tree */
111112
def apply(clsd: ClassDenotation, unpickled: Tree, forceTrees: Boolean)(using Context): CompilationUnit =

compiler/src/dotty/tools/dotc/Run.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
182182
if units.nonEmpty then report.echo(i"exception occurred while compiling $units%, %")
183183
else report.echo(s"exception occurred while compiling ${files.map(_.name).mkString(", ")}")
184184
throw ex
185+
case _: Implosion =>
186+
// All handling related to the Implosion is done during creation, so we can swallow this
185187

186188
/** TODO: There's a fundamental design problem here: We assemble phases using `fusePhases`
187189
* when we first build the compiler. But we modify them with -Yskip, -Ystop

compiler/src/dotty/tools/dotc/cc/CaptureSet.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package dotc
33
package cc
44

55
import core.*
6-
import Types.*, Symbols.*, Flags.*, Contexts.*, Decorators.*
6+
import Types.*, Symbols.*, Flags.*, Contexts.*, ContextOps.*, Decorators.*
77
import config.Printers.capt
88
import Annotations.Annotation
99
import annotation.threadUnsafe
@@ -690,7 +690,7 @@ object CaptureSet:
690690
upper.isAlwaysEmpty || upper.isConst && upper.elems.size == 1 && upper.elems.contains(r1)
691691
if variance > 0 || isExact then upper
692692
else if variance < 0 then CaptureSet.empty
693-
else assert(false, i"trying to add $upper from $r via ${tm.getClass} in a non-variant setting")
693+
else ctx.implode( i"trying to add $upper from $r via ${tm.getClass} in a non-variant setting")
694694

695695
/** Apply `f` to each element in `xs`, and join result sets with `++` */
696696
def mapRefs(xs: Refs, f: CaptureRef => CaptureSet)(using Context): CaptureSet =

compiler/src/dotty/tools/dotc/core/ContextOps.scala

Lines changed: 26 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,37 @@ import Contexts._, Symbols._, Types._, Flags._
55
import Denotations._, SymDenotations._
66
import Names.Name, StdNames.nme
77
import ast.untpd
8+
import util.*
89
import scala.util.control.NonFatal
910

1011
/** Extension methods for contexts where we want to keep the ctx.<methodName> syntax */
1112
object ContextOps:
12-
case class Implosion(ex: Throwable) extends Exception(ex)
13+
1314
extension (ctx: Context)
14-
/** Crash somewhat gracefully, reporting useful info.*/
15-
def implode(message: => Any, ex: Throwable): Nothing = {
16-
ex match
17-
case NonFatal(Implosion(_)) => throw ex // avoid implosions all the way down, ideally we should avoid catching Implosions in the first place
18-
case NonFatal(ex) =>
19-
given Context = ctx
20-
report.error(
21-
s"""|An unhandled exception was thrown in the compiler. Please file a crash
22-
|report here: https://github.com/lampepfl/dotty/issues/new/choose
23-
|
24-
|Exception: ${ex.getClass}
25-
| message: ${ex.getMessage()}
26-
| cause: ${ex.getCause()}
27-
| (truncated) stack trace:
28-
| ${ex.getStackTrace().nn.take(10).nn.mkString("\n ")}
29-
|Sources:
30-
| ${ctx.base.sources.keysIterator.mkString(":")}
31-
|Files:
32-
| ${ctx.base.files.keysIterator.mkString(":")}
33-
|Context:
34-
| phase: ${ctx.phase}
35-
| owner: ${ctx.owner}
36-
| unit: ${ctx.compilationUnit}
37-
| mode: ${ctx.mode}
38-
| source: ${ctx.source}:${ctx.tree.sourcePos.line + 1}:${ctx.tree.sourcePos.column + 1}
39-
| ${ctx.tree}
40-
|
41-
|""".stripMargin,
42-
ctx.tree.sourcePos)
43-
44-
throw Implosion(ex)
45-
case _ => throw ex
46-
}
15+
/** Crash the compiler with a `Throwable` in a controlled way, reporting useful info
16+
* and asking users to submit a crash report.
17+
* This helps users by providing information they can use to work around the crash
18+
* and helps compiler maintainers by capturing important information about the crash.
19+
* With the exception of `ControlThrowable`s, this method should be used instead of
20+
* throwing exceptions whenever a context is available.
21+
*
22+
* instead of:
23+
* `throw Exception("foo")`
24+
* use:
25+
* `ctx.implode(Exception("foo"))`
26+
*/
27+
inline def implode(inline cause: Throwable): Nothing =
28+
Implosion(cause)(using ctx)
29+
30+
inline def implode(inline msg: String): Nothing =
31+
implode(AssertionError(msg))
32+
33+
/**
34+
* A fallback for when an exception has been thrown without using `implode`
35+
*
36+
*/
37+
def lateImplode(cause: Throwable): Nothing =
38+
Implosion(Exception("Context was thrown away, this crash report may not be accurate", (cause)))(using ctx)
4739

4840
/** Enter symbol into current class, if current class is owner of current context,
4941
* or into current scope, if not. Should always be called instead of scope.enter

compiler/src/dotty/tools/dotc/core/Phases.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,9 @@ object Phases {
325325
units.map { unit =>
326326
val unitCtx = ctx.fresh.setPhase(this.start).setCompilationUnit(unit).withRootImports
327327
try run(using unitCtx)
328-
catch case ex: Throwable if !(ex.isInstanceOf[Implosion] || ex.isInstanceOf[SuspendException]) =>
329-
unitCtx.implode(i"$ex while running $phaseName on $unit", ex)
328+
catch
329+
case NonFatal(ex) =>
330+
unitCtx.lateImplode(Exception(i"$ex while running $phaseName on $unit", ex))
330331

331332
unitCtx.compilationUnit
332333
}

compiler/src/dotty/tools/dotc/transform/Erasure.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import ContextFunctionResults._
3434
import ExplicitOuter._
3535
import core.Mode
3636
import util.Property
37-
import util.Assert.assert
37+
3838
import reporting._
3939

4040
class Erasure extends Phase with DenotTransformer {

compiler/src/dotty/tools/dotc/transform/Recheck.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ abstract class Recheck extends Phase, SymTransformer:
285285
constFold(tree, instantiate(fntpe, argTypes, tree.fun.symbol))
286286
//.showing(i"typed app $tree : $fntpe with ${tree.args}%, % : $argTypes%, % = $result")
287287
case tp =>
288-
assert(false, i"unexpected type of ${tree.fun}: $funtpe")
288+
ctx.implode(i"unexpected type of ${tree.fun}: $funtpe")
289289

290290
def recheckTypeApply(tree: TypeApply, pt: Type)(using Context): Type =
291291
recheck(tree.fun).widen match

compiler/src/dotty/tools/dotc/typer/ReTyper.scala

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,8 @@ class ReTyper(nestingLevel: Int = 0) extends Typer(nestingLevel) with ReChecking
127127
try super.typedUnadapted(tree, pt, locked)
128128
catch {
129129
case NonFatal(ex) if ctx.phase != Phases.typerPhase && ctx.phase != Phases.inliningPhase =>
130-
ctx.implode(
131-
i"exception while typing $tree of class ${tree.getClass} # ${tree.uniqueId}",
132-
ex
130+
ctx.lateImplode(
131+
Exception(i"exception while typing $tree of class ${tree.getClass} # ${tree.uniqueId}", ex)
133132
)
134133

135134
}

compiler/src/dotty/tools/dotc/util/Assertion.scala

Lines changed: 0 additions & 32 deletions
This file was deleted.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package dotty.tools.dotc
2+
package util
3+
4+
import core.*
5+
import Contexts.*, Flags.*, ContextOps.*, Symbols.*, Decorators.*
6+
import reporting.*
7+
import util.{SourcePosition, NoSourcePosition, SrcPos}
8+
import ast.*
9+
10+
11+
import scala.util.control.ControlThrowable
12+
13+
/**
14+
* An `Error` that indicates that the compiler has crashed.
15+
* This is used to capture and report important information from the context when the compiler crashes.
16+
* An `Implosion` should only be caught at the edge of the compiler to prevent the crash propagating to the caller of the compiler.
17+
*
18+
* No handling is required
19+
*
20+
* @param cause The exception that caused the crash.
21+
*/
22+
class Implosion private (message: String, val cause: Throwable) extends ControlThrowable:
23+
override def getCause(): Throwable = cause
24+
override def stackTrace(): Array[StackTraceElement] = cause.getStackTrace()
25+
cause.setStackTrace(Array()) //Avoid printing the stack trace twice
26+
27+
28+
object Implosion :
29+
30+
def apply(cause: Throwable)(using ctx: Context): Nothing =
31+
if cause.isInstanceOf[ControlThrowable] then
32+
//not rethrown because `implode` should always crash the compiler
33+
report.error("A `ControlThrowable` was used to crash the compiler")
34+
val message = enrichErrorMessage(cause.getMessage())
35+
report.error(message, ctx.tree.sourcePos)
36+
throw new Implosion(message, cause)
37+
38+
private object messageRendering extends MessageRendering
39+
40+
private def enrichErrorMessage(errorMessage: String)(using Context): String =
41+
// record state as we go along so we can report something more than the errorMessage if there's an error while reporting
42+
val blackBox = new StringBuilder()
43+
44+
extension [A](a: A)
45+
transparent inline def record: A =
46+
blackBox.append(a).append("\n")
47+
a
48+
49+
50+
"""|Error during error reporting, black box recording:
51+
|An unhandled exception was thrown in the compiler. Please file a crash
52+
|report here: https://github.com/lampepfl/dotty/issues/new/choose)""".stripMargin.record
53+
54+
errorMessage.record
55+
56+
try {
57+
def formatExplain(pairs: List[(String, Any)]) =
58+
pairs.map((k, v) => f"$k%20s: $v").mkString("\n")
59+
60+
61+
val settings = ctx.settings.userSetSettings(ctx.settingsState).sortBy(_.name).record
62+
val tree = ctx.tree.record
63+
val sym = tree.symbol.record
64+
val pos = tree.sourcePos.record
65+
val path = pos.source.path.record
66+
val site = ctx.outersIterator.map(_.owner).filter(sym => !sym.exists || sym.isClass || sym.is(Method)).next().record
67+
68+
import untpd.*
69+
extension (tree: Tree) def summaryString: String = tree match
70+
case Literal(const) => s"Literal($const)"
71+
case Ident(name) => s"Ident(${name.decode})"
72+
case Select(qual, name) => s"Select(${qual.summaryString}, ${name.decode})"
73+
case tree: NameTree => (if tree.isType then "type " else "") + tree.name.decode
74+
case tree => s"${tree.className}${if tree.symbol.exists then s"(${tree.symbol})" else ""}"
75+
76+
"info1:".record
77+
val info1 = formatExplain(List(
78+
"while compiling".record -> ctx.compilationUnit.record,
79+
"during phase".record -> ctx.phase.prevMega.record,
80+
"mode".record -> ctx.mode.record,
81+
"library version".record -> scala.util.Properties.versionString.record,
82+
"compiler version".record -> dotty.tools.dotc.config.Properties.versionString.record,
83+
"settings".record -> settings.map(s => if s.value == "" then s"${s.name} \"\"" else s"${s.name} ${s.value}").mkString(" ").record,
84+
))
85+
"symbolInfos:".record
86+
val symbolInfos = if sym eq NoSymbol then List("symbol".record -> sym.record) else List(
87+
"symbol".record -> sym.showLocated.record,
88+
"symbol definition".record -> s"${sym.showDcl} (a ${sym.className})".record,
89+
"symbol package".record -> sym.enclosingPackageClass.fullName.record,
90+
"symbol owners".record -> sym.showExtendedLocation.record,
91+
)
92+
"info2:".record
93+
val info2 = formatExplain(List(
94+
"tree".record -> tree.summaryString,
95+
"tree position".record -> (if pos.exists then s"$path:${pos.line + 1}:${pos.column}" else s"$path:<unknown>"),
96+
"tree type".record -> tree.typeOpt.show.record,
97+
) ::: symbolInfos.record ::: List(
98+
"call site".record -> s"${site.showLocated} in ${site.enclosingPackageClass}".record
99+
))
100+
"context_s:".record
101+
val context_s = try
102+
s""" == Source file context for tree position ==
103+
|
104+
|${messageRendering.messageAndPos(Diagnostic.Error("", pos))}""".stripMargin.record
105+
catch case _: Exception => "<Cannot read source file>".record
106+
s"""
107+
| $errorMessage
108+
|
109+
| An unhandled exception was thrown in the compiler. Please file a crash
110+
| report here: https://github.com/lampepfl/dotty/issues/new/choose
111+
|
112+
|$info1
113+
|
114+
|$info2
115+
|
116+
|$context_s""".stripMargin
117+
} catch case _: Throwable =>
118+
// don't introduce new errors trying to report errors, so swallow exceptions and fall back to the blackBox recording
119+
blackBox.toString()

compiler/src/dotty/tools/package.scala

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,24 @@ package object tools {
3838

3939
def unreachable(x: Any = "<< this case was declared unreachable >>"): Nothing =
4040
throw new MatchError(x)
41+
42+
import dotty.tools.dotc.core.Contexts.Context
43+
transparent inline def assertShort(inline assertion: Boolean, inline message: Any = null)(using inline ctx: Context | Null = null): Unit =
44+
if !assertion then throwAssertionError(message, truncateStack = true)
45+
46+
//todo get location of assertion and add to message
47+
transparent inline def assert(inline assertion: Boolean, inline message: Any | Null = null)(using inline ctx: Context | Null = null): Unit =
48+
if !assertion then throwAssertionError(message)
4149

42-
transparent inline def assertShort(inline assertion: Boolean, inline message: Any = null): Unit =
43-
if !assertion then
44-
val msg = message
45-
val e = if msg == null then AssertionError() else AssertionError("assertion failed: " + msg)
46-
e.setStackTrace(Array())
47-
throw e
50+
// extracted from `assert` to make it as small (and inlineable) as possible
51+
private def throwAssertionError(message: Any | Null, truncateStack: Boolean = false)(using ctx: Context | Null): Unit =
52+
import dotty.tools.dotc.core.ContextOps.implode
53+
val assertionError = AssertionError("assertion failed: " + String.valueOf(message).nn)
54+
if truncateStack then assertionError.setStackTrace(Array())
55+
if ctx == null then
56+
throw assertionError
57+
else
58+
ctx.implode(assertionError)
4859

4960
// Ensure this object is already classloaded, since it's only actually used
5061
// when handling stack overflows and every operation (including class loading)

0 commit comments

Comments
 (0)