Replace catching StackOverflowError with fuel - #25937
Conversation
| T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# | ||
| T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# | ||
| T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# | ||
| T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# |
958aabd to
1e2b230
Compare
e3ff246 to
44aa990
Compare
This comment was marked as outdated.
This comment was marked as outdated.
|
Benchmarks completed. Overview. |
|
|
||
| /** Ensures recursive operations obey the fuel limit, and throws user-friendly errors when they do not. */ | ||
| inline final def handleRecursive[T](name: String, details: => String, weight: Int = 1)(inline block: T): T = | ||
| val op = RecursiveOperation(name, details, weight) |
There was a problem hiding this comment.
also isnt this in the past this metadata only needed for the "outermost" operation, so perhaps another optimisation (rather than capturing a whole trace even in happy path)
There was a problem hiding this comment.
or tune the level of tracing captured via settings?
There was a problem hiding this comment.
we need the entire trace if something goes wrong, so we can show a message with the most frequent operations etc
but yeah the problem is fundamentally since we can't catch SOs we have to store info even in the happy path; I'll look at ways to lower the impact of this
There was a problem hiding this comment.
given that you actually still use the stack to recurse, cant you only construct the metadata when you throw the exception and then catch the exception from each outer layer to add its trace in and rethrow?
There was a problem hiding this comment.
yeah, I'm a little allergic to using exceptions for control flow, and it might negate the perf gains of not building the metadata, but we'll see if this latest benchmark run isn't good enough
This comment was marked as outdated.
This comment was marked as outdated.
|
Benchmarks completed. Overview. |
|
|
||
| /** Check type members inherited from different `parents` of `joint` type for cycles, | ||
| * unless a type with the same name already appears in `decls`. | ||
| * @return true iff no cycles were detected |
There was a problem hiding this comment.
unused return value (was already Unit, I guess it used to be a Boolean), noticed while working on this
| |For the unprocessed stack overflow trace, compile with -Xno-enrich-error-messages. | ||
| |A recurring operation is (inner to outer): | ||
| | | ||
| | reduce type t match ... |
There was a problem hiding this comment.
this changed message allows the details to be just an object instead of having to be computed to add a string afterwards
| withTyperState(typerState.uncommittedAncestor) | ||
|
|
||
| /** Ensures recursive operations obey the fuel limit, and throws user-friendly errors when they do not. */ | ||
| final inline def handleRecursive[T](title: String, details: RecursiveOperationDetails, weight: Int = 1)(inline block: T): T = |
There was a problem hiding this comment.
the weight is only used in one place, not sure if it's that useful
694009c to
f1b787e
Compare
|
Benchmarks started. Workflow run. |
| @@ -0,0 +1,20 @@ | |||
| //> using options -Xmax-inlines:100000 | |||
| def combineAll[A <: SemigroupStructural[A]]( | ||
| i: A, l: List[A] // error | ||
| ): A = l.foldLeft(i)(_.combine(_)) // error | ||
| ): A = l.foldLeft(i)(_.combine(_)) |
There was a problem hiding this comment.
Since we stop at the first potentially-infinite recursion in order to make sure we have enough stack to display nice error messages (we need to unwind first), some errors no longer appear. I split tests that were testing multiple independent infinite recursions into multiple tests.
jchyb
left a comment
There was a problem hiding this comment.
Looks great, I just have a few questions for parts I don't understand yet
| implicit val testGroup: TestGroup = TestGroup("compileNeg") | ||
|
|
||
| aggregateTests( | ||
| withCoverage(aggregateTests( |
There was a problem hiding this comment.
withCoverage? Does it expose any recursion overflows, or is it an unrelated change (probably fine either way, just wondering)?
There was a problem hiding this comment.
It filters using the scoverage exclude list when running under coverage; there's one neg test I had to add in there
There was a problem hiding this comment.
This is more of a bugfix for the testing harness, since all testing kinds should be going through withCoverage already. (I agree the name is... dubious. It doesn't actually run anything with coverage, it just does stuff if coverage is enabled)
There was a problem hiding this comment.
ah, that makes sense, thank you!
| try | ||
| if isFullyDefined(tp, ForceDegree.all) then tp | ||
| else throw new Error(i"internal error: type of $what $tp is not fully defined, pos = $pos") | ||
| catch case ex: RecursionOverflow => |
There was a problem hiding this comment.
Now that the RecursionOverflow exception is safe (and not tied to any undefined behaviors), it feels weird that we only catch it and report in Driver, only reporting the first encountered one, where previously we were able to report multiple different ones. Is there some specific reason for that? I see there are handlers added in typedUnadapted and other places, where there weren't any before - is it related to that?
There was a problem hiding this comment.
Catching RecursionOverflow outside of the Driver means you probably don't have that much stack left yourself, and in the worst case, you can't even properly print what caused the recursion overflow because the printer can use a fair amount of stack. :(
b271fa1 to
e40f0fd
Compare
…ave enough stack to display
e40f0fd to
c845bcf
Compare
|
The first Scoverage nightly after this merged exposed coverage failures in 16463.scala, i10605.scala, i15158.scala, i15311.scala, i21015.scala, i2887b.scala, i5877.scala, matchtype-loop2.scala, recursive-lower-constraint.scala, and i20516.scala. The first nine stopped producing scoverage.coverage; i20516.scala exceeded the recursion limit. See the failing CI job. These tests are temporarily quarantined in #26894. |
Fixes scoverage CI breakage by recent PRs. Also re-enable scoverage tests to run on every PR. The coverage invocation now runs only test suites explicitly marked as supporting coverage instrumentation, instead of also executing ordinary uninstrumented suites. It seems that the policy to only run scoverage tests nightly, intended to reduce friction on new semantic compiler changes, does not work so well, as the breaking changes fall between the cracks, ending up in the specialized Nightly CI Scoverage workflow. In August alone, the following were the breaking PRs for scoverage: - #26697 - #25937 - #26156 - #26813 Therefore, to prevent scoverage from drifting overtime, I believe running scoverage tests on CI should be done on each PR, while compiling subprojects with scoverage can stay in Nightly build. ## Have you relied on LLM-based tools in this contribution? Yes, and I checked the output by local testing, review, running CI (as this is the CI change). ## How was the solution tested? Non-code change, no tests needed
Part of #25799
Fixes #26159
Fixes #25724
Fixes #26458
Fixes #25718
Fixes #24683
Some neg tests no longer fail; all of them were failing with infinite recursions, I checked
Benchmarks within noise: https://lampepfl.github.io/scala3-benchmarks/#compare/3.9.0-RC1-bin-20260518-0ce4c47-NIGHTLY,3.9.0-RC1-bin-a8f3042310c66f22322ed9743c00872e8c6c5d21-BENCH
How much have you relied on LLM-based tools in this contribution?
Not at all
How was the solution tested?
Covered by existing tests (this is a refactoring)