From 3fdae37f76863c723d2db3051c772a8e475d0b83 Mon Sep 17 00:00:00 2001 From: Yichen Xu Date: Wed, 12 Nov 2025 19:38:14 +0100 Subject: [PATCH 1/3] Implement sub-diagnostics --- .../tools/dotc/reporting/Diagnostic.scala | 20 +++++++- .../dotc/reporting/MessageRendering.scala | 50 +++++++++++++------ 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala b/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala index a4c30b4658e9..90b841a1adcb 100644 --- a/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala +++ b/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala @@ -11,6 +11,8 @@ import dotty.tools.dotc.util.chaining.* import java.util.{Collections, Optional, List => JList} import core.Decorators.toMessage +import collection.mutable.ArrayBuffer + object Diagnostic: def shouldExplain(dia: Diagnostic)(using Context): Boolean = @@ -117,6 +119,22 @@ class Diagnostic( msg.message.replaceAll("\u001B\\[[;\\d]*m", "") override def diagnosticRelatedInformation: JList[interfaces.DiagnosticRelatedInformation] = Collections.emptyList() - override def toString: String = s"$getClass at $pos L${pos.line+1}: $message" + + private val subdiags: ArrayBuffer[Subdiagnostic] = ArrayBuffer.empty + + def addSubdiag(diag: Subdiagnostic): Unit = + subdiags += diag + + def addSubdiag(msg: Message, pos: SourcePosition): Unit = + addSubdiag(Subdiagnostic(msg, pos)) + + def withSubdiags(diags: List[Subdiagnostic]): this.type = + diags.foreach(addSubdiag) + this + + def getSubdiags: List[Subdiagnostic] = subdiags.toList end Diagnostic + +class Subdiagnostic(val msg: Message, val pos: SourcePosition) + diff --git a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala index 9366050a5a17..68b4b74089f2 100644 --- a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala +++ b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala @@ -77,10 +77,11 @@ trait MessageRendering { * -- Error: source.scala --------------------- * ``` */ - private def boxTitle(title: String)(using Context, Level, Offset): String = + private def boxTitle(title: String, isSubtitle: Boolean = false)(using Context, Level, Offset): String = val pageWidth = ctx.settings.pageWidth.value val line = "-" * (pageWidth - title.length - 4) - hl(s"-- $title $line") + val starter = if isSubtitle then ".." else "--" + hl(s"$starter $title $line") /** The position markers aligned under the error * @@ -169,7 +170,8 @@ trait MessageRendering { private def posStr( pos: SourcePosition, message: Message, - diagnosticString: String + diagnosticString: String, + isSubdiag: Boolean = false )(using Context, Level, Offset): String = assert( message.errorId.isActive, @@ -191,7 +193,7 @@ trait MessageRendering { val title = if fileAndPos.isEmpty then s"$errId$kind:" // this happens in dotty.tools.repl.ScriptedTests // TODO add name of source or remove `:` (and update test files) else s"$errId$kind: $fileAndPos" - boxTitle(title) + boxTitle(title, isSubtitle = isSubdiag) }) else "" end posStr @@ -232,6 +234,18 @@ trait MessageRendering { if origin.nonEmpty then addHelp("origin=")(origin) + // adjust a pos at EOF if preceded by newline + private def adjust(pos: SourcePosition): SourcePosition = + if pos.span.isSynthetic + && pos.span.isZeroExtent + && pos.span.exists + && pos.span.start == pos.source.length + && pos.source(pos.span.start - 1) == '\n' + then + pos.withSpan(pos.span.shift(-1)) + else + pos + /** The whole message rendered from `dia.msg`. * * For a position in an inline expansion, choose `pos1` @@ -252,17 +266,6 @@ trait MessageRendering { * */ def messageAndPos(dia: Diagnostic)(using Context): String = - // adjust a pos at EOF if preceded by newline - def adjust(pos: SourcePosition): SourcePosition = - if pos.span.isSynthetic - && pos.span.isZeroExtent - && pos.span.exists - && pos.span.start == pos.source.length - && pos.source(pos.span.start - 1) == '\n' - then - pos.withSpan(pos.span.shift(-1)) - else - pos val msg = dia.msg val pos = dia.pos val pos1 = adjust(pos.nonInlined) // innermost pos contained by call.pos @@ -296,6 +299,9 @@ trait MessageRendering { sb.append(EOL).append(endBox) end if else sb.append(msg.message) + + dia.getSubdiags.foreach(addSubdiagnostic(sb, _)) + if dia.isVerbose then appendFilterHelp(dia, sb) @@ -313,6 +319,20 @@ trait MessageRendering { sb.toString end messageAndPos + private def addSubdiagnostic(sb: StringBuilder, subdiag: Subdiagnostic)(using Context, Level, Offset): Unit = + val pos1 = adjust(subdiag.pos) + val msg = subdiag.msg + assert(pos1.exists && pos1.source.file.exists) + + val posString = posStr(pos1, msg, "Note", isSubdiag = true) + val (srcBefore, srcAfter, offset) = sourceLines(pos1) + val marker = positionMarker(pos1) + val err = errorMsg(pos1, msg.message) + + val diagText = (posString :: srcBefore ::: marker :: err :: srcAfter).mkString(EOL) + sb.append(EOL) + sb.append(diagText) + private def hl(str: String)(using Context, Level): String = summon[Level].value match case interfaces.Diagnostic.ERROR => Red(str).show From 09ee14e27e098d56d61a48bc7549daf6901a5582 Mon Sep 17 00:00:00 2001 From: Yichen Xu Date: Wed, 12 Nov 2025 19:38:25 +0100 Subject: [PATCH 2/3] Use sub-diagnostics for consume error --- compiler/src/dotty/tools/dotc/cc/SepCheck.scala | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index b98f07080181..d9fcf30cc6d0 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -443,17 +443,21 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: |No clashing definitions were found. This might point to an internal error.""", tree.srcPos) + class UseAfterConsume(ref: Capability, consumedLoc: SrcPos, useLoc: SrcPos)(using Context) extends reporting.Diagnostic.Error( + em"""Separation failure: Illegal access to $ref, which was consumed + |and therefore is no longer available.""", + useLoc.sourcePos + ): + addSubdiag(em"""$ref was passed to a consume parameter or + |was used as a prefix of a consume method here.""", consumedLoc.sourcePos) + /** Report a failure where a previously consumed capability is used again, * @param ref the capability that is used after being consumed * @param loc the position where the capability was consumed * @param pos the position where the capability was used again */ def consumeError(ref: Capability, loc: SrcPos, pos: SrcPos)(using Context): Unit = - report.error( - em"""Separation failure: Illegal access to $ref, which was passed to a - |consume parameter or was used as a prefix to a consume method on line ${loc.line + 1} - |and therefore is no longer available.""", - pos) + ctx.reporter.report(UseAfterConsume(ref, loc, pos)) /** Report a failure where a capability is consumed in a loop. * @param ref the capability From 885904828c6530ebaa9b3bc00a1f8b5fa554ea27 Mon Sep 17 00:00:00 2001 From: Yichen Xu Date: Thu, 13 Nov 2025 13:57:08 +0100 Subject: [PATCH 3/3] Tweak the error message --- compiler/src/dotty/tools/dotc/cc/SepCheck.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index d9fcf30cc6d0..3c9998132250 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -444,12 +444,12 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: tree.srcPos) class UseAfterConsume(ref: Capability, consumedLoc: SrcPos, useLoc: SrcPos)(using Context) extends reporting.Diagnostic.Error( - em"""Separation failure: Illegal access to $ref, which was consumed + em"""Separation failure: Illegal access to $ref, which was passed to a + |consume parameter or was used as a prefix to a consume method |and therefore is no longer available.""", useLoc.sourcePos ): - addSubdiag(em"""$ref was passed to a consume parameter or - |was used as a prefix of a consume method here.""", consumedLoc.sourcePos) + addSubdiag(em"... $ref was consumed here.", consumedLoc.sourcePos) /** Report a failure where a previously consumed capability is used again, * @param ref the capability that is used after being consumed