Skip to content

Commit f87153b

Browse files
committed
Detect cycles and protected legal ones with LazyRefs
Cycles are now detected early, when an info is first completed. Legal, f-bounded cycles are broken by a LazyRef, which will construct its type lazily. This makes checkBounds validation of AppliedTypeTrees work (in FirstTransform). Formerly, this stackoverflowed despite the laziness precautions in findMember. Todo: Do the same for class files coming from Java and Scala 2.x.
1 parent 9748c9b commit f87153b

File tree

8 files changed

+183
-28
lines changed

8 files changed

+183
-28
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,8 @@ object SymDenotations {
14711471
def complete(denot: SymDenotation)(implicit ctx: Context): Unit = unsupported("complete")
14721472
}
14731473

1474+
object NoCompleter extends NoCompleter
1475+
14741476
/** A lazy type for modules that points to the module class.
14751477
* Needed so that `moduleClass` works before completion.
14761478
* Completion of modules is always completion of the underlying

src/dotty/tools/dotc/core/TypeComparer.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,8 @@ class TypeComparer(initctx: Context) extends DotClass {
315315
private def rebase(tp: NamedType): Type = {
316316
def rebaseFrom(prefix: Type): Type = {
317317
rebaseQual(prefix, tp.name, considerBoth = true) match {
318-
case rt: RefinedType if rt ne prefix => tp.derivedSelect(RefinedThis(rt))
318+
case rt: RefinedType if rt ne prefix =>
319+
tp.derivedSelect(RefinedThis(rt)).dealias // dealias to short-circuit cycles spanning type aliases or LazyRefs
319320
case _ => tp
320321
}
321322
}
@@ -511,6 +512,8 @@ class TypeComparer(initctx: Context) extends DotClass {
511512
case NoType => true
512513
}
513514
compareWild
515+
case tp2: LazyRef =>
516+
isSubType(tp1, tp2.ref)
514517
case tp2: AnnotatedType =>
515518
isSubType(tp1, tp2.tpe) // todo: refine?
516519
case AndType(tp21, tp22) =>
@@ -568,6 +571,8 @@ class TypeComparer(initctx: Context) extends DotClass {
568571
case _ => true
569572
}
570573
compareWild
574+
case tp1: LazyRef =>
575+
isSubType(tp1.ref, tp2)
571576
case tp1: AnnotatedType =>
572577
isSubType(tp1.tpe, tp2)
573578
case ErrorType =>

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import language.implicitConversions
3131

3232
object Types {
3333

34+
private var recCount = 0
35+
3436
/** The class of types.
3537
* The principal subclasses and sub-objects are as follows:
3638
*
@@ -379,6 +381,8 @@ object Types {
379381
* flags in `excluded` from consideration.
380382
*/
381383
final def findMember(name: Name, pre: Type, excluded: FlagSet)(implicit ctx: Context): Denotation = try {
384+
recCount += 1
385+
assert(recCount < 20)
382386
@tailrec def go(tp: Type): Denotation = tp match {
383387
case tp: RefinedType =>
384388
if (name eq tp.refinedName) goRefined(tp) else go(tp.parent)
@@ -436,8 +440,10 @@ object Types {
436440
case ex: MergeError =>
437441
throw new MergeError(s"${ex.getMessage} as members of type ${pre.show}")
438442
case ex: Throwable =>
439-
println(s"error occurred during: $this: ${this.widen} member $name")
443+
println(i"findMember exception for $this: ${this.widen} member $name")
440444
throw ex // DEBUG
445+
} finally {
446+
recCount -= 1
441447
}
442448

443449
/** The set of names of members of this type that pass the given name filter
@@ -599,7 +605,9 @@ object Types {
599605
case _ => this
600606
}
601607

602-
/** Follow aliases until type is no longer an alias type. */
608+
/** Follow aliases and derefernces LazyRefs and instantiated TypeVars until type
609+
* is no longer alias type, LazyRef, or instantiated type variable.
610+
*/
603611
final def dealias(implicit ctx: Context): Type = this match {
604612
case tp: TypeRef =>
605613
tp.info match {
@@ -609,6 +617,8 @@ object Types {
609617
case tp: TypeVar =>
610618
val tp1 = tp.instanceOpt
611619
if (tp1.exists) tp1.dealias else tp
620+
case tp: LazyRef =>
621+
tp.ref.dealias
612622
case tp => tp
613623
}
614624

@@ -643,6 +653,14 @@ object Types {
643653
case _ => NoType
644654
}
645655

656+
/** The chain of underlying types as long as type is a TypeProxy.
657+
* Useful for diagnostics
658+
*/
659+
def underlyingChain(implicit ctx: Context): List[Type] = this match {
660+
case tp: TypeProxy => tp :: tp.underlying.underlyingChain
661+
case _ => Nil
662+
}
663+
646664
/** A prefix-less termRef to a new skolem symbol that has the given type as info */
647665
def narrow(implicit ctx: Context): TermRef = TermRef(NoPrefix, ctx.newSkolem(this))
648666

@@ -1469,6 +1487,12 @@ object Types {
14691487
unique(new CachedConstantType(value))
14701488
}
14711489

1490+
case class LazyRef(refFn: () => Type) extends UncachedProxyType with TermType {
1491+
lazy val ref = refFn()
1492+
override def underlying(implicit ctx: Context) = ref
1493+
override def toString = s"LazyRef($ref)"
1494+
}
1495+
14721496
// --- Refined Type ---------------------------------------------------------
14731497

14741498
/** A refined type parent { refinement }
@@ -2408,6 +2432,9 @@ object Types {
24082432
case tp @ SuperType(thistp, supertp) =>
24092433
tp.derivedSuperType(this(thistp), this(supertp))
24102434

2435+
case tp: LazyRef =>
2436+
LazyRef(() => this(tp.ref))
2437+
24112438
case tp: ClassInfo =>
24122439
mapClassInfo(tp)
24132440

src/dotty/tools/dotc/transform/FirstTransform.scala

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,11 @@ class FirstTransform extends TreeTransform with IdentityDenotTransformer { thisT
8383
override def transformOther(tree: Tree)(implicit ctx: Context, info: TransformerInfo) = tree match {
8484
case tree: Import => EmptyTree
8585
case tree: NamedArg => tree.arg
86-
/* not yet enabled
8786
case AppliedTypeTree(tycon, args) =>
88-
8987
val tparams = tycon.tpe.typeSymbol.typeParams
9088
Checking.checkBounds(
9189
args, tparams.map(_.info.bounds), (tp, argTypes) => tp.substDealias(tparams, argTypes))
9290
TypeTree(tree.tpe).withPos(tree.pos)
93-
*/
9491
case tree =>
9592
if (tree.isType) TypeTree(tree.tpe, tree).withPos(tree.pos)
9693
else tree

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

Lines changed: 130 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@ package typer
44

55
import core._
66
import ast._
7-
import Contexts._, Types._, Flags._, Denotations._, Names._, StdNames._, NameOps._, Symbols._
8-
import Trees._, ProtoTypes._
7+
import Contexts._
8+
import Types._
9+
import Flags._
10+
import Denotations._
11+
import Names._
12+
import StdNames._
13+
import NameOps._
14+
import Symbols._
15+
import Trees._
16+
import ProtoTypes._
917
import Constants._
1018
import Scopes._
1119
import annotation.unchecked
@@ -14,13 +22,125 @@ import util.{Stats, SimpleMap}
1422
import util.common._
1523
import Decorators._
1624
import Uniques._
17-
import ErrorReporting.{errorType, DiagnosticString}
25+
import ErrorReporting.{err, errorType, DiagnosticString}
1826
import config.Printers._
1927
import collection.mutable
28+
import SymDenotations.NoCompleter
29+
30+
object Checking {
31+
import tpd._
32+
33+
/** A general checkBounds method that can be used for TypeApply nodes as
34+
* well as for AppliedTypeTree nodes.
35+
*/
36+
def checkBounds(args: List[tpd.Tree], bounds: List[TypeBounds], instantiate: (Type, List[Type]) => Type)(implicit ctx: Context) = {
37+
val argTypes = args.tpes
38+
for ((arg, bounds) <- args zip bounds) {
39+
def notConforms(which: String, bound: Type) = {
40+
ctx.error(
41+
d"Type argument ${arg.tpe} does not conform to $which bound $bound ${err.whyNoMatchStr(arg.tpe, bound)}",
42+
arg.pos)
43+
}
44+
def checkOverlapsBounds(lo: Type, hi: Type): Unit = {
45+
//println(i"instantiating ${bounds.hi} with $argTypes")
46+
//println(i" = ${instantiate(bounds.hi, argTypes)}")
47+
val hiBound = instantiate(bounds.hi, argTypes)
48+
if (!(lo <:< hiBound)) notConforms("upper", hiBound)
49+
if (!(bounds.lo <:< hi)) notConforms("lower", bounds.lo)
50+
}
51+
arg.tpe match {
52+
case TypeBounds(lo, hi) => checkOverlapsBounds(lo, hi)
53+
case tp => checkOverlapsBounds(tp, tp)
54+
}
55+
}
56+
}
57+
58+
/** A type map which checks that the only cycles in a type are F-bounds
59+
* and that protects all F-bounded references by LazyRefs.
60+
*/
61+
class CheckNonCyclicMap(implicit ctx: Context) extends TypeMap {
62+
63+
/** Are cycles allowed within nested refinedInfos of currently checked type? */
64+
private var nestedCycleOK = false
65+
66+
/** Are cycles allwoed within currently checked type? */
67+
private var cycleOK = false
68+
69+
/** A diagnostic output string that indicates the position of the last
70+
* part of a type bounds checked by checkInfo. Possible choices:
71+
* alias, lower bound, upper bound.
72+
*/
73+
var where: String = ""
74+
75+
/** Check info `tp` for cycles. Throw CyclicReference for illegal cycles,
76+
* break direct cycle with a LazyRef for legal, F-bounded cycles.
77+
*/
78+
def checkInfo(tp: Type): Type = tp match {
79+
case tp @ TypeBounds(lo, hi) =>
80+
if (lo eq hi)
81+
try tp.derivedTypeAlias(apply(lo))
82+
finally where = "alias"
83+
else {
84+
val lo1 = try apply(lo) finally where = "lower bound"
85+
val saved = nestedCycleOK
86+
nestedCycleOK = true
87+
try tp.derivedTypeBounds(lo1, apply(hi))
88+
finally {
89+
nestedCycleOK = saved
90+
where = "upper bound"
91+
}
92+
}
93+
case _ =>
94+
tp
95+
}
96+
97+
def apply(tp: Type) = tp match {
98+
case tp @ RefinedType(parent, name) =>
99+
val parent1 = this(parent)
100+
val saved = cycleOK
101+
cycleOK = nestedCycleOK
102+
try tp.derivedRefinedType(parent1, name, this(tp.refinedInfo))
103+
finally cycleOK = saved
104+
case tp @ TypeRef(pre, name) =>
105+
try {
106+
// Check info of typeref recursively, marking the referred symbol
107+
// with NoCompleter. This provokes a CyclicReference when the symbol
108+
// is hit again. Without this precaution we could stackoverflow here.
109+
val info = tp.info
110+
val symInfo = tp.symbol.info
111+
if (tp.symbol.exists) tp.symbol.info = SymDenotations.NoCompleter
112+
try checkInfo(info)
113+
finally if (tp.symbol.exists) tp.symbol.info = symInfo
114+
tp
115+
} catch {
116+
case ex: CyclicReference =>
117+
ctx.debuglog(i"cycle detected for $tp, $nestedCycleOK, $cycleOK")
118+
if (cycleOK) LazyRef(() => tp) else throw ex
119+
}
120+
case _ => mapOver(tp)
121+
}
122+
}
123+
}
20124

21125
trait Checking {
22126

23127
import tpd._
128+
import Checking._
129+
130+
/** Check that `info` of symbol `sym` is not cyclic.
131+
* @pre sym is not yet initialized (i.e. its type is a Completer).
132+
* @return `info` where every legal F-bounded reference is proctected
133+
* by a `LazyRef`, or `ErrorType` if a cycle was detected and reported.
134+
*/
135+
def checkNonCyclic(sym: Symbol, info: TypeBounds)(implicit ctx: Context): Type = {
136+
val checker = new CheckNonCyclicMap
137+
try checker.checkInfo(info)
138+
catch {
139+
case ex: CyclicReference =>
140+
ctx.error(i"illegal cyclic reference: ${checker.where} $info of $sym refers back to the type itself", sym.pos)
141+
ErrorType
142+
}
143+
}
24144

25145
/** Check that Java statics and packages can only be used in selections.
26146
*/
@@ -32,17 +152,13 @@ trait Checking {
32152
tree
33153
}
34154

35-
/** Check that type arguments `args` conform to corresponding bounds in `poly` */
36-
def checkBounds(args: List[tpd.Tree], poly: PolyType, pos: Position)(implicit ctx: Context): Unit = {
37-
val argTypes = args.tpes
38-
def substituted(tp: Type) = tp.substParams(poly, argTypes)
39-
for ((arg, bounds) <- args zip poly.paramBounds) {
40-
def notConforms(which: String, bound: Type) =
41-
ctx.error(d"Type argument ${arg.tpe} does not conform to $which bound $bound", arg.pos)
42-
if (!(arg.tpe <:< substituted(bounds.hi))) notConforms("upper", bounds.hi)
43-
if (!(bounds.lo <:< arg.tpe)) notConforms("lower", bounds.lo)
44-
}
45-
}
155+
/** Check that type arguments `args` conform to corresponding bounds in `poly`
156+
* Note: This does not check the bounds of AppliedTypeTrees. These
157+
* are handled by method checkBounds in FirstTransform
158+
* TODO: remove pos parameter
159+
*/
160+
def checkBounds(args: List[tpd.Tree], poly: PolyType, pos: Position)(implicit ctx: Context): Unit = Checking.checkBounds(
161+
args, poly.paramBounds, (tp, argTypes) => tp.substParams(poly, argTypes))
46162

47163
/** Check that type `tp` is stable. */
48164
def checkStable(tp: Type, pos: Position)(implicit ctx: Context): Unit =

src/dotty/tools/dotc/typer/ErrorReporting.scala

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,19 +97,21 @@ object ErrorReporting {
9797
errorTree(tree, typeMismatchStr(tree.tpe, pt) + implicitFailure.postscript)
9898
}
9999

100+
/** A subtype log explaining why `found` does not conform to `expected` */
101+
def whyNoMatchStr(found: Type, expected: Type) =
102+
if (ctx.settings.explaintypes.value)
103+
"\n" + ctx.typerState.show + "\n" + TypeComparer.explained((found <:< expected)(_))
104+
else
105+
""
106+
100107
def typeMismatchStr(found: Type, expected: Type) = disambiguated { implicit ctx =>
101-
val (typerStateStr, explanationStr) =
102-
if (ctx.settings.explaintypes.value)
103-
("\n" + ctx.typerState.show, "\n" + TypeComparer.explained((found <:< expected)(_)))
104-
else
105-
("", "")
106108
def infoStr = found match { // DEBUG
107109
case tp: TypeRef => s"with info ${tp.info} / ${tp.prefix.toString} / ${tp.prefix.dealias.toString}"
108110
case _ => ""
109111
}
110112
d"""type mismatch:
111113
| found : $found
112-
| required: $expected""".stripMargin + typerStateStr + explanationStr
114+
| required: $expected""".stripMargin + whyNoMatchStr(found, expected)
113115
}
114116
}
115117

src/dotty/tools/dotc/typer/Namer.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -674,9 +674,11 @@ class Namer { typer: Typer =>
674674
if (needsLambda) rhsType.LambdaAbstract(tparamSyms)
675675
else if (toParameterize) rhsType.parameterizeWith(tparamSyms)
676676
else rhsType
677-
rhsType match {
678-
case _: TypeBounds => abstractedRhsType
677+
val unsafeInfo = rhsType match {
678+
case _: TypeBounds => abstractedRhsType.asInstanceOf[TypeBounds]
679679
case _ => TypeAlias(abstractedRhsType, if (sym is Local) sym.variance else 0)
680680
}
681+
sym.info = NoCompleter
682+
checkNonCyclic(sym, unsafeInfo)
681683
}
682684
}

tests/neg/cycles.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class Foo[T <: U, U <: T]
2+
3+
class Bar[T >: T]
4+

0 commit comments

Comments
 (0)