Skip to content

Commit 1aa8fbb

Browse files
committed
Merge pull request #1014 from dotty-staging/fix/deep-subtypes
Check all bounds and avoid infinite subtyping checks when intersecting denotations
2 parents dd73318 + 224232d commit 1aa8fbb

File tree

12 files changed

+248
-53
lines changed

12 files changed

+248
-53
lines changed

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,11 +259,12 @@ object Denotations {
259259
*
260260
* If there is no preferred accessible denotation, return a JointRefDenotation
261261
* with one of the operand symbols (unspecified which one), and an info which
262-
* is intersection (&) of the infos of the operand denotations.
262+
* is the intersection (using `&` or `safe_&` if `safeIntersection` is true)
263+
* of the infos of the operand denotations.
263264
*
264265
* If SingleDenotations with different signatures are joined, return NoDenotation.
265266
*/
266-
def & (that: Denotation, pre: Type)(implicit ctx: Context): Denotation = {
267+
def & (that: Denotation, pre: Type, safeIntersection: Boolean = false)(implicit ctx: Context): Denotation = {
267268

268269
/** Try to merge denot1 and denot2 without adding a new signature. */
269270
def mergeDenot(denot1: Denotation, denot2: SingleDenotation): Denotation = denot1 match {
@@ -317,7 +318,11 @@ object Denotations {
317318
else if (preferSym(sym2, sym1)) sym2
318319
else sym1
319320
val jointInfo =
320-
try info1 & info2
321+
try
322+
if (safeIntersection)
323+
info1 safe_& info2
324+
else
325+
info1 & info2
321326
catch {
322327
case ex: MergeError =>
323328
if (pre.widen.classSymbol.is(Scala2x) || ctx.scala2Mode)

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

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ object Types {
459459
val jointInfo =
460460
if (rinfo.isAlias) rinfo
461461
else if (pdenot.info.isAlias) pdenot.info
462-
else if (ctx.pendingMemberSearches.contains(name)) safeAnd(pdenot.info, rinfo)
462+
else if (ctx.pendingMemberSearches.contains(name)) pdenot.info safe_& rinfo
463463
else
464464
try pdenot.info & rinfo
465465
catch {
@@ -470,11 +470,15 @@ object Types {
470470
// the special shortcut for Any in derivesFrom was as yet absent. To reproduce,
471471
// remove the special treatment of Any in derivesFrom and compile
472472
// sets.scala.
473-
safeAnd(pdenot.info, rinfo)
473+
pdenot.info safe_& rinfo
474474
}
475475
pdenot.asSingleDenotation.derivedSingleDenotation(pdenot.symbol, jointInfo)
476-
} else
477-
pdenot & (new JointRefDenotation(NoSymbol, rinfo, Period.allInRun(ctx.runId)), pre)
476+
} else {
477+
pdenot & (
478+
new JointRefDenotation(NoSymbol, rinfo, Period.allInRun(ctx.runId)),
479+
pre,
480+
safeIntersection = ctx.pendingMemberSearches.contains(name))
481+
}
478482
}
479483
def goThis(tp: ThisType) = {
480484
val d = go(tp.underlying)
@@ -501,12 +505,10 @@ object Types {
501505
go(next)
502506
}
503507
}
504-
def goAnd(l: Type, r: Type) = go(l) & (go(r), pre)
505-
def goOr(l: Type, r: Type) = go(l) | (go(r), pre)
506-
def safeAnd(tp1: Type, tp2: Type): Type = (tp1, tp2) match {
507-
case (TypeBounds(lo1, hi1), TypeBounds(lo2, hi2)) => TypeBounds(lo1 | lo2, AndType(hi1, hi2))
508-
case _ => tp1 & tp2
508+
def goAnd(l: Type, r: Type) = {
509+
go(l) & (go(r), pre, safeIntersection = ctx.pendingMemberSearches.contains(name))
509510
}
511+
def goOr(l: Type, r: Type) = go(l) | (go(r), pre)
510512

511513
{ val recCount = ctx.findMemberCount + 1
512514
ctx.findMemberCount = recCount
@@ -705,6 +707,19 @@ object Types {
705707
ctx.typeComparer.glb(this, that)
706708
}
707709

710+
/** Safer version of `&`.
711+
*
712+
* This version does not simplify the upper bound of the intersection of
713+
* two TypeBounds. The simplification done by `&` requires subtyping checks
714+
* which may end up calling `&` again, in most cases this should be safe
715+
* but because of F-bounded types, this can result in an infinite loop
716+
* (which will be masked unless `-Yno-deep-subtypes` is enabled).
717+
*/
718+
def safe_& (that: Type)(implicit ctx: Context): Type = (this, that) match {
719+
case (TypeBounds(lo1, hi1), TypeBounds(lo2, hi2)) => TypeBounds(lo1 | lo2, AndType(hi1, hi2))
720+
case _ => this & that
721+
}
722+
708723
def | (that: Type)(implicit ctx: Context): Type = track("|") {
709724
ctx.typeComparer.lub(this, that)
710725
}

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

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisTran
6868
// TODO fill in
6969
}
7070

71-
/** Check bounds of AppliedTypeTrees and TypeApplys.
71+
/** Check bounds of AppliedTypeTrees.
7272
* Replace type trees with TypeTree nodes.
7373
* Replace constant expressions with Literal nodes.
7474
* Note: Demanding idempotency instead of purity in literalize is strictly speaking too loose.
@@ -97,29 +97,17 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisTran
9797
* Revisit this issue once we have implemented `inline`. Then we can demand
9898
* purity of the prefix unless the selection goes to an inline val.
9999
*/
100-
private def normalizeTree(tree: Tree)(implicit ctx: Context): Tree = {
101-
def literalize(tp: Type): Tree = tp.widenTermRefExpr match {
102-
case ConstantType(value) if isIdempotentExpr(tree) => Literal(value)
103-
case _ => tree
104-
}
105-
def norm(tree: Tree) =
106-
if (tree.isType) TypeTree(tree.tpe).withPos(tree.pos)
107-
else literalize(tree.tpe)
108-
tree match {
109-
case tree: TypeTree =>
110-
tree
111-
case AppliedTypeTree(tycon, args) =>
112-
val tparams = tycon.tpe.typeSymbol.typeParams
113-
val bounds = tparams.map(tparam =>
114-
tparam.info.asSeenFrom(tycon.tpe.normalizedPrefix, tparam.owner.owner).bounds)
115-
Checking.checkBounds(args, bounds, _.substDealias(tparams, _))
116-
norm(tree)
117-
case TypeApply(fn, args) =>
118-
Checking.checkBounds(args, fn.tpe.widen.asInstanceOf[PolyType])
119-
norm(tree)
120-
case _ =>
121-
norm(tree)
122-
}
100+
private def normalizeTree(tree: Tree)(implicit ctx: Context): Tree = tree match {
101+
case tree: TypeTree => tree
102+
case _ =>
103+
if (tree.isType) {
104+
Checking.boundsChecker.traverse(tree)
105+
TypeTree(tree.tpe).withPos(tree.pos)
106+
}
107+
else tree.tpe.widenTermRefExpr match {
108+
case ConstantType(value) if isIdempotentExpr(tree) => Literal(value)
109+
case _ => tree
110+
}
123111
}
124112

125113
class PostTyperTransformer extends Transformer {
@@ -161,10 +149,16 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisTran
161149
}
162150
case tree: Select =>
163151
transformSelect(paramFwd.adaptRef(tree), Nil)
164-
case tree @ TypeApply(sel: Select, args) =>
165-
val args1 = transform(args)
166-
val sel1 = transformSelect(sel, args1)
167-
if (superAcc.isProtectedAccessor(sel1)) sel1 else cpy.TypeApply(tree)(sel1, args1)
152+
case tree @ TypeApply(fn, args) =>
153+
Checking.checkBounds(args, fn.tpe.widen.asInstanceOf[PolyType])
154+
fn match {
155+
case sel: Select =>
156+
val args1 = transform(args)
157+
val sel1 = transformSelect(sel, args1)
158+
if (superAcc.isProtectedAccessor(sel1)) sel1 else cpy.TypeApply(tree)(sel1, args1)
159+
case _ =>
160+
super.transform(tree)
161+
}
168162
case tree @ Assign(sel: Select, _) =>
169163
superAcc.transformAssign(super.transform(tree))
170164
case tree: Template =>
@@ -180,6 +174,16 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisTran
180174
case tree: DefDef =>
181175
transformAnnots(tree)
182176
superAcc.wrapDefDef(tree)(super.transform(tree).asInstanceOf[DefDef])
177+
case tree: TypeDef =>
178+
transformAnnots(tree)
179+
val sym = tree.symbol
180+
val tree1 =
181+
if (sym.isClass) tree
182+
else {
183+
Checking.boundsChecker.traverse(tree.rhs)
184+
cpy.TypeDef(tree)(rhs = TypeTree(tree.symbol.info))
185+
}
186+
super.transform(tree1)
183187
case tree: MemberDef =>
184188
transformAnnots(tree)
185189
super.transform(tree)

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,6 @@ trait Applications extends Compatibility { self: Typer =>
607607
case pt: PolyType =>
608608
if (typedArgs.length <= pt.paramBounds.length)
609609
typedArgs = typedArgs.zipWithConserve(pt.paramBounds)(adaptTypeArg)
610-
Checking.checkBounds(typedArgs, pt)
611610
case _ =>
612611
}
613612
assignType(cpy.TypeApply(tree)(typedFn, typedArgs), typedFn, typedArgs)

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,21 @@ object Checking {
4848
def checkBounds(args: List[tpd.Tree], poly: PolyType)(implicit ctx: Context): Unit =
4949
checkBounds(args, poly.paramBounds, _.substParams(poly, _))
5050

51+
/** Check all AppliedTypeTree nodes in this tree for legal bounds */
52+
val boundsChecker = new TreeTraverser {
53+
def traverse(tree: Tree)(implicit ctx: Context) = {
54+
tree match {
55+
case AppliedTypeTree(tycon, args) =>
56+
val tparams = tycon.tpe.typeSymbol.typeParams
57+
val bounds = tparams.map(tparam =>
58+
tparam.info.asSeenFrom(tycon.tpe.normalizedPrefix, tparam.owner.owner).bounds)
59+
checkBounds(args, bounds, _.substDealias(tparams, _))
60+
case _ =>
61+
}
62+
traverseChildren(tree)
63+
}
64+
}
65+
5166
/** Check that `tp` refers to a nonAbstract class
5267
* and that the instance conforms to the self type of the created class.
5368
*/

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -869,7 +869,9 @@ class Namer { typer: Typer =>
869869
if (tparamSyms.nonEmpty && !isDerived) tp.LambdaAbstract(tparamSyms)
870870
//else if (toParameterize) tp.parameterizeWith(tparamSyms)
871871
else tp
872-
sym.info = abstracted(TypeBounds.empty)
872+
873+
val dummyInfo = abstracted(TypeBounds.empty)
874+
sym.info = dummyInfo
873875
// Temporarily set info of defined type T to ` >: Nothing <: Any.
874876
// This is done to avoid cyclic reference errors for F-bounds.
875877
// This is subtle: `sym` has now an empty TypeBounds, but is not automatically
@@ -890,6 +892,19 @@ class Namer { typer: Typer =>
890892
sym.info = NoCompleter
891893
sym.info = checkNonCyclic(sym, unsafeInfo, reportErrors = true)
892894
}
895+
896+
// Here we pay the price for the cavalier setting info to TypeBounds.empty above.
897+
// We need to compensate by invalidating caches in references that might
898+
// still contain the TypeBounds.empty. If we do not do this, stdlib factories
899+
// fail with a bounds error in PostTyper.
900+
def ensureUpToDate(tp: Type, outdated: Type) = tp match {
901+
case tref: TypeRef if tref.info == outdated && sym.info != outdated =>
902+
tref.uncheckedSetSym(null)
903+
case _ =>
904+
}
905+
ensureUpToDate(sym.typeRef, dummyInfo)
906+
ensureUpToDate(sym.typeRef.appliedTo(tparamSyms.map(_.typeRef)), TypeBounds.empty)
907+
893908
etaExpandArgs.apply(sym.info)
894909
}
895910

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -961,8 +961,7 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
961961
val TypeDef(name, rhs) = tdef
962962
checkLowerNotHK(sym, tdef.tparams.map(symbolOfTree), tdef.pos)
963963
completeAnnotations(tdef, sym)
964-
val _ = typedType(rhs) // unused, typecheck only to remove from typedTree
965-
assignType(cpy.TypeDef(tdef)(name, TypeTree(sym.info), Nil), sym)
964+
assignType(cpy.TypeDef(tdef)(name, typedType(rhs), Nil), sym)
966965
}
967966

968967
def typedClassDef(cdef: untpd.TypeDef, cls: ClassSymbol)(implicit ctx: Context) = track("typedClassDef") {

test/dotc/tests.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ class tests extends CompilerTest {
108108

109109
@Test def neg_abstractOverride() = compileFile(negDir, "abstract-override", xerrors = 2)
110110
@Test def neg_blockescapes() = compileFile(negDir, "blockescapesNeg", xerrors = 1)
111-
@Test def neg_bounds() = compileFile(negDir, "bounds", xerrors = 2)
112-
@Test def neg_typedapply() = compileFile(negDir, "typedapply", xerrors = 4)
111+
@Test def neg_bounds() = compileFile(negDir, "bounds", xerrors = 3)
112+
@Test def neg_typedapply() = compileFile(negDir, "typedapply", xerrors = 3)
113113
@Test def neg_typedIdents() = compileDir(negDir, "typedIdents", xerrors = 2)
114114
@Test def neg_assignments() = compileFile(negDir, "assignments", xerrors = 3)
115115
@Test def neg_typers() = compileFile(negDir, "typers", xerrors = 14)(allowDoubleBindings)
@@ -167,6 +167,7 @@ class tests extends CompilerTest {
167167
@Test def neg_selfreq = compileFile(negDir, "selfreq", xerrors = 2)
168168
@Test def neg_singletons = compileFile(negDir, "singletons", xerrors = 8)
169169
@Test def neg_shadowedImplicits = compileFile(negDir, "arrayclone-new", xerrors = 2)
170+
@Test def neg_ski = compileFile(negDir, "ski", xerrors = 2)
170171
@Test def neg_traitParamsTyper = compileFile(negDir, "traitParamsTyper", xerrors = 5)
171172
@Test def neg_traitParamsMixin = compileFile(negDir, "traitParamsMixin", xerrors = 2)
172173
@Test def neg_firstError = compileFile(negDir, "firstError", xerrors = 3)
@@ -175,6 +176,7 @@ class tests extends CompilerTest {
175176
@Test def neg_validateParsing = compileFile(negDir, "validate-parsing", xerrors = 7)
176177
@Test def neg_validateRefchecks = compileFile(negDir, "validate-refchecks", xerrors = 2)
177178
@Test def neg_skolemize = compileFile(negDir, "skolemize", xerrors = 2)
179+
@Test def neg_nested_bounds = compileFile(negDir, "nested_bounds", xerrors = 1)
178180

179181
@Test def run_all = runFiles(runDir)
180182

tests/neg/bounds.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@ object Test {
55
g("foo")
66
new C("bar")
77
}
8+
def baz[X >: Y, Y <: String](x: X, y: Y) = (x, y)
9+
10+
baz[Int, String](1, "abc")
11+
812
}

tests/neg/nested_bounds.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class OnlyInt[T <: Int]
2+
3+
object Test {
4+
type T = List[OnlyInt[String]] // error: Type argument String does not conform to upper bound Int
5+
}

0 commit comments

Comments
 (0)