Skip to content

Eliminate TypeVars and Wildcards early in glb/lub#6257

Merged
adriaanm merged 2 commits intoscala:2.13.xfrom
joroKr21:solve-wildcards
Aug 8, 2018
Merged

Eliminate TypeVars and Wildcards early in glb/lub#6257
adriaanm merged 2 commits intoscala:2.13.xfrom
joroKr21:solve-wildcards

Conversation

@joroKr21
Copy link
Member

@joroKr21 joroKr21 commented Jan 10, 2018

Type variables and wildcards can act both as subtypes and
supertypes depending on the direction of the test. This means
types with variables and/or wildcards can shadow other types in
glb/lub.

Consider the following examples:

lub((Int, ? ) :: (Int, String) :: Nil)
glb((Int, ?A) :: (Int, String) :: Nil)

Intuitively both should return (Int, String), but this depends
on the order of types in the list passed to glb/lub.
This commit incorporates the simple heuristic of moving types that
contain variables and/or wildcards to the end of the list.

Fixes scala/bug#10686 and fixes scala/bug#10740

@scala-jenkins scala-jenkins added this to the 2.13.0-M4 milestone Jan 10, 2018
@joroKr21
Copy link
Member Author

I'm not sure how this would affect performance

@joroKr21
Copy link
Member Author

Is it possible that there is a deadlock somewhere in concurrent.TrieMap that is sometimes triggered by the scalacheck tests?

@joroKr21
Copy link
Member Author

/rebuild

@joroKr21 joroKr21 changed the title Avoid instantiating TypeVars with types containing Wildcards Avoid returning types containing ? from glb/lub Jan 11, 2018
@joroKr21
Copy link
Member Author

@retronym how does this look?

@retronym
Copy link
Member

To help me understand your patch, I've attempted to split it into a refactoring and a behavior change.

Does that split look faithful to your intent?

I still need a bit more time to understand the change.

@joroKr21
Copy link
Member Author

Your split is almost correct. Apologies for not doing this myself.

The semantic change is just the addition of this switch of the current type from t to t1:

if (lteq(t, t1) && t.exists(_.isWildcard)) t1 else t

Note that if we're not going to check for wildcards the switch is pointless and there would be no need to compare lteq(t, t1).

The (tail)-recursive version of the "find all maximal elements in a list given a partial order" (here maxTypes) made it easier for me to understand what it's actually doing. Here the worst running time is inherently O(n^2). There is a difference in the order that types are compared. In maxTypes if we find a bigger element than the current one, we immediately switch to it and go back to the beginning of the list. The old implementation always uses the head of the list to eliminate smaller elements and then recurses into the tail.

@joroKr21
Copy link
Member Author

Hold on, it looks to me that maxTypes doesn't do enough comparisons, so there must be a bug.
The most annoying part is to preserve the original order of the list. This shouldn't matter right?

@joroKr21 joroKr21 force-pushed the solve-wildcards branch 2 times, most recently from 44df6c6 to 5730ed1 Compare January 15, 2018 15:10
@joroKr21
Copy link
Member Author

Should be good now

@tailrec def inner(work: List[Type], done: List[Type]): List[Type] = work match {
case t2 :: ts2 if po(t1, t2) => // found a bigger type, we're done here
if (!t2.exists(_.isWildcard) || !po(t2, t1)) max
else t1 :: (ts2 reverse_::: done)
Copy link
Member Author

Choose a reason for hiding this comment

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

Could also do ts2 reverse_::: t1 :: done here, but it doesn't seem to matter.

Copy link
Member Author

Choose a reason for hiding this comment

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

Huh, I guess it does matter after all - so frustrating that with is not symmetric.

@joroKr21 joroKr21 force-pushed the solve-wildcards branch 2 times, most recently from 0ecd58f to bd71ab8 Compare January 15, 2018 20:46
@joroKr21
Copy link
Member Author

joroKr21 commented Jan 15, 2018

@retronym It looks like the list passed to lub/glb can contain TypeVars which depend on the exact order of comparisons as being called in the current implementation to be constrained 😭 so I can't change it in any way whatsoever. All I can do is push the partition on ? inside.

@joroKr21 joroKr21 changed the title Avoid returning types containing ? from glb/lub Eliminate TypeVars and Wildcards early in glb/lub Feb 20, 2018
@joroKr21
Copy link
Member Author

/sync

@joroKr21
Copy link
Member Author

joroKr21 commented Feb 21, 2018

Updated to fix scala/bug#10740

An alternative would be to use a Queue for type constraints and prepend / append as needed.

Note that this also compiles in Dotty, but neither on Scala 2.13.x nor with this PR:

def foo[A, B](implicit ev: (Int, A) =:= (B, String)) = 42
foo

This would require a redesign of how type variables and constraints are handled.

Edit: Should I add it as a negative test case?

@joroKr21
Copy link
Member Author

/sync

@joroKr21 joroKr21 force-pushed the solve-wildcards branch 2 times, most recently from 2fd6ffd to f56e2e5 Compare April 26, 2018 21:54
@adriaanm adriaanm modified the milestones: 2.13.0-M4, 2.13.0-M5 Apr 30, 2018
@adriaanm adriaanm added this to the 2.13.0-M4 milestone Apr 30, 2018
@adriaanm adriaanm self-requested a review April 30, 2018 14:29
@joroKr21
Copy link
Member Author

/sync

@szeiger szeiger modified the milestones: 2.13.0-M4, 2.13.0-M5 May 2, 2018
joroKr21 added 2 commits May 13, 2018 08:51
Type variables and wildcards can act both as subtypes and
supertypes depending on the direction of the test. This means
types with variables and/or wildcards can shadow other types in
`glb/lub`.

Consider the following examples:
```scala
lub((Int, ? ) :: (Int, String) :: Nil)
glb((Int, ?A) :: (Int, String) :: Nil)
```

Intuitively both should return `(Int, String)`, but this depends
on the order of types in the list passed to `glb/lub`.
This commit incorporates the simple heuristic of moving types that
contain variables and/or wildcards to the end of the list.

Fixes scala/bug#10686 and fixes scala/bug#10740

/** From a list of types, retain only maximal types as determined by the partial order `po`. */
private def maxTypes(ts: List[Type])(po: (Type, Type) => Boolean): List[Type] = {
def loop(ts: List[Type]): List[Type] = ts match {

This comment was marked as resolved.

case t :: ts1 =>
val ts2 = loop(ts1.filterNot(po(_, t)))
if (ts2.exists(po(t, _))) ts2 else t :: ts2
case Nil => Nil
Copy link
Contributor

Choose a reason for hiding this comment

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

I assume we special-cased Nil and the singleton list based on profiling before? Before we drop these cases, let's gather some statistics on the frequency with which they occur (from casual inspection, not all callers ensure the list's length is >= 2).

Copy link
Member Author

Choose a reason for hiding this comment

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

It would be a good exercise for me - any pointers how to do this?

Copy link
Contributor

Choose a reason for hiding this comment

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

Check out Statistics.incCounter

loop(ts)
// The order here matters because type variables and
// wildcards can act both as subtypes and supertypes.
val (ts2, ts1) = ts.partition(_ exists {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is the right solution. We shouldn't be calling lub/glb with a list of types that includes type variables or wildcards. These test cases are a great help in debugging this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Any idea how we could avoid that?

Copy link
Member Author

Choose a reason for hiding this comment

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

Recalling my experiments here, the current tests exercise this code path, i.e. type variables being inferred by virtue of participating in the lub/glb.

The complete solution would probably come with Dotty but in the mean time it's unsatisfactory explaining to people that they have to swap the arguments to =:= to make their code compile 😄 (due to scala/bug#10686). How about approximating just the wildcards?

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree we should improve, but I'd like to take a look at the actual code paths that lead here, and how to remove those non-sensical types from the beginning. The type checker already approximates the expected type using wildcards (as wildPt), which shares a similar motiviation.

Copy link
Member Author

Choose a reason for hiding this comment

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

One code path is via ExistentialType.withTypeVars in subtyping checks when the other type also has type vars. Try this and see a few of the pos partests fail with "trying to do lub/glb of typevar":

--- a/src/reflect/scala/reflect/internal/tpe/TypeComparers.scala
+++ b/src/reflect/scala/reflect/internal/tpe/TypeComparers.scala
@@ -529,7 +529,7 @@ trait TypeComparers {
         (rt2.parents forall (isSubType(tp1, _, depth))) &&
           (rt2.decls forall (specializesSym(tp1, _, depth)))
       case et2: ExistentialType =>
-        et2.withTypeVars(isSubType(tp1, _, depth), depth) || fourthTry
+        suspendingTypeVars(typeVarsInType(tp1))(et2.withTypeVars(isSubType(tp1, _, depth), depth)) || fourthTry
       case mt2: MethodType =>
         tp1 match {
           case mt1 @ MethodType(params1, res1) =>

I'm not sure what is the correct thing to do here. Since we only care if there is any solution to the existential skolems but not what that solution is, if feels unexpected that this would affect the inference of some unrelated type variables.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed that those unrelated type vars should not be constrained -- they should act like skolems, I guess. I'm reluctant to change this until I fully understand how this works, which is not likely to happen anymore for 2.13.0, sadly.

Copy link
Contributor

Choose a reason for hiding this comment

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

My initial comment was wrong -- I see now from your ltr/rtl test case that we get into the situation with wildcards in types. I can't think of a better way to make this more consistent, so, let's go with yours.

@adriaanm
Copy link
Contributor

adriaanm commented Jun 4, 2018

Also, I was poking around here and was reminded of stripExistentialsAndTypeVars. Maybe that method is the right target for this PR?

@joroKr21
Copy link
Member Author

joroKr21 commented Jun 6, 2018

In its current form it doesn't help, because it handles only top-level type variables and it just bails out, but I will try out something similar with a TypeMap.

@adriaanm
Copy link
Contributor

adriaanm commented Jun 13, 2018

Yes, I meant this PR could "target" that method so that it also handles the general case. Maybe it was a mistake that it only handles top-level ones?

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.

Agreed this is a problem, and I can't think of a better way to solve it. So, let's book the progress. Would like to make sure performance is not affected before merging /cc @retronym.

@adriaanm adriaanm requested a review from retronym August 7, 2018 13:45
@adriaanm
Copy link
Contributor

adriaanm commented Aug 8, 2018

Since we have a lot going on, optimistically merging -- if performance is affected, we can fix before RC1.

@adriaanm adriaanm merged commit 30e689c into scala:2.13.x Aug 8, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bad type inference when type parameter as existential bound has invariant position Order of TypeVar constraints affects unification

5 participants