Skip to content
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

Merged
merged 1 commit into from
Sep 29, 2017

Conversation

TomasMikula
Copy link
Contributor

@TomasMikula TomasMikula commented Sep 11, 2017

Currently, when checking whether [α]Foo[Bar[α]] is a subtype of (higher-kinded) type variable ?F, the current implementation eta-expands F to [β]F[β], substitutes the same type parameters [α]F[α] and then checks whether Foo[Bar[α]] <: F[α]. Then (I think) F is unified with Foo and α fails to unify with Bar[α].

This PR changes that to immediately unify F with [α]Foo[Bar[α]] if α is compatible with F'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.

@scala-jenkins scala-jenkins added this to the 2.12.4 milestone Sep 11, 2017
@TomasMikula TomasMikula force-pushed the hk-typevar-unification branch from 65bddd5 to 9987289 Compare September 12, 2017 10:48
@TomasMikula
Copy link
Contributor Author

TomasMikula commented Sep 12, 2017

The build failed due to binary compatibility issues. How can I run the binary compatibility test locally?

@TomasMikula TomasMikula force-pushed the hk-typevar-unification branch from 9987289 to db768cf Compare September 12, 2017 11:01
@lrytz
Copy link
Member

lrytz commented Sep 12, 2017

If you want to run all tests you can use testAll in sbt. For MiMa specifically, use mimaReportBinaryIssues.

@TomasMikula
Copy link
Contributor Author

Thanks. I get

...
[info] scala-compiler: previous-artifact not set, not analyzing binary compatibility
...

@lrytz
Copy link
Member

lrytz commented Sep 12, 2017

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.

@TomasMikula
Copy link
Contributor Author

Oh, right, the incompatible change was in scala-reflect and my speculative "fix"

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 mimaReportBinaryIssues kept crashing with

java.nio.charset.MalformedInputException: Input length = 1

Turns out I cannot have the file src/reflect/mima-filters/2.12.0.forwards.excludes open in vim, because it creates a temporary .2.12.0.forwards.excludes.swp file in the same directory, and mimaReportBinaryIssues apparently doesn't like that.

@non
Copy link
Contributor

non commented Sep 12, 2017

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
}
Copy link
Member

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 =:=.

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor Author

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
Copy link
Member

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.

Copy link
Contributor Author

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 }
Copy link
Member

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?

Copy link
Contributor Author

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.

@TomasMikula
Copy link
Contributor Author

Added some tests, some of them currently failing. Pending resolution of #6080 before I continue on this one.

@Atry
Copy link
Contributor

Atry commented Sep 19, 2017

@TomasMikula
Copy link
Contributor Author

@Atry That is not solved by this PR.

Copy link
Contributor

@adriaanm adriaanm left a 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 =>
Copy link
Contributor

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?)

Copy link
Contributor Author

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 }
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will investigate.

Copy link
Contributor Author

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)?

Copy link
Contributor

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).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Member

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

// 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) &&
Copy link
Contributor

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?)

Copy link
Contributor Author

@TomasMikula TomasMikula Sep 26, 2017

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
}
Copy link
Contributor

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

@TomasMikula TomasMikula force-pushed the hk-typevar-unification branch from 2f97833 to 25db6f7 Compare September 26, 2017 13:23
@adriaanm
Copy link
Contributor

Needs a mima exception:

ProblemFilters.exclude[DirectMissingMethodProblem]("scala.reflect.runtime.Settings.isScala213")

@TomasMikula TomasMikula force-pushed the hk-typevar-unification branch from 25db6f7 to f3e1d33 Compare September 27, 2017 17:01
@TomasMikula
Copy link
Contributor Author

Thanks! Fixed.

@TomasMikula TomasMikula force-pushed the hk-typevar-unification branch 2 times, most recently from d944d07 to c6c10b1 Compare September 27, 2017 23:18
@TomasMikula
Copy link
Contributor Author

TomasMikula commented Sep 27, 2017

Just realized that this PR also fixes some problems with pattern matching:

Added positive tests for those to this PR.

@TomasMikula TomasMikula force-pushed the hk-typevar-unification branch 2 times, most recently from 279d3cf to ef39378 Compare September 28, 2017 01:17
@TomasMikula
Copy link
Contributor Author

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.

@adriaanm
Copy link
Contributor

adriaanm commented Sep 28, 2017

Our policy is not to merge PRs that contain failing commits (so, let's testHigherKindedTypeVarUnification). Also, on second thought (sorry), I'd rather not ignore MiMa's advice (I've come to regret that before). Let's inline isScala213.

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.

@TomasMikula
Copy link
Contributor Author

Ideas how I can inline isScala213? In scala.tools.nsc.settings.ScalaSettings it is defined as source.value >= version213, but source is not a member of scala.reflect.internal.settings.MutableSettings.

Copy link
Contributor

@adriaanm adriaanm left a 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!

@adriaanm
Copy link
Contributor

adriaanm commented Sep 29, 2017

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 scala/bug#NNNNN (I updated the description)

@TomasMikula
Copy link
Contributor Author

TomasMikula commented Sep 29, 2017

Thanks! I thought it was sufficient that the "fixes" were spelled out this way in the commit message.

@adriaanm adriaanm merged commit 64dd408 into scala:2.12.x Sep 29, 2017
@SethTisue SethTisue added the release-notes worth highlighting in next release notes label Oct 2, 2017
lrytz added a commit to lrytz/scala that referenced this pull request Dec 1, 2017
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).
retronym added a commit to retronym/scala that referenced this pull request Jul 12, 2018
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
retronym added a commit to retronym/scala that referenced this pull request Jul 12, 2018
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
adriaanm pushed a commit to adriaanm/scala that referenced this pull request Jul 12, 2018
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
adriaanm pushed a commit to adriaanm/scala that referenced this pull request Jul 13, 2018
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
adriaanm pushed a commit to adriaanm/scala that referenced this pull request Jul 13, 2018
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
adriaanm pushed a commit to adriaanm/scala that referenced this pull request Jul 13, 2018
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
adriaanm pushed a commit to adriaanm/scala that referenced this pull request Jul 13, 2018
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
adriaanm pushed a commit to adriaanm/scala that referenced this pull request Aug 8, 2018
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
@joroKr21
Copy link
Member

Regression: scala/bug#11303

""".trim)
false
case (tp1, tv2 @ TypeVar(_, _)) =>
val ntp1 = tp1.normalize
Copy link
Member

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release-notes worth highlighting in next release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants