-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix #4368: Avoid stackoverflows for some cyclic definitions #4385
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello, and thank you for opening this PR! 🎉
All contributors have signed the CLA, thank you! ❤️
Commit Messages
We want to keep history, but for that to actually be useful we have
some rules on how to format our commit messages (relevant xkcd).
Please stick to these guidelines for commit messages:
- Separate subject from body with a blank line
- When fixing an issue, start your commit message with
Fix #<ISSUE-NBR>:
- Limit the subject line to 72 characters
- Capitalize the subject line
- Do not end the subject line with a period
- Use the imperative mood in the subject line ("Add" instead of "Added")
- Wrap the body at 80 characters
- Use the body to explain what and why vs. how
adapted from https://chris.beams.io/posts/git-commit
Have an awesome day! ☀️
test performance please |
performance test scheduled: 2 job(s) in queue, 1 running. |
try go(this) | ||
catch { | ||
case ex: Throwable => | ||
core.println(i"findMember exception for $this member $name, pre = $pre") | ||
throw ex // DEBUG | ||
core.println(s"findMember exception for $this member $name, pre = $pre, recCount = $recCount") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: why change formatter to s
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid possible crashes when trying to show pre
.
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/4385/ to see the changes. Benchmarks is based on merging with master (d73d9bc) |
Cool that you're looking at this! I suppose this also fixes #318, and that you already looked at Samuel's proposals from back then and they don't work? |
Yes, #318 is handled, albeit crudely. |
test performance please |
performance test scheduled: 1 job(s) in queue, 0 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/4385/ to see the changes. Benchmarks is based on merging with master (d73d9bc) |
So, this is a rather sneaky way to fix a whole class of problems that cause the compiler to stackoverflow in some way or another. One would hope there was a principled way to detect and repair all illegal cyclic references before they cause stackoverflows. Unfortunately, that looks impossible or at least impossibly hard: Scala's type system subsumes F_sub and is therefore undecidable. So some infinite loops/stackoverflows are unavoidable. What's more, checking for cycles early has the unfortunate consequence that we force types and thereby can cause spurious cyclic references! So the mere act of checking generates spurious errors. This is a real problem in practice, observed in both the compiler codebase and stdlib. Another possibility would be to attach a cycle detection component to every operation that might stackoverflow. But these tend to be also operations that are most time-critical, so doing the extra work is prohibitive. The idea in this pull request is to act only if there is a stackoverflow. In that case we unroll the stack until we hit a safepoint (typically: a point where we typecheck an AST node and therefore have an error position to record). While unrolling we keep a count of all important method frames that were unrolled. Important means: the method contains a try/catch that does the unrolling and counting. At the safepoint, we check which method appeared most often in the unrolled frames. We then compose a nice error message showing only calls info for that method and leaving out the middle of the stack if it is too large. A typical error message looks like this:
Of course, it's still nicer to pinpoint the cycle where it arises, and we try to do this also, as long as it can be done safely and efficiently. But we have transformed the game from a bug that needs fixing, (a stackoverflow is a compiler crash, so it's wrong and embarrassing) to a quality of error message question. Such a question can be answered on a case-by-case basis, taking into account the frequency of the error (in my experience most of these cyclic reference errors causing infinite loops are synthetic and don't tend to occur in practice) and the effort needed to get a more specific error message. |
test performance please |
performance test scheduled: 1 job(s) in queue, 0 running. |
test performance please |
performance test scheduled: 3 job(s) in queue, 1 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/4385/ to see the changes. Benchmarks is based on merging with master (d73d9bc) |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/4385/ to see the changes. Benchmarks is based on merging with master (1e9430a) |
While fuzzing #4489 I found these two samples that produce rather unintelligible error messages: trait I0 {
type Dynamic <: Any with i1
trait i1
type I2 >: i1
type I3 <: I0 {
type i1 <: I2;
val I4: I2; }
def I5(implicit I5: => I5.I6): Unit = {}
}
class I5 {
assert(I3(, I2 == I5.i1))
}
}
}
} produces 5 |type I3 <: i0 {
| ^
|Recursion limit exceeded.
|Maybe there is an illegal cyclic reference?
|If that's not the case, you could also try to increase the stacksize using the -Xss JVM option.
|A recurring operation is (inner to outer):
|
| subtype Object{...} <:< [cannot display due to dotty.tools.dotc.core.RecursionOverflow: , raw string = TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Nothing),TypeRef(RecThis(3689420),I2))]
| subtype Object{...} <:< [cannot display due to dotty.tools.dotc.core.RecursionOverflow: , raw string = TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Nothing),TypeRef(RecThis(3689420),I2))]
| subtype Object{...} <:< [cannot display due to dotty.tools.dotc.core.RecursionOverflow: , raw string = TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Nothing),TypeRef(RecThis(3689420),I2))]
| subtype Object{...} <:< [cannot display due to dotty.tools.dotc.core.RecursionOverflow: , raw string = TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Nothing),TypeRef(RecThis(3689420),I2))] and object i0 {
def I1(i4: Any) = i4 match {
case _: Some[String] => case _ => println()
case _ => println
class i0 {
def I1[i4](i4: i4): i4 = ???
type i4[i4] = I1[i4]
type i4 = i4[i4]
type i4 = String
val i4: i4[i4]
def I1(I1: i4[Int]) = i4
val I3 = I1 _
val I1 = i4
val i4 = I1
val I1 = new i4
val I1 = i4
val I2 = I1
val I3 = I3
val i4 = 1
val i4 = I1
def I1[i4](i4: i4[i4]): I1[I3] = ???
val I2 = i4[String](1)
val i4: i4[I1] = ???
implicit def I1[i4](I2: I1)(implicit i4: i4[I2], I1: i4[I1]): Unit = {}
class I1 extends i4[i4] with i4[i4, I1] with i4[I1, I1] with i0[I1, i4];
trait i4 extends I1 {
type i4 <: i0[I1]
implicit val i4: I1[i4] = new i4[I1] {
type i4[I1] = i4[I1]
}
def I2[I3](I1: i4[I1]): i4[i4] = new I1[i4](I1)
val I2 = new i4[Int]
} produces 22 |val I2 = i3[String](1)
| ^^
|Recursion limit exceeded.
|Maybe there is an illegal cyclic reference?
|If that's not the case, you could also try to increase the stacksize using the -Xss JVM option.
|A recurring operation is (inner to outer):
|
| subtype i0.this.i3[<error Type argument i0.this.i3 has not the same kind as its bound >](i0.this.i3) <:< PolyProto(String): FunProto(1):?
| subtype i0(i0.this) <:< ?{ i3: PolyProto(String): FunProto(1):? } |
I won't invest more time in this. So, ready to get this in? |
Hm. Especially since But otherwise, it seems reasonable to classify those as requests for enhancements. I'll have to look at the first more closely, because the output contradicts quite a few of my expectations and one of the two must be corrected. |
I think it'd be good to have a -Y setting to turn off the exception catching, so that the root cause of issues can be investigated more easily. |
@smarter The recommended way to find out more is to turn the |
@odersky The issue is that by turning everything into errors, @alexknvl fuzzer will never detect any stackoverflow again, even though some of them might be legitimate bugs we can fix. |
I plan to implement that flag. I've started playing more with this, running a few experiments and talking with @allanrenucci and @smarter — I have a few examples where stack overflows disappear altogether from the output, such as: class i0 {
class i1 { type i2 }
type i3 = i1.i2 // error, comment this out to _see_ the stack overflow
type i4 = i0 { type i1 <: i4 } // stack overflow, hidden by the above error
} but I have pushed fixes for that. EDIT: gotta review the few extra errors from testcases, but they seem generally reasonable. I'll probably make that a separate PR. |
@@ -26,7 +26,7 @@ trait UniqueMessagePositions extends Reporter { | |||
case _ => positions((ctx.source, pos)) = m.level | |||
} | |||
} | |||
shouldHide | |||
shouldHide && !ctx.settings.YshowSuppressedErrors.value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it would make sense to test this first
- Move them all to TypeErrors.scala - Systematically use `toMessage` for reporting - Catch stack overflows instead of counting recursions
This now handles all errors from scala#4368 to scala#4372 and also scala#318.
Not needed here, but I missed the functionality elsewhere.
Once we are at it, let's try to bullet-proof everything.
Move it from `transformNode` to `goNamed` and `goUnnamed`. This might be faster since that way `transformNode` should be an inline candidate since it is short.
Thanks `@smarter` for guidance.
As a followup to the earlier bugfix.
Ensures stackoverflows aren't hidden by errors, without needing scala#4511. Also add relevant testcase from scala#4385 (comment); I confirmed this fails without the position fix.
@smarter: can you take a look? This can now be disabled, and the remaining issues here (in comments above) don't seem blockers, so I think this should be mergeable. |
private def showType(tp: Type)(implicit ctx: Context) = tp match { | ||
case ClassInfo(_, cls, _, _, _) => cls.showLocated | ||
case _ => tp.show |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this refactoring intentionally drop the case for type bounds here (search and compare with the removed showType
)? But guess yes, the old output wasn’t ideal either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise LGTM
@@ -146,6 +146,7 @@ class ScalaSettings extends Settings.SettingGroup { | |||
val Xlink = BooleanSetting("-Xlink", "Recompile library code with the application.") | |||
val YoptPhases = PhasesSetting("-Yopt-phases", "Restrict the optimisation phases to execute under -optimise.") | |||
val YoptFuel = IntSetting("-Yopt-fuel", "Maximum number of optimisations performed under -optimise.", -1) | |||
val YnoDecodeStacktraces = BooleanSetting("-Yno-decode-stacktraces", "Show operations that triggered StackOverflows instead of printing the raw stacktraces.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment is misleading. How I understand it: "if you pass the flag, you won't see original stack traces". I guess it is the opposite
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh right, fixing it
Probably fixed by scala#4385
We use a two-pronged approach:
First, we now detect some cycles that span inherited or intersected classes or traits. Unfortunately, this operation forces the types of type members, which means it can lead to spurious cycles itself. So we can do this only after a lot of types are already computed, which means some cycles lead to stackoverflows before they are detected.
Hence the second strategy: give more useful information when deep recursions happen.