Skip to content

Disable distribution of intersection types over applied types #23441

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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ class Definitions {
@tu lazy val CBCompanion: TypeSymbol = // type `<context-bound-companion>`[-Refs]
enterPermanentSymbol(tpnme.CBCompanion,
TypeBounds(NothingType,
HKTypeLambda(tpnme.syntheticTypeParamName(0) :: Nil, Contravariant :: Nil)(
HKTypeLambda(tpnme.syntheticTypeParamName(0) :: Nil)(
tl => TypeBounds.empty :: Nil,
tl => AnyType))).asType

Expand Down
5 changes: 2 additions & 3 deletions compiler/src/dotty/tools/dotc/core/NamerOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ object NamerOps:
* The context-bound companion has as name the name of `tsym` translated to
* a term name. We create a synthetic val of the form
*
* val A: `<context-bound-companion>`[witnessRef1 | ... | witnessRefN]
* val A: `<context-bound-companion>`[witnessRef1] & ... & `<context-bound-companion>`[witnessRefN]
*
* where
*
Expand All @@ -325,8 +325,7 @@ object NamerOps:
prefix.select(params.find(_.name == witnessName).get)
else
witnessNames.map(TermRef(prefix, _))
val cbtype = defn.CBCompanion.typeRef.appliedTo:
witnessRefs.reduce[Type](OrType(_, _, soft = false))
val cbtype = witnessRefs.map(defn.CBCompanion.typeRef.appliedTo).reduce(AndType.apply)
val cbc = newSymbol(
ctx.owner, companionName,
(tsym.flagsUNSAFE & (AccessFlags)).toTermFlags | Synthetic,
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/SymUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class SymUtils:
}

def isContextBoundCompanion(using Context): Boolean =
self.is(Synthetic) && self.infoOrCompleter.typeSymbol == defn.CBCompanion
self.is(Synthetic) && self.infoOrCompleter.isContextBoundCompanion

def isDummyCaptureParam(using Context): Boolean =
self.isAllOf(CaptureParam) && !(self.isClass || self.is(Method))
Expand Down
10 changes: 8 additions & 2 deletions compiler/src/dotty/tools/dotc/core/TypeApplications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -475,13 +475,19 @@ class TypeApplications(val self: Type) extends AnyVal {
self.derivedExprType(tp.translateParameterized(from, to))
case _ =>
if (self.derivesFrom(from)) {
// NOTE: we assume the `To` class is covariant s.t.
// `To[T] X To[U] <:< To[T | U]` where X ::= `&` | `|`
def elemType(tp: Type): Type = tp.widenDealias match
case tp: OrType =>
if tp.tp1.isBottomType then elemType(tp.tp2)
else if tp.tp2.isBottomType then elemType(tp.tp1)
else tp.derivedOrType(elemType(tp.tp1), elemType(tp.tp2))
case tp: AndType => tp.derivedAndType(elemType(tp.tp1), elemType(tp.tp2))
case _ => tp.baseType(from).argInfos.headOption.getOrElse(defn.NothingType)
case AndType(tp1, tp2) =>
// see #23435 for why this is not `tp.derivedAndType(elemType(tp1), ...)`
OrType(elemType(tp1), elemType(tp2), soft = false)
case _ =>
tp.baseType(from).argInfos.headOption.getOrElse(defn.NothingType)
end elemType
val arg = elemType(self)
val arg1 = if (wildcardArg) TypeBounds.upper(arg) else arg
to.typeRef.appliedTo(arg1)
Expand Down
14 changes: 2 additions & 12 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2767,19 +2767,10 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
}

/** Try to distribute `&` inside type, detect and handle conflicts
* Note that an intersection cannot be pushed into an applied type, see tests/neg/i23435-min.
* @pre !(tp1 <: tp2) && !(tp2 <:< tp1) -- these cases were handled before
*/
private def distributeAnd(tp1: Type, tp2: Type): Type = tp1 match {
case tp1 @ AppliedType(tycon1, args1) =>
tp2 match {
case AppliedType(tycon2, args2)
if tycon1.typeSymbol == tycon2.typeSymbol && tycon1 =:= tycon2 =>
val jointArgs = glbArgs(args1, args2, tycon1.typeParams)
if (jointArgs.forall(_.exists)) (tycon1 & tycon2).appliedTo(jointArgs)
else NoType
case _ =>
NoType
}
case tp1: RefinedType =>
// opportunistically merge same-named refinements
// this does not change anything semantically (i.e. merging or not merging
Expand Down Expand Up @@ -2818,8 +2809,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
}

/** Try to distribute `|` inside type, detect and handle conflicts
* Note that, unlike for `&`, a disjunction cannot be pushed into
* a refined or applied type. Example:
* Note that a disjunction cannot be pushed into a refined or applied type. Example:
*
* List[T] | List[U] is not the same as List[T | U].
*
Expand Down
8 changes: 8 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,14 @@ object Types extends TypeUtils {
case AppliedType(tycon: TypeRef, arg :: Nil) => defn.isInto(tycon.symbol)
case _ => false

/** Is this type of the form `<context-bound-companion>[Ref1] & ... & <context-bound-companion>[RefN]`?
* Where the intersection may be introduced by `NamerOps.addContextBoundCompanionFor`
* or by inheriting multiple context bound companions for the same name.
*/
def isContextBoundCompanion(using Context): Boolean = this.widen match
case AndType(tp1, tp2) => tp1.isContextBoundCompanion.ensuring(_ == tp2.isContextBoundCompanion)
case tp => tp.typeSymbol == defn.CBCompanion

/** Is this type a legal target type for an implicit conversion, so that
* no `implicitConversions` language import is necessary?
*/
Expand Down
34 changes: 16 additions & 18 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
// Otherwise, if the qualifier is a context bound companion, handle
// by selecting a witness in typedCBSelect
def tryCBCompanion() =
if qual.tpe.typeSymbol == defn.CBCompanion then
if qual.tpe.isContextBoundCompanion then
typedCBSelect(tree0, pt, qual)
else EmptyTree

Expand Down Expand Up @@ -997,13 +997,13 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
* alternatives referred to by `witnesses`.
* @param prevs a list of (ref tree, typer state, term ref) tripls that
* represents previously identified alternatives
* @param witnesses a type of the form ref_1 | ... | ref_n containing references
* @param witnesses a type of the form `isContextBoundCompanion` containing references
* still to be considered.
*/
def tryAlts(prevs: Alts, witnesses: Type): Alts = witnesses match
case OrType(wit1, wit2) =>
def tryAlts(prevs: Alts, witnesses: Type): Alts = witnesses.widen match
case AndType(wit1, wit2) =>
tryAlts(tryAlts(prevs, wit1), wit2)
case witness: TermRef =>
case AppliedType(_, List(witness: TermRef)) =>
val altQual = tpd.ref(witness).withSpan(qual.span)
val altCtx = ctx.fresh.setNewTyperState()
val alt = typedSelectWithAdapt(tree, pt, altQual)(using altCtx)
Expand All @@ -1015,19 +1015,17 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
if comparisons.exists(_ == 1) then prevs
else current :: prevs.zip(comparisons).collect{ case (prev, cmp) if cmp != -1 => prev }

qual.tpe.widen match
case AppliedType(_, arg :: Nil) =>
tryAlts(Nil, arg) match
case Nil => EmptyTree
case (best @ (bestTree, bestState, _)) :: Nil =>
bestState.commit()
bestTree
case multiAlts =>
report.error(
em"""Ambiguous witness reference. None of the following alternatives is more specific than the other:
|${multiAlts.map((alt, _, witness) => i"\n $witness.${tree.name}: ${alt.tpe.widen}")}""",
tree.srcPos)
EmptyTree
tryAlts(Nil, qual.tpe) match
case Nil => EmptyTree
case (best @ (bestTree, bestState, _)) :: Nil =>
bestState.commit()
bestTree
case multiAlts =>
report.error(
em"""Ambiguous witness reference. None of the following alternatives is more specific than the other:
|${multiAlts.map((alt, _, witness) => i"\n $witness.${tree.name}: ${alt.tpe.widen}")}""",
tree.srcPos)
EmptyTree
end typedCBSelect

def typedSelect(tree: untpd.Select, pt: Type)(using Context): Tree = {
Expand Down
5 changes: 0 additions & 5 deletions docs/_docs/reference/new-types/intersection-types-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,6 @@ A & B <: B A & B <: A
In another word, `A & B` is the same type as `B & A`, in the sense that the two types
have the same values and are subtypes of each other.

If `C` is a co- or contravariant type constructor, then `C[A] & C[B]` can be simplified using the following rules:

- If `C` is covariant, `C[A] & C[B] ~> C[A & B]`
- If `C` is contravariant, `C[A] & C[B] ~> C[A | B]`

When `C` is covariant, `C[A & B] <: C[A] & C[B]` can be derived:

```
Expand Down
24 changes: 24 additions & 0 deletions docs/_docs/reference/new-types/union-types-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ case _: (A | B) => ...
A & (B | C) =:= A & B | A & C
```

When `C` is covariant, `C[A] | C[B] <: C[A | B]` can be derived:

```
A <: A B <: B
---------- ---------
A <: A | B B <: A | B
---------------- ----------------
C[A] <: C[A | B] C[B] <: C[A | B]
-----------------------------------------
C[A] | C[B] <: C[A | B]
```

When `C` is contravariant, `C[A] | C[B] <: C[A & B]` can be derived:

```
A <: A B <: B
---------- ----------
A & B <: A A & B <: B
---------------- ----------------
C[A] <: C[A & B] C[B] <: C[A & B]
-----------------------------------------
C[A] | C[B] <: C[A & B]
```

From these rules it follows that the _least upper bound_ (LUB) of a set of types
is the union of these types. This replaces the
[definition of least upper bound in the Scala 2 specification](https://www.scala-lang.org/files/archive/spec/2.13/03-types.html#least-upper-bounds-and-greatest-lower-bounds).
Expand Down
4 changes: 2 additions & 2 deletions scala2-library-cc/src/scala/collection/Iterable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -992,7 +992,7 @@ trait EvidenceIterableFactoryDefaults[+A, +CC[x] <: IterableOps[x, CC, CC[x]], E
trait SortedSetFactoryDefaults[+A,
+CC[X] <: SortedSet[X] with SortedSetOps[X, CC, CC[X]],
+WithFilterCC[x] <: IterableOps[x, WithFilterCC, WithFilterCC[x]] with Set[x]] extends SortedSetOps[A @uncheckedVariance, CC, CC[A @uncheckedVariance]] {
self: IterableOps[A, WithFilterCC, _] =>
self: IterableOps[A, WithFilterCC, CC[A @uncheckedVariance]] =>

override protected def fromSpecific(coll: IterableOnce[A @uncheckedVariance]^): CC[A @uncheckedVariance] = sortedIterableFactory.from(coll)(using ordering)
override protected def newSpecificBuilder: mutable.Builder[A @uncheckedVariance, CC[A @uncheckedVariance]] = sortedIterableFactory.newBuilder[A](using ordering)
Expand Down Expand Up @@ -1047,7 +1047,7 @@ trait SortedMapFactoryDefaults[K, +V,
+CC[x, y] <: Map[x, y] with SortedMapOps[x, y, CC, CC[x, y]] with UnsortedCC[x, y],
+WithFilterCC[x] <: IterableOps[x, WithFilterCC, WithFilterCC[x]] with Iterable[x],
+UnsortedCC[x, y] <: Map[x, y]] extends SortedMapOps[K, V, CC, CC[K, V @uncheckedVariance]] with MapOps[K, V, UnsortedCC, CC[K, V @uncheckedVariance]] {
self: IterableOps[(K, V), WithFilterCC, _] =>
self: IterableOps[(K, V), WithFilterCC, CC[K, V @uncheckedVariance]] =>

override def empty: CC[K, V @uncheckedVariance] = sortedMapFactory.empty(using ordering)
override protected def fromSpecific(coll: IterableOnce[(K, V @uncheckedVariance)]^): CC[K, V @uncheckedVariance] = sortedMapFactory.from(coll)(using ordering)
Expand Down
9 changes: 0 additions & 9 deletions tests/neg-deep-subtype/i11064.scala

This file was deleted.

7 changes: 7 additions & 0 deletions tests/neg/conflicting-inst-basetypes.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

object Test:
trait A[T]
trait B1 extends A[Int]
trait B2 extends A[String]
class D extends B1, B2 // error: cannot be instantiated since it has conflicting base types Test.A[Int] and Test.A[String]
// NOTE this is not accepted in Scala 2 either
4 changes: 0 additions & 4 deletions tests/neg/i11103.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,4 @@
case p: P =>
new Foo // error
}

class UpBndAndB extends UpBnd[Bar] with P
// ClassCastException: Foo cannot be cast to Bar
val x = pmatch(new UpBndAndB)
}
14 changes: 14 additions & 0 deletions tests/neg/i23435-min.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

type Or[+A, +B] = A | B

object Test1:
val x: Or[Int, String] & Or[String, Int] = 3
val y: Or[Int & String, String & Int] = x // error
val z: String = y

// shows the distributeAnd logic should not be applied even when
// the targs are pairwise TypeComparer#singletonInterval
object Test2:
val x: Or["3", Singleton] & Or[Singleton, "3"] = 3
val y: Or["3", "3"] = x // error
val z: String = y
16 changes: 16 additions & 0 deletions tests/neg/i23435.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

trait L[+A]{val a:A}
trait R[+B]{val b: B}

class LR(val a: Int, val b: String) extends L[Int] with R[String]

type E[+A] = L[A] | R[A]

val x: E[Int] & E[String] = LR(4, "hi")
val y: E[Int&String] = x // error

val z = y match
case l : L[Int&String] => l.a
case r : R[Int&String] => r.b

val _ = z:String // was: java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String
2 changes: 1 addition & 1 deletion tests/neg/i3989e.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
object Test extends App {
trait A[+X](val x: X)
class B extends A(5) with A("hello") // error: A is extended twice
class B extends A(5) with A("hello") // error: A is extended twice // error: class B cannot be instantiated since it has conflicting base types Test.A[Int] and Test.A[String]

def f(a: A[Int]): Int = a match {
case b: B => b.x
Expand Down
15 changes: 15 additions & 0 deletions tests/neg/lucre-23441-min.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

trait Txn[T]
trait Expr[X <: Txn[X], +Y]
trait BinOp[T, Repr[X <: Txn[X]] <: Expr[X, T]]

trait IntObj[T] extends Expr[T, Int]
trait IntBinOp extends BinOp[Int, IntObj]
object IntEq extends IntBinOp

object Test:
val r: BinOp[?, ?] = IntEq : BinOp[Int, IntObj] // error: Required: BinOp[?, ?[X] <: Expr[X, BinOp[?, ?]#T]]
// We would need the second wildcard to "depend" on the 1st one,
// e.g. have SomeBinop[?] where `type SomeBinop[X] = BinOp[X, ? <: [Y] =>> Expr[Y, X]]`,
// but this is an instance of an unreducible application of higher-kinded type to a wildcard argument.
// Also note there would be no error if we made BinOp covariant in T.
18 changes: 18 additions & 0 deletions tests/neg/specs2-23441-min.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

// NOTE: should _not_ necessarily be a neg test,
// added as minimization from community-build specs2 for #23441

def sameElementsAs[T](xs: Iterable[T], ys: Iterable[T], f: (T, T) => Boolean): Boolean =
val ita = xs.toList
val itb = ys.toList

(ita, itb) match
case (a: Iterable[?], b: Iterable[?]) =>
sameElementsAs(a, b, f) // ok

val resta/* : Iterable[?] & List[T] inferred */= a.drop(1)
val restb/* : Iterable[?] & List[T] inferred */= b.drop(1)

sameElementsAs[T](resta, restb, f) // ok
sameElementsAs(resta : List[T], restb, f) // ok
sameElementsAs(resta, restb, f) // error, would be ok if we had inferred as above
2 changes: 1 addition & 1 deletion tests/pos-macros/quoted-pattern-type.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ object Lib {
e

case e @ '{ Some($x: Int) } =>
e: Expr[T & Some[Int]]
e: Expr[T] & Expr[Some[Int]]
x: Expr[Int]
e

Expand Down
47 changes: 36 additions & 11 deletions tests/pos/cb-companion-joins.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,39 @@ trait M[Self]:
trait Num[Self]:
def zero: Self

trait A extends M[A]
trait B extends M[A]

trait AA:
type X: M
trait BB:
type X: Num
class CC[X1: {M, Num}] extends AA, BB:
type X = X1
X.zero
X.unit
object Test1:
trait X extends M[X]
trait Y extends M[Y]

object Test2:
trait A[X: Num]:
X.zero
trait B[X: {M, Num}]:
X.unit
X.zero

object Test3:

trait A:
type X: M
X.unit

trait B:
type X: Num
X.zero

trait C extends A, B:
X.zero
X.unit

class AA[Y: M] extends A:
type X = Y
X.unit
Y.unit

class CC[Y: {M, Num}] extends C:
type X = Y
X.zero
X.unit
Y.zero
Y.unit
Loading
Loading