Skip to content

Commit e51f85d

Browse files
committed
Add test for cyclic references involving members of inherited and intersected types
Test every template and every type intersection for cycles in its members. The tests are done as early possible without causing spurious cycles, which for intersections means PostTyper, unfortunately. Still missing: Do the test also for members reachable in common value definitions.
1 parent e9a95bb commit e51f85d

File tree

5 files changed

+102
-1
lines changed

5 files changed

+102
-1
lines changed

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4560,8 +4560,15 @@ object Types {
45604560
if (ctx.debug) printStackTrace()
45614561
}
45624562

4563+
def showPrefixSafely(pre: Type)(implicit ctx: Context): String = pre.stripTypeVar match {
4564+
case pre: TermRef => i"${pre.termSymbol.name}."
4565+
case pre: TypeRef => i"${pre.typeSymbol.name}#"
4566+
case pre: TypeProxy => showPrefixSafely(pre.underlying)
4567+
case _ => if (pre.typeSymbol.exists) i"${pre.typeSymbol.name}#" else "."
4568+
}
4569+
45634570
class CyclicFindMember(pre: Type, name: Name)(implicit ctx: Context) extends TypeError(
4564-
i"""member search with prefix $pre too deep.
4571+
i"""member search for ${showPrefixSafely(pre)}$name too deep.
45654572
|searches, from inner to outer: .${ctx.pendingMemberSearches}% .%""")
45664573

45674574
private def otherReason(pre: Type)(implicit ctx: Context): String = pre match {

compiler/src/dotty/tools/dotc/transform/PostTyper.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
285285
case tpe => tpe
286286
}
287287
)
288+
case tree: AndTypeTree =>
289+
// Ideally, this should be done by Typer, but we run into cyclic references
290+
// when trying to typecheck self types which are intersections.
291+
Checking.checkNonCyclicInherited(tree.tpe, tree.left.tpe :: tree.right.tpe :: Nil, EmptyScope, tree.pos)
292+
super.transform(tree)
288293
case Import(expr, selectors) =>
289294
val exprTpe = expr.tpe
290295
val seen = mutable.Set.empty[Name]

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,36 @@ object Checking {
332332
checkTree((), refinement)
333333
}
334334

335+
/** Check type members inherited from different `parents` of `joint` type for cycles,
336+
* unless a type with the same name aleadry appears in `decls`.
337+
* @return true iff no cycles were detected
338+
*/
339+
def checkNonCyclicInherited(joint: Type, parents: List[Type], decls: Scope, pos: Position)(implicit ctx: Context): Unit = {
340+
def qualifies(sym: Symbol) = sym.name.isTypeName && !sym.is(Private)
341+
val abstractTypeNames =
342+
for (parent <- parents; mbr <- parent.abstractTypeMembers if qualifies(mbr.symbol))
343+
yield mbr.name.asTypeName
344+
345+
for (name <- abstractTypeNames)
346+
try {
347+
val mbr = joint.member(name)
348+
mbr.info match {
349+
case bounds: TypeBounds =>
350+
val res = checkNonCyclic(mbr.symbol, bounds, reportErrors = true).isError
351+
if (res)
352+
println(i"cyclic ${mbr.symbol}, $bounds -> $res")
353+
res
354+
case _ =>
355+
false
356+
}
357+
}
358+
catch {
359+
case ex: CyclicFindMember =>
360+
ctx.error(em"cyclic reference involving type $name", pos)
361+
true
362+
}
363+
}
364+
335365
/** Check that symbol's definition is well-formed. */
336366
def checkWellFormed(sym: Symbol)(implicit ctx: Context): Unit = {
337367
def fail(msg: Message) = ctx.error(msg, sym.pos)
@@ -528,6 +558,9 @@ trait Checking {
528558
def checkNonCyclic(sym: Symbol, info: TypeBounds, reportErrors: Boolean)(implicit ctx: Context): Type =
529559
Checking.checkNonCyclic(sym, info, reportErrors)
530560

561+
def checkNonCyclicInherited(joint: Type, parents: List[Type], decls: Scope, pos: Position)(implicit ctx: Context): Unit =
562+
Checking.checkNonCyclicInherited(joint, parents, decls, pos)
563+
531564
/** Check that Java statics and packages can only be used in selections.
532565
*/
533566
def checkValue(tree: Tree, proto: Type)(implicit ctx: Context): tree.type = {
@@ -866,6 +899,7 @@ trait ReChecking extends Checking {
866899
trait NoChecking extends ReChecking {
867900
import tpd._
868901
override def checkNonCyclic(sym: Symbol, info: TypeBounds, reportErrors: Boolean)(implicit ctx: Context): Type = info
902+
override def checkNonCyclicInherited(joint: Type, parents: List[Type], decls: Scope, pos: Position)(implicit ctx: Context): Unit = ()
869903
override def checkValue(tree: Tree, proto: Type)(implicit ctx: Context): tree.type = tree
870904
override def checkStable(tp: Type, pos: Position)(implicit ctx: Context): Unit = ()
871905
override def checkClassType(tp: Type, pos: Position, traitReq: Boolean, stablePrefixReq: Boolean)(implicit ctx: Context): Type = tp

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1547,6 +1547,8 @@ class Typer extends Namer
15471547
cls, isRequired, cdef.pos)
15481548
}
15491549

1550+
checkNonCyclicInherited(cls.thisType, cls.classParents, cls.info.decls, cdef.pos)
1551+
15501552
// check value class constraints
15511553
checkDerivedValueClass(cls, body1)
15521554

tests/run/returning.scala

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package scala.util.control {
2+
3+
object NonLocalReturns {
4+
5+
class ReturnThrowable[T] extends ControlThrowable {
6+
private var myResult: T = _
7+
def throwReturn(result: T): Nothing = {
8+
myResult = result
9+
throw this
10+
}
11+
def result: T = myResult
12+
}
13+
14+
def throwReturn[T](result: T)(implicit returner: ReturnThrowable[T]): Nothing =
15+
returner.throwReturn(result)
16+
17+
def returning[T](op: implicit ReturnThrowable[T] => T): T = {
18+
val returner = new ReturnThrowable[T]
19+
try op(returner)
20+
catch {
21+
case ex: ReturnThrowable[_] =>
22+
if (ex `eq` returner) ex.result.asInstanceOf[T] else throw ex
23+
}
24+
}
25+
}
26+
}
27+
28+
object Test extends App {
29+
30+
import scala.util.control.NonLocalReturns._
31+
import scala.collection.mutable.ListBuffer
32+
33+
def has(xs: List[Int], elem: Int) =
34+
returning {
35+
for (x <- xs)
36+
if (x == elem) throwReturn(true)
37+
false
38+
}
39+
40+
def takeUntil(xs: List[Int], elem: Int) =
41+
returning {
42+
var buf = new ListBuffer[Int]
43+
for (x <- xs)
44+
yield {
45+
if (x == elem) throwReturn(buf.toList)
46+
buf += x
47+
x
48+
}
49+
}
50+
51+
assert(has(List(1, 2, 3), 2))
52+
assert(takeUntil(List(1, 2, 3), 3) == List(1, 2))
53+
}

0 commit comments

Comments
 (0)