-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Open
Labels
area:varianceIssues related to covariance & contravariance.Issues related to covariance & contravariance.itype:bugitype:soundnessSoundness bug (it lets us compile code that crashes at runtime with a ClassCastException)Soundness bug (it lets us compile code that crashes at runtime with a ClassCastException)
Description
Compiler version
3.3.6 and 3.7.0
Minimized code
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
val z = y match
case l : L[Int&String] => l.a
case r : R[Int&String] => r.b
z:String // java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String
Expectation
I expect line val y: E[Int&String] = x
to raise an error.
The problem is due to the normalization
E[Int] & E[String] =:= E[Int&String]
as described in https://dotty.epfl.ch/docs/reference/new-types/intersection-types-spec.html
"if C is covariant, C[A] & C[B] ~> C[A & B]
"
I found this rule logically dubious (it certainly does not hold in a set-theoretic model), even if it is convenient for typechecking. So I tried to explicitely construct an unsoundness.
Note that the same issue happen if E
is replaced by a trait with
sealed trait E[+A]
trait L[+A] extends E[A] :
def a : A
trait R[+A] extends E[A] :
def b : A
class LR(val a: Int, val b: String) extends L[Int] with R[String]
summon[(E[Int] & E[String]) =:= E[Int&String]]
val x: E[Int] & E[String] = LR(4, "hi")
val y: E[Int&String] = x
val z = y match
case l : L[Int&String] => l.a
case r : R[Int&String] => r.b
z:String
Note also that the matching over x
is proven exhaustive.
mbovel
Metadata
Metadata
Assignees
Labels
area:varianceIssues related to covariance & contravariance.Issues related to covariance & contravariance.itype:bugitype:soundnessSoundness bug (it lets us compile code that crashes at runtime with a ClassCastException)Soundness bug (it lets us compile code that crashes at runtime with a ClassCastException)
Type
Projects
Milestone
Relationships
Development
Select code repository
Activity
[-]Unsoundness with distribution of union over covariant types[/-][+]Unsoundness with distribution of intersection over covariant types[/+]EugeneFlesselle commentedon Jun 26, 2025
Nice catch!
Minimized, using commutativity of union:
SimonGuilloud commentedon Jun 26, 2025
This one is pretty fun !
odersky commentedon Jun 27, 2025
I think the original intuition was that if C is a covariant class or trait, then
C[A & B] =:= C[A & B]
. But even thatdoes not seem to be the case as Simon's second example shows. Can one of you make a PR to drop the rule and check what breaks if we do that?
EugeneFlesselle commentedon Jun 27, 2025
Yes, I think it would perhaps hold (though still uncertain) if we ruled out inheriting from the base class
E
through multiple direct parents.Will do
SimonGuilloud commentedon Jun 27, 2025
Intuitively, it should hold for some types, such as options or lists. My intuition says the following:
We have both (semantically)
Option[A] & Option[B] = Option[A&B]
andOption[A] | Option[B] = Option[A|B]
on the other hand, we have
List[A] & List[B] = List[A&B]
but notList[A] | list[B] = List[A|B]
The difference is that
List[T]
can hold multipleT
. In a sense,List[T]
is a "true product" over T, whileOption[T]
is not.Meanwhile, both of them are not "true sum" over T, since in each case only one branch (
Some
andCons
) depend onT
.This was my intuition to construct my example, which is inspired from the true sum "either" construct.
So here is my conjecture for when such equalities can hold:
C[A|B] = C[A] | C[B]
holds whenC[T]
is linear inT
, i.e.T
is only used once (in each branch)C[A&B] = C[A] & C[B]
holds whenC[T]
is colinear inT
, i.e.T
is only used in a single branch (but possibly multiple times)or something like that. For example,
Either[String|Int] =:= Either[String] | Either[Int]
(where Either is the symmetrical-typed version of the usualEither
) intuitively make sense, andEither
is linear:T
is only used once inLeft
and once inRight
.For function types, I'm not sure. I don't like anyway that
(A => B) & (C => D) =:= (A|C) => (B&D)
, because it doesn't make sense semantically and if it is sound it is only because the language is not expressive enough to build a counterexample. I expect that it will be an issue with refinement types. But I also believe it is a very convenient assumption for typechecking?Linyxus commentedon Jun 29, 2025
I find this quite interesting so I had some thoughts. Here are some quick notes:
trait
s beingsealed
andclass
es beingfinal
), otherwise it will be really hard. Specifically, like what Simon gave,&
and|
). One interesting analogy is to understand&
as*
and|
as+
, then type constructors satisfyingF[A] & F[B] = F[A & B]
are like "multiplicative functions" and those thatF[A] | F[B] = F[A | B]
like "additive functions". We have another perspective of Simon's conjecture thatC[A | B] = C[A] | C[B]
for a linearC[_]
: all linear functions (f(x) = ax + b
) are additive. If a type parameter is used twice in a branch, then it becomes a quadratic function.SimonGuilloud commentedon Jun 29, 2025
I'll add that a homomorphism over either | or & implies monotonicity.
suppose and . Then
Alternatively if ,
So covariance is a necessary condition (but not sufficient).
Add original issue test
Add original issue test
Disable distribution of intersection types over applied types