Skip to content

Track type variable dependencies to guide instantiation decisions #16042

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

Merged
merged 17 commits into from
Oct 31, 2022

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Sep 15, 2022

We now keep track of reverse type variable dependencies in constraints.
E.g. if a constraint contains a clause like

A >: List[B]

We associate with B info that A depends co-variantly on it. Or, if

A <: B => C

we associate with B that A depends co-variantly on it and with C
that A depends contra-variantly on it. Here, co-variant means that
the allowable range of A narrows if the referred-to variable B grows, and
contra-variant means that the allowable range of A narrows if the referred-to
variable C shrinks. If there's an invariant reference such as

A <: Array[B]

Then A depends both co- and contra-variantly on B.

These dependencies are then used to guide type variable instantiation.
If an eligible type variable does not appear in the type of a typed expression,
we interpolate it to one of its bounds. Previously this was done in an ad-hoc
manner where we minimized the type variable if it had a lower bound and maximized
it otherwise. We now take reverse dependencies into account. If maximizing a type
variable would narrow the remaining constraint we minimize, and if minimizing
a type variable would narrow the remaining constraint we maximize. Only if
the type variable is not referred to from the remaining constraint we resort
to the old heuristic based on the lower bound.

Fixes #15864

Todo: This could be generalized in several directions:

  • We could base the dependency tracking on type param refs instead of type variables.
    That could make the replace operation in a constraint more efficient.
  • We could base more interpolation decisions on dependencies. E.g. we could
    interpolate a type variable only if both the type of an expression and the enclosing
    constraint agree in which direction this should be done.

@odersky odersky changed the title Streamline OrderingConstraint#newConstraint Track type variable dependencies to guide instantiation decisions Sep 15, 2022
@odersky odersky requested a review from smarter September 15, 2022 17:15
@odersky
Copy link
Contributor Author

odersky commented Sep 15, 2022

test performance please

@odersky
Copy link
Contributor Author

odersky commented Sep 16, 2022

@mbovel @anatoliykmetyuk Benchmark bot seems to be down?

@mbovel
Copy link
Member

mbovel commented Sep 17, 2022

test performance please

@dottybot
Copy link
Member

performance test scheduled: 355 job(s) in queue, 1 running.

4 similar comments
@dottybot

This comment was marked as duplicate.

@dottybot

This comment was marked as duplicate.

@dottybot

This comment was marked as duplicate.

@dottybot

This comment was marked as duplicate.

@dottybot
Copy link
Member

Performance test finished successfully:

Visit https://dotty-bench.epfl.ch/16042/ to see the changes.

Benchmarks is based on merging with main (afc6ce4)

@odersky odersky force-pushed the fix-15864 branch 4 times, most recently from 614c5b9 to 06f4cff Compare September 20, 2022 13:02
@odersky odersky mentioned this pull request Sep 21, 2022
@odersky
Copy link
Contributor Author

odersky commented Sep 23, 2022

Some info on the parboiled2 test. It seems to be a hygiene violation caused somewhere in typedQuotePattern. The test failed when adding a new polytype to the constraint as follows (with -uniqid on, to disambiguate)

error while adding [T#1874900402]#1874900402
  (m#569565916: Map[String#6487, T#1874900402])
    (implicit h#1341031137: 
      org#15.parboiled2#2768.support#2770.HListable#2786[T#1874900402]
    ): org#15.parboiled2#2768.RuleN#2986[h#1341031137.Out#15632]/[T#1874900402] to #66778 with uninstantiated variables: T#1986710687, (param)1#489820775, 
  (param)2#1856085236
 constrained types: 
  [T#1986710687]#1986710687
    (m#620022507: Map[String#6487, T#1986710687], ignoreCase#620022507: 
      Boolean#88
    )
      (implicit h#1277453506: 
        org#15.parboiled2#2768.support#2770.HListable#2786[T#1986710687]
      ): org#15.parboiled2#2768.RuleN#2986[h#1277453506.Out#15632]
, 
  [(param)1#489820775 <: 
    org#15.parboiled2#2768.support#2770.HListable#2786[T#1986710687]
   & Singleton#1670]#489820775: h#1277453506.type
, 
  [(param)2#1856085236 <: 
    org#15.parboiled2#2768.support#2770.HListable#2786[T#1874900402]
   & Singleton#1670]#1856085236: h#1341031137.type
 bounds: 
     T#1986710687
     (param)1#489820775
     <: org#15.parboiled2#2768.support#2770.HListable#2786[T#1986710687] & 
      Singleton#1670
     (param)2#1856085236
     <: org#15.parboiled2#2768.support#2770.HListable#2786[T#1874900402] & 
      Singleton#1670
 ordering: 
 co-deps:
     T#1986710687 --> (param)1#489820775
 contra-deps:
     T#1986710687 --> (param)1#489820775

T#1874900402 gets added as bound parameter to the constraint, but it is already referenced in the constraint. We then get:

java.lang.AssertionError: assertion failed: missing contra backwards reference T#1874900402 -> (param)2#1856085236 in #66779 with uninstantiated variables: T#1986710687, (param)1#489820775, 
  (param)2#1856085236
, T#1874900402
 constrained types: 
  [T#1986710687]#1986710687
    (m#620022507: Map[String#6487, T#1986710687], ignoreCase#620022507: 
      Boolean#88
    )
      (implicit h#1277453506: 
        org#15.parboiled2#2768.support#2770.HListable#2786[T#1986710687]
      ): org#15.parboiled2#2768.RuleN#2986[h#1277453506.Out#15632]
, 
  [(param)1#489820775 <: 
    org#15.parboiled2#2768.support#2770.HListable#2786[T#1986710687]
   & Singleton#1670]#489820775: h#1277453506.type
, 
  [(param)2#1856085236 <: 
    org#15.parboiled2#2768.support#2770.HListable#2786[T#1874900402]
   & Singleton#1670]#1856085236: h#1341031137.type
, 
  [T#1874900402]#1874900402
    (m#569565916: Map[String#6487, T#1874900402])
      (implicit h#1341031137: 
        org#15.parboiled2#2768.support#2770.HListable#2786[T#1874900402]
      ): org#15.parboiled2#2768.RuleN#2986[h#1341031137.Out#15632]
 bounds: 
     T#1986710687
     (param)1#489820775
     <: org#15.parboiled2#2768.support#2770.HListable#2786[T#1986710687] & 
      Singleton#1670
     (param)2#1856085236
     <: org#15.parboiled2#2768.support#2770.HListable#2786[T#1874900402] & 
      Singleton#1670
     T#1874900402
 ordering: 
 co-deps:
     T#1986710687 --> (param)1#489820775
 contra-deps:
     T#1986710687 --> (param)1#489820775
 while typechecking /Users/odersky/workspace/dotty/community-build/community-projects/parboiled2/parboiled-core/src/main/scala-3/org/parboiled2/support/OpTreeContext.scala

@odersky
Copy link
Contributor Author

odersky commented Sep 23, 2022

The perspective failure is also a hygiene violation (both added parameters are already referred to in the constraint). This time it's called from TypeComparer#matchCase.

error while adding [h#1295220417, t#1295220417 <: Tuple#52]#1295220417 =>> 
  scala#23.runtime#430.MatchCase#5264[h#1295220417 *: t#1295220417, h#1295220417
     
  | 
    scala#23.Tuple#53.Fold#23132[t#1295220417, Nothing#1128, 
      [x#-1965483054, y#-1965483054]#-1965483054 =>> x#-1965483054 | 
        y#-1965483054
    ]
  ]/[] to #23870 with uninstantiated variables: (param)1#673691532, (param)2#392956106, 
  (param)3#666882910
, B#1367453971, X0#1173905467
 constrained types: 
  [(param)1#673691532 <: 
    perspective#25.derivation#2791.HKDProductGeneric#11888[
      perspective#25.examples#2907.Cat#2932
    ]
   & Singleton#1676]#673691532: gen#1729214832.type
, 
  [(param)2#392956106 <: 
    perspective#25.derivation#2791.HKDProductGeneric#11888[
      perspective#25.examples#2907.Cat#2932
    ]
   & Singleton#1676]#392956106: gen#1729214832.type
, 
  [(param)3#666882910 <: 
    deriving#436.Mirror#11941.ProductOf#23108[
      perspective#25.examples#2907.Cat#2932
    ]{
      MirroredElemTypes = m$proxy1#40129.MirroredElemTypes; 
        MirroredLabel = m$proxy1#40129.MirroredLabel#23214
    }
   & Singleton#1676]#666882910: m#223342788.type
, 
  [B#1367453971 >: 
    m$proxy1#40129.MirroredElemLabels#23215 match {
      case EmptyTuple#12278 => Nothing#1128
      case h#1134074693 *: t#1134074693 => h#1134074693 | 
        scala#23.Tuple#53.Fold#23132[t#1134074693, Nothing#1128, 
          [x#-1965483054, y#-1965483054]#-1965483054 =>> x#-1965483054 | 
            y#-1965483054
        ]
    }
  ]#1367453971: Set[B#1367453971]
, [X0#1173905467]#1173905467: X0#1173905467
 bounds: 
     (param)1#673691532
     <: 
      perspective#25.derivation#2791.HKDProductGeneric#11888[
        perspective#25.examples#2907.Cat#2932
      ]
     & Singleton#1676
     (param)2#392956106
     <: 
      perspective#25.derivation#2791.HKDProductGeneric#11888[
        perspective#25.examples#2907.Cat#2932
      ]
     & Singleton#1676
     (param)3#666882910
     <: 
      deriving#436.Mirror#11941.ProductOf#23108[
        perspective#25.examples#2907.Cat#2932
      ]{
        MirroredElemTypes = m$proxy1#40129.MirroredElemTypes; 
          MirroredLabel = m$proxy1#40129.MirroredLabel#23214
      }
     & Singleton#1676
     B#1367453971
     >: 
      m$proxy1#40129.MirroredElemLabels#23215 match {
        case EmptyTuple#12278 => Nothing#1128
        case h#1295220417 *: t#1295220417 => h#1295220417 | 
          scala#23.Tuple#53.Fold#23132[t#1295220417, Nothing#1128, 
            [x#-1965483054, y#-1965483054]#-1965483054 =>> x#-1965483054 | 
              y#-1965483054
          ]
      }
     X0#1173905467
 ordering: 
 co-deps:
 contra-deps:

@odersky
Copy link
Contributor Author

odersky commented Sep 23, 2022

So, the question to decide is this:

  • We have evidence that reverse dependencies work except that there can be rare hygiene violations (2 cases in CB, none in test suite).

  • replace was not affected by these violations on the tests or the CB. It works either based on dependencies or doing a brute force search of all parameters (which was the state before the last commit).

  • It's debatable what replace should do when faced with a hygiene problem. I.e say we have the constraint

     A >: C[B]
    

    then we add the binder [B] yielding

    A >: C[B]
    B
    

    and then we replace the bound B by B1. Should the pre-existing C[B] be replaced as well? Previously the answer was yes, with the replace optimization the answer is no.

So in the end the question is: Do we prefer to keep the behavior stable wrt replace or do we choose to optimize, with potentially big gains for large constraints?

@odersky odersky marked this pull request as ready for review September 24, 2022 08:21
@odersky odersky force-pushed the fix-15864 branch 4 times, most recently from 60af4b9 to 7bda186 Compare September 24, 2022 12:59
@odersky
Copy link
Contributor Author

odersky commented Sep 25, 2022

test performance please

@dottybot
Copy link
Member

performance test scheduled: 355 job(s) in queue, 1 running.

We now keep track of reverse type variable dependencies in constraints.
E.g. is a constraint contains a clause like

    A >: List[B]

We associate with `B` info that A depends co-variantly on it. Or, if

    A <: B => C

we associate with `B` that `A` depends co-variantly on it and with `C`
that `A` depends contra-variantly on it.

These dependencies are then used to guide type variable instantiation.
If an eligible type variable does not appear in the type of a typed expression,
we interpolate it to one of its bounds. Previously this was done in an ad-hoc
manner where we minimized the type variable if it had a lower bound and maximized
it otherwise. We now take reverse dependencies into account. If maximizing a type
variable would narrow the remaining constraint we minimize, and if minimizing
a type variable would narrow the remaining constraint we maximize. Only if
the type variable is not referred to from the remaining constraint we resort
to the old heuristic based on the lower bound.

Fixes scala#15864
Can be enabled globally with Config option (set to false by default) or locally
through option -Ycheck-constraint-deps. The option is currently turned on for pos,
run, and patmatExhaustivity tests.
@odersky odersky assigned smarter and unassigned odersky Oct 30, 2022
Copy link
Member

@smarter smarter left a comment

Choose a reason for hiding this comment

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

There were two other uses of isInstantiated in OrderingConstraint which I suggest replacements for, otherwise LGTM.

@smarter smarter assigned odersky and unassigned smarter Oct 30, 2022
Co-authored-by: Guillaume Martres <[email protected]>
@odersky
Copy link
Contributor Author

odersky commented Oct 31, 2022

@smarter The two occurrences were replaced following your suggestions.

* type parameter is instantiated to a larger type, the constraint would be narrowed
* (i.e. solution set changes other than simply being made larger).
*/
private var coDeps: ReverseDeps = SimpleIdentityMap.empty
Copy link
Member

@dwijnand dwijnand Dec 15, 2022

Choose a reason for hiding this comment

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

I only just realised that this (and its co-conspirator contraDeps) made OrderingConstraint into a mutable data structure (previously it was immutable).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's actually not mutable. coDeps and contraDeps are views that are maintained incrementally.

Copy link
Member

Choose a reason for hiding this comment

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

That would only make it not destructive. And even so, there is a dropDeps, which does remove from them, if I'm not mistaken.

Copy link
Contributor Author

@odersky odersky Dec 15, 2022

Choose a reason for hiding this comment

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

I am not sure what you mean by destructive vs mutable. Are LazyLists mutable? In my book they aren't. And, dropDeps is not exported in Constraint so it is morally private (we should probably make it private to be clear about it)

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I understand better now. You're right.

Even though it looks very different, I think I prefer the approach that's taken with the original components of OrderingConstraint, i.e. in ConstraintLens update, where depending on the previous constraint (which is the external, public reference) it was either mutated in place or copied on write, and then always returned the updated reference. And that logic was well separate from the rest of the domain logic. With these new variables, the updates just happen at some point of the logic, mutating the vars in place.

@Kordyjan Kordyjan added this to the 3.3.0 milestone Aug 1, 2023
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.

Error on type inference
6 participants