Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cut the Gordian Knot: Don't widen unions to transparent #15642

Merged
merged 11 commits into from
Nov 9, 2022
31 changes: 22 additions & 9 deletions compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala
Original file line number Diff line number Diff line change
Expand Up @@ -550,35 +550,39 @@ trait ConstraintHandling {
inst
end approximation

private def isTransparent(tp: Type)(using Context): Boolean = tp match
case AndType(tp1, tp2) => isTransparent(tp1) && isTransparent(tp2)
case _ => tp.typeSymbol.isTransparentClass && !tp.isLambdaSub

/** If `tp` is an intersection such that some operands are transparent trait instances
* and others are not, replace as many transparent trait instances as possible with Any
* as long as the result is still a subtype of `bound`. But fall back to the
* original type if the resulting widened type is a supertype of all dropped
* types (since in this case the type was not a true intersection of transparent traits
* and other types to start with).
*/
def dropTransparentTraits(tp: Type, bound: Type)(using Context): Type =
def dropTransparentClasses(tp: Type, bound: Type)(using Context): Type =
var kept: Set[Type] = Set() // types to keep since otherwise bound would not fit
var dropped: List[Type] = List() // the types dropped so far, last one on top

def dropOneTransparentTrait(tp: Type): Type =
def dropOneTransparentClass(tp: Type): Type =
val tpd = tp.dealias
if tpd.typeSymbol.isTransparentTrait && !tpd.isLambdaSub && !kept.contains(tpd) then
if isTransparent(tpd) && !kept.contains(tpd) then
dropped = tpd :: dropped
defn.AnyType
else tpd match
case AndType(tp1, tp2) =>
val tp1w = dropOneTransparentTrait(tp1)
val tp1w = dropOneTransparentClass(tp1)
if tp1w ne tp1 then tp1w & tp2
else
val tp2w = dropOneTransparentTrait(tp2)
val tp2w = dropOneTransparentClass(tp2)
if tp2w ne tp2 then tp1 & tp2w
else tpd
case _ =>
tp

def recur(tp: Type): Type =
val tpw = dropOneTransparentTrait(tp)
val tpw = dropOneTransparentClass(tp)
if tpw eq tp then tp
else if tpw <:< bound then recur(tpw)
else
Expand All @@ -595,7 +599,7 @@ trait ConstraintHandling {
tp
else
tpw
end dropTransparentTraits
end dropTransparentClasses

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

val wideInst =
if isSingleton(bound) then inst
else dropTransparentTraits(widenIrreducible(widenOr(widenSingle(inst))), bound)
else
val widenedFromSingle = widenSingle(inst)
val widenedFromUnion = widenOr(widenedFromSingle)
val widened =
if (widenedFromUnion ne widenedFromSingle) && isTransparent(widenedFromUnion) then
widenedFromSingle
else
dropTransparentClasses(widenedFromUnion, bound)
widenIrreducible(widened)

wideInst match
case wideInst: TypeRef if wideInst.symbol.is(Module) =>
TermRef(wideInst.prefix, wideInst.symbol.sourceModule)
Expand Down
61 changes: 47 additions & 14 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1827,20 +1827,53 @@ class Definitions {
def isInfix(sym: Symbol)(using Context): Boolean =
(sym eq Object_eq) || (sym eq Object_ne)

@tu lazy val assumedTransparentTraits =
Set[Symbol](ComparableClass, ProductClass, SerializableClass,
// add these for now, until we had a chance to retrofit 2.13 stdlib
// we should do a more through sweep through it then.
requiredClass("scala.collection.SortedOps"),
requiredClass("scala.collection.StrictOptimizedSortedSetOps"),
requiredClass("scala.collection.generic.DefaultSerializable"),
requiredClass("scala.collection.generic.IsIterable"),
requiredClass("scala.collection.generic.IsIterableOnce"),
requiredClass("scala.collection.generic.IsMap"),
requiredClass("scala.collection.generic.IsSeq"),
requiredClass("scala.collection.generic.Subtractable"),
requiredClass("scala.collection.immutable.StrictOptimizedSeqOps")
)
@tu lazy val assumedTransparentNames: Map[Name, Set[Symbol]] =
// add these for now, until we had a chance to retrofit 2.13 stdlib
// we should do a more through sweep through it then.
val strs = Map(
"Any" -> Set("scala"),
"AnyVal" -> Set("scala"),
"Matchable" -> Set("scala"),
"Product" -> Set("scala"),
"Object" -> Set("java.lang"),
"Comparable" -> Set("java.lang"),
"Serializable" -> Set("java.io"),
"BitSetOps" -> Set("scala.collection"),
"IndexedSeqOps" -> Set("scala.collection", "scala.collection.mutable", "scala.collection.immutable"),
"IterableOnceOps" -> Set("scala.collection"),
"IterableOps" -> Set("scala.collection"),
"LinearSeqOps" -> Set("scala.collection", "scala.collection.immutable"),
"MapOps" -> Set("scala.collection", "scala.collection.mutable", "scala.collection.immutable"),
"SeqOps" -> Set("scala.collection", "scala.collection.mutable", "scala.collection.immutable"),
"SetOps" -> Set("scala.collection", "scala.collection.mutable", "scala.collection.immutable"),
"SortedMapOps" -> Set("scala.collection", "scala.collection.mutable", "scala.collection.immutable"),
"SortedOps" -> Set("scala.collection"),
"SortedSetOps" -> Set("scala.collection", "scala.collection.mutable", "scala.collection.immutable"),
"StrictOptimizedIterableOps" -> Set("scala.collection"),
"StrictOptimizedLinearSeqOps" -> Set("scala.collection"),
"StrictOptimizedMapOps" -> Set("scala.collection", "scala.collection.immutable"),
"StrictOptimizedSeqOps" -> Set("scala.collection", "scala.collection.immutable"),
"StrictOptimizedSetOps" -> Set("scala.collection", "scala.collection.immutable"),
"StrictOptimizedSortedMapOps" -> Set("scala.collection", "scala.collection.immutable"),
"StrictOptimizedSortedSetOps" -> Set("scala.collection", "scala.collection.immutable"),
"ArrayDequeOps" -> Set("scala.collection.mutable"),
"DefaultSerializable" -> Set("scala.collection.generic"),
"IsIterable" -> Set("scala.collection.generic"),
"IsIterableLowPriority" -> Set("scala.collection.generic"),
"IsIterableOnce" -> Set("scala.collection.generic"),
"IsIterableOnceLowPriority" -> Set("scala.collection.generic"),
"IsMap" -> Set("scala.collection.generic"),
"IsSeq" -> Set("scala.collection.generic"))
strs.map { case (simple, pkgs) => (
simple.toTypeName,
pkgs.map(pkg => staticRef(pkg.toTermName, isPackage = true).symbol.moduleClass)
)
}

def isAssumedTransparent(sym: Symbol): Boolean =
assumedTransparentNames.get(sym.name) match
case Some(pkgs) => pkgs.contains(sym.owner)
case none => false

// ----- primitive value class machinery ------------------------------------------

Expand Down
3 changes: 1 addition & 2 deletions compiler/src/dotty/tools/dotc/core/Flags.scala
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ object Flags {
val (_, DefaultMethod @ _, _) = newFlags(38, "<defaultmethod>")

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

/** Symbol is an enum class or enum case (if used with case) */
val (Enum @ _, EnumVal @ _, _) = newFlags(40, "enum")
Expand Down Expand Up @@ -609,5 +609,4 @@ object Flags {
val SyntheticParam: FlagSet = Synthetic | Param
val SyntheticTermParam: FlagSet = Synthetic | TermParam
val SyntheticTypeParam: FlagSet = Synthetic | TypeParam
val TransparentTrait: FlagSet = Trait | Transparent
}
6 changes: 3 additions & 3 deletions compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1151,9 +1151,9 @@ object SymDenotations {
final def isEffectivelySealed(using Context): Boolean =
isOneOf(FinalOrSealed) || isClass && !isOneOf(EffectivelyOpenFlags)

final def isTransparentTrait(using Context): Boolean =
isAllOf(TransparentTrait)
|| defn.assumedTransparentTraits.contains(symbol)
final def isTransparentClass(using Context): Boolean =
is(TransparentType)
|| defn.isAssumedTransparent(symbol)
|| isClass && hasAnnotation(defn.TransparentTraitAnnot)

/** The class containing this denotation which has the given effective name. */
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3007,8 +3007,8 @@ object TypeComparer {
def widenInferred(inst: Type, bound: Type, widenUnions: Boolean)(using Context): Type =
comparing(_.widenInferred(inst, bound, widenUnions))

def dropTransparentTraits(tp: Type, bound: Type)(using Context): Type =
comparing(_.dropTransparentTraits(tp, bound))
def dropTransparentClasses(tp: Type, bound: Type)(using Context): Type =
comparing(_.dropTransparentClasses(tp, bound))

def constrainPatternType(pat: Type, scrut: Type, forceInvariantRefinement: Boolean = false)(using Context): Boolean =
comparing(_.constrainPatternType(pat, scrut, forceInvariantRefinement))
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1211,7 +1211,7 @@ trait Applications extends Compatibility {
&& tree.tpe.classSymbol.isEnumCase
&& tree.tpe.widen.isValueType
then
val widened = TypeComparer.dropTransparentTraits(
val widened = TypeComparer.dropTransparentClasses(
tree.tpe.parents.reduceLeft(TypeComparer.andType(_, _)),
pt)
if widened <:< pt then Typed(tree, TypeTree(widened))
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ object Checking {
}
if sym.is(Transparent) then
if sym.isType then
if !sym.is(Trait) then fail(em"`transparent` can only be used for traits".toMessage)
if !sym.isExtensibleClass then fail(em"`transparent` can only be used for extensible classes and traits".toMessage)
else
if !sym.isInlineMethod then fail(em"`transparent` can only be used for inline methods".toMessage)
if (!sym.isClass && sym.is(Abstract))
Expand Down
2 changes: 1 addition & 1 deletion compiler/test/dotty/tools/repl/ShadowingTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ class ShadowingTests extends ReplTest(options = ShadowingTests.options):
|val y: String = foo
|
|scala> if (true) x else y
|val res0: Matchable = 42
|val res0: Int | String = 42
|""".stripMargin.linesIterator.toList
)

Expand Down
57 changes: 44 additions & 13 deletions docs/_docs/reference/other-new-features/transparent-traits.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
layout: doc-page
title: "Transparent Traits"
title: "Transparent Traits and Classes"
nightlyOf: https://docs.scala-lang.org/scala3/reference/other-new-features/transparent-traits.html
---

Expand All @@ -20,12 +20,13 @@ val x = Set(if condition then Val else Var)

Here, the inferred type of `x` is `Set[Kind & Product & Serializable]` whereas one would have hoped it to be `Set[Kind]`. The reasoning for this particular type to be inferred is as follows:

- The type of the conditional above is the [union type](../new-types/union-types.md) `Val | Var`.
- A union type is widened in type inference to the least supertype that is not a union type.
In the example, this type is `Kind & Product & Serializable` since all three traits are traits of both `Val` and `Var`.
- The type of the conditional above is the [union type](../new-types/union-types.md) `Val | Var`. This union type is treated as "soft", which means it was not explicitly written in the source program, but came from forming an upper bound of the types of
some alternatives.
- A soft union type is widened in type inference to the least product of class or trait types that is a supertype of the union type.
In the example, this type is `Kind & Product & Serializable` since all three traits are super-traits of both `Val` and `Var`.
So that type becomes the inferred element type of the set.

Scala 3 allows one to mark a mixin trait as `transparent`, which means that it can be suppressed in type inference. Here's an example that follows the lines of the code above, but now with a new transparent trait `S` instead of `Product`:
Scala 3 allows one to mark a trait or class as `transparent`, which means that it can be suppressed in type inference. Here's an example that follows the lines of the code above, but now with a new transparent trait `S` instead of `Product`:

```scala
transparent trait S
Expand All @@ -38,13 +39,40 @@ val x = Set(if condition then Val else Var)
Now `x` has inferred type `Set[Kind]`. The common transparent trait `S` does not
appear in the inferred type.

## Transparent Traits
In the previous example, one could also declare `Kind` as `transparent`:
```scala
transparent trait Kind
```
The widened union type of `if condition then Val else Var` would then
_only_ contain the transparent traits `Kind` and `S`. In this case,
the widening is not performed at all, so `x` would have type `Set[Val | Var]`.

The root classes and traits `Any`, `AnyVal`, `Object`, and `Matchable` are
considered to be transparent. This means that an expression such
as
```scala
if condition then 1 else "hello"
```
will have type `Int | String` instead of the widened type `Any`.


The traits [`scala.Product`](https://scala-lang.org/api/3.x/scala/Product.html), [`java.io.Serializable`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/Serializable.html) and [`java.lang.Comparable`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Comparable.html)
are treated automatically as transparent. Other traits are turned into transparent traits using the modifier `transparent`. Scala 2 traits can also be made transparent
by adding a [`@transparentTrait`](https://scala-lang.org/api/3.x/scala/annotation/transparentTrait.html) annotation. This annotation is defined in [`scala.annotation`](https://scala-lang.org/api/3.x/scala/annotation.html). It will be deprecated and phased out once Scala 2/3 interoperability is no longer needed.

Typically, transparent traits are traits
## Which Traits and Classes Are Transparent?

Traits and classes are declared transparent by adding the modifier `transparent`. Scala 2 traits and classes can also be declared transparent by adding a [`@transparentTrait`](https://scala-lang.org/api/3.x/scala/annotation/transparentTrait.html) annotation. This annotation is defined in [`scala.annotation`](https://scala-lang.org/api/3.x/scala/annotation.html). It will be deprecated and phased out once Scala 2/3 interoperability is no longer needed.

The following classes and traits are automatically treated as transparent:
```scala
scala.Any
scala.AnyVal
scala.Matchable
scala.Product
java.lang.Object
java.lang.Comparable
java.io.Serializable
```

Typically, transparent types other than the root classes are traits
that influence the implementation of inheriting classes and traits that are not usually used as types by themselves. Two examples from the standard collection library are:

- [`IterableOps`](https://scala-lang.org/api/3.x/scala/collection/IterableOps.html), which provides method implementations for an [`Iterable`](https://scala-lang.org/api/3.x/scala/collection/Iterable.html).
Expand All @@ -55,7 +83,10 @@ declared transparent.

## Rules for Inference

Transparent traits can be given as explicit types as usual. But they are often elided when types are inferred. Roughly, the rules for type inference say that transparent traits are dropped from intersections where possible.
Transparent traits and classes can be given as explicit types as usual. But they are often elided when types are inferred. Roughly, the rules for type inference imply the following.

- Transparent traits and classes are dropped from intersections where possible.
- Union types are not widened if widening would result in only transparent supertypes.

The precise rules are as follows:

Expand All @@ -65,6 +96,6 @@ The precise rules are as follows:
- If the type inferred so far is of the form `T1 & ... & Tn` where
`n >= 1`, replace the maximal number of transparent `Ti`s by `Any`, while ensuring that
the resulting type is still a subtype of the bound `B`.
- However, do not perform this widening if all transparent traits `Ti` can get replaced in that way.
- However, do not perform this widening if all transparent types `Ti` can get replaced in that way. This clause ensures that a single transparent trait instance such as [`Product`](https://scala-lang.org/api/3.x/scala/Product.html) is not widened to [`Any`](https://scala-lang.org/api/3.x/scala/Any.html). Transparent trait instances are only dropped when they appear in conjunction with some other type.

The last clause ensures that a single transparent trait instance such as [`Product`](https://scala-lang.org/api/3.x/scala/Product.html) is not widened to [`Any`](https://scala-lang.org/api/3.x/scala/Any.html). Transparent trait instances are only dropped when they appear in conjunction with some other type.
- If the original type was a is union type that got widened in a previous step to a product consisting only of transparent types, keep the original union type instead of its widened form.
4 changes: 2 additions & 2 deletions tests/neg/harmonize.scala
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ object Test {
val a4 = ArrayBuffer(1.0f, 1L)
val b4: ArrayBuffer[Double] = a4 // error: no widening
val a5 = ArrayBuffer(1.0f, 1L, f())
val b5: ArrayBuffer[AnyVal] = a5
val b5: ArrayBuffer[Float | Long | Int] = a5
val a6 = ArrayBuffer(1.0f, 1234567890)
val b6: ArrayBuffer[AnyVal] = a6
val b6: ArrayBuffer[Float | Int] = a6

def totalDuration(results: List[Long], cond: Boolean): Long =
results.map(r => if (cond) r else 0).sum
Expand Down
11 changes: 6 additions & 5 deletions tests/neg/supertraits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ class C extends A, S
val x = if ??? then B() else C()
val x1: S = x // error

case object a
case object b
class Top
case object a extends Top
case object b extends Top
val y = if ??? then a else b
val y1: Product = y // error
val y2: Serializable = y // error

enum Color {
enum Color extends Top {
case Red, Green, Blue
}

enum Nucleobase {
enum Nucleobase extends Top {
case A, C, G, T
}

val z = if ??? then Color.Red else Nucleobase.G
val z1: reflect.Enum = z // error: Found: (z : Object) Required: reflect.Enum
val z1: reflect.Enum = z // error: Found: (z : Top) Required: reflect.Enum
3 changes: 2 additions & 1 deletion tests/neg/transparent.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
transparent def foo = 1 // error
transparent inline def bar = 2 // ok
transparent inline val x = 2 // error
transparent class c // error
transparent class c // ok
transparent final class d // error
transparent object y // error
transparent trait t // ok
transparent type T = c // error
Expand Down
5 changes: 3 additions & 2 deletions tests/neg/union.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ object Test {
}

object O {
class A
class B
class Top
class A extends Top
class B extends Top
def f[T](x: T, y: T): T = x

val x: A = f(new A { }, new A)
Expand Down
9 changes: 6 additions & 3 deletions tests/pos/i14494.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
object ImplNotFound:
class TOP
class STR(s: String) extends TOP
class INT(i: Int) extends TOP
def main(args: Array[String]): Unit =
val res: Seq[String | Int] = (??? : Seq[Int]).collect {
case 1 => Seq("")
case 2 => Seq(1)
val res: Seq[STR | INT] = (??? : Seq[Int]).collect {
case 1 => Seq(STR(""))
case 2 => Seq(INT(1))
}.flatten
File renamed without changes.
Loading