-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Skolem based gadt constraints #5736
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
Skolem based gadt constraints #5736
Conversation
ea52ba8
to
a0dbebb
Compare
@odersky this looks ready to go! Commit messages contain basically all the information. Typeclass derivation needed to be altered, since from: (t: T) match {
case _: U => ...
} we're not allowed to deduce that I realize that approximating Skolem bounds in the way I'm doing it is unsound, but for now it prevents an even worse unsoundness. Exact problem is that |
Wait, if the constraint solver does this kind of stuff then surely it's already unsound and we should fix it ? |
@smarter if I understood @Blaisorblade, that's the difference between "necessary" and "sufficient". What the current constraint solver does is "sufficient", and that is what is needed for type inference. What GADT constraints need is "necessary", and, well, they don't have it (yet). For now, if constraint already contains final case class Inv[T](t: T)
def foo[T](t: T): T = Inv(t) match { case Inv(_: Int) => 0 }
val notFive: 5 = foo[5](5) // actually zero |
I fail to see why simplifying |
That looks like a good characterization of the problem, yes. It also looks hard to achieve, no? Maybe we do need two different data structures and schemes for type inference and GADTs after 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.
I have the impression we need a deeper analysis where the gap between allowed normalizations in inference and GADT computations makes a difference.
In inference: Unforced narrowing of bounds is sound, but loses solutions (which is acceptable).
In GADT bounds: Unforced narrowing of bounds is unsound, hence unacceptable.
Vice versa, in GADT computations we are allowed to widen bounds (which loses precision, but is sound) whereas in inference such widening is unsound.
So it seems to me a systematic solution would entail replacing lubs with glbs and vice versa in all constraint solving code. EDIT: No that would not work either. The issue is in what direction are we allowed to lose precision. So, let's say we have the situation
a1.type | a2.type <: B
and suppose we want to avoid unions of singleton types. In constraint solving for inference, it's OK to approximate this with A <: B
where A
is the common type of a1
, and a2
. In GADT computation it is not, but we could approximate by Nothing <: B
instead. Correct?
@@ -779,6 +779,21 @@ object Contexts { | |||
}, gadts) | |||
} | |||
|
|||
// avoid recording skolems in lower bounds | |||
// recording two skolem bounds results in an union, which is then simplified | |||
// T >: Sko(U) | Sko(U) is simplified to T >: U, which is simply wrong |
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.
Where is this simplification of T >: Sko(U) | Sko(U)
to T >: U
done? If this done only for Skolemtypes? I suspect the answer is that it happens for all singleton types. But then the fix should also be generalized to all singleton types, right?
test performance please |
performance test scheduled: 1 job(s) in queue, 0 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/5736/ to see the changes. Benchmarks is based on merging with master (da1e196) |
f43c6f5
to
d30c925
Compare
Rebased on top of master. More changes to typeclass derivation tests were needed - I'll open a separate PR for replacing erasedValue with typeOf (see #5564, last 3 comments specifically). @smarter I stared very hard at EDIT: to clarify, the above assertions about @odersky I believe GADT bounds cannot ever be approximated, only simplified. I think it'd be EDIT: see conversation below reg. simplifying skolem unions. |
If those are the same Now, BTW, unsound inference results are usually caught later, but only if the violation is visible in the trees produced by inference. Conversely, |
@Blaisorblade I'm less and less clear on whether it's actually sound or not to simplify the bounds in such a manner. For typechecking, intuitively it should be acceptable to know less bounds, because then we will simply not accept some "morally valid" code. Note that this'd mean that it'd be sound to drop both skolems from the lower bound without losing soundness. For exhaustivity and useless pattern checks (which you may have meant by "some constraints are unsatisfiable"), we actually would not want to simplify the constraints in such a way (since |
e96e094
to
1ef5cfd
Compare
Not approximating is better than approximating if we can afford it, of course. So if you can avoid simplifying those skolems without performance loss, IMHO go ahead. But you talk about soundness, and I disagree there. And I think the two scenarios you describe should work the same way. Overall, we seem to have three scenarios:
It seems the solver can only be in sufficient or (if implemented) necessary mode. If we agree on this behavior for scenario 3, then the solver must search for necessary constraints, like scenario 2 — and if the resulting constraint is unsatisfiable, the original ones were as well and the considered branch is impossible. The above implies the compiler might require unused cases. You seem to disagree. We should pick somehow and stick to that. |
@Blaisorblade that comment was from two weeks ago, before we discussed everything thoroughly and I came to the conclusions from my presentation/report. The situation, as I understand it now, is as follows: Type inference is concerned with finding a solution satisfying constraint * - allow me to use the syntax GADT constraint inference is concerned with finding the strongest possible constraint To emit safe GADT exhaustivity warnings, we must decide if the constraint What this means is that for GADT constraints, we can simplify |
1763131
to
d0f48b8
Compare
If we do not insert TypeVars into the bounds every time, then the only time we need to remove them is when taking the full bounds of some type. Since that logic now resides in ConstraintHandling and replaces all TypeParamRefs internal to SmartGADTMap, we have no need to perform expensive type traversals. This removes the only reason for caching bounds. The addition of HK parameter variance adaptation was necessary to make tests/pos/i6014-gadt.scala pass.
gadtSyms/gadtContext became redundant, so they were removed. The logic in typedDefDef was adjusted to only create a fresh context when necessary.
The rationale for using a Skolem here is: we want to record that there is at least one value that is both of the pattern type and the scrutinee type. All symbols are now considered valid for adding GADT constraints - the rationale is that set of constrainable symbols should be either selected on a per-(sub)pattern basis, or be the same for all matches. Previously, symbols which were only appearing variantly in a scrutinee type could be considered constrainable anyway because of an outer pattern match.
Also rename the classes to better reflect their role, and document and reorder definitions to make more sense.
The added symbols can have inter-dependencies in their bounds.
constrainPatternType is specific to term patterns, whereas in match types there is a simple subtyping relationship between the pattern and the scrutinee. In the future, simply calling isSubType in GADTFlexible context would likely be sufficient.
0b2fa94
to
cb15213
Compare
I added recaps of what we talked about in person to all the discussions. Compared to when we talked, the only non-trivial change not related to any discussion is adding |
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. Can be merged after final fixes.
true | ||
} || op2 | ||
|
||
if (ctx.mode.is(Mode.GADTflexible)) { |
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 code should be moved out to a separate method (e.g. gadtEither
), and there should be a doc comment explaining what it does, analogous to the original either
.
@@ -208,7 +208,10 @@ class PlainPrinter(_ctx: Context) extends Printer { | |||
else { | |||
val constr = ctx.typerState.constraint | |||
val bounds = | |||
if (constr.contains(tp)) constr.fullBounds(tp.origin)(ctx.addMode(Mode.Printing)) | |||
if (constr.contains(tp)) { | |||
val ctx0 = ctx.addMode(Mode.Printing) |
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.
Why not leave it inline? ctx.addMode(Mode.Printing).typeComparer.fullBounds(tp.origin)
Fixes #5667.
Fixes #2985.