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

Handle recursions in isFullyDefined #15443

Merged
merged 1 commit into from
Jun 15, 2022
Merged

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Jun 14, 2022

If one indulges in cycles too heavily it can happen that we run into
an infinite recursion in fullyDefinedType even before we had a chance
to run checkNonCyclic. In this case we now diagnose the stack overflow
as an error message instead of failing with an internal error.

Fixes #15311

If one indulges in cycles to heavily it can happen that we run into
an infinite recursion in `fullyDefinedType` even before we had a chance
to run `checkNonCyclic`. In this case we now diagnose the stack overflow
as an error message instead of fialing with an internal error.

Fixes scala#15311
@smarter
Copy link
Member

smarter commented Jun 14, 2022

Note that the original example can be turned into an infinite loop by replacing type Clone = by type Clone <::

trait Template[+T <: Template[T]]:
   type Clone <: T { type Clone <: Template.this.Clone }
   val self :Clone

type Food = Template[_]

class Ham extends Template[Ham]:
   type Clone = Ham
   val self = this

def eat[F <: Template[F]](food :F) :F = food.self.self

val ham = new Ham
val food :Food = ham

def test = 
  eat(ham)
  eat(food.self)

@odersky
Copy link
Contributor Author

odersky commented Jun 14, 2022

It's not an infinite loop, it just takes a minute or two to spit out the "Recursion limit exceeded" error.

@odersky
Copy link
Contributor Author

odersky commented Jun 15, 2022

How I fixed it:

That one took some time to diagnose the root cause. The error message was

java.lang.Error: internal error: type of right-hand side Playground.Template[Playground.food.T] is not fully defined, pos = [929..933..952]

I printed the type with -Yprint-debug, and then by displaying its toString directly at the site where the error occurred. But it looked OK, just a TypeRef. There was no TypeVar or WildcardType in that type that would have caused it to be not fully defined. After a lot more instrumentation I finally got to the root cause. In Inferencing.isFullyDefined:

    val result =
      try new IsFullyDefinedAccumulator(force)(using nestedCtx).process(tp)
      catch case ex: StackOverflowError =>
        false

Indeed there was a stack overflow. That caused isFullyDefined to return false, which caused the error.
Now, this is very old code. We now have better means to deal with stack overflow, using handleRecursive.
I added that to the apply method of the IsFullyDefinedAccumulator. The problem was how to recover from it in a way that's compatible with the way isFullyDefined is used. Normally handleRecursive will just throw
a RecursionOverflow exception, which is a subclass of TypeError. TypeErrors are reported in a summary manner at key points of type checking by catching that exception. In our case, we had to convert a RecursionOverflow back to a false result in isFullyDefined because that is its contract, but had to report it in the caller fullyDefinedType instead of throwing the previous Error.

That presented another problem: fullyDefinedType just got a span as a parameter instead of a full source position. That was done for performance, source positions are objects and creating them in operations like fullyDefinedType that are called very often incurs a cost. But since fullyDefinedType was written, we now have a cheap way to generate source positions using SrcPos (it's worthwhile to look that up to see how it is done!). So one could have kept the old signature and just converted a span to a source position using ctx.source.atSpan(span) in fullyDefinedType. But I thought it was more idiomatic to use SrcPos instead of Span since we do that almost everywhere else. That caused quite a few fixes all over the code base in the calls to fullyDefinedType.

@smarter smarter merged commit 004a2fd into scala:main Jun 15, 2022
@smarter smarter deleted the fix-15311 branch June 15, 2022 21:15
@@ -28,11 +28,11 @@ object Inferencing {
* but only if the overall result of `isFullyDefined` is `true`.
* Variables that are successfully minimized do not count as uninstantiated.
*/
def isFullyDefined(tp: Type, force: ForceDegree.Value)(using Context): Boolean = {
def isFullyDefined(tp: Type, force: ForceDegree.Value, handleOverflow: Boolean = false)(using Context): Boolean = {
Copy link
Member

Choose a reason for hiding this comment

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

I just re-read the diff and the "how I fixed it" comment (because I'm looking at handleRecursive and trace) and shouldn't the default here be true and fullDefinedType pass true so that it can do it's special handling? My assumption is that we would want all other calls of isFullyDefined to continue to return false instead of throwing.

Or are do we want to throw because isFullyDefined is call transitively from IsFullyDefinedAccumulator and we want those handled by the handleRecursive you added? My point is handleOverflow seems to never be non-default, was that for future possible uses?

@Kordyjan Kordyjan added this to the 3.2.1 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 this pull request may close these issues.

java.lang.Error when compiling code using a value and a pdt defined as a wildcard type argument
5 participants