Skip to content

Commit 42e478f

Browse files
committed
Fix #4709 and #3253: report CyclicError in implicit search better
- Record in CyclicReference if it's emitted during implicit search. - Before checking if the error is on an implicit, check if we have an error during implicit search that happens while inferring a return type; that can happen whether the member we're typechecking is implicit or not, and needs the same cure. - Add ErrorMessagesTests as requested This improves the error message, and also puts more effort into diagnosing the cycle: we now use similar logic as for recursive/overloaded members to check we're indeed dealing with a (potentially) recursive implicit. Remaining `CyclicReferenceInvolvingImplicit` are unexpected and might involve bugs, so make the error message and explanation more tentative, following `CyclicReferenceInvolving`. *However*, this does not yet handle mutually recursive methods in a reasonable way.
1 parent ea648c3 commit 42e478f

File tree

8 files changed

+182
-14
lines changed

8 files changed

+182
-14
lines changed

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

+24-8
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,28 @@ object handleRecursive {
9898
}
9999
}
100100

101+
/**
102+
* This TypeError signals that completing denot encountered a cycle: it asked for denot.info (or similar),
103+
* so it requires knowing denot already.
104+
* @param denot
105+
*/
101106
class CyclicReference private (val denot: SymDenotation) extends TypeError {
107+
var inImplicitSearch: Boolean = false
102108

103-
override def toMessage(implicit ctx: Context) = {
109+
override def toMessage(implicit ctx: Context): Message = {
110+
val cycleSym = denot.symbol
104111

105-
def errorMsg(cx: Context): Message =
112+
/* This CyclicReference might have arisen from asking for `m`'s type while trying to infer it.
113+
* To try to diagnose this, walk the context chain searching for context in
114+
* Mode.InferringReturnType for the innermost member without type
115+
* annotations (!tree.tpt.typeOpt.exists).
116+
*/
117+
def errorMsg(cx: Context): Message = {
106118
if (cx.mode is Mode.InferringReturnType) {
107119
cx.tree match {
120+
case tree: untpd.ValOrDefDef if inImplicitSearch && !tree.tpt.typeOpt.exists =>
121+
// Can happen in implicit defs (#4709) or outside (#3253).
122+
TermMemberNeedsResultTypeForImplicitSearch(cycleSym)
108123
case tree: untpd.DefDef if !tree.tpt.typeOpt.exists =>
109124
OverloadedOrRecursiveMethodNeedsResultType(tree.name)
110125
case tree: untpd.ValDef if !tree.tpt.typeOpt.exists =>
@@ -113,13 +128,14 @@ class CyclicReference private (val denot: SymDenotation) extends TypeError {
113128
errorMsg(cx.outer)
114129
}
115130
}
116-
else CyclicReferenceInvolving(denot)
131+
// Give up and give generic errors.
132+
else if (cycleSym.is(Implicit, butNot = Method) && cycleSym.owner.isTerm)
133+
CyclicReferenceInvolvingImplicit(cycleSym)
134+
else
135+
CyclicReferenceInvolving(denot)
136+
}
117137

118-
val cycleSym = denot.symbol
119-
if (cycleSym.is(Implicit, butNot = Method) && cycleSym.owner.isTerm)
120-
CyclicReferenceInvolvingImplicit(cycleSym)
121-
else
122-
errorMsg(ctx)
138+
errorMsg(ctx)
123139
}
124140
}
125141

compiler/src/dotty/tools/dotc/reporting/diagnostic/ErrorMessageID.java

+1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ public enum ErrorMessageID {
131131
MatchCaseOnlyNullWarningID,
132132
ImportRenamedTwiceID,
133133
TypeTestAlwaysSucceedsID,
134+
TermMemberNeedsNeedsResultTypeForImplicitSearchID
134135
;
135136

136137
public int errorNumber() {

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

+16-3
Original file line numberDiff line numberDiff line change
@@ -1260,7 +1260,7 @@ object messages {
12601260
|""".stripMargin
12611261
}
12621262

1263-
case class OverloadedOrRecursiveMethodNeedsResultType(method: Names.TermName)(implicit ctx: Context)
1263+
case class OverloadedOrRecursiveMethodNeedsResultType(method: Names.Name)(implicit ctx: Context)
12641264
extends Message(OverloadedOrRecursiveMethodNeedsResultTypeID) {
12651265
val kind = "Syntax"
12661266
val msg = hl"""overloaded or recursive method ${method} needs return type"""
@@ -1300,8 +1300,10 @@ object messages {
13001300
val kind = "Syntax"
13011301
val msg = hl"""cyclic reference involving implicit $cycleSym"""
13021302
val explanation =
1303-
hl"""|This happens when the right hand-side of $cycleSym's definition involves an implicit search.
1304-
|To avoid this error, give `${cycleSym.name}` an explicit type.
1303+
hl"""|$cycleSym is declared as part of a cycle which makes it impossible for the
1304+
|compiler to decide upon ${cycleSym.name}'s type.
1305+
|This might happen when the right hand-side of $cycleSym's definition involves an implicit search.
1306+
|To avoid this error, try giving `${cycleSym.name}` an explicit type.
13051307
|""".stripMargin
13061308
}
13071309

@@ -2107,4 +2109,15 @@ object messages {
21072109
}
21082110
val explanation = ""
21092111
}
2112+
2113+
// Relative of CyclicReferenceInvolvingImplicit and RecursiveValueNeedsResultType
2114+
case class TermMemberNeedsResultTypeForImplicitSearch(cycleSym: Symbol)(implicit ctx: Context)
2115+
extends Message(TermMemberNeedsNeedsResultTypeForImplicitSearchID) {
2116+
val kind = "Syntax"
2117+
val msg = hl"""$cycleSym needs result type because its right-hand side attempts implicit search"""
2118+
val explanation =
2119+
hl"""|The right hand-side of $cycleSym's definition requires an implicit search at the highlighted position.
2120+
|To avoid this error, give `${cycleSym.name}` an explicit type.
2121+
|""".stripMargin
2122+
}
21102123
}

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,15 @@ trait Implicits { self: Typer =>
845845
else i"type error: ${argument.tpe} does not conform to $pt${err.whyNoMatchStr(argument.tpe, pt)}")
846846
trace(s"search implicit ${pt.show}, arg = ${argument.show}: ${argument.tpe.show}", implicits, show = true) {
847847
assert(!pt.isInstanceOf[ExprType])
848-
val result = new ImplicitSearch(pt, argument, pos).bestImplicit(contextual = true)
848+
val result =
849+
try {
850+
new ImplicitSearch(pt, argument, pos).bestImplicit(contextual = true)
851+
} catch {
852+
case ce: CyclicReference =>
853+
ce.inImplicitSearch = true
854+
throw ce
855+
}
856+
849857
result match {
850858
case result: SearchSuccess =>
851859
result.tstate.commit()

compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala

+103-2
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,65 @@ class ErrorMessagesTests extends ErrorMessagesTest {
260260
assertEquals("value x", denot.show)
261261
}
262262

263-
@Test def cyclicReferenceInvolvingImplicit =
263+
@Test def cyclicReferenceInvolving2 =
264+
checkMessagesAfter(FrontEnd.name) {
265+
"""
266+
|class A {
267+
| implicit val x: T = ???
268+
| type T <: x.type // error: cyclic reference involving value x
269+
|}
270+
""".stripMargin
271+
}
272+
.expect { (ictx, messages) =>
273+
implicit val ctx: Context = ictx
274+
275+
assertMessageCount(1, messages)
276+
val CyclicReferenceInvolving(denot) :: Nil = messages
277+
assertEquals("value x", denot.show)
278+
}
279+
280+
@Test def mutualRecursionre_i2001 =
281+
checkMessagesAfter(FrontEnd.name) {
282+
"""
283+
|class A {
284+
| def odd(x: Int) = if (x == 0) false else !even(x-1)
285+
| def even(x: Int) = if (x == 0) true else !odd(x-1) // error: overloaded or recursive method needs result type
286+
|}
287+
""".stripMargin
288+
}
289+
.expect { (ictx, messages) =>
290+
implicit val ctx: Context = ictx
291+
292+
assertMessageCount(1, messages)
293+
val OverloadedOrRecursiveMethodNeedsResultType(name) :: Nil = messages
294+
assertEquals("even", name.show)
295+
}
296+
297+
@Test def mutualRecursion_i2001a =
298+
checkMessagesAfter(FrontEnd.name) {
299+
"""
300+
|class A {
301+
| def odd(x: Int) = if (x == 0) false else !even(x-1)
302+
| def even(x: Int) = {
303+
| def foo = {
304+
| if (x == 0) true else !odd(x-1) // error: overloaded or recursive method needs result type
305+
| }
306+
| false
307+
| }
308+
|}
309+
""".stripMargin
310+
}
311+
.expect { (ictx, messages) =>
312+
implicit val ctx: Context = ictx
313+
314+
assertMessageCount(1, messages)
315+
val OverloadedOrRecursiveMethodNeedsResultType(denot) :: Nil = messages
316+
// Not ideal behavior
317+
assertEquals("foo", denot.show)
318+
}
319+
320+
321+
@Test def termMemberNeedsNeedsResultTypeForImplicitSearch =
264322
checkMessagesAfter(FrontEnd.name) {
265323
"""
266324
|object implicitDefs {
@@ -276,10 +334,53 @@ class ErrorMessagesTests extends ErrorMessagesTest {
276334
implicit val ctx: Context = ictx
277335

278336
assertMessageCount(1, messages)
279-
val CyclicReferenceInvolvingImplicit(tree) :: Nil = messages
337+
val TermMemberNeedsResultTypeForImplicitSearch(tree) :: Nil = messages
280338
assertEquals("x", tree.name.show)
281339
}
282340

341+
@Test def implicitSearchForcesImplicitRetType_i4709 =
342+
checkMessagesAfter(FrontEnd.name) {
343+
"""
344+
|import scala.language.implicitConversions
345+
|
346+
|class Context
347+
|class ContextBase { def settings = 1 }
348+
|
349+
|class Test {
350+
| implicit def toBase(ctx: Context): ContextBase = ???
351+
|
352+
| def test(ctx0: Context) = {
353+
| implicit val ctx = { ctx0.settings; ??? }
354+
| }
355+
|}
356+
""".stripMargin
357+
}
358+
.expect{ (ictx, messages) =>
359+
implicit val ctx: Context = ictx
360+
361+
assertMessageCount(1, messages)
362+
val TermMemberNeedsResultTypeForImplicitSearch(tree) :: Nil = messages
363+
assertEquals("ctx", tree.name.show)
364+
}
365+
366+
@Test def implicitSearchForcesNonImplicitRetTypeOnExplicitImport_i3253 =
367+
checkMessagesAfter(FrontEnd.name) {
368+
"""
369+
|import Test.test
370+
|
371+
|object Test {
372+
| def test = " " * 10
373+
|}
374+
""".stripMargin
375+
}
376+
.expect{ (ictx, messages) =>
377+
implicit val ctx: Context = ictx
378+
379+
assertMessageCount(1, messages)
380+
val TermMemberNeedsResultTypeForImplicitSearch(tree) :: Nil = messages
381+
assertEquals("test", tree.name.show)
382+
}
383+
283384
@Test def superQualMustBeParent =
284385
checkMessagesAfter(FrontEnd.name) {
285386
"""

tests/neg/i2001a.scala

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class A {
2+
def odd(x: Int) = if (x == 0) false else !even(x-1)
3+
def even(x: Int) = {
4+
def foo = {
5+
if (x == 0) true else !odd(x-1) // error: overloaded or recursive method needs result type
6+
}
7+
foo
8+
}
9+
lazy val x = {
10+
def foo = x // error
11+
foo
12+
}
13+
}

tests/neg/i3253.scala

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Test.test
2+
3+
object Test {
4+
def test = " " * 10 // error
5+
}

tests/neg/i4709.scala

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class Context
2+
class ContextBase { def settings = 1 }
3+
4+
class Test {
5+
implicit def toBase(ctx: Context): ContextBase = ???
6+
7+
def test(ctx0: Context) = {
8+
implicit val ctx = { ctx0.settings; ??? } // error
9+
}
10+
def f: Unit = { implicitly[Int]; implicit val i = implicitly[Int] } // error
11+
}

0 commit comments

Comments
 (0)