Skip to content

Replace catching StackOverflowError with fuel - #25937

Merged
SolalPirelli merged 12 commits into
scala:mainfrom
dotty-staging:solal/fuel
Aug 13, 2026
Merged

SolalPirelli merged 12 commits into
scala:mainfrom
dotty-staging:solal/fuel

Conversation

@SolalPirelli

@SolalPirelli SolalPirelli commented Apr 27, 2026 •

Copy link
Copy Markdown
Contributor

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)

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#

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

avoids a stack overflow; this test was introduced purely as a stress test in d3ac612 and the other elements of it were already reduced in 0ee31f4 so I think removing this line is fine

@SolalPirelli
SolalPirelli force-pushed the solal/fuel branch 2 times, most recently from 958aabd to 1e2b230 Compare April 29, 2026 11:17
@SolalPirelli SolalPirelli changed the title no-review: Fuel Replace catching StackOverflowError with fuel Apr 29, 2026
Comment thread compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala Outdated
Comment thread compiler/src/dotty/tools/dotc/core/SymDenotations.scala
@SolalPirelli
SolalPirelli force-pushed the solal/fuel branch 3 times, most recently from e3ff246 to 44aa990 Compare May 18, 2026 13:00
@SolalPirelli
SolalPirelli requested review from mbovel and sjrd May 18, 2026 14:23
@mbovel

This comment was marked as outdated.

@mbovel

mbovel commented May 20, 2026

Copy link
Copy Markdown
Member

Benchmarks completed. Overview.

Comment thread compiler/src/dotty/tools/dotc/core/Contexts.scala Outdated

/** 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)

@bishabosha bishabosha May 20, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

or tune the level of tracing captured via settings?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

@mbovel

This comment was marked as outdated.

@mbovel

mbovel commented May 21, 2026

Copy link
Copy Markdown
Member

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this changed message allows the details to be just an object instead of having to be computed to add a string afterwards

@SolalPirelli
SolalPirelli marked this pull request as ready for review May 26, 2026 13:56
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 =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

the weight is only used in one place, not sure if it's that useful

@mbovel

mbovel commented May 29, 2026

Copy link
Copy Markdown
Member

Benchmarks started. Workflow run.

@@ -0,0 +1,20 @@
//> using options -Xmax-inlines:100000

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this is #26159

Comment thread tests/neg/i20317a.scala
def combineAll[A <: SemigroupStructural[A]](
i: A, l: List[A] // error
): A = l.foldLeft(i)(_.combine(_)) // error
): A = l.foldLeft(i)(_.combine(_))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@SolalPirelli SolalPirelli added needs-squashing PR whose commits should be squashed by the author or via the "Squash and Merge" button release-notes Should be mentioned in the release notes labels Aug 11, 2026

@jchyb jchyb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks great, I just have a few questions for parts I don't understand yet

implicit val testGroup: TestGroup = TestGroup("compileNeg")

aggregateTests(
withCoverage(aggregateTests(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

withCoverage? Does it expose any recursion overflows, or is it an unrelated change (probably fine either way, just wondering)?

@SolalPirelli SolalPirelli Aug 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It filters using the scoverage exclude list when running under coverage; there's one neg test I had to add in there

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

@SolalPirelli SolalPirelli Aug 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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. :(

@anatoliykmetyuk

anatoliykmetyuk commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

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.

SolalPirelli pushed a commit that referenced this pull request Aug 25, 2026
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
@WojciechMazur WojciechMazur added this to the 3.10.0 milestone Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-squashing PR whose commits should be squashed by the author or via the "Squash and Merge" button release-notes Should be mentioned in the release notes

Projects

None yet

8 participants