-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Try either
when compare an intersection against an applied type in GADT mode
#15175
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
Conversation
Nice, @Linyxus! I wonder what use cases require that approximation state reset, because it seems problematic to the dual use of TypeComparer and preserving GADT soundness... @LPTK WDYT of this? Is this too narrowly snapping up intersection types before tryBaseType makes mistakes? Or are intersection types indeed the only types that need this? |
I agree with @dwijnand that the whole idea of "resetting approximations" feels very fishy. Where else is such resetting potentially done while in GADT mode? These would likely be soundness holes too. Regarding the PR, I don't like the looks of the fix. Looking at the diff, you're only adding a IMO it would be much more logical to fix the unsoundness in |
@@ -1198,6 +1198,14 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling | |||
case _ => false | |||
} && { GADTused = true; true } | |||
|
|||
def tryGadtAnd1: Boolean = | |||
ctx.mode.is(Mode.GadtConstraintInference) && { |
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.
ctx.mode.is(Mode.GadtConstraintInference) && { | |
necessaryConstraintsOnly && { |
Perhaps is better?
Thanks, @LPTK - I see what you're saying. After this use of Also, isn't the fall-through to |
Regarding the way to fix this issue, agree that based on the usage of
Maybe we should merge #12087 instead of this one? |
Regarding the soundness holes caused by resetting approx state: yes, we have more soundness holes caused by this. Each time enum SUB[-A, +B]:
case Refl[C]() extends SUB[C, C]
trait Tag { type T }
def foo[A, B, X <: Tag{type T <: A}](e: SUB[X, Tag{type T <: B}], x: A): B = e match {
case SUB.Refl() =>
// unsound GADT constr because of approx state resetting: A <: B
x
}
def bad(x: Int): String =
foo[Int, String, Tag{type T = Nothing}](SUB.Refl(), x) // cast Int to String In this example, we are trying to extract necessary constraint from |
I am not sure how the reset of approx state will interact with the type comparison process. But it seems that the situation is very tricky (@abgruszecki and I have discussed this before): SR sometimes relies on the reset of approx state to derive the desired constraint. So solutions like freezing GADT constraints when we reset approx state will not work. Consider the following example: enum SUB[-A, +B]:
case Refl[X]() extends SUB[X, X]
trait E[+X]
case class L[A]() extends E[A]
def foo[A, B](e: SUB[L[A], E[B]], x: A): B = e match {
case SUB.Refl() =>
x
} Here we are trying to extract necessary constraint from |
It seems that the approx state is not only unstable (may be reset), but also unprecise for the use of GADT reasoning (it works fine for type inference). For example, |
SR? Great thoughts. I think we should experiment with the impact of disabling the reset while inferring GADT constraints (or, more generally, necessaryConstraintsOnly?). It's the only way to be aware of possible unsoundness in GADT reasoning. Correct me if I'm wrong, but I believe we significantly bias to soundness rather than completeness for type inference/checking, so we should do the same for GADT reasoning. I also think we should make it harder and/or more obvious that the approximation is reset: |
SR = Subtyping Reconstruction.
Looking at the two PRs once again, I think #12087 might indeed be just better than this approach. The way it's implemented, we go from @LPTK: the entire TypeComparer is written in an extremely order-dependent way. We can always rely that aliases etc. are handled very early, and we can rely on the type shapes being handled in a very particular way. Changing that would be fundamentally changing TypeComparer. |
Hello! Since it's been a while I wanted to check in on this @Linyxus. Do you plan on returning here, or are you waiting for more input, etc? |
This PR fixes #11545 by trying
either
before approximating LHS withtryBaseType
when comparing an intersection type against an applied type in GADT inference mode.The unsoundness of #11545 comes from the following steps taken by
TypeComparer
:In the above trace, when comparing an intersection type
Inv[A] & Y
againstInv[String]
, the type comparer approximate LHS toInv[A]
directly and compareInv[A] <:< Inv[String]
. Since the approximation state gets reset when comparing the arguments of applied types, this comparison results in unsound GADT constraints.In this PR, when comparing
S ∧ T <:< F[A]
, instead of trying to approximate LHS type to the base typeF
, we will try bothS <:< F[A]
andT <:< F[A]
and merge GADT constraints from the two branches. This fixes the unsoundness brought by approximating the intersection (and forgetting about the approximation afterwards as shown in the trace) by trying to inference GADT constraints in a more precise way.@abgruszecki