Skip to content

Commit d2becfe

Browse files
committed
Better error diagnostics for cyclic references
We now suggest to compile with -explain-cyclic, in which case we give a trace of the forcings that led to the cycle. The reason for the separate option is that maintaining a trace is not free so we should not be doing it by default.
1 parent 708e640 commit d2becfe

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
@@ -343,6 +343,8 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
343343
runCtx.setProfiler(Profiler())
344344
unfusedPhases.foreach(_.initContext(runCtx))
345345
val fusedPhases = runCtx.base.allPhases
346+
if ctx.settings.explainCyclic.value then
347+
runCtx.setProperty(CyclicReference.Trace, new CyclicReference.Trace())
346348
runCtx.withProgressCallback: cb =>
347349
_progress = Progress(cb, this, fusedPhases.map(_.traversals).sum)
348350
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
@@ -120,6 +120,7 @@ trait CommonScalaSettings:
120120
// -explain-types setting is necessary for cross compilation, since it is mentioned in sbt-tpolecat, for instance
121121
// it is otherwise subsumed by -explain, and should be dropped as soon as we can.
122122
val explainTypes: Setting[Boolean] = BooleanSetting("-explain-types", "Explain type errors in more detail (deprecated, use -explain instead).", aliases = List("--explain-types", "-explaintypes"))
123+
val explainCyclic: Setting[Boolean] = BooleanSetting("-explain-cyclic", "Explain cyclic reference errors in more detail.", aliases = List("--explain-cyclic"))
123124
val unchecked: Setting[Boolean] = BooleanSetting("-unchecked", "Enable additional warnings where generated code depends on assumptions.", initialValue = true, aliases = List("--unchecked"))
124125
val language: Setting[List[String]] = MultiStringSetting("-language", "feature", "Enable one or more language features.", aliases = List("--language"))
125126
val experimental: Setting[Boolean] = BooleanSetting("-experimental", "Annotate all top-level definitions with @experimental. This enables the use of experimental features anywhere in the project.")
@@ -351,6 +352,7 @@ private sealed trait YSettings:
351352
val YdebugTypeError: Setting[Boolean] = BooleanSetting("-Ydebug-type-error", "Print the stack trace when a TypeError is caught", false)
352353
val YdebugError: Setting[Boolean] = BooleanSetting("-Ydebug-error", "Print the stack trace when any error is caught.", false)
353354
val YdebugUnpickling: Setting[Boolean] = BooleanSetting("-Ydebug-unpickling", "Print the stack trace when an error occurs when reading Tasty.", false)
355+
val YdebugCyclic: Setting[Boolean] = BooleanSetting("-Ydebug-cyclic", "Print the stack trace when a cyclic reference error occurs.", false)
354356
val YtermConflict: Setting[String] = ChoiceSetting("-Yresolve-term-conflict", "strategy", "Resolve term conflicts", List("package", "object", "error"), "error")
355357
val Ylog: Setting[List[String]] = PhasesSetting("-Ylog", "Log operations during")
356358
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
@@ -167,12 +167,17 @@ object SymDenotations {
167167
println(i"${" " * indent}completed $name in $owner")
168168
}
169169
}
170-
else {
171-
if (myFlags.is(Touched))
172-
throw CyclicReference(this)(using ctx.withOwner(symbol))
173-
myFlags |= Touched
174-
atPhase(validFor.firstPhaseId)(completer.complete(this))
175-
}
170+
else
171+
val traceCycles = CyclicReference.isTraced
172+
try
173+
if traceCycles then
174+
CyclicReference.pushTrace("complete the info of ", symbol, "")
175+
if myFlags.is(Touched) then
176+
throw CyclicReference(this)(using ctx.withOwner(symbol))
177+
myFlags |= Touched
178+
atPhase(validFor.firstPhaseId)(completer.complete(this))
179+
finally
180+
if traceCycles then CyclicReference.popTrace()
176181

177182
protected[dotc] def info_=(tp: Type): Unit = {
178183
/* // DEBUG
@@ -2971,7 +2976,10 @@ object SymDenotations {
29712976
def apply(clsd: ClassDenotation)(implicit onBehalf: BaseData, ctx: Context)
29722977
: (List[ClassSymbol], BaseClassSet) = {
29732978
assert(isValid)
2979+
val traceCycles = CyclicReference.isTraced
29742980
try
2981+
if traceCycles then
2982+
CyclicReference.pushTrace("compute the base classes of ", clsd.symbol, "")
29752983
if (cache != null) cache.uncheckedNN
29762984
else {
29772985
if (locked) throw CyclicReference(clsd)
@@ -2984,7 +2992,9 @@ object SymDenotations {
29842992
else onBehalf.signalProvisional()
29852993
computed
29862994
}
2987-
finally addDependent(onBehalf)
2995+
finally
2996+
if traceCycles then CyclicReference.popTrace()
2997+
addDependent(onBehalf)
29882998
}
29892999

29903000
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
@@ -141,20 +141,25 @@ class TreeUnpickler(reader: TastyReader,
141141
val mode = ctx.mode
142142
val source = ctx.source
143143
def complete(denot: SymDenotation)(using Context): Unit =
144-
def fail(ex: Throwable) =
145-
def where =
146-
val f = denot.symbol.associatedFile
147-
if f == null then "" else s" in $f"
148-
throw UnpicklingError(denot, where, ex)
144+
def where =
145+
val f = denot.symbol.associatedFile
146+
if f == null then "" else s" in $f"
147+
def fail(ex: Throwable) = throw UnpicklingError(denot, where, ex)
149148
treeAtAddr(currentAddr) =
149+
val traceCycles = CyclicReference.isTraced
150150
try
151+
if traceCycles then
152+
CyclicReference.pushTrace("read the definition of ", denot.symbol, where)
151153
atPhaseBeforeTransforms {
152154
new TreeReader(reader).readIndexedDef()(
153155
using ctx.withOwner(owner).withModeBits(mode).withSource(source))
154156
}
155157
catch
158+
case ex: CyclicReference => throw ex
156159
case ex: AssertionError => fail(ex)
157160
case ex: Exception => fail(ex)
161+
finally
162+
if traceCycles then CyclicReference.popTrace()
158163
}
159164

160165
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

@@ -1249,9 +1266,9 @@ class UnreducibleApplication(tycon: Type)(using Context) extends TypeMsg(Unreduc
12491266
|Such applications are equivalent to existential types, which are not
12501267
|supported in Scala 3."""
12511268

1252-
class OverloadedOrRecursiveMethodNeedsResultType(cycleSym: Symbol)(using Context)
1269+
class OverloadedOrRecursiveMethodNeedsResultType(val ex: CyclicReference)(using Context)
12531270
extends CyclicMsg(OverloadedOrRecursiveMethodNeedsResultTypeID) {
1254-
def msg(using Context) = i"""Overloaded or recursive $cycleSym needs return type"""
1271+
def msg(using Context) = i"""Overloaded or recursive $cycleSym needs return type$context"""
12551272
def explain(using Context) =
12561273
i"""Case 1: $cycleSym is overloaded
12571274
|If there are multiple methods named $cycleSym and at least one definition of
@@ -1263,29 +1280,29 @@ extends CyclicMsg(OverloadedOrRecursiveMethodNeedsResultTypeID) {
12631280
|"""
12641281
}
12651282

1266-
class RecursiveValueNeedsResultType(cycleSym: Symbol)(using Context)
1283+
class RecursiveValueNeedsResultType(val ex: CyclicReference)(using Context)
12671284
extends CyclicMsg(RecursiveValueNeedsResultTypeID) {
1268-
def msg(using Context) = i"""Recursive $cycleSym needs type"""
1285+
def msg(using Context) = i"""Recursive $cycleSym needs type$context"""
12691286
def explain(using Context) =
12701287
i"""The definition of $cycleSym is recursive and you need to specify its type.
12711288
|"""
12721289
}
12731290

1274-
class CyclicReferenceInvolving(denot: SymDenotation)(using Context)
1291+
class CyclicReferenceInvolving(val ex: CyclicReference)(using Context)
12751292
extends CyclicMsg(CyclicReferenceInvolvingID) {
12761293
def msg(using Context) =
1277-
val where = if denot.exists then s" involving $denot" else ""
1278-
i"Cyclic reference$where"
1294+
val where = if ex.denot.exists then s" involving ${ex.denot}" else ""
1295+
i"Cyclic reference$where$context"
12791296
def explain(using Context) =
1280-
i"""|$denot is declared as part of a cycle which makes it impossible for the
1281-
|compiler to decide upon ${denot.name}'s type.
1282-
|To avoid this error, try giving ${denot.name} an explicit type.
1297+
i"""|${ex.denot} is declared as part of a cycle which makes it impossible for the
1298+
|compiler to decide upon ${ex.denot.name}'s type.
1299+
|To avoid this error, try giving ${ex.denot.name} an explicit type.
12831300
|"""
12841301
}
12851302

1286-
class CyclicReferenceInvolvingImplicit(cycleSym: Symbol)(using Context)
1303+
class CyclicReferenceInvolvingImplicit(val ex: CyclicReference)(using Context)
12871304
extends CyclicMsg(CyclicReferenceInvolvingImplicitID) {
1288-
def msg(using Context) = i"""Cyclic reference involving implicit $cycleSym"""
1305+
def msg(using Context) = i"""Cyclic reference involving implicit $cycleSym$context"""
12891306
def explain(using Context) =
12901307
i"""|$cycleSym is declared as part of a cycle which makes it impossible for the
12911308
|compiler to decide upon ${cycleSym.name}'s type.
@@ -2340,9 +2357,9 @@ class TypeTestAlwaysDiverges(scrutTp: Type, testTp: Type)(using Context) extends
23402357
}
23412358

23422359
// Relative of CyclicReferenceInvolvingImplicit and RecursiveValueNeedsResultType
2343-
class TermMemberNeedsResultTypeForImplicitSearch(cycleSym: Symbol)(using Context)
2360+
class TermMemberNeedsResultTypeForImplicitSearch(val ex: CyclicReference)(using Context)
23442361
extends CyclicMsg(TermMemberNeedsNeedsResultTypeForImplicitSearchID) {
2345-
def msg(using Context) = i"""$cycleSym needs result type because its right-hand side attempts implicit search"""
2362+
def msg(using Context) = i"""$cycleSym needs result type because its right-hand side attempts implicit search$context"""
23462363
def explain(using Context) =
23472364
i"""|The right hand-side of $cycleSym's definition requires an implicit search at the highlighted position.
23482365
|To avoid this error, give `$cycleSym` an explicit type.
@@ -2553,8 +2570,9 @@ class UnknownNamedEnclosingClassOrObject(name: TypeName)(using Context)
25532570
"""
25542571
}
25552572

2556-
class IllegalCyclicTypeReference(sym: Symbol, where: String, lastChecked: Type)(using Context)
2573+
class IllegalCyclicTypeReference(val ex: CyclicReference, sym: Symbol, where: String, lastChecked: Type)(using Context)
25572574
extends CyclicMsg(IllegalCyclicTypeReferenceID) {
2575+
override def context = ""
25582576
def msg(using Context) =
25592577
val lastCheckedStr =
25602578
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
@@ -391,7 +391,7 @@ object Checking {
391391
catch {
392392
case ex: CyclicReference =>
393393
if (reportErrors)
394-
errorType(IllegalCyclicTypeReference(sym, checker.where, checker.lastChecked), sym.srcPos)
394+
errorType(IllegalCyclicTypeReference(ex, sym, checker.where, checker.lastChecked), sym.srcPos)
395395
else info
396396
}
397397
}

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 complete the info of method $anonfun
7+
| which required to complete the info 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 complete the info of method test
9+
| which required to complete the info of value o2
10+
| which required to complete the info 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 complete the info of method f
7+
| which required to complete the info of method g
8+
| which required to complete the info of method h
9+
| which required to complete the info of method i
10+
| which required to complete the info 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)