Skip to content

Commit 6d207ab

Browse files
Backport "Better error diagnostics for cyclic references" to LTS (#20824)
Backports #19408 to the LTS branch. PR submitted by the release tooling. [skip ci]
2 parents 6fe1132 + 770481e commit 6d207ab

16 files changed

+151
-45
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,8 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
342342
runCtx.setProfiler(Profiler())
343343
unfusedPhases.foreach(_.initContext(runCtx))
344344
val fusedPhases = runCtx.base.allPhases
345+
if ctx.settings.explainCyclic.value then
346+
runCtx.setProperty(CyclicReference.Trace, new CyclicReference.Trace())
345347
runCtx.withProgressCallback: cb =>
346348
_progress = Progress(cb, this, fusedPhases.map(_.traversals).sum)
347349
runPhases(allPhases = fusedPhases)(using runCtx)

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ trait CommonScalaSettings:
118118
// -explain-types setting is necessary for cross compilation, since it is mentioned in sbt-tpolecat, for instance
119119
// it is otherwise subsumed by -explain, and should be dropped as soon as we can.
120120
val explainTypes: Setting[Boolean] = BooleanSetting("-explain-types", "Explain type errors in more detail (deprecated, use -explain instead).", aliases = List("--explain-types", "-explaintypes"))
121+
val explainCyclic: Setting[Boolean] = BooleanSetting("-explain-cyclic", "Explain cyclic reference errors in more detail.", aliases = List("--explain-cyclic"))
121122
val unchecked: Setting[Boolean] = BooleanSetting("-unchecked", "Enable additional warnings where generated code depends on assumptions.", initialValue = true, aliases = List("--unchecked"))
122123
val language: Setting[List[String]] = MultiStringSetting("-language", "feature", "Enable one or more language features.", aliases = List("--language"))
123124

@@ -347,6 +348,7 @@ private sealed trait YSettings:
347348
val YdebugTypeError: Setting[Boolean] = BooleanSetting("-Ydebug-type-error", "Print the stack trace when a TypeError is caught", false)
348349
val YdebugError: Setting[Boolean] = BooleanSetting("-Ydebug-error", "Print the stack trace when any error is caught.", false)
349350
val YdebugUnpickling: Setting[Boolean] = BooleanSetting("-Ydebug-unpickling", "Print the stack trace when an error occurs when reading Tasty.", false)
351+
val YdebugCyclic: Setting[Boolean] = BooleanSetting("-Ydebug-cyclic", "Print the stack trace when a cyclic reference error occurs.", false)
350352
val YtermConflict: Setting[String] = ChoiceSetting("-Yresolve-term-conflict", "strategy", "Resolve term conflicts", List("package", "object", "error"), "error")
351353
val Ylog: Setting[List[String]] = PhasesSetting("-Ylog", "Log operations during")
352354
val YlogClasspath: Setting[Boolean] = BooleanSetting("-Ylog-classpath", "Output information about what classpath is being applied.")

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

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,17 @@ object SymDenotations {
166166
println(i"${" " * indent}completed $name in $owner")
167167
}
168168
}
169-
else {
170-
if (myFlags.is(Touched))
171-
throw CyclicReference(this)(using ctx.withOwner(symbol))
172-
myFlags |= Touched
173-
atPhase(validFor.firstPhaseId)(completer.complete(this))
174-
}
169+
else
170+
val traceCycles = CyclicReference.isTraced
171+
try
172+
if traceCycles then
173+
CyclicReference.pushTrace("compute the signature of ", symbol, "")
174+
if myFlags.is(Touched) then
175+
throw CyclicReference(this)(using ctx.withOwner(symbol))
176+
myFlags |= Touched
177+
atPhase(validFor.firstPhaseId)(completer.complete(this))
178+
finally
179+
if traceCycles then CyclicReference.popTrace()
175180

176181
protected[dotc] def info_=(tp: Type): Unit = {
177182
/* // DEBUG
@@ -2965,7 +2970,10 @@ object SymDenotations {
29652970
def apply(clsd: ClassDenotation)(implicit onBehalf: BaseData, ctx: Context)
29662971
: (List[ClassSymbol], BaseClassSet) = {
29672972
assert(isValid)
2973+
val traceCycles = CyclicReference.isTraced
29682974
try
2975+
if traceCycles then
2976+
CyclicReference.pushTrace("compute the base classes of ", clsd.symbol, "")
29692977
if (cache != null) cache.uncheckedNN
29702978
else {
29712979
if (locked) throw CyclicReference(clsd)
@@ -2978,7 +2986,9 @@ object SymDenotations {
29782986
else onBehalf.signalProvisional()
29792987
computed
29802988
}
2981-
finally addDependent(onBehalf)
2989+
finally
2990+
if traceCycles then CyclicReference.popTrace()
2991+
addDependent(onBehalf)
29822992
}
29832993

29842994
def sameGroup(p1: Phase, p2: Phase) = p1.sameParentsStartId == p2.sameParentsStartId

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

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import Denotations.*
1212
import Decorators.*
1313
import reporting.*
1414
import ast.untpd
15+
import util.Property
1516
import config.Printers.{cyclicErrors, noPrinter}
17+
import collection.mutable
1618

1719
import scala.annotation.constructorOnly
1820

@@ -27,6 +29,7 @@ abstract class TypeError(using creationContext: Context) extends Exception(""):
2729
|| ctx.settings.YdebugTypeError.value
2830
|| ctx.settings.YdebugError.value
2931
|| ctx.settings.YdebugUnpickling.value
32+
|| ctx.settings.YdebugCyclic.value
3033

3134
override def fillInStackTrace(): Throwable =
3235
if computeStackTrace then super.fillInStackTrace().nn
@@ -72,8 +75,7 @@ extends TypeError:
7275
def explanation: String = s"$op $details"
7376

7477
private def recursions: List[RecursionOverflow] = {
75-
import scala.collection.mutable.ListBuffer
76-
val result = ListBuffer.empty[RecursionOverflow]
78+
val result = mutable.ListBuffer.empty[RecursionOverflow]
7779
@annotation.tailrec def loop(throwable: Throwable): List[RecursionOverflow] = throwable match {
7880
case ro: RecursionOverflow =>
7981
result += ro
@@ -135,7 +137,10 @@ end handleRecursive
135137
* so it requires knowing denot already.
136138
* @param denot
137139
*/
138-
class CyclicReference(val denot: SymDenotation)(using Context) extends TypeError:
140+
class CyclicReference(
141+
val denot: SymDenotation,
142+
val optTrace: Option[Array[CyclicReference.TraceElement]])(using Context)
143+
extends TypeError:
139144
var inImplicitSearch: Boolean = false
140145

141146
val cycleSym = denot.symbol
@@ -161,11 +166,11 @@ class CyclicReference(val denot: SymDenotation)(using Context) extends TypeError
161166
cx.tree match {
162167
case tree: untpd.ValOrDefDef if !tree.tpt.typeOpt.exists =>
163168
if (inImplicitSearch)
164-
TermMemberNeedsResultTypeForImplicitSearch(cycleSym)
169+
TermMemberNeedsResultTypeForImplicitSearch(this)
165170
else if (isMethod)
166-
OverloadedOrRecursiveMethodNeedsResultType(cycleSym)
171+
OverloadedOrRecursiveMethodNeedsResultType(this)
167172
else if (isVal)
168-
RecursiveValueNeedsResultType(cycleSym)
173+
RecursiveValueNeedsResultType(this)
169174
else
170175
errorMsg(cx.outer)
171176
case _ =>
@@ -174,22 +179,38 @@ class CyclicReference(val denot: SymDenotation)(using Context) extends TypeError
174179

175180
// Give up and give generic errors.
176181
else if (cycleSym.isOneOf(GivenOrImplicitVal, butNot = Method) && cycleSym.owner.isTerm)
177-
CyclicReferenceInvolvingImplicit(cycleSym)
182+
CyclicReferenceInvolvingImplicit(this)
178183
else
179-
CyclicReferenceInvolving(denot)
184+
CyclicReferenceInvolving(this)
180185

181186
errorMsg(ctx)
182187
end toMessage
183188

184189
object CyclicReference:
190+
185191
def apply(denot: SymDenotation)(using Context): CyclicReference =
186-
val ex = new CyclicReference(denot)
192+
val ex = new CyclicReference(denot, ctx.property(Trace).map(_.toArray))
187193
if ex.computeStackTrace then
188194
cyclicErrors.println(s"Cyclic reference involving! $denot")
189195
val sts = ex.getStackTrace.asInstanceOf[Array[StackTraceElement]]
190196
for (elem <- sts take 200)
191197
cyclicErrors.println(elem.toString)
192198
ex
199+
200+
type TraceElement = (/*prefix:*/ String, Symbol, /*suffix:*/ String)
201+
type Trace = mutable.ArrayBuffer[TraceElement]
202+
val Trace = Property.Key[Trace]
203+
204+
def isTraced(using Context) =
205+
ctx.property(CyclicReference.Trace).isDefined
206+
207+
def pushTrace(info: TraceElement)(using Context): Unit =
208+
for buf <- ctx.property(CyclicReference.Trace) do
209+
buf += info
210+
211+
def popTrace()(using Context): Unit =
212+
for buf <- ctx.property(CyclicReference.Trace) do
213+
buf.dropRightInPlace(1)
193214
end CyclicReference
194215

195216
class UnpicklingError(denot: Denotation, where: String, cause: Throwable)(using Context) extends TypeError:

compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,20 +124,25 @@ class TreeUnpickler(reader: TastyReader,
124124
val mode = ctx.mode
125125
val source = ctx.source
126126
def complete(denot: SymDenotation)(using Context): Unit =
127-
def fail(ex: Throwable) =
128-
def where =
129-
val f = denot.symbol.associatedFile
130-
if f == null then "" else s" in $f"
131-
throw UnpicklingError(denot, where, ex)
127+
def where =
128+
val f = denot.symbol.associatedFile
129+
if f == null then "" else s" in $f"
130+
def fail(ex: Throwable) = throw UnpicklingError(denot, where, ex)
132131
treeAtAddr(currentAddr) =
132+
val traceCycles = CyclicReference.isTraced
133133
try
134+
if traceCycles then
135+
CyclicReference.pushTrace("read the definition of ", denot.symbol, where)
134136
atPhaseBeforeTransforms {
135137
new TreeReader(reader).readIndexedDef()(
136138
using ctx.withOwner(owner).withModeBits(mode).withSource(source))
137139
}
138140
catch
141+
case ex: CyclicReference => throw ex
139142
case ex: AssertionError => fail(ex)
140143
case ex: Exception => fail(ex)
144+
finally
145+
if traceCycles then CyclicReference.popTrace()
141146
}
142147

143148
class TreeReader(val reader: TastyReader) {

compiler/src/dotty/tools/dotc/reporting/messages.scala

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,23 @@ abstract class PatternMatchMsg(errorId: ErrorMessageID)(using Context) extends M
8484
abstract class CyclicMsg(errorId: ErrorMessageID)(using Context) extends Message(errorId):
8585
def kind = MessageKind.Cyclic
8686

87+
val ex: CyclicReference
88+
protected def cycleSym = ex.denot.symbol
89+
90+
protected def debugInfo =
91+
if ctx.settings.YdebugCyclic.value then
92+
"\n\nStacktrace:" ++ ex.getStackTrace().nn.mkString("\n ", "\n ", "")
93+
else "\n\n Run with both -explain-cyclic and -Ydebug-cyclic to see full stack trace."
94+
95+
protected def context: String = ex.optTrace match
96+
case Some(trace) =>
97+
s"\n\nThe error occurred while trying to ${
98+
trace.map((prefix, sym, suffix) => i"$prefix$sym$suffix").mkString("\n which required to ")
99+
}$debugInfo"
100+
case None =>
101+
"\n\n Run with -explain-cyclic for more details."
102+
end CyclicMsg
103+
87104
abstract class ReferenceMsg(errorId: ErrorMessageID)(using Context) extends Message(errorId):
88105
def kind = MessageKind.Reference
89106

@@ -1246,9 +1263,9 @@ class UnreducibleApplication(tycon: Type)(using Context) extends TypeMsg(Unreduc
12461263
|Such applications are equivalent to existential types, which are not
12471264
|supported in Scala 3."""
12481265

1249-
class OverloadedOrRecursiveMethodNeedsResultType(cycleSym: Symbol)(using Context)
1266+
class OverloadedOrRecursiveMethodNeedsResultType(val ex: CyclicReference)(using Context)
12501267
extends CyclicMsg(OverloadedOrRecursiveMethodNeedsResultTypeID) {
1251-
def msg(using Context) = i"""Overloaded or recursive $cycleSym needs return type"""
1268+
def msg(using Context) = i"""Overloaded or recursive $cycleSym needs return type$context"""
12521269
def explain(using Context) =
12531270
i"""Case 1: $cycleSym is overloaded
12541271
|If there are multiple methods named $cycleSym and at least one definition of
@@ -1260,29 +1277,29 @@ extends CyclicMsg(OverloadedOrRecursiveMethodNeedsResultTypeID) {
12601277
|"""
12611278
}
12621279

1263-
class RecursiveValueNeedsResultType(cycleSym: Symbol)(using Context)
1280+
class RecursiveValueNeedsResultType(val ex: CyclicReference)(using Context)
12641281
extends CyclicMsg(RecursiveValueNeedsResultTypeID) {
1265-
def msg(using Context) = i"""Recursive $cycleSym needs type"""
1282+
def msg(using Context) = i"""Recursive $cycleSym needs type$context"""
12661283
def explain(using Context) =
12671284
i"""The definition of $cycleSym is recursive and you need to specify its type.
12681285
|"""
12691286
}
12701287

1271-
class CyclicReferenceInvolving(denot: SymDenotation)(using Context)
1288+
class CyclicReferenceInvolving(val ex: CyclicReference)(using Context)
12721289
extends CyclicMsg(CyclicReferenceInvolvingID) {
12731290
def msg(using Context) =
1274-
val where = if denot.exists then s" involving $denot" else ""
1275-
i"Cyclic reference$where"
1291+
val where = if ex.denot.exists then s" involving ${ex.denot}" else ""
1292+
i"Cyclic reference$where$context"
12761293
def explain(using Context) =
1277-
i"""|$denot is declared as part of a cycle which makes it impossible for the
1278-
|compiler to decide upon ${denot.name}'s type.
1279-
|To avoid this error, try giving ${denot.name} an explicit type.
1294+
i"""|${ex.denot} is declared as part of a cycle which makes it impossible for the
1295+
|compiler to decide upon ${ex.denot.name}'s type.
1296+
|To avoid this error, try giving ${ex.denot.name} an explicit type.
12801297
|"""
12811298
}
12821299

1283-
class CyclicReferenceInvolvingImplicit(cycleSym: Symbol)(using Context)
1300+
class CyclicReferenceInvolvingImplicit(val ex: CyclicReference)(using Context)
12841301
extends CyclicMsg(CyclicReferenceInvolvingImplicitID) {
1285-
def msg(using Context) = i"""Cyclic reference involving implicit $cycleSym"""
1302+
def msg(using Context) = i"""Cyclic reference involving implicit $cycleSym$context"""
12861303
def explain(using Context) =
12871304
i"""|$cycleSym is declared as part of a cycle which makes it impossible for the
12881305
|compiler to decide upon ${cycleSym.name}'s type.
@@ -2314,9 +2331,9 @@ class TypeTestAlwaysDiverges(scrutTp: Type, testTp: Type)(using Context) extends
23142331
}
23152332

23162333
// Relative of CyclicReferenceInvolvingImplicit and RecursiveValueNeedsResultType
2317-
class TermMemberNeedsResultTypeForImplicitSearch(cycleSym: Symbol)(using Context)
2334+
class TermMemberNeedsResultTypeForImplicitSearch(val ex: CyclicReference)(using Context)
23182335
extends CyclicMsg(TermMemberNeedsNeedsResultTypeForImplicitSearchID) {
2319-
def msg(using Context) = i"""$cycleSym needs result type because its right-hand side attempts implicit search"""
2336+
def msg(using Context) = i"""$cycleSym needs result type because its right-hand side attempts implicit search$context"""
23202337
def explain(using Context) =
23212338
i"""|The right hand-side of $cycleSym's definition requires an implicit search at the highlighted position.
23222339
|To avoid this error, give `$cycleSym` an explicit type.
@@ -2527,8 +2544,9 @@ class UnknownNamedEnclosingClassOrObject(name: TypeName)(using Context)
25272544
"""
25282545
}
25292546

2530-
class IllegalCyclicTypeReference(sym: Symbol, where: String, lastChecked: Type)(using Context)
2547+
class IllegalCyclicTypeReference(val ex: CyclicReference, sym: Symbol, where: String, lastChecked: Type)(using Context)
25312548
extends CyclicMsg(IllegalCyclicTypeReferenceID) {
2549+
override def context = ""
25322550
def msg(using Context) =
25332551
val lastCheckedStr =
25342552
try lastChecked.show

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ object Checking {
378378
catch {
379379
case ex: CyclicReference =>
380380
if (reportErrors)
381-
errorType(IllegalCyclicTypeReference(sym, checker.where, checker.lastChecked), sym.srcPos)
381+
errorType(IllegalCyclicTypeReference(ex, sym, checker.where, checker.lastChecked), sym.srcPos)
382382
else info
383383
}
384384
}

tests/neg-macros/i14772.check

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
-- [E044] Cyclic Error: tests/neg-macros/i14772.scala:7:7 --------------------------------------------------------------
2-
7 | foo(a) // error
1+
-- [E044] Cyclic Error: tests/neg-macros/i14772.scala:8:7 --------------------------------------------------------------
2+
8 | foo(a) // error
33
| ^
44
| Overloaded or recursive method impl needs return type
55
|
6+
| The error occurred while trying to compute the signature of method $anonfun
7+
| which required to compute the signature of method impl
8+
|
9+
| Run with both -explain-cyclic and -Ydebug-cyclic to see full stack trace.
10+
|
611
| longer explanation available when compiling with `-explain`

tests/neg-macros/i14772.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//> using options -explain-cyclic
12
import scala.quoted.*
23

34
object A {

tests/neg-macros/i16582.check

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11

2-
-- Error: tests/neg-macros/i16582/Test_2.scala:5:27 --------------------------------------------------------------------
3-
5 | val o2 = ownerDoesNotWork(2) // error
2+
-- Error: tests/neg-macros/i16582/Test_2.scala:6:27 --------------------------------------------------------------------
3+
6 | val o2 = ownerDoesNotWork(2) // error
44
| ^^^^^^^^^^^^^^^^^^^
55
| Exception occurred while executing macro expansion.
66
| dotty.tools.dotc.core.CyclicReference: Recursive value o2 needs type
77
|
8+
| The error occurred while trying to compute the signature of method test
9+
| which required to compute the signature of value o2
10+
| which required to compute the signature of value o2
11+
|
12+
| Run with both -explain-cyclic and -Ydebug-cyclic to see full stack trace.
13+
|
814
| See full stack trace using -Ydebug
915
|---------------------------------------------------------------------------------------------------------------------
1016
|Inline stack trace

tests/neg-macros/i16582/Test_2.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//> using options -explain-cyclic
12
def test=
23
val o1 = ownerWorks(1)
34
println(o1)

tests/neg/cyclic.check

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-- [E044] Cyclic Error: tests/neg/cyclic.scala:6:12 --------------------------------------------------------------------
2+
6 | def i() = f() // error
3+
| ^
4+
| Overloaded or recursive method f needs return type
5+
|
6+
| The error occurred while trying to compute the signature of method f
7+
| which required to compute the signature of method g
8+
| which required to compute the signature of method h
9+
| which required to compute the signature of method i
10+
| which required to compute the signature of method f
11+
|
12+
| Run with both -explain-cyclic and -Ydebug-cyclic to see full stack trace.
13+
|
14+
| longer explanation available when compiling with `-explain`

tests/neg/cyclic.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//> using options -explain-cyclic
2+
object O:
3+
def f() = g()
4+
def g() = h()
5+
def h() = i()
6+
def i() = f() // error

tests/neg/i10870.check

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55
| Extension methods were tried, but the search failed with:
66
|
77
| Overloaded or recursive method x needs return type
8+
|
9+
| Run with -explain-cyclic for more details.

0 commit comments

Comments
 (0)