-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Use GADT constraints in maximiseType #15544
Conversation
f7bb6e1
to
8811725
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise LGTM. 👍
6011d24
to
b6fc951
Compare
It seems that since I do not have the write permission yet, my approval alone does not meet the merging requirements. |
@abgruszecki would you be happy to rubberstamp this through, on Linyxus' review approval? |
// if it doesn't occur then it's safe to instantiate the tvar | ||
// Eg neg/i14983 the C in Node[+C] is in the GADT lower bound X >: List[C] so maximising to Node[Any] is unsound | ||
// Eg pos/precise-pattern-type the T in Tree[-T] is in no GADT upper bound so can maximise to Tree[Type] | ||
val safeToInstantiate = v != 0 && gadtBounds.forall(tb => !tvar.occursIn(if v == 1 then tb.lo else tb.hi)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't entirely feel correct, we're not taking into account the variance at which tvar
is occuring at in tb
. I would disallow instantiating if tvar
occurs at all in tb
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's correct. And I was hoping to find a legitimate case in either bootstrapping or in the community build which would help me figure out what variance flipping I need to be doing. But I didn't encounter any. Do you have in mind an example where this would be negatively impacted?
I proposed this version because it fixes a soundness issue even if it might have this incompleteness issue (correct me if I'm misdiagnosing).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would worry that it's a partial fix, not that it's incomplete. Variables always occur at some variance in types: A is negative in A => Unit
, positive in A => Unit => Unit
, invariant in A => A
. Here we partially take variance in account by only inspecting one bound, which feels very counterintuitive. Unless we have a clear argument that doing what the code is doing right now is OK, we should just disregard variance at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This indeed results in soundness problems. Consider the following example, whose main difference with #14983 is that the value encapsulated in Node
has a contravariant type argument.
case class Showing[-C](show: C => String)
sealed trait Tree[+A]
final case class Leaf[+B](b: B) extends Tree[B]
final case class Node[-C](l: Showing[C]) extends Tree[Showing[C]]
object Test:
def meth[X](tree: Tree[X]): X = tree match
case Leaf(v) => v
case Node(x) =>
// GADT bounds: X >: Showing[C]
// maximise Node[C] to Node[Nothing].
// Since C is contravariant and in GADT lower bound,
// instantiation is currently assumed to be safe.
// After that: X >: Showing[Nothing]
Showing[String](_ + " boom!")
def main(args: Array[String]): Unit =
val tree = Node(Showing[Int](_.toString))
val res = meth(tree)
println(res.show(42)) // runtime crash: ClassCastException
The problem here is that the type variable C
are contravariant in both places. So maximising Showing[C]
also results in maximising the GADT lower bound of X
, which makes the constraint unsound.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should disable the instantiation as long as the type variable appears in GADT bounds as suggested by Alex? Or shall we query the type variable variances in GADT bounds using variances
, and only instantiate the tvar when its variance conforms with that in GADT bounds?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to take variance into account, we will need an argument why that's sound.
The way I understand things, the type variable is supposed to correspond to a type member in DOT terms: type variables in PatternTypeConstrainer will only come from type arguments to unapply
functions, and in the pattern type they will always appear as type arguments to some class, unless we're dealing with an unapply
with a very strange signature. If we do instantiate the type variable, we turn it into an abstract type, which makes it clearly correspond to the type member. In certain situations it's OK to approximate the type variable to avoid unnecessary problems with things like implicit search, but if the type variable appears in the constraint, approximating the type variable could be unsound.
@Linyxus What do you think we should do? Can you come up with an argument why taking variance into account is sound? |
I do not have an argument for the safety regarding the instantiated type variable (since its bounds get narrowed), but I could try to (partially and informally) show that the GADT constraints of other variables will be sound after instantiation. Assuming that (I have poor knowledge of type inferencing so correct me if I'm wrong :P),
Now, we view We have To see this, we can show that instantiating Therefore, given that the GADT constraints before instantiation is sound, then its entailment (or its weaker version) is guaranteed to be sound. |
I'm not fussed. Just checking occurrences is also cheaper. |
Sure! I just wanted to record these thoughts on the soundness of such approximation. Considering the simplicity of checking occurrence, I agree that we should go with this solution too. A bit off-topic: I am a bit confused about why we would need type variable instantiation in constraint inferencing. Would it be good to always avoid instantiating type variables, turn it into abstract types and record their type bounds? |
Implicit lookup, for example, is impacted. Here's a semi-minimal example: trait Show[T]:
def show(x: T): String
object Show:
def show[T](x: T)(using z: Show[T]): String = z.show(x)
given Show[String] with { def show(x: String): String = x }
opaque type Email = String
object Email:
given (using s: Show[String]): Show[Email] = s
class Test:
def test(opt: Option[Email]): String = opt match
case Some(email) => Show.show(email)
case None => "none" Without instantiating
It's a slightly bad example because it doesn't justify why Show isn't contravariant. And it's manageable with a new instance of Show defined for |
In a modified version of maximizeType I ended up with picking test failures, and this seems to be the right position.
... by keeping an memory-optimised path & extracting a ExternalizeMap. Also a small fixup in ContainsNoInternalTypesAccumulator using itself.
Consider the GADT constraints during Inferencing's maximiseType to avoid instantiating type variables that lead to GADT casting inserting unsound casts.
bfaaf4f
to
32826d8
Compare
No description provided.