Skip to content

Commit ea87402

Browse files
Merge pull request #10938 from dotty-staging/match-tpe-enums
fix #10511: compare enumvalues in provably disjoint
2 parents 4403a80 + b0aeefc commit ea87402

File tree

6 files changed

+85
-3
lines changed

6 files changed

+85
-3
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2404,14 +2404,20 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
24042404
* 1. Single inheritance of classes
24052405
* 2. Final classes cannot be extended
24062406
* 3. ConstantTypes with distinct values are non intersecting
2407-
* 4. There is no value of type Nothing
2407+
* 4. TermRefs with distinct values are non intersecting
2408+
* 5. There is no value of type Nothing
24082409
*
24092410
* Note on soundness: the correctness of match types relies on on the
24102411
* property that in all possible contexts, the same match type expression
24112412
* is either stuck or reduces to the same case.
24122413
*/
24132414
def provablyDisjoint(tp1: Type, tp2: Type)(using Context): Boolean = {
24142415
// println(s"provablyDisjoint(${tp1.show}, ${tp2.show})")
2416+
2417+
def isEnumValueOrModule(ref: TermRef): Boolean =
2418+
val sym = ref.termSymbol
2419+
sym.isAllOf(EnumCase, butNot=JavaDefined) || sym.is(Module)
2420+
24152421
/** Can we enumerate all instantiations of this type? */
24162422
def isClosedSum(tp: Symbol): Boolean =
24172423
tp.is(Sealed) && tp.isOneOf(AbstractOrTrait) && !tp.hasAnonymousChild
@@ -2517,6 +2523,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
25172523
provablyDisjoint(gadtBounds(tp1.symbol).hi, tp2) || provablyDisjoint(tp1.superType, tp2)
25182524
case (_, tp2: NamedType) if gadtBounds(tp2.symbol) != null =>
25192525
provablyDisjoint(tp1, gadtBounds(tp2.symbol).hi) || provablyDisjoint(tp1, tp2.superType)
2526+
case (tp1: TermRef, tp2: TermRef) if isEnumValueOrModule(tp1) && isEnumValueOrModule(tp2) =>
2527+
tp1.termSymbol != tp2.termSymbol
25202528
case (tp1: TypeProxy, tp2: TypeProxy) =>
25212529
provablyDisjoint(matchTypeSuperType(tp1), tp2) || provablyDisjoint(tp1, matchTypeSuperType(tp2))
25222530
case (tp1: TypeProxy, _) =>

compiler/test/dotc/pos-from-tasty.blacklist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ t802.scala
88

99
# missing position
1010
rbtree.scala
11+
12+
# transitive reduction of match types
13+
i10511.scala

docs/docs/reference/new-types/match-types.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Recursive match type definitions can also be given an upper bound, like this:
4848

4949
```scala
5050
type Concat[Xs <: Tuple, +Ys <: Tuple] <: Tuple = Xs match
51-
case Unit => Ys
51+
case EmptyTuple => Ys
5252
case x *: xs => x *: Concat[xs, Ys]
5353
```
5454

@@ -126,6 +126,7 @@ Disjointness proofs rely on the following properties of Scala types:
126126
1. Single inheritance of classes
127127
2. Final classes cannot be extended
128128
3. Constant types with distinct values are nonintersecting
129+
4. Singleton paths to distinct values are nonintersecting, such as `object` definitions or singleton enum cases.
129130

130131
Type parameters in patterns are minimally instantiated when computing `S <: Pi`.
131132
An instantiation `Is` is _minimal_ for `Xs` if all type variables in `Xs` that
@@ -240,4 +241,3 @@ main differences here are:
240241
whereas match types also work for type parameters and abstract types.
241242
- Match types support direct recursion.
242243
- Conditional types distribute through union types.
243-

tests/neg/i10511.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
enum Bool {
2+
case True
3+
case False
4+
}
5+
6+
import Bool._
7+
8+
type Not[B <: Bool] = B match {
9+
case True.type => False.type
10+
case False.type => True.type
11+
}
12+
13+
def not[B <: Bool & Singleton](b: B): Not[B] = b match {
14+
case b: False.type => True // error
15+
case b: True.type => False // error
16+
}
17+
18+
val f: Not[False.type] = False // error: Found: (Bool.False : Bool) Required: (Bool.True : Bool)

tests/pos/i10511.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
enum Bool {
2+
case True
3+
case False
4+
}
5+
6+
import Bool._
7+
8+
type Not[B <: Bool] = B match {
9+
case True.type => False.type
10+
case False.type => True.type
11+
}
12+
13+
val t: True.type = True
14+
val f: False.type = False
15+
16+
val g: Not[False.type] = t
17+
18+
val t1: Not[f.type] = t // transitivity
19+
val f1: Not[t.type] = f // transitivity
20+
21+
val t2: Not[f1.type] = t1 // transitivity x2
22+
val f2: Not[t1.type] = f1 // transitivity x2

tests/run/i10511.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
enum Bool {
2+
case True
3+
case False
4+
5+
// just to make sure we are using reference equality
6+
override def equals(a: Any) = false
7+
8+
}
9+
10+
import Bool._
11+
12+
type Not[B <: Bool] = B match {
13+
case True.type => False.type
14+
case False.type => True.type
15+
}
16+
17+
def not[B <: Bool & Singleton](b: B): Not[B] = b match {
18+
case b: True.type => False
19+
case b: False.type => True
20+
}
21+
22+
@main def Test =
23+
24+
val t: True.type = True
25+
val f: False.type = False
26+
27+
val t1: Not[False.type] = t
28+
val f1: Not[True.type] = f
29+
30+
assert(not(True).asInstanceOf[AnyRef] eq False)
31+
assert(not(False).asInstanceOf[AnyRef] eq True)

0 commit comments

Comments
 (0)