-
Notifications
You must be signed in to change notification settings - Fork 3.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
Higher-kinded type variable unification. #6069
Conversation
65bddd5
to
9987289
Compare
The build failed due to binary compatibility issues. How can I run the binary compatibility test locally? |
9987289
to
db768cf
Compare
If you want to run all tests you can use |
Thanks. I get
|
That's expected, we only check binary compatibility for scala-library and scala-reflect. Minor versions of the scala-compiler jar are not binary compatible. |
Oh, right, the incompatible change was in ProblemFilters.exclude[DirectMissingMethodProblem]("scala.reflect.runtime.Settings.YhkTypevarUnification") worked, so hopefully the current build will succeed. It also took me a while to figure out why
Turns out I cannot have the file |
Great work @TomasMikula! This is really exciting! ✨ |
(ntp2.typeParams corresponds tv1.params)(methodHigherOrderTypeParamsSubVariance) && | ||
{ tv1.addHiBound(ntp2); true } | ||
case (tp1, tp2) => isSub2(tp1.normalize, tp2.normalize) // @M! normalize reduces higher-kinded case to PolyType's | ||
} |
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 implementations of <:<
and =:=
usually follow the same pattern. I wonder if a similar change needed in:
private def isSameHKTypes(tp1: Type, tp2: Type) = (
tp1.isHigherKinded
&& tp2.isHigherKinded
&& (tp1.normalize =:= tp2.normalize)
)
It would be great to setup a unit test in the same vein as https://github.com/scala/scala/blob/9acab45aeeadef2f63da69faf81465cc15599789/test/junit/scala/reflect/internal/TypesTest.scala to directly demonstrate the new unification (rather than indirectly via compilation), and to explore whether the change preserves transitivity of subtyping and consistency with =:=
.
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 wonder if a similar change needed in:
private def isSameHKTypes(tp1: Type, tp2: Type)
If =:=
also performs unification as a side effect, then yes. Otherwise, I think it is not necessary.
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 does, via mutateNonTypeConstructs
and then registerTypeEquality
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.
Yeah. Nevertheless, mutateNonTypeConstructs
already seems to handle the higher-kinded TypeVar
case correctly.
@@ -365,9 +365,23 @@ trait TypeComparers { | |||
|
|||
// @assume tp1.isHigherKinded || tp2.isHigherKinded | |||
def isHKSubType(tp1: Type, tp2: Type, depth: Depth): Boolean = { | |||
def isSub(ntp1: Type, ntp2: Type) = (ntp1.withoutAnnotations, ntp2.withoutAnnotations) match { | |||
def isSub(tp1: Type, tp2: Type) = (tp1.withoutAnnotations, tp2.withoutAnnotations) match { | |||
case (TypeRef(_, AnyClass, _), _) => false // avoid some warnings when Nothing/Any are on the other side | |||
case (_, TypeRef(_, NothingClass, _)) => 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.
Type aliases for Any
and Nothing
would evade the short circuit now that the caller doesn't normalize.
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.
Thanks, will move these 2 lines down to isSub2
.
case (tp1, tv2 @ TypeVar(_, _)) if settings.YhkTypevarUnification => | ||
val ntp1 = tp1.normalize | ||
(tv2.params corresponds ntp1.typeParams)(methodHigherOrderTypeParamsSubVariance) && | ||
{ tv2.addLoBound(ntp1); 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.
The existing code in TypeVar.registerBound
that responsible for unifying applied hk type vars walks up the base type sequence to find an arity match. What do we know about tv2.params.length == ntp1.typeParams.length
by virtue of making it to this line of code?
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 know tv2.params.length == ntp1.typeParams.length
thanks to a successful corresponds
on the previous line.
db768cf
to
2f97833
Compare
Added some tests, some of them currently failing. Pending resolution of #6080 before I continue on this one. |
@Atry That is not solved by this PR. |
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.
Exciting! I have a few questions/suggestions. Thanks for taking this on.
def isSub(ntp1: Type, ntp2: Type) = (ntp1.withoutAnnotations, ntp2.withoutAnnotations) match { | ||
|
||
def isSub(tp1: Type, tp2: Type) = (tp1.withoutAnnotations, tp2.withoutAnnotations) match { | ||
case (tv1 @ TypeVar(_, _), tv2 @ TypeVar(_, _)) if settings.YhkTypevarUnification => |
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'd rather not add more individual feature flags, they end up sticking around and complicating the user experience. Let's just use -Xsource:2.13, and let's test that flag only once, instead of in each guard (pull out the more advanced type var unification into another method?)
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.
Done.
def isSub(tp1: Type, tp2: Type) = (tp1.withoutAnnotations, tp2.withoutAnnotations) match { | ||
case (tv1 @ TypeVar(_, _), tv2 @ TypeVar(_, _)) if settings.YhkTypevarUnification => | ||
(tv2.params corresponds tv1.params)(methodHigherOrderTypeParamsSubVariance) && | ||
{ tv1.addHiBound(tv2); tv2.addLoBound(tv1); 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.
Let's debug this and get some statistics on how (often) we actually get here during a bootstrap/test suite. As Jason said, inference that compares TVs on both ends is likely to fail in the end.
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.
Will investigate.
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 to add assert(false)
to this branch and it is not triggered by tests or bootstrap. What do you suggest? Should I just change that branch to assert(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.
I wouldn't risk a crash, but we could consider a warning to test our theory since this codepath isn't enabled by default. I haven't thought about it much, but there's precedent (grep "please report" for another warning).
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.
Done.
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 can get here thanks to
scala/src/compiler/scala/tools/nsc/typechecker/Infer.scala
Lines 538 to 550 in 3a76f41
// Then define remaining type variables from argument types. | |
map2(argtpes, formals) { (argtpe, formal) => | |
val tp1 = argtpe.deconst.instantiateTypeParams(tparams, tvars) | |
val pt1 = formal.instantiateTypeParams(tparams, tvars) | |
// Note that isCompatible side-effects: subtype checks involving typevars | |
// are recorded in the typevar's bounds (see TypeConstraint) | |
if (!isCompatible(tp1, pt1)) { | |
throw new DeferredNoInstance(() => | |
"argument expression's type is not compatible with formal parameter type" + foundReqMsg(tp1, pt1)) | |
} | |
} | |
val targs = solvedTypes(tvars, tparams, tparams map varianceInTypes(formals), upper = false, lubDepth(formals) max lubDepth(argtpes)) |
|
||
def isSub(tp1: Type, tp2: Type) = (tp1.withoutAnnotations, tp2.withoutAnnotations) match { | ||
case (tv1 @ TypeVar(_, _), tv2 @ TypeVar(_, _)) if settings.YhkTypevarUnification => | ||
(tv2.params corresponds tv1.params)(methodHigherOrderTypeParamsSubVariance) && |
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's kind of confusing that we check methodHigherOrderTypeParamsSubVariance(tp2.typeParams, tp1.typeParams)
to compute isSub(tp1, tp2)
(what's the logic behind going in the opposite direction?)
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 took this from isPolySubType, assuming the reason for opposite direction was that type parameters in [X, Y] => T
appear in contravariant position. Looking at the implementation of methodHigherOrderTypeParamsSubVariance
, from 2.11 onwards it always returns true
, so could now be removed altogether?
(ntp2.typeParams corresponds tv1.params)(methodHigherOrderTypeParamsSubVariance) && | ||
{ tv1.addHiBound(ntp2); true } | ||
case (tp1, tp2) => isSub2(tp1.normalize, tp2.normalize) // @M! normalize reduces higher-kinded case to PolyType's | ||
} |
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 does, via mutateNonTypeConstructs
and then registerTypeEquality
2f97833
to
25db6f7
Compare
Needs a mima exception:
|
25db6f7
to
f3e1d33
Compare
Thanks! Fixed. |
d944d07
to
c6c10b1
Compare
Just realized that this PR also fixes some problems with pattern matching:
Added positive tests for those to this PR. |
279d3cf
to
ef39378
Compare
As per comments in #6080, seems like the two failing tests added in the latter commit can be ignored, since those cases should be unreachable through compilation. |
Our policy is not to merge PRs that contain failing commits (so, let's We'll allow PRs currently scheduled for 2.12.4 to be merged until Friday, run benchmarks and community builds over the weekend and Monday, and cut the release on Tuesday. Any PRs that regress performance / break the community build will be reverted and rescheduled for 2.12.5. |
Ideas how I can inline |
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.
Nice! Thanks for pushing this all the way through!
PS: for the "fixes" command in the PR description to be picked up (and bugs to be auto-closed on merge), the syntax for the bug reference is |
Thanks! I thought it was sufficient that the "fixes" were spelled out this way in the commit message. |
In scala#6092, `-Xsource:2.13` was enabled by default. Between this PRs parent and current 2.13.x, there were some changes that depended on this flag (scala#5983, scala#6069).
Since scala#6069, it is possible to have type variables compared to the eta expansion of a class type, which is `[CLASSTPARAM] => Class[CLASSTPARAM]`. After separate compilation, the owner this higher kinded type param is adjusted to the root class (see the comments and special cases in Pickler.localizedOwner), which makes it show up to certain subsequent as-seen-from maps as a type parameter of an enclosing class. SBT's ExtractAPI phase runs into this problem. This commit pins down the problem with a test and offers a straw-man fix, which clones the type params at a different owner (currently, NoOwner, but with a bit more refactoring this should be context.owner) when type when instantiating the type variables. An alternative might be to clone during eta-expansion (although to which owner? NoSymbol? A user provided one? What does this mean for our strategy of caching normalized types for performance?) A third fix would be to do the `cloneInfo` before registering the bound in `TypeComparers`. We'd need to refactor somewhat to thread through `context.owner` to that code, however. Fixes scala/bug#t10762
Since scala#6069, it is possible to have type variables compared to the eta expansion of a class type, which is `[CLASSTPARAM] => Class[CLASSTPARAM]`. After separate compilation, the owner this higher kinded type param is adjusted to the root class (see the comments and special cases in Pickler.localizedOwner), which makes it show up to certain subsequent as-seen-from maps as a type parameter of an enclosing class. SBT's ExtractAPI phase runs into this problem. This commit pins down the problem with a test and offers a straw-man fix, which clones the type params at a different owner (currently, NoOwner, but with a bit more refactoring this should be context.owner) when type when instantiating the type variables. An alternative might be to clone during eta-expansion (although to which owner? NoSymbol? A user provided one? What does this mean for our strategy of caching normalized types for performance?) A third fix would be to do the `cloneInfo` before registering the bound in `TypeComparers`. We'd need to refactor somewhat to thread through `context.owner` to that code, however. Fixes scala/bug#t10762
Since scala#6069, it is possible to have type variables compared to the eta expansion of a class type, which is `[CLASSTPARAM] => Class[CLASSTPARAM]`. After separate compilation, the owner this higher kinded type param is adjusted to the root class (see the comments and special cases in Pickler.localizedOwner), which makes it show up to certain subsequent as-seen-from maps as a type parameter of an enclosing class. SBT's ExtractAPI phase runs into this problem. This commit pins down the problem with a test and offers a straw-man fix, which clones the type params at a different owner (currently, NoOwner, but with a bit more refactoring this should be context.owner) when type when instantiating the type variables. An alternative might be to clone during eta-expansion (although to which owner? NoSymbol? A user provided one? What does this mean for our strategy of caching normalized types for performance?) A third fix would be to do the `cloneInfo` before registering the bound in `TypeComparers`. We'd need to refactor somewhat to thread through `context.owner` to that code, however. Fixes scala/bug#t10762
Since scala#6069, it is possible to have type variables compared to the eta expansion of a class type, which is `[CLASSTPARAM] => Class[CLASSTPARAM]`. After separate compilation, the owner of this type constructor param is adjusted to the root class (see the comments and special cases in Pickler.localizedOwner), which makes it show up to certain subsequent as-seen-from maps as a type parameter of an enclosing class. SBT's ExtractAPI phase runs into this problem. Eta expansion of the tycon type ref uses an owner (NoSymbol) that will not become a class after pickling. Fixes scala/bug#10762
Since scala#6069, it is possible to have type variables compared to the eta expansion of a class type, which is `[CLASSTPARAM] => Class[CLASSTPARAM]`. After separate compilation, the owner of this type constructor param is adjusted to the root class (see the comments and special cases in Pickler.localizedOwner), which makes it show up to certain subsequent as-seen-from maps as a type parameter of an enclosing class. SBT's ExtractAPI phase runs into this problem. Eta expansion of the tycon type ref uses an owner (NoSymbol) that will not become a class after pickling. Fixes scala/bug#10762
Since scala#6069, it is possible to have type variables compared to the eta expansion of a class type, which is `[CLASSTPARAM] => Class[CLASSTPARAM]`. After separate compilation, the owner of this type constructor param is adjusted to the root class (see the comments and special cases in Pickler.localizedOwner), which makes it show up to certain subsequent as-seen-from maps as a type parameter of an enclosing class. SBT's ExtractAPI phase runs into this problem. Eta expansion of the tycon type ref uses an owner (NoSymbol) that will not become a class after pickling. Fixes scala/bug#10762
Since scala#6069, it is possible to have type variables compared to the eta expansion of a class type, which is `[CLASSTPARAM] => Class[CLASSTPARAM]`. After separate compilation, the owner of this type constructor param is adjusted to the root class (see the comments and special cases in Pickler.localizedOwner), which makes it show up to certain subsequent as-seen-from maps as a type parameter of an enclosing class. SBT's ExtractAPI phase runs into this problem. Eta expansion of the tycon type ref uses an owner (NoSymbol) that will not become a class after pickling. Fixes scala/bug#10762
Since scala#6069, it is possible to have type variables compared to the eta expansion of a class type, which is `[CLASSTPARAM] => Class[CLASSTPARAM]`. After separate compilation, the owner of this type constructor param is adjusted to the root class (see the comments and special cases in Pickler.localizedOwner), which makes it show up to certain subsequent as-seen-from maps as a type parameter of an enclosing class. SBT's ExtractAPI phase runs into this problem. Eta expansion of the tycon type ref uses an owner (NoSymbol) that will not become a class after pickling. Fixes scala/bug#10762
Regression: scala/bug#11303 |
""".trim) | ||
false | ||
case (tp1, tv2 @ TypeVar(_, _)) => | ||
val ntp1 = tp1.normalize |
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, what was the reason for calling normalize
here on the to-be-bound?
Currently, when checking whether
[α]Foo[Bar[α]]
is a subtype of (higher-kinded) type variable?F
, the current implementation eta-expandsF
to[β]F[β]
, substitutes the same type parameters[α]F[α]
and then checks whetherFoo[Bar[α]] <: F[α]
. Then (I think)F
is unified withFoo
andα
fails to unify withBar[α]
.This PR changes that to immediately unify
F
with[α]Foo[Bar[α]]
ifα
is compatible withF
's type parameter.This change can cause ambiguous implicits, so is under a compiler flag
-Yhk-typevar-unification
-Xsource:2.13
.Fixes scala/bug#10185, scala/bug#10195, scala/bug#10197, scala/bug#10213, scala/bug#10238, scala/bug#10372.
Also fixes scala/bug#6895 in a different way than
-Ypartial-unification
.