Skip to content

Also retain runtime-invokable inline methods when they are indirectly overriden - #26644

Open
jchyb wants to merge 2 commits into
scala:mainfrom
dotty-staging:fix-i25206-indirect-inline-override
Open

Also retain runtime-invokable inline methods when they are indirectly overriden#26644
jchyb wants to merge 2 commits into
scala:mainfrom
dotty-staging:fix-i25206-indirect-inline-override

Conversation

@jchyb

@jchyb jchyb commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Fixes #25206

As described in the reference (and in the Inlining paper), we allow inline methods to override abstract ones (treating those inline methods as effectively final). we do that by checking if the inline method overrides something from the perspective of it's own class, creating a $retainedBody method that holds the inlined implementation. This $retainedBody then replaces the inline method in erasure (since inline methods are usually completely erased there). The issue arises with cases where we indirectly override something, like in the issue reproducer:

trait Settings:
  inline def switch: Boolean = true

trait Inner:
  def switch: Boolean
  def go2: String = if switch then "Yes" else "No"

object Outer extends Inner, Settings

Here, the switch from Settings overrides switch from Inner, where we don't create a retainedBody in Settings, thus completely erasing switch, causing a runtime error.

We fix this by separately recognizing this case and adding a similar switch$indirectRetainedBody method in Outer, later replaced with the actual switch in erasure. So we effectively copy the implementation into the class where the issue can appear (similarly to how inline traits work in #26156, where I got the idea from). We cannot retroactively put it in Settings, as under separate compilation, by the time we compile Outer, Settings might have had been already compiled to a classfile, so there is no other solution possible here.

Some additional considerations:

  • I've made the $indirectRetainedBody definition procedure purposefully similar to $retainedBody, to avoid new issues - this is also why it's in Typer, theoretically it could have been in any phase before Inlining, but then we would have $indirectRetainedBody and $retainedBody inlined in different phases, which seems like a recipe for confusion.
  • Despite the above, we don't really need this method to be pickled. But it should harm us in any way, or introduce any incompatibilities between versions (and is still backportable), so for simplicity's sake I don't check for it in pickler or anything, it's pickled as usual.
  • I don't want to mess with TASTy and want this to be backportable, so I don't use add any tagged SuffixNameKind
  • Only the "topmost" class where the issue appears gets the copy - they aren't duplicated, e.g., in:
trait Settings:
  inline def switch: Boolean = true

trait Inner:
  def switch: Boolean
  def go2: String = if switch then "Yes" else "No"

class Outer extends Inner, Settings
class OuterOuter

only Outer gets the runtime-invoked inlined switch copy.

Have you relied on LLM-based tools in this contribution?

Yes, mostly for refactors (move X into Y phase and querying about the compiler), and I checked the output by manually reading through the code and confirming it matched what I wanted.

How was the solution tested?

New automated tests (including the issue's reproducer, if applicable)

@jchyb jchyb changed the title Fix i25206 indirect inline override Also retain runtime-invokable inline methods when they are indirectly overriden Jul 28, 2026
@jchyb
jchyb force-pushed the fix-i25206-indirect-inline-override branch from c673c6d to 0e45f9e Compare July 29, 2026 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inline def implementation in mixin results in java.lang.AbstractMethodError

1 participant