Skip to content

Error on type inference #15864

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

Closed
flomebul opened this issue Aug 15, 2022 · 3 comments · Fixed by #16042
Closed

Error on type inference #15864

flomebul opened this issue Aug 15, 2022 · 3 comments · Fixed by #16042
Assignees
Milestone

Comments

@flomebul
Copy link
Contributor

flomebul commented Aug 15, 2022

Compiler version

3.1.3

Minimized code

A similar issue (#11556) was already corrected several month ago (#12847)

object Issue15864:
  type Traverser[-I, +O] = I => LazyList[(O)]
  extension[I, O](ta: Traverser[I, O])
    def ~[P](tb: Traverser[O, P]): Traverser[I, P] = ???

  class Graph { class Node; class Link }
  
  case class Path[+E](e: E)
  type Query[-U, +V] = Traverser[Path[U], Path[V]]

  def nodesQ(using g: Graph): Query[Nothing, g.Node] = ???
  def outsQ(using g: Graph): Query[g.Node, g.Node] = ???

  object aGraph extends Graph
  import aGraph._
  given aGraph.type = aGraph

  val q1: Query[Nothing, Node] = nodesQ ~ (outsQ ~ outsQ)
  implicitly[q1.type <:< Query[Nothing, Node]]

  val q2 = nodesQ ~ (outsQ ~ outsQ)
  implicitly[q2.type <:< Query[Nothing, Node]]

  val q3: Query[Nothing, Node] = q2
end Issue15864

Output

Cannot prove that (asuivre.Issue15864.q2 : 
  asuivre.Issue15864.Traverser[asuivre.Issue15864.Path[Nothing], 
    asuivre.Issue15864.Path[Any]
  ]
) <:< asuivre.Issue15864.Query[Nothing, asuivre.Issue15864.aGraph.Node].
  implicitly[q2.type <:< Query[Nothing, Node]]

Expectation

Supposed to compile without error.

@flomebul flomebul added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Aug 15, 2022
@dwijnand dwijnand added stat:needs minimization Needs a self contained minimization stat:needs triage Every issue needs to have an "area" and "itype" label and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Aug 15, 2022
@szymon-rd szymon-rd added area:typer stat:needs minimization Needs a self contained minimization and removed stat:needs minimization Needs a self contained minimization stat:needs triage Every issue needs to have an "area" and "itype" label labels Aug 16, 2022
@flomebul
Copy link
Contributor Author

I take some time to try to minimize at maximum the case :

object Issue15864:
  def op[O, P](ta: List[O], tb: List[P]): List[P] = ???

  class Graph { class Node }
  
  def outsQ(using g: Graph): List[List[g.Node]] = ???

  object aGraph extends Graph
  given aGraph.type = aGraph

  val q1: List[List[aGraph.Node]] = op(outsQ, op(outsQ, outsQ))
  implicitly[q1.type <:< List[List[aGraph.Node]]]

  val q2 = op(outsQ, op(outsQ, outsQ))
  implicitly[q2.type <:< List[List[aGraph.Node]]]

  val q3: List[List[aGraph.Node]] = q2
end Issue15864

And the unexpected error is now

Cannot prove that (asuivre.Issue15864.q2 : List[List[Any]]) <:< List[List[asuivre.Issue15864.aGraph.Node]].
  implicitly[q2.type <:< List[List[aGraph.Node]]]

I hope this can help

@odersky odersky removed the stat:needs minimization Needs a self contained minimization label Sep 4, 2022
@odersky
Copy link
Contributor

odersky commented Sep 4, 2022

I had a closer look at this. What goes on here is that a dependent type variable (param)4 introduced by constrainResult does not occur in the type List[List[Test.implA.Node]]> of Test.outsQ(Test.implA) and therefore is arbitrarily instantiated to the upper bound. But the type variable does appear in the remaining constraint. I see:

interpolate non-occurring (param)4 in TS[72, 12, 1, 0] in Test.outsQ(Test.implA): List[List[Test.implA.Node]], fromBelow = false, qualifying = (param)3, (param)4,  uninstantiated variables: O, P, O, P, (param)3, (param)4
 constrained types: [O, P](ta: List[O], tb: List[P]): List[P], 
  [O, P](ta: List[O], tb: List[P]): List[P]
, [(param)3]: Any, [(param)4]: Any
 bounds: 
     O >: List[Test.implA.Node]
     P >: List[(param)4]
     O >: List[Test.implA.Node]
     P >: List[(param)3]
     (param)3
     (param)4
 ordering: 
     P <: P

We should instantiate the type variable to Nothing instead, since that keeps the constraint as general as possible. But to see that we have to look at all other bindings of the constraint. This can get very expensive.

It does seem to have a lot to do with level checking and I see some code that uses a variable's name to decide interpolation direction in interpolateTypeVars:

              val fromBelow =
                name.is(AvoidNameKind.UpperBound) ||
                !name.is(AvoidNameKind.LowerBound) && tvar.hasLowerBound

But the problem persists even if I turn Config.checkLevelsOnConstraints on. @smarter Do you have a suggestion how to proceed here?

@odersky
Copy link
Contributor

odersky commented Sep 5, 2022

Maybe should bite the bullet and introduce reverse indices in constraints. By this I mean two maps

val coDeps, contraDeps: Map[TypeVar, Set[TypeVar]]
  • coDeps(x) contains all type variables of level smaller than x.nestingLevel where x occurs covariantly or invariantly in their bound.
  • contraDeps(x) contains all type variables of level smaller than x.nestingLevel where x occurs contravariantly or invariantly in their bound.

The sets would have to be maintained in constraints. We can use these sets to make decisions about instantiation directions.

odersky added a commit to dotty-staging/dotty that referenced this issue Sep 15, 2022
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

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 an expression and the enclosing
   constraint agree in which direction this should be done.
odersky added a commit to dotty-staging/dotty that referenced this issue Sep 19, 2022
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

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 an expression and the enclosing
   constraint agree in which direction this should be done.
odersky added a commit to dotty-staging/dotty that referenced this issue Sep 20, 2022
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

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 an expression and the enclosing
   constraint agree in which direction this should be done.
odersky added a commit to dotty-staging/dotty that referenced this issue Sep 24, 2022
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
odersky added a commit to dotty-staging/dotty that referenced this issue Oct 30, 2022
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
odersky added a commit that referenced this issue Oct 31, 2022
…6042)

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.
little-inferno pushed a commit to little-inferno/dotty that referenced this issue Jan 25, 2023
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
@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
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants