Skip to content

Commit 174d5d0

Browse files
Merge pull request #8443 from dotty-staging/fix-#6635
Fix #6635: Improve subtype tests for aliases and singleton types
2 parents 6948038 + 3a2831e commit 174d5d0

File tree

3 files changed

+135
-62
lines changed

3 files changed

+135
-62
lines changed

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

+62-62
Original file line numberDiff line numberDiff line change
@@ -233,54 +233,45 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w
233233

234234
def firstTry: Boolean = tp2 match {
235235
case tp2: NamedType =>
236-
def compareNamed(tp1: Type, tp2: NamedType): Boolean = {
236+
def compareNamed(tp1: Type, tp2: NamedType): Boolean =
237237
implicit val ctx: Context = this.ctx
238-
tp2.info match {
238+
val info2 = tp2.info
239+
info2 match
239240
case info2: TypeAlias =>
240-
recur(tp1, info2.alias)
241-
case _ => tp1 match {
242-
case tp1: NamedType =>
243-
tp1.info match {
244-
case info1: TypeAlias =>
245-
if (recur(info1.alias, tp2)) return true
246-
if (tp1.prefix.isStable) return false
247-
// If tp1.prefix is stable, the alias does contain all information about the original ref, so
248-
// there's no need to try something else. (This is important for performance).
249-
// To see why we cannot in general stop here, consider:
250-
//
251-
// trait C { type A }
252-
// trait D { type A = String }
253-
// (C & D)#A <: C#A
254-
//
255-
// Following the alias leads to the judgment `String <: C#A` which is false.
256-
// However the original judgment should be true.
257-
case _ =>
258-
}
259-
val sym2 = tp2.symbol
260-
var sym1 = tp1.symbol
261-
if (sym1.is(ModuleClass) && sym2.is(ModuleVal))
262-
// For convenience we want X$ <:< X.type
263-
// This is safe because X$ self-type is X.type
264-
sym1 = sym1.companionModule
265-
if ((sym1 ne NoSymbol) && (sym1 eq sym2))
266-
ctx.erasedTypes ||
267-
sym1.isStaticOwner ||
268-
isSubType(tp1.prefix, tp2.prefix) ||
269-
thirdTryNamed(tp2)
270-
else
271-
( (tp1.name eq tp2.name)
272-
&& tp1.isMemberRef
273-
&& tp2.isMemberRef
274-
&& isSubType(tp1.prefix, tp2.prefix)
275-
&& tp1.signature == tp2.signature
276-
&& !(sym1.isClass && sym2.isClass) // class types don't subtype each other
277-
) ||
278-
thirdTryNamed(tp2)
279-
case _ =>
280-
secondTry
281-
}
282-
}
283-
}
241+
if recur(tp1, info2.alias) then return true
242+
if tp2.asInstanceOf[TypeRef].canDropAlias then return false
243+
case _ =>
244+
tp1 match
245+
case tp1: NamedType =>
246+
tp1.info match {
247+
case info1: TypeAlias =>
248+
if recur(info1.alias, tp2) then return true
249+
if tp1.asInstanceOf[TypeRef].canDropAlias then return false
250+
case _ =>
251+
}
252+
val sym2 = tp2.symbol
253+
var sym1 = tp1.symbol
254+
if (sym1.is(ModuleClass) && sym2.is(ModuleVal))
255+
// For convenience we want X$ <:< X.type
256+
// This is safe because X$ self-type is X.type
257+
sym1 = sym1.companionModule
258+
if ((sym1 ne NoSymbol) && (sym1 eq sym2))
259+
ctx.erasedTypes ||
260+
sym1.isStaticOwner ||
261+
isSubType(tp1.prefix, tp2.prefix) ||
262+
thirdTryNamed(tp2)
263+
else
264+
( (tp1.name eq tp2.name)
265+
&& tp1.isMemberRef
266+
&& tp2.isMemberRef
267+
&& isSubType(tp1.prefix, tp2.prefix)
268+
&& tp1.signature == tp2.signature
269+
&& !(sym1.isClass && sym2.isClass) // class types don't subtype each other
270+
) ||
271+
thirdTryNamed(tp2)
272+
case _ =>
273+
secondTry
274+
end compareNamed
284275
compareNamed(tp1, tp2)
285276
case tp2: ProtoType =>
286277
isMatchedByProto(tp2, tp1)
@@ -753,20 +744,16 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w
753744
case tp1 @ AppliedType(tycon1, args1) =>
754745
compareAppliedType1(tp1, tycon1, args1)
755746
case tp1: SingletonType =>
756-
/** if `tp2 == p.type` and `p: q.type` then try `tp1 <:< q.type` as a last effort.*/
757-
def comparePaths = tp2 match {
747+
def comparePaths = tp2 match
758748
case tp2: TermRef =>
759-
tp2.info.widenExpr.dealias match {
760-
case tp2i: SingletonType =>
761-
recur(tp1, tp2i)
762-
// see z1720.scala for a case where this can arise even in typer.
763-
// Also, i1753.scala, to show why the dealias above is necessary.
764-
case _ => false
749+
compareAtoms(tp1, tp2, knownSingletons = true).getOrElse(false)
750+
|| { // needed to make from-tasty work. test cases: pos/i1753.scala, pos/t839.scala
751+
tp2.info.widenExpr.dealias match
752+
case tp2i: SingletonType => recur(tp1, tp2i)
753+
case _ => false
765754
}
766-
case _ =>
767-
false
768-
}
769-
isNewSubType(tp1.underlying.widenExpr) || comparePaths
755+
case _ => false
756+
comparePaths || isNewSubType(tp1.underlying.widenExpr)
770757
case tp1: RefinedType =>
771758
isNewSubType(tp1.parent)
772759
case tp1: RecType =>
@@ -1177,8 +1164,18 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w
11771164

11781165
/** If both `tp1` and `tp2` have atoms information, compare the atoms
11791166
* in a Some, otherwise None.
1167+
* @param knownSingletons If true, we are coming from a comparison of two singleton types
1168+
* This influences the comparison as shown below:
1169+
*
1170+
* Say you have singleton types p.type and q.type the atoms of p.type are `{p.type}..{p.type}`,
1171+
* and the atoms of `q.type` are `{}..{p.type}`. Normally the atom comparison between p's
1172+
* atoms and q's atoms gives false. But in this case we know that `q.type` is an alias of `p.type`
1173+
* so we are still allowed to conclude that `p.type <:< q.type`. A situation where this happens
1174+
* is in i6635.scala. Here,
1175+
*
1176+
* p: A, q: B & p.type and we want to conclude that p.type <: q.type.
11801177
*/
1181-
def compareAtoms(tp1: Type, tp2: Type): Option[Boolean] =
1178+
def compareAtoms(tp1: Type, tp2: Type, knownSingletons: Boolean = false): Option[Boolean] =
11821179

11831180
/** Check whether we can compare the given set of atoms with another to determine
11841181
* a subtype test between OrTypes. There is one situation where this is not
@@ -1212,9 +1209,12 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w
12121209
case Atoms.Range(lo2, hi2) if canCompareAtoms && canCompare(hi2) =>
12131210
tp1.atoms match
12141211
case Atoms.Range(lo1, hi1) =>
1215-
if hi1.subsetOf(lo2) then Some(verified(true))
1216-
else if !lo1.subsetOf(hi2) then Some(verified(false))
1217-
else None
1212+
if hi1.subsetOf(lo2) || knownSingletons && hi2.size == 1 && hi1 == hi2 then
1213+
Some(verified(true))
1214+
else if !lo1.subsetOf(hi2) then
1215+
Some(verified(false))
1216+
else
1217+
None
12181218
case _ => Some(verified(recur(tp1, NothingType)))
12191219
case _ => None
12201220

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

+27
Original file line numberDiff line numberDiff line change
@@ -2406,6 +2406,33 @@ object Types {
24062406
type ThisType = TypeRef
24072407
type ThisName = TypeName
24082408

2409+
private var myCanDropAliasPeriod: Period = Nowhere
2410+
private var myCanDropAlias: Boolean = _
2411+
2412+
/** Given an alias type `type A = B` where a recursive comparison with `B` yields
2413+
* `false`, can we conclude that the comparison is definitely false?
2414+
* This could not be the case if `A` overrides some abstract type. Example:
2415+
*
2416+
* class C { type A }
2417+
* class D { type A = Int }
2418+
* val c: C
2419+
* val d: D & c.type
2420+
* c.A <:< d.A ?
2421+
*
2422+
* The test should return true, by performing the logic in the bottom half of
2423+
* firstTry (where we check the names of types). But just following the alias
2424+
* from d.A to Int reduces the problem to `c.A <:< Int`, which returns `false`.
2425+
* So we can't drop the alias here, we need to do the backtracking to the name-
2426+
* based tests.
2427+
*/
2428+
def canDropAlias(using ctx: Context) =
2429+
if myCanDropAliasPeriod != ctx.period then
2430+
myCanDropAlias =
2431+
!symbol.canMatchInheritedSymbols
2432+
|| !prefix.baseClasses.exists(_.info.decls.lookup(name).is(Deferred))
2433+
myCanDropAliasPeriod = ctx.period
2434+
myCanDropAlias
2435+
24092436
override def designator: Designator = myDesignator
24102437
override protected def designator_=(d: Designator): Unit = myDesignator = d
24112438

tests/pos/i6635.scala

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
object Test {
2+
abstract class ExprBase { s =>
3+
type A
4+
}
5+
6+
abstract class Lit extends ExprBase { s =>
7+
type A = Int
8+
val n: A
9+
}
10+
11+
abstract class LitU extends ExprBase { s =>
12+
type A <: Int
13+
val n: A
14+
}
15+
16+
abstract class LitL extends ExprBase { s =>
17+
type A <: Int
18+
val n: A
19+
}
20+
21+
def castTest1(e1: ExprBase)(e2: e1.type)(x: e1.A): e2.A = x
22+
def castTest2(e1: ExprBase { type A = Int })(e2: e1.type)(x: e1.A): e2.A = x
23+
def castTest3(e1: ExprBase)(e2: ExprBase with e1.type)(x: e2.A): e1.A = x
24+
25+
def castTest4(e1: ExprBase { type A = Int })(e2: ExprBase with e1.type)(x: e2.A): e1.A = x
26+
27+
def castTest5a(e1: ExprBase)(e2: LitU with e1.type)(x: e2.A): e1.A = x
28+
def castTest5b(e1: ExprBase)(e2: LitL with e1.type)(x: e2.A): e1.A = x
29+
30+
//fail:
31+
def castTestFail1(e1: ExprBase)(e2: Lit with e1.type)(x: e2.A): e1.A = x // this is like castTest5a/b, but with Lit instead of LitU/LitL
32+
// the other direction never works:
33+
def castTestFail2a(e1: ExprBase)(e2: Lit with e1.type)(x: e1.A): e2.A = x
34+
def castTestFail2b(e1: ExprBase)(e2: LitL with e1.type)(x: e1.A): e2.A = x
35+
def castTestFail2c(e1: ExprBase)(e2: LitU with e1.type)(x: e1.A): e2.A = x
36+
37+
// the problem isn't about order of intersections.
38+
def castTestFail2bFlip(e1: ExprBase)(e2: e1.type with LitL)(x: e1.A): e2.A = x
39+
def castTestFail2cFlip(e1: ExprBase)(e2: e1.type with LitU)(x: e1.A): e2.A = x
40+
41+
def castTestFail3(e1: ExprBase)(e2: Lit with e1.type)(x: e1.A): e2.A = {
42+
val y: e1.type with e2.type = e2
43+
val z = x: y.A
44+
z
45+
}
46+
}

0 commit comments

Comments
 (0)