-
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
Instantiate more type variables to hard unions #15632
Conversation
I was hoping this would give some leverage for avoiding errors caused by level checking, but alas that's not the case. Nevertheless it seems useful to support the scenario of #14770. |
|
||
val res = widenOK | ||
|| joinOK | ||
|| (tp1.isSoft || constrainRHSVars(tp2)) && recur(tp11, tp2) && recur(tp12, tp2) |
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.
Can this mechanism replaces the existing constrainRHSVars
hack? As mentioned in #14770 (comment) this is supposed to handle this sort of cases, but it's incomplete and break internal invariants. I'd feel more comfortable adding new code here if we could remove the existing hacks first.
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 tried now, and the answer is no. We already can't build the compiler itself if we disable constrainRHSVars
.
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 won't have time reviewing this in more details but I'm not comfortable merging this as is. I think a TypeComparer-wide mode that gets set temporarily as we add the constraint as I hypothesized in #14770 would be easier to reason about.
// loop for a very long time without the recursion brake. | ||
def hardenTypeVars(tp2: Type): Unit = tp2.dealiasKeepRefiningAnnots match | ||
case tvar: TypeVar if constraint.contains(tvar.origin) => | ||
tvar.widenUnions = false |
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 seems very dangerous to me. The particular subtype check we're doing might be part of a TyperState that ends up being rolled back, but the mutation here will be preserved. This will make it extremely hard to debug any issue involving widening, and any change in inference could break existing code because this flag happens to be toggled.
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.
bccbaf7 now keeps this info in TyperState.
I think that would be harder to reason about since it is a global side effect, and you don't know what it will influence. Propagating |
* to hard unions instead of being widened in `widenOr`. | ||
*/ | ||
private var myRigidVars: TypeVars = _ | ||
def rigidVars: TypeVars = myRigidVars |
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 like this much more 😅, but I think the name could be misleading: usually "rigid variables" are variables that cannot be constrained at all (like the type parameters of a method as seen from inside that method). Maybe go with hardVars
instead in keeping with hardenVars
?
@@ -548,24 +554,41 @@ trait ConstraintHandling { | |||
wideInst.dropRepeatedAnnot | |||
end widenInferred | |||
|
|||
/** Convert all toplevel union types in `tp` to hard unions */ | |||
extension (tp: Type) private def hardenUnions(using Context): Type = tp.widen match |
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.
My original idea was to change https://github.com/lampepfl/dotty/blob/6540ad9154797164561281080b6a4e24a191691d/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala#L266 which currently uses |
(and thus creates soft unions) to instead check a flag to decide whether to use a soft or hard union. This would avoid having to convert unions after creating them, and it means we can reset the flag after adding the constraint.
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.
The problem I see with this is that the lower bound might be a union with type variables in it. In that case the type variables would end up in the ordering constraint and there would be no union to harden until the time we instantiate the type variable.
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 see, so we couldn't reset the flag but could check hardVars
in ConstraintHandling
when we create the unions I think, that would cover unions created later from propagating constraints based on ordering.
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 think hardVars
belongs in TyperState
. If it's in ConstraintHandling
we would lose it when we commit a constraint.
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.
Right, I meant we could leave it in TyperState but check it in ConstraintHandling#addOneBound instead of checking it in TypeVar#instantiate, but I don't know if we're actually supposed to access the currrent TyperState from ConstraintHandling.
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.
But addOneBound
does not necessarily create a union. The union might be created by fullBounds when we instantiate the variable.
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.
But as far as I can tell we don't use fullBounds
to instantiate a type variable (except for GADTs but we know that's problematic: #14288)
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.
We do use fullLowerBound
in approximation
.
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.
Ah that's right, so we'd have to check hardVars both in addOneBound and fullLowerBound, I'm neutral on whether this is better than the current scheme.
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 think it's better to tie it to typevariable instantiation, since we do fullLowerBound
also elsewhere. We could possibly add it to approximate
, but maybe that mixes up things too much.
This looks too involved for me to review with little time, but I'll get back to this when I'm back - sorry for the delay. |
Try alternative to track hard typevars in constraint instead of typer state. This avoids the problem that a failing subtype comparison can also mark type variables as hard.
It seems constrainRHSVars is no longer needed with the new way to keep track of hard type variables.
@smarter I think it now addresses your concerns. Do you have time to take a look again? |
Nice! I'll have a look tomorrow. |
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!
tvars.foreach(tvar => | ||
if !tvar.inst.exists then | ||
if !isOwnedAnywhere(this, tvar) then includeVar(tvar) | ||
if other.isHard(tvar) then constraint = constraint.withHard(tvar)) |
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 path is only executed if constraint.contains(tl)
is false above, but if the same type lambda appears in both constraints, they could still have different sets of hardened tvars. So I think this logic needs to be moved below inside the argument to constraint.uninstVars.forall
.
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.
Ah yes, well spotted.
Fixes #14770