diff --git a/compiler/src/dotty/tools/backend/jvm/BTypes.scala b/compiler/src/dotty/tools/backend/jvm/BTypes.scala index 57bd343b6658..652fa239043f 100644 --- a/compiler/src/dotty/tools/backend/jvm/BTypes.scala +++ b/compiler/src/dotty/tools/backend/jvm/BTypes.scala @@ -256,7 +256,7 @@ abstract class BTypes { */ final def maxValueType(other: BType): BType = { - def uncomparable: Nothing = throw new AssertionError(s"Cannot compute maxValueType: $this, $other") + def uncomparable: Nothing = ctx.implode(s"Cannot compute maxValueType: $this, $other") if (!other.isPrimitive && !other.isNothingType) uncomparable diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 4caf1f6b5fa2..2efc0a2b30bc 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -2368,10 +2368,10 @@ class JSCodeGen()(using genCtx: Context) { // Construct inline class definition val jsClassCaptures = originalClassDef.jsClassCaptures.getOrElse { - throw new AssertionError(s"no class captures for anonymous JS class at $pos") + genCtx.implode(s"no class captures for anonymous JS class at $pos") } val js.JSConstructorDef(_, ctorParams, ctorRestParam, ctorBody) = constructor.getOrElse { - throw new AssertionError("No ctor found") + genCtx.implode("No ctor found") } assert(ctorParams.isEmpty && ctorRestParam.isEmpty, s"non-empty constructor params for anonymous JS class at $pos") @@ -2396,17 +2396,17 @@ class JSCodeGen()(using genCtx: Context) { val memberDefinitions0 = instanceMembers.toList.map { case fdef: js.FieldDef => - throw new AssertionError("unexpected FieldDef") + genCtx.implode("unexpected FieldDef") case fdef: js.JSFieldDef => implicit val pos = fdef.pos js.Assign(js.JSSelect(selfRef, fdef.name), jstpe.zeroOf(fdef.ftpe)) case mdef: js.MethodDef => - throw new AssertionError("unexpected MethodDef") + genCtx.implode("unexpected MethodDef") case cdef: js.JSConstructorDef => - throw new AssertionError("unexpected JSConstructorDef") + genCtx.implode("unexpected JSConstructorDef") case mdef: js.JSMethodDef => implicit val pos = mdef.pos @@ -2773,7 +2773,7 @@ class JSCodeGen()(using genCtx: Context) { if (from == to || from == jstpe.NothingType) { value } else if (from == jstpe.BooleanType || to == jstpe.BooleanType) { - throw new AssertionError(s"Invalid genConversion from $from to $to") + genCtx.implode(s"Invalid genConversion from $from to $to") } else { def intValue = (from: @unchecked) match { case jstpe.IntType => value @@ -3134,7 +3134,7 @@ class JSCodeGen()(using genCtx: Context) { case value :: Nil => genSelectSet(genExpr(jsName), value) case _ => - throw new AssertionError(s"property methods should have 0 or 1 non-varargs arguments at $pos") + genCtx.implode(s"property methods should have 0 or 1 non-varargs arguments at $pos") } case JSCallingConvention.BracketAccess => @@ -3144,7 +3144,7 @@ class JSCodeGen()(using genCtx: Context) { case keyArg :: valueArg :: Nil => genSelectSet(keyArg, valueArg) case _ => - throw new AssertionError(s"@JSBracketAccess methods should have 1 or 2 non-varargs arguments at $pos") + genCtx.implode(s"@JSBracketAccess methods should have 1 or 2 non-varargs arguments at $pos") } case JSCallingConvention.BracketCall => @@ -3238,7 +3238,7 @@ class JSCodeGen()(using genCtx: Context) { s"Trying to call the super constructor of Object in a non-native JS class at $pos") genApplyMethod(genReceiver, sym, genScalaArgs) } else if (sym.isClassConstructor) { - throw new AssertionError( + genCtx.implode( s"calling a JS super constructor should have happened in genPrimaryJSClassCtor at $pos") } else if (sym.owner.isNonNativeJSClass && !sym.isJSExposed) { // Reroute to the static method @@ -3358,7 +3358,7 @@ class JSCodeGen()(using genCtx: Context) { clauses = clauses.reverse val defaultClause = optDefaultClause.getOrElse { - throw new AssertionError("No elseClause in pattern match") + genCtx.implode("No elseClause in pattern match") } /* Builds a `js.Match`, but simplifies it to a `js.If` if there is only diff --git a/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala b/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala index 78412999bb34..169153a1157b 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala @@ -176,7 +176,7 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { js.TopLevelMethodExportDef(info.moduleID, methodDef) case Property => - throw new AssertionError("found top-level exported property") + ctx.implode("found top-level exported property") case Field => val sym = checkSingleField(tups) @@ -222,7 +222,7 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { js.JSFieldDef(flags, name, irTpe) case kind => - throw new AssertionError(s"unexpected static export kind: $kind") + ctx.implode(s"unexpected static export kind: $kind") } }).toList } @@ -743,7 +743,7 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { val modAccessor = outer.info.allMembers.find { denot => denot.symbol.is(Module) && denot.name.unexpandedName == name }.getOrElse { - throw new AssertionError(i"could not find module accessor for ${targetSym.fullName} at $pos") + ctx.implode(i"could not find module accessor for ${targetSym.fullName} at $pos") }.symbol val receiver = captures.head diff --git a/compiler/src/dotty/tools/dotc/Driver.scala b/compiler/src/dotty/tools/dotc/Driver.scala index 0af35dd4068a..4892769dadb5 100644 --- a/compiler/src/dotty/tools/dotc/Driver.scala +++ b/compiler/src/dotty/tools/dotc/Driver.scala @@ -4,6 +4,7 @@ import dotty.tools.FatalError import config.CompilerCommand import core.Comments.{ContextDoc, ContextDocstrings} import core.Contexts._ +import util.Implosion import core.{MacroClassLoader, TypeError} import dotty.tools.dotc.ast.Positioned import dotty.tools.io.AbstractFile @@ -35,14 +36,11 @@ class Driver { run.compile(files) finish(compiler, run) catch - case ex: FatalError => - report.error(ex.getMessage.nn) // signals that we should fail compilation. - case ex: TypeError => - println(s"${ex.toMessage} while compiling ${files.map(_.path).mkString(", ")}") - throw ex + case ex: Implosion => + // All handling related to the Implosion is done during creation, so we can swallow this case ex: Throwable => - println(s"$ex while compiling ${files.map(_.path).mkString(", ")}") - throw ex + ctx.lateImplode(ex) //this should never happen except in the case of `FatalError`s + ctx.reporter protected def finish(compiler: Compiler, run: Run)(using Context): Unit = diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index f7a08d1640ee..5c54e20d47ae 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -179,10 +179,15 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint compileSources(sources) catch case NonFatal(ex) => - if units.nonEmpty then report.echo(i"exception occurred while compiling $units%, %") - else report.echo(s"exception occurred while compiling ${files.map(_.name).mkString(", ")}") - throw ex - + try + if units.nonEmpty then ctx.lateImplode(i"exception occurred while compiling $units%, %") + else ctx.lateImplode(s"exception occurred while compiling ${files.map(_.name).mkString(", ")}") + catch + case _: Implosion => + + case _ : Implosion => + // All handling related to the Implosion is done during creation, so we can swallow this + /** TODO: There's a fundamental design problem here: We assemble phases using `fusePhases` * when we first build the compiler. But we modify them with -Yskip, -Ystop * on each run. That modification needs to either transform the tree structure, diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 2b9fe9d3d923..f1f348ecacef 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -690,7 +690,7 @@ object CaptureSet: upper.isAlwaysEmpty || upper.isConst && upper.elems.size == 1 && upper.elems.contains(r1) if variance > 0 || isExact then upper else if variance < 0 then CaptureSet.empty - else assert(false, i"trying to add $upper from $r via ${tm.getClass} in a non-variant setting") + else ctx.implode( i"trying to add $upper from $r via ${tm.getClass} in a non-variant setting") /** Apply `f` to each element in `xs`, and join result sets with `++` */ def mapRefs(xs: Refs, f: CaptureRef => CaptureSet)(using Context): CaptureSet = diff --git a/compiler/src/dotty/tools/dotc/core/ContextOps.scala b/compiler/src/dotty/tools/dotc/core/ContextOps.scala index aa85f714a8e5..3d0b5903a2a2 100644 --- a/compiler/src/dotty/tools/dotc/core/ContextOps.scala +++ b/compiler/src/dotty/tools/dotc/core/ContextOps.scala @@ -8,7 +8,6 @@ import ast.untpd /** Extension methods for contexts where we want to keep the ctx. syntax */ object ContextOps: - extension (ctx: Context) /** Enter symbol into current class, if current class is owner of current context, diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index f920027032a7..9ebbe0de2496 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -13,7 +13,7 @@ import Scopes._ import Uniques._ import ast.Trees._ import ast.untpd -import util.{NoSource, SimpleIdentityMap, SourceFile, HashSet, ReusableInstance} +import util.{NoSource, SimpleIdentityMap, SourceFile, HashSet, ReusableInstance, Implosion} import typer.{Implicits, ImportInfo, SearchHistory, SearchRoot, TypeAssigner, Typer, Nullables} import inlines.Inliner import Nullables._ @@ -454,6 +454,37 @@ object Contexts { util.Stats.record("Context.fresh") FreshContext(base).init(outer, this).setTyperState(this.typerState) + /** Crash the compiler with a `Throwable` in a controlled way, reporting useful info + * and asking users to submit a crash report. + * This helps users by providing information they can use to work around the crash + * and helps compiler maintainers by capturing important information about the crash. + * With the exception of `ControlThrowable`s, this method should be used instead of + * throwing exceptions whenever a context is available. + * + * instead of: + * `throw Exception("foo")` + * use: + * `ctx.implode(Exception("foo"))` + */ + inline def implode(inline cause: Throwable): Nothing = + Implosion(cause)(using thiscontext) + + /** + * Crash the compiler with a message in a controlled way + */ + inline def implode(inline msg: Any): Nothing = + implode(AssertionError(msg)) + + /** + * A fallback for when an exception has been thrown without using `implode` + * This will capture context from this context which may be a parent of the context the actual exception was thrown in. + */ + def lateImplode(cause: Throwable): Nothing = + Implosion(Exception("Context was thrown away, this crash report may not be accurate", (cause)))(using ctx) + + def lateImplode(msg: Any): Nothing = + lateImplode(AssertionError(msg)) + final def withOwner(owner: Symbol): Context = if (owner ne this.owner) fresh.setOwner(owner) else this diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index 205554e418ed..56635f514eac 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -22,6 +22,8 @@ import ast.{tpd, untpd} import scala.annotation.internal.sharable import scala.util.control.NonFatal + + object Phases { inline def phaseOf(id: PhaseId)(using Context): Phase = @@ -322,9 +324,10 @@ object Phases { units.map { unit => val unitCtx = ctx.fresh.setPhase(this.start).setCompilationUnit(unit).withRootImports try run(using unitCtx) - catch case ex: Throwable => - println(s"$ex while running $phaseName on $unit") - throw ex + catch + case NonFatal(ex) => + unitCtx.lateImplode(Exception(i"$ex while running $phaseName on $unit", ex)) + unitCtx.compilationUnit } diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index ea8dcee5fca5..0bedee4e0011 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -250,7 +250,7 @@ object TypeOps: } def mergeRefinedOrApplied(tp1: Type, tp2: Type): Type = { - def fail = throw new AssertionError(i"Failure to join alternatives $tp1 and $tp2") + def fail = ctx.implode(i"Failure to join alternatives $tp1 and $tp2") def fallback = tp2 match case AndType(tp21, tp22) => mergeRefinedOrApplied(tp1, tp21) & mergeRefinedOrApplied(tp1, tp22) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index b8b6b7248ee2..5b1edbb6910e 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3088,8 +3088,8 @@ object Types { override def underlying(using Context): Type = parent - private def badInst = - throw new AssertionError(s"bad instantiation: $this") + private def badInst(using Context) = + ctx.implode(s"bad instantiation: $this") def checkInst(using Context): this.type = this // debug hook diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index 7702e6a93446..d8760d88ac66 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -863,7 +863,7 @@ class ClassfileParser( // create a new class member for immediate inner classes if entry.outer.name == currentClassName then val file = ctx.platform.classPath.findClassFile(entry.externalName.toString) getOrElse { - throw new AssertionError(entry.externalName) + ctx.implode(entry.externalName) } enterClassAndModule(entry, file, entry.jflags) } diff --git a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala index f53359fb8b19..5a31a829355f 100644 --- a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala +++ b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala @@ -280,7 +280,7 @@ trait MessageRendering { sb.toString } - private def hl(str: String)(using Context, Level): String = + private def hl(str: String)(using Context, Level): String = summon[Level].value match case interfaces.Diagnostic.ERROR => Red(str).show case interfaces.Diagnostic.WARNING => Yellow(str).show diff --git a/compiler/src/dotty/tools/dotc/transform/Memoize.scala b/compiler/src/dotty/tools/dotc/transform/Memoize.scala index 5a2eda4101a4..5896a8c0c0ef 100644 --- a/compiler/src/dotty/tools/dotc/transform/Memoize.scala +++ b/compiler/src/dotty/tools/dotc/transform/Memoize.scala @@ -65,7 +65,7 @@ class Memoize extends MiniPhase with IdentityDenotTransformer { thisPhase => override def checkPostCondition(tree: Tree)(using Context): Unit = { def errorLackImplementation(t: Tree) = { val definingPhase = phaseOf(t.symbol.initial.validFor.firstPhaseId) - throw new AssertionError( + ctx.implode( i"Non-deferred definition introduced by $definingPhase lacks implementation: $t") } tree match { diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index c524bbb7702f..7ac7297b3e82 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -285,7 +285,7 @@ abstract class Recheck extends Phase, SymTransformer: constFold(tree, instantiate(fntpe, argTypes, tree.fun.symbol)) //.showing(i"typed app $tree : $fntpe with ${tree.args}%, % : $argTypes%, % = $result") case tp => - assert(false, i"unexpected type of ${tree.fun}: $funtpe") + ctx.implode(i"unexpected type of ${tree.fun}: $funtpe") def recheckTypeApply(tree: TypeApply, pt: Type)(using Context): Type = recheck(tree.fun).widen match diff --git a/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala b/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala index a897503ef275..b094bf2eaa45 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala @@ -88,7 +88,7 @@ object TypeUtils { def toNestedPairs(using Context): Type = tupleElementTypes match case Some(types) => TypeOps.nestedPairs(types) - case None => throw new AssertionError("not a tuple") + case None => ctx.implode("not a tuple") def refinedWith(name: Name, info: Type)(using Context) = RefinedType(self, name, info) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index e7197f75cd14..354374410bae 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -752,7 +752,7 @@ class SpaceEngine(using Context) extends SpaceLogic { /** Whether the counterexample is satisfiable. The space is flattened and non-empty. */ def satisfiable(sp: Space): Boolean = { - def impossible: Nothing = throw new AssertionError("`satisfiable` only accepts flattened space.") + def impossible: Nothing = ctx.implode("`satisfiable` only accepts flattened space.") def genConstraint(space: Space): List[(Type, Type)] = space match { case Prod(tp, unappTp, ss) => diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSExports.scala b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSExports.scala index 25ab46712e70..ef1ed2807c60 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSExports.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSExports.scala @@ -248,7 +248,7 @@ object PrepJSExports { } case _: ExportDestination.TopLevel => - throw new AssertionError( + ctx.implode( em"Found a top-level export without an explicit name at ${exportPos.sourcePos}") case ExportDestination.Static => diff --git a/compiler/src/dotty/tools/dotc/typer/ReTyper.scala b/compiler/src/dotty/tools/dotc/typer/ReTyper.scala index b53b2f9ec57a..42f9b3fc4dbd 100644 --- a/compiler/src/dotty/tools/dotc/typer/ReTyper.scala +++ b/compiler/src/dotty/tools/dotc/typer/ReTyper.scala @@ -125,10 +125,11 @@ class ReTyper(nestingLevel: Int = 0) extends Typer(nestingLevel) with ReChecking override def typedUnadapted(tree: untpd.Tree, pt: Type, locked: TypeVars)(using Context): Tree = try super.typedUnadapted(tree, pt, locked) catch { - case NonFatal(ex) => - if ctx.phase != Phases.typerPhase && ctx.phase != Phases.inliningPhase then - println(i"exception while typing $tree of class ${tree.getClass} # ${tree.uniqueId}") - throw ex + case NonFatal(ex) if ctx.phase != Phases.typerPhase && ctx.phase != Phases.inliningPhase => + ctx.lateImplode( + Exception(i"exception while typing $tree of class ${tree.getClass} # ${tree.uniqueId}", ex) + ) + } override def inlineExpansion(mdef: DefDef)(using Context): List[Tree] = mdef :: Nil diff --git a/compiler/src/dotty/tools/dotc/util/Implosion.scala b/compiler/src/dotty/tools/dotc/util/Implosion.scala new file mode 100644 index 000000000000..c1258294ae03 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/util/Implosion.scala @@ -0,0 +1,129 @@ +package dotty.tools +package dotc +package util + +import core.* +import Contexts.*, Flags.*, ContextOps.*, Symbols.*, Decorators.* +import reporting.* +import util.{SourcePosition, NoSourcePosition, SrcPos} +import ast.* + +import scala.util.control.ControlThrowable +import java.io.{PrintStream, PrintWriter, StringWriter} + + +/** + * An `Error` that indicates that the compiler has crashed. + * This is used to capture and report important information from the context when the compiler crashes. + * An `Implosion` should only be caught at the edge of the compiler to prevent the crash propagating to the caller of the compiler. + * + * No handling is required + * + * @param cause The exception that caused the crash. + */ +class Implosion private (message: String, val cause: Throwable) extends ControlThrowable: + import scala.language.unsafeNulls + override def toString: String = cause.toString() + override def getCause(): Throwable = cause.getCause() + override def getStackTrace(): Array[StackTraceElement] = cause.getStackTrace() + + override def printStackTrace(s: PrintStream): Unit = cause.printStackTrace(s) + override def printStackTrace(s: PrintWriter): Unit = cause.printStackTrace(s) + + + +object Implosion : + + def apply(cause: Throwable)(using ctx: Context): Nothing = + if cause.isInstanceOf[ControlThrowable] then + //not rethrown because `implode` should always crash the compiler + report.error("A `ControlThrowable` was used to crash the compiler") + val message = enrichErrorMessage(cause) + report.error(message, ctx.tree.sourcePos) + throw new Implosion(message, cause) + + private def enrichErrorMessage(cause: Throwable)(using Context): String = + // record state as we go along so we can report something more than the errorMessage if there's an error while reporting + val blackBox = new StringBuilder() + + extension [A](a: A) + transparent inline def record: A = + blackBox.append(a).append("\n") + a + + + """|Error during error reporting, black box recording: + |An unhandled exception was thrown in the compiler. Please file a crash + |report here: https://github.com/lampepfl/dotty/issues/new/choose)""".stripMargin.record + + val errorMessage = cause.toString().record + + val writer = new StringWriter() + cause.printStackTrace(new PrintWriter(writer)) + + val stackTrace = writer.toString().record + + try { + def formatExplain(pairs: List[(String, Any)]) = + pairs.map((k, v) => f"$k%20s: $v").mkString("\n") + + + val settings = ctx.settings.userSetSettings(ctx.settingsState).sortBy(_.name).record + val tree = ctx.tree.record + val sym = tree.symbol.record + val pos = tree.sourcePos.record + val path = pos.source.path.record + val site = ctx.outersIterator.map(_.owner).filter(sym => !sym.exists || sym.isClass || sym.is(Method)).next().record + + import untpd.* + extension (tree: Tree) def summaryString: String = tree match + case Literal(const) => s"Literal($const)" + case Ident(name) => s"Ident(${name.decode})" + case Select(qual, name) => s"Select(${qual.summaryString}, ${name.decode})" + case tree: NameTree => (if tree.isType then "type " else "") + tree.name.decode + case tree => s"${tree.className}${if tree.symbol.exists then s"(${tree.symbol})" else ""}" + + "info1:".record + val info1 = formatExplain(List( + "while compiling".record -> ctx.compilationUnit.record, + "during phase".record -> ctx.phase.prevMega.record, + "mode".record -> ctx.mode.record, + "library version".record -> scala.util.Properties.versionString.record, + "compiler version".record -> dotty.tools.dotc.config.Properties.versionString.record, + "settings".record -> settings.map(s => if s.value == "" then s"${s.name} \"\"" else s"${s.name} ${s.value}").mkString(" ").record, + )) + "symbolInfos:".record + val symbolInfos = if sym eq NoSymbol then List("symbol".record -> sym.record) else List( + "symbol".record -> sym.showLocated.record, + "symbol definition".record -> s"${sym.showDcl} (a ${sym.className})".record, + //"???" -> ??? //to try out black box + "symbol package".record -> sym.enclosingPackageClass.fullName.record, + "symbol owners".record -> sym.showExtendedLocation.record + ) + "info2:".record + val info2 = formatExplain(List( + "tree".record -> tree.summaryString, + "tree position".record -> (if pos.exists then s"$path:${pos.line + 1}:${pos.column}" else s"$path:"), + "tree type".record -> tree.typeOpt.show.record, + ) ::: symbolInfos.record ::: List( + "call site".record -> s"${site.showLocated} in ${site.enclosingPackageClass}".record + )) + + s""" + | An unhandled exception was thrown in the compiler. Please file a crash + | report here: https://github.com/lampepfl/dotty/issues/new/choose + | with the following information: + | + |$errorMessage + | + |$stackTrace + | + |$info1 + | + |$info2 + | + | + |""".stripMargin + } catch case _: Throwable => + // don't introduce new errors trying to report errors, so swallow exceptions and fall back to the blackBox recording + blackBox.toString() diff --git a/compiler/src/dotty/tools/package.scala b/compiler/src/dotty/tools/package.scala index f90355b1fa8e..5222fa0a4070 100644 --- a/compiler/src/dotty/tools/package.scala +++ b/compiler/src/dotty/tools/package.scala @@ -1,5 +1,8 @@ package dotty +import scala.compiletime.* +import dotty.tools.dotc.core.Contexts.* + package object tools { val ListOfNil: List[Nil.type] = Nil :: Nil @@ -38,13 +41,34 @@ package object tools { def unreachable(x: Any = "<< this case was declared unreachable >>"): Nothing = throw new MatchError(x) + transparent inline def assertShort(inline assertion: Boolean, inline message: Any = null): Unit = if !assertion then - val msg = message - val e = if msg == null then AssertionError() else AssertionError("assertion failed: " + msg) - e.setStackTrace(Array()) - throw e + summonFrom { + case ctx: Context => throwAssertionError(message, ctx, truncateStack = true) + case _ => throwAssertionError(message, null, truncateStack = true) + } + + transparent inline def assert(inline assertion: Boolean): Unit = + assert(assertion, null) + + transparent inline def assert(inline assertion: Boolean, inline message: Any): Unit = + if !assertion then + summonFrom { + case ctx: Context => throwAssertionError(message, ctx) + case _ => throwAssertionError(message, null) + } + + // extracted from `assert` to make it as small (and inlineable) as possible + private def throwAssertionError(message: Any | Null, ctx: Context | Null, truncateStack: Boolean = false): Nothing = + + val assertionError = AssertionError("assertion failed: " + String.valueOf(message).nn) + if truncateStack then assertionError.setStackTrace(Array()) + if ctx == null then + throw assertionError + else + ctx.implode(assertionError) // Ensure this object is already classloaded, since it's only actually used // when handling stack overflows and every operation (including class loading)