-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix #5980 #6023
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
Fix #5980 #6023
Conversation
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.
Hello, and thank you for opening this PR! 🎉
All contributors have signed the CLA, thank you! ❤️
Commit Messages
We want to keep history, but for that to actually be useful we have
some rules on how to format our commit messages (relevant xkcd).
Please stick to these guidelines for commit messages:
- Separate subject from body with a blank line
- When fixing an issue, start your commit message with
Fix #<ISSUE-NBR>:
- Limit the subject line to 72 characters
- Capitalize the subject line
- Do not end the subject line with a period
- Use the imperative mood in the subject line ("Add" instead of "Added")
- Wrap the body at 80 characters
- Use the body to explain what and why vs. how
adapted from https://chris.beams.io/posts/git-commit
Have an awesome day! ☀️
More thoughts on why
If used on types which cannot be simplified, So basically in all cases, in case of type parameters, the semantics of However, I'm not completely sure whether it makes for the consistent subtyping behaviour between type params and traits since there's a lot going on there. Would appreciate some feedback on the above logic regarding whether |
test performance please |
performance test scheduled: 1 job(s) in queue, 1 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/6023/ to see the changes. Benchmarks is based on merging with master (5f5b5b0) |
@@ -737,7 +737,12 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] { | |||
return recur(AndType(tp11, tp121), tp2) && recur(AndType(tp11, tp122), tp2) | |||
case _ => | |||
} | |||
either(recur(tp11, tp2), recur(tp12, tp2)) | |||
|
|||
def maybeAndType = { |
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.
either
by itself is expensive, so it would be good to avoid the extra call.
Furthermore, andType(tp11, tp12)
is known to be a subtype of AndType(tp1, tp2)
and we can exploit that.
Finally, !=
should not be used for type comparisons, since it is meaningless and potentially expensive. Use
=:=
for semantic equality or eq
for syntactic equality.
So how about:
val tp1norm = andType(tp11, tp12)
if (tp1norm ne tp1) recur(tp1norm, tp2)
else either(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.
Ok, though this solution kept failing run/mixins1/A_1.scala – minimised example:
trait A[This <: A[This] with B]
trait B extends A[B]
At some point, A[LazyRef(This)] & A[B] ne A[LazyRef(This)] & A[B]
returned true
. This is due to LazyRef
, which is uncached. I modified the solution to avoid needless construction of the AndType
if we already have the equivalent object.
Probably related to #6057.
Looks good now, thanks! |
This comment has been minimized.
This comment has been minimized.
a4d6e7c
to
8f0f13c
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
49ae979
to
938c259
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
@@ -1641,6 +1643,18 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] { | |||
NoType | |||
} | |||
|
|||
private[this] def andTypeGen(tp1: Type, tp2: Type, op: (Type, Type) => Type | |||
, original: (Type, Type) => Type, isErased: Boolean = ctx.erasedTypes): Type = trace(s"glb(${tp1.show}, ${tp2.show})", subtyping, show = true) { |
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.
it seems original
is always _ & _
. Why not inline it?
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 didn't inline it for consistency reasons: if there is a degree of freedom in the op
parameter to liftIfHK
, it would IMO be weird if there wouldn't be a degree of freedom in the original
as well.
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.
On the second thought, I agree, _ & _
repeated twice doesn't look nice. I have found a compromise in terms of making the original
in andTypeGen
a default parameter.
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 don't understand why this function signature should be consistent with liftIfHK
. And I don't think having a default parameter is the right way to remove the duplication.
Bonus point: if you inline original
, you will avoid allocating a lambda most of the time.
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 don't understand why this function signature should be consistent with
liftIfHK
.
The problem of the control over the execution flow of the function is solved by decoupling the logic from the data. Doing so partially is inconsistent (even if sufficient for the issue in question).
Bonus point: if you inline original, you will avoid allocating a lambda most of the time.
How?
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.
When calling andTypeGen
, a lambda is allocated for original
. But unless liftIfHK
is called, there is no need to allocate this lambda.
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.
Does it have to do with the fact that andTypeGen
passes the reference along but liftIfHK
calls it in its body?
…tors When performing subtyping comparisons of the `F[A] & F[B] <:< C` kind, if `F` is a type parameter (e.g. as in `trait Foo[F[+_]]`), the compiler decides the relationship to be true if either `F[A] <: C` or `F[B] <: C`. However, in case of covariant `F[+_]`, it is possible to distribute the type arguments when performing the `&` operation: `F[A] & F[B] ~> F[A & B]`. Hence there is one more case to be considered when checking `<:<` relationship. This commit adds such a check to the `<:<` logic. Now, `F[A] & F[B] <:< C` is true (for covariant `F`) if either of the following is true: - F[A] <: C - F[B] <: C - F[A & B] <: C
This is a fix for #5980.
Motivation
I have not found a proof anywhere that
F[A] & F[B] <:< F[A & B]
for all covariantF[+_]
. However, this property is featured in tests and docs, and there is a deliberate logic in the sources to distribute type parameters for covariant type constructors.Also, it seems that the bug originates not really because of the distribution itself but because of inconsistent treatment of how subtyping relationship is inferred for traits and type params. Even if the
F[A] & F[B] <:< F[A & B]
is not true for all types, I think the relationship should be inferred the same way (in the sense elaborated in the Solution section below) for both type params and traits.Solution
Consider that we need to compute
F[A] & F[B] <:< F[A & B]
.F
can be either a type param or a trait (see the issue description above). The<:<
relationship needs to be computed e.g. when assigningF[A] & F[B]
to a value of typeF[A & B]
.The difference between how traits and type params are treated originates at the match inside compareAppliedType2 of
isSubType
ofTypeComparer
.TypeBounds
handles type params among other things and, viacompareLower
, ultimately leads to thefourthTry
. InforthTry
, ultimately the following logic is used: "A & B <: C
is true if eitherA <: C
orB <: C
is true".In case of ClassInfo, the branch will ultimately lead us to this line which computes the
&
operation over the two type bounds.Since the
&
operation sometimes simplifies the types (as in for covariantF[+_]
, it may distribute type parameters, so thatF[A] & F[B]
becomesF[A & B]
), the two behaviors (for traits and type bounds) are inconsistent. For traits, the result of the&
operation will be checked to be a subtype of the target type, and for type params, no such check is performed.To summarise, when inferring
F[A] & F[B] <:< F[A & B]
, in case ifF
is a class or a trait, the result of the&
operation on types will be compared to be a subtype ofF[A & B]
. No such check will be performed ifF
is a type param.This PR adds such a check to the corresponding inference logic.
Notes on
andType
The problem with
&
andType
is still not the same as&
. However, if you plug in&
instead ofandType
there, Scala 2 library compilation tests will not pass. Precisely, this line will cause an infinite loop when inferring a subtype viaisSubType
we have just modified. In a minimised form, the offending example looks like this (also added to the tests):The loop seems to happen on the
case pc: Min[_] =>
typecheck. When traced which types get compared in turn inrecur
ofTypeComparer
(viaprintln(s"recCount = ${recCount}; ${tp1.show} <: ${tp2.show}")
), under-Yno-deep-subtypes
compiler option, the trace is:Basically for some reason at some point
foo.B & foo.A[LazyRef(foo.Min[_]#S)]
(which is the type bounds ofS
) gets compared toNothing
. Given that&
operation on types as part of its workflow checks its operands to be a subtype of one another, we end up with the above periodic situation if we solve the bug with&
. It seems that type aliases are involved here:S
is aTypeAlias
. Normally, the compiler does not allow for things liketype S = B with A[S]
because of cyclic references. However, it seems to allow for such a cyclic reference under the special conditions presented above.Why andType does the job
&
delegates toglb
computation, and andType basically checks whether it can distribute the type arguments if the two operands are type constructors, falling back to the originalAndType
otherwise.glb
involves subtyping checks between its two arguments, andandType
does not. Hence the risk of getting an infinite subtyping check loop is eliminated. This also is enough to make sure the bug in question is solved, since the behavior now is more consistent between traits and type params – more consistent distribution-wise.Tests
With the
&
solution, among all thetestCompilation
, only the issue above was encountered. After replacing&
withandType
all tests, including the one testing the issue in question, pass. I've also included the tests to describe that weird typecheck looping behavior I encountered, just in case.