Skip to content

Commit 1276ba5

Browse files
authored
Cut the Gordian Knot: Don't widen unions to transparent (#15642)
The idea is that some unions usually make more sense than others. For instance, if `Apply` and `Ident` are case classes that extend `Tree`, it makes sense to widen `Apply | Ident` to `Tree`. But it makes less sense to widen `String | Int` to `Matchable`. Making sense means: (1) Matches our intuitive understanding, and (2) choosing not to widen would usually not cause errors. To explain (2): In the `Tree` case it might well be that we define a given for `Inv[Tree]` for invariant class `Inv`, and then we would not find that given for `Inv[Apply | Ident]`. But it's much less likely that we are looking for a given of type `Inv[Any]`. This commit does two things: - add logic not to widen a union if the result is a product of only transparent traits or classes. - treat `Any`, `AnyVal`, `Object`, and `Matchable` as transparent.
2 parents 04418c3 + 4a51300 commit 1276ba5

23 files changed

+248
-87
lines changed

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

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -550,35 +550,39 @@ trait ConstraintHandling {
550550
inst
551551
end approximation
552552

553+
private def isTransparent(tp: Type)(using Context): Boolean = tp match
554+
case AndType(tp1, tp2) => isTransparent(tp1) && isTransparent(tp2)
555+
case _ => tp.typeSymbol.isTransparentClass && !tp.isLambdaSub
556+
553557
/** If `tp` is an intersection such that some operands are transparent trait instances
554558
* and others are not, replace as many transparent trait instances as possible with Any
555559
* as long as the result is still a subtype of `bound`. But fall back to the
556560
* original type if the resulting widened type is a supertype of all dropped
557561
* types (since in this case the type was not a true intersection of transparent traits
558562
* and other types to start with).
559563
*/
560-
def dropTransparentTraits(tp: Type, bound: Type)(using Context): Type =
564+
def dropTransparentClasses(tp: Type, bound: Type)(using Context): Type =
561565
var kept: Set[Type] = Set() // types to keep since otherwise bound would not fit
562566
var dropped: List[Type] = List() // the types dropped so far, last one on top
563567

564-
def dropOneTransparentTrait(tp: Type): Type =
568+
def dropOneTransparentClass(tp: Type): Type =
565569
val tpd = tp.dealias
566-
if tpd.typeSymbol.isTransparentTrait && !tpd.isLambdaSub && !kept.contains(tpd) then
570+
if isTransparent(tpd) && !kept.contains(tpd) then
567571
dropped = tpd :: dropped
568572
defn.AnyType
569573
else tpd match
570574
case AndType(tp1, tp2) =>
571-
val tp1w = dropOneTransparentTrait(tp1)
575+
val tp1w = dropOneTransparentClass(tp1)
572576
if tp1w ne tp1 then tp1w & tp2
573577
else
574-
val tp2w = dropOneTransparentTrait(tp2)
578+
val tp2w = dropOneTransparentClass(tp2)
575579
if tp2w ne tp2 then tp1 & tp2w
576580
else tpd
577581
case _ =>
578582
tp
579583

580584
def recur(tp: Type): Type =
581-
val tpw = dropOneTransparentTrait(tp)
585+
val tpw = dropOneTransparentClass(tp)
582586
if tpw eq tp then tp
583587
else if tpw <:< bound then recur(tpw)
584588
else
@@ -595,7 +599,7 @@ trait ConstraintHandling {
595599
tp
596600
else
597601
tpw
598-
end dropTransparentTraits
602+
end dropTransparentClasses
599603

600604
/** If `tp` is an applied match type alias which is also an unreducible application
601605
* of a higher-kinded type to a wildcard argument, widen to the match type's bound,
@@ -621,7 +625,7 @@ trait ConstraintHandling {
621625
* union type (except for unions | Null, which are kept in the state they were).
622626
* 3. Widen some irreducible applications of higher-kinded types to wildcard arguments
623627
* (see @widenIrreducible).
624-
* 4. Drop transparent traits from intersections (see @dropTransparentTraits).
628+
* 4. Drop transparent traits from intersections (see @dropTransparentClasses).
625629
*
626630
* Don't do these widenings if `bound` is a subtype of `scala.Singleton`.
627631
* Also, if the result of these widenings is a TypeRef to a module class,
@@ -648,7 +652,16 @@ trait ConstraintHandling {
648652

649653
val wideInst =
650654
if isSingleton(bound) then inst
651-
else dropTransparentTraits(widenIrreducible(widenOr(widenSingle(inst))), bound)
655+
else
656+
val widenedFromSingle = widenSingle(inst)
657+
val widenedFromUnion = widenOr(widenedFromSingle)
658+
val widened =
659+
if (widenedFromUnion ne widenedFromSingle) && isTransparent(widenedFromUnion) then
660+
widenedFromSingle
661+
else
662+
dropTransparentClasses(widenedFromUnion, bound)
663+
widenIrreducible(widened)
664+
652665
wideInst match
653666
case wideInst: TypeRef if wideInst.symbol.is(Module) =>
654667
TermRef(wideInst.prefix, wideInst.symbol.sourceModule)

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

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1827,20 +1827,53 @@ class Definitions {
18271827
def isInfix(sym: Symbol)(using Context): Boolean =
18281828
(sym eq Object_eq) || (sym eq Object_ne)
18291829

1830-
@tu lazy val assumedTransparentTraits =
1831-
Set[Symbol](ComparableClass, ProductClass, SerializableClass,
1832-
// add these for now, until we had a chance to retrofit 2.13 stdlib
1833-
// we should do a more through sweep through it then.
1834-
requiredClass("scala.collection.SortedOps"),
1835-
requiredClass("scala.collection.StrictOptimizedSortedSetOps"),
1836-
requiredClass("scala.collection.generic.DefaultSerializable"),
1837-
requiredClass("scala.collection.generic.IsIterable"),
1838-
requiredClass("scala.collection.generic.IsIterableOnce"),
1839-
requiredClass("scala.collection.generic.IsMap"),
1840-
requiredClass("scala.collection.generic.IsSeq"),
1841-
requiredClass("scala.collection.generic.Subtractable"),
1842-
requiredClass("scala.collection.immutable.StrictOptimizedSeqOps")
1843-
)
1830+
@tu lazy val assumedTransparentNames: Map[Name, Set[Symbol]] =
1831+
// add these for now, until we had a chance to retrofit 2.13 stdlib
1832+
// we should do a more through sweep through it then.
1833+
val strs = Map(
1834+
"Any" -> Set("scala"),
1835+
"AnyVal" -> Set("scala"),
1836+
"Matchable" -> Set("scala"),
1837+
"Product" -> Set("scala"),
1838+
"Object" -> Set("java.lang"),
1839+
"Comparable" -> Set("java.lang"),
1840+
"Serializable" -> Set("java.io"),
1841+
"BitSetOps" -> Set("scala.collection"),
1842+
"IndexedSeqOps" -> Set("scala.collection", "scala.collection.mutable", "scala.collection.immutable"),
1843+
"IterableOnceOps" -> Set("scala.collection"),
1844+
"IterableOps" -> Set("scala.collection"),
1845+
"LinearSeqOps" -> Set("scala.collection", "scala.collection.immutable"),
1846+
"MapOps" -> Set("scala.collection", "scala.collection.mutable", "scala.collection.immutable"),
1847+
"SeqOps" -> Set("scala.collection", "scala.collection.mutable", "scala.collection.immutable"),
1848+
"SetOps" -> Set("scala.collection", "scala.collection.mutable", "scala.collection.immutable"),
1849+
"SortedMapOps" -> Set("scala.collection", "scala.collection.mutable", "scala.collection.immutable"),
1850+
"SortedOps" -> Set("scala.collection"),
1851+
"SortedSetOps" -> Set("scala.collection", "scala.collection.mutable", "scala.collection.immutable"),
1852+
"StrictOptimizedIterableOps" -> Set("scala.collection"),
1853+
"StrictOptimizedLinearSeqOps" -> Set("scala.collection"),
1854+
"StrictOptimizedMapOps" -> Set("scala.collection", "scala.collection.immutable"),
1855+
"StrictOptimizedSeqOps" -> Set("scala.collection", "scala.collection.immutable"),
1856+
"StrictOptimizedSetOps" -> Set("scala.collection", "scala.collection.immutable"),
1857+
"StrictOptimizedSortedMapOps" -> Set("scala.collection", "scala.collection.immutable"),
1858+
"StrictOptimizedSortedSetOps" -> Set("scala.collection", "scala.collection.immutable"),
1859+
"ArrayDequeOps" -> Set("scala.collection.mutable"),
1860+
"DefaultSerializable" -> Set("scala.collection.generic"),
1861+
"IsIterable" -> Set("scala.collection.generic"),
1862+
"IsIterableLowPriority" -> Set("scala.collection.generic"),
1863+
"IsIterableOnce" -> Set("scala.collection.generic"),
1864+
"IsIterableOnceLowPriority" -> Set("scala.collection.generic"),
1865+
"IsMap" -> Set("scala.collection.generic"),
1866+
"IsSeq" -> Set("scala.collection.generic"))
1867+
strs.map { case (simple, pkgs) => (
1868+
simple.toTypeName,
1869+
pkgs.map(pkg => staticRef(pkg.toTermName, isPackage = true).symbol.moduleClass)
1870+
)
1871+
}
1872+
1873+
def isAssumedTransparent(sym: Symbol): Boolean =
1874+
assumedTransparentNames.get(sym.name) match
1875+
case Some(pkgs) => pkgs.contains(sym.owner)
1876+
case none => false
18441877

18451878
// ----- primitive value class machinery ------------------------------------------
18461879

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ object Flags {
357357
val (_, DefaultMethod @ _, _) = newFlags(38, "<defaultmethod>")
358358

359359
/** Symbol is a transparent inline method or trait */
360-
val (Transparent @ _, _, _) = newFlags(39, "transparent")
360+
val (Transparent @ _, _, TransparentType @ _) = newFlags(39, "transparent")
361361

362362
/** Symbol is an enum class or enum case (if used with case) */
363363
val (Enum @ _, EnumVal @ _, _) = newFlags(40, "enum")
@@ -609,5 +609,4 @@ object Flags {
609609
val SyntheticParam: FlagSet = Synthetic | Param
610610
val SyntheticTermParam: FlagSet = Synthetic | TermParam
611611
val SyntheticTypeParam: FlagSet = Synthetic | TypeParam
612-
val TransparentTrait: FlagSet = Trait | Transparent
613612
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,9 +1151,9 @@ object SymDenotations {
11511151
final def isEffectivelySealed(using Context): Boolean =
11521152
isOneOf(FinalOrSealed) || isClass && !isOneOf(EffectivelyOpenFlags)
11531153

1154-
final def isTransparentTrait(using Context): Boolean =
1155-
isAllOf(TransparentTrait)
1156-
|| defn.assumedTransparentTraits.contains(symbol)
1154+
final def isTransparentClass(using Context): Boolean =
1155+
is(TransparentType)
1156+
|| defn.isAssumedTransparent(symbol)
11571157
|| isClass && hasAnnotation(defn.TransparentTraitAnnot)
11581158

11591159
/** The class containing this denotation which has the given effective name. */

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3007,8 +3007,8 @@ object TypeComparer {
30073007
def widenInferred(inst: Type, bound: Type, widenUnions: Boolean)(using Context): Type =
30083008
comparing(_.widenInferred(inst, bound, widenUnions))
30093009

3010-
def dropTransparentTraits(tp: Type, bound: Type)(using Context): Type =
3011-
comparing(_.dropTransparentTraits(tp, bound))
3010+
def dropTransparentClasses(tp: Type, bound: Type)(using Context): Type =
3011+
comparing(_.dropTransparentClasses(tp, bound))
30123012

30133013
def constrainPatternType(pat: Type, scrut: Type, forceInvariantRefinement: Boolean = false)(using Context): Boolean =
30143014
comparing(_.constrainPatternType(pat, scrut, forceInvariantRefinement))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1210,7 +1210,7 @@ trait Applications extends Compatibility {
12101210
&& tree.tpe.classSymbol.isEnumCase
12111211
&& tree.tpe.widen.isValueType
12121212
then
1213-
val widened = TypeComparer.dropTransparentTraits(
1213+
val widened = TypeComparer.dropTransparentClasses(
12141214
tree.tpe.parents.reduceLeft(TypeComparer.andType(_, _)),
12151215
pt)
12161216
if widened <:< pt then Typed(tree, TypeTree(widened))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ object Checking {
495495
}
496496
if sym.is(Transparent) then
497497
if sym.isType then
498-
if !sym.is(Trait) then fail(em"`transparent` can only be used for traits".toMessage)
498+
if !sym.isExtensibleClass then fail(em"`transparent` can only be used for extensible classes and traits".toMessage)
499499
else
500500
if !sym.isInlineMethod then fail(em"`transparent` can only be used for inline methods".toMessage)
501501
if (!sym.isClass && sym.is(Abstract))

compiler/test/dotty/tools/repl/ShadowingTests.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ class ShadowingTests extends ReplTest(options = ShadowingTests.options):
122122
|val y: String = foo
123123
|
124124
|scala> if (true) x else y
125-
|val res0: Matchable = 42
125+
|val res0: Int | String = 42
126126
|""".stripMargin.linesIterator.toList
127127
)
128128

docs/_docs/reference/new-types/union-types-spec.md

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ a non-union type, for this purpose we define the _join_ of a union type `T1 |
7272
`T1`,...,`Tn`. Note that union types might still appear as type arguments in the
7373
resulting type, this guarantees that the join is always finite.
7474

75+
The _visible join_ of a union type is its join where all operands of the intersection that
76+
are instances of [transparent](../other-new-features/transparent-traits.md) traits or classes are removed.
77+
78+
7579
### Example
7680

7781
Given
@@ -80,31 +84,50 @@ Given
8084
trait C[+T]
8185
trait D
8286
trait E
83-
class A extends C[A] with D
84-
class B extends C[B] with D with E
87+
transparent trait X
88+
class A extends C[A], D, X
89+
class B extends C[B], D, E, X
8590
```
8691

87-
The join of `A | B` is `C[A | B] & D`
92+
The join of `A | B` is `C[A | B] & D & X` and the visible join of `A | B` is `C[A | B] & D`.
93+
94+
## Hard and Soft Union Types
95+
96+
We distinguish between hard and soft union types. A _hard_ union type is a union type that's explicitly
97+
written in the source. For instance, in
98+
```scala
99+
val x: Int | String = ...
100+
```
101+
`Int | String` would be a hard union type. A _soft_ union type is a type that arises from type checking
102+
an alternative of expressions. For instance, the type of the expression
103+
```scala
104+
val x = 1
105+
val y = "abc"
106+
if cond then x else y
107+
```
108+
is the soft unon type `Int | String`. Similarly for match expressions. The type of
109+
```scala
110+
x match
111+
case 1 => x
112+
case 2 => "abc"
113+
case 3 => List(1, 2, 3)
114+
```
115+
is the soft union type `Int | "abc" | List[Int]`.
116+
88117

89118
## Type inference
90119

91120
When inferring the result type of a definition (`val`, `var`, or `def`) and the
92-
type we are about to infer is a union type, then we replace it by its join.
121+
type we are about to infer is a soft union type, then we replace it by its visible join,
122+
provided it is not empty.
93123
Similarly, when instantiating a type argument, if the corresponding type
94124
parameter is not upper-bounded by a union type and the type we are about to
95-
instantiate is a union type, we replace it by its join. This mirrors the
125+
instantiate is a soft union type, we replace it by its visible join, provided it is not empty.
126+
This mirrors the
96127
treatment of singleton types which are also widened to their underlying type
97128
unless explicitly specified. The motivation is the same: inferring types
98129
which are "too precise" can lead to unintuitive typechecking issues later on.
99130

100-
**Note:** Since this behavior limits the usability of union types, it might
101-
be changed in the future. For example by not widening unions that have been
102-
explicitly written down by the user and not inferred, or by not widening a type
103-
argument when the corresponding type parameter is covariant.
104-
105-
See [PR #2330](https://github.com/lampepfl/dotty/pull/2330) and
106-
[Issue #4867](https://github.com/lampepfl/dotty/issues/4867) for further discussions.
107-
108131
### Example
109132

110133
```scala

docs/_docs/reference/new-types/union-types.md

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ A union type `A | B` has as values all values of type `A` and also all values of
88

99

1010
```scala
11-
case class UserName(name: String)
12-
case class Password(hash: Hash)
11+
trait ID
12+
case class UserName(name: String) extends ID
13+
case class Password(hash: Hash) extends ID
1314

1415
def help(id: UserName | Password) =
1516
val user = id match
@@ -22,7 +23,10 @@ Union types are duals of intersection types. `|` is _commutative_:
2223
`A | B` is the same type as `B | A`.
2324

2425
The compiler will assign a union type to an expression only if such a
25-
type is explicitly given. This can be seen in the following [REPL](https://docs.scala-lang.org/overviews/repl/overview.html) transcript:
26+
type is explicitly given or if the common supertype of all alternatives is [transparent](../other-new-features/transparent-traits.md).
27+
28+
29+
This can be seen in the following [REPL](https://docs.scala-lang.org/overviews/repl/overview.html) transcript:
2630

2731
```scala
2832
scala> val password = Password(123)
@@ -32,15 +36,36 @@ scala> val name = UserName("Eve")
3236
val name: UserName = UserName(Eve)
3337

3438
scala> if true then name else password
35-
val res2: Object = UserName(Eve)
39+
val res1: ID = UserName(Eve)
3640

3741
scala> val either: Password | UserName = if true then name else password
38-
val either: Password | UserName = UserName(Eve)
42+
val either: UserName | Password = UserName(Eve)
3943
```
40-
41-
The type of `res2` is `Object & Product`, which is a supertype of
42-
`UserName` and `Password`, but not the least supertype `Password |
43-
UserName`. If we want the least supertype, we have to give it
44+
The type of `res1` is `ID`, which is a supertype of
45+
`UserName` and `Password`, but not the least supertype `UserName | Password`.
46+
If we want the least supertype, we have to give it
4447
explicitly, as is done for the type of `either`.
4548

49+
The inference behavior changes if the common supertrait `ID` is declared `transparent`:
50+
```scala
51+
transparent trait ID
52+
```
53+
In that case the union type is not widened.
54+
```scala
55+
scala> if true then name else password
56+
val res2: UserName | Password = UserName(Eve)
57+
```
58+
The more precise union type is also inferred if `UserName` and `Password` are declared without an explicit
59+
parent, since in that case their implied superclass is `Object`, which is among the classes that are
60+
assumed to be transparent. See [Transparent Traits and Classes](../other-new-features/transparent-traits.md)
61+
for a list of such classes.
62+
```scala
63+
case class UserName(name: String)
64+
case class Password(hash: Hash)
65+
66+
scala> if true then UserName("Eve") else Password(123)
67+
val res3: UserName | Password = UserName(Eve)
68+
```
69+
70+
4671
[More details](./union-types-spec.md)

0 commit comments

Comments
 (0)