From baa24b20459eb9612903e886e6dfe95b91b3e82a Mon Sep 17 00:00:00 2001 From: Jan Chyb <48855024+jchyb@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:32:29 +0200 Subject: [PATCH 1/2] Initial fix --- .../dotty/tools/dotc/typer/RefChecks.scala | 121 ++++++++++++++---- .../backend/jvm/InlineBytecodeTests.scala | 34 +++++ .../_docs/reference/metaprogramming/inline.md | 16 +++ tests/run/25206.check | 3 + tests/run/25206.scala | 17 +++ 5 files changed, 163 insertions(+), 28 deletions(-) create mode 100644 tests/run/25206.check create mode 100644 tests/run/25206.scala diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 2940135f53ed..fa74dae219c0 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -11,6 +11,7 @@ import util.Spans.* import scala.collection.{mutable, immutable} import ast.* import MegaPhase.* +import inlines.Inlines import config.Printers.{checks, noPrinter, capt} import Decorators.* import OverridingPairs.isOverridingPair @@ -282,6 +283,33 @@ object RefChecks { def checkInheritedTraitParameters: Boolean = true end OverridingPairsChecker + def implementationOf(clazz: ClassSymbol, mbr: Symbol)(using Context): PreDenotation = + val mbrDenot = mbr.asSeenFrom(clazz.thisType) + def isConcrete(sym: Symbol) = sym.exists && !sym.isOneOf(NotConcrete) + clazz.nonPrivateMembersNamed(mbr.name) + .filterWithPredicate( + impl => isConcrete(impl.symbol) + && withMode(Mode.IgnoreCaptures)(mbrDenot.matchesLoosely(impl, alwaysCompareTypes = true))) + + def hasJavaErasedOverriding(clazz: ClassSymbol, sym: Symbol)(using Context): Boolean = + !erasurePhase.exists || // can't do the test, assume the best + atPhase(erasurePhase.next) { + clazz.info.nonPrivateMember(sym.name).hasAltWith { alt => + alt.symbol.is(JavaDefined, butNot = Deferred) && + !sym.owner.derivesFrom(alt.symbol.owner) && + alt.matches(sym) + } + } + + def ignoreDeferred(clazz: ClassSymbol, mbr: Symbol)(using Context): Boolean = + mbr.isType + || mbr.isSuperAccessor // not yet synthesized + || mbr.is(JavaDefined) && hasJavaErasedOverriding(clazz, mbr) + || mbr.is(Tracked) + // Tracked members correspond to existing val parameters, so they don't + // count as deferred. The val parameter could not implement the tracked + // refinement since it usually has a wider type. + /** 1. Check all members of class `clazz` for overriding conditions. * That is for overriding member M and overridden member O: * @@ -687,33 +715,9 @@ object RefChecks { else abstractErrors += msg } - def hasJavaErasedOverriding(sym: Symbol): Boolean = - !erasurePhase.exists || // can't do the test, assume the best - atPhase(erasurePhase.next) { - clazz.info.nonPrivateMember(sym.name).hasAltWith { alt => - alt.symbol.is(JavaDefined, butNot = Deferred) && - !sym.owner.derivesFrom(alt.symbol.owner) && - alt.matches(sym) - } - } + def ignoreDeferred(mbr: Symbol): Boolean = RefChecks.ignoreDeferred(clazz, mbr) - def ignoreDeferred(mbr: Symbol) = - mbr.isType - || mbr.isSuperAccessor // not yet synthesized - || mbr.is(JavaDefined) && hasJavaErasedOverriding(mbr) - || mbr.is(Tracked) - // Tracked members correspond to existing val parameters, so they don't - // count as deferred. The val parameter could not implement the tracked - // refinement since it usually has a wider type. - - def isImplemented(mbr: Symbol) = - val mbrDenot = mbr.asSeenFrom(clazz.thisType) - def isConcrete(sym: Symbol) = sym.exists && !sym.isOneOf(NotConcrete) - clazz.nonPrivateMembersNamed(mbr.name) - .filterWithPredicate( - impl => isConcrete(impl.symbol) - && withMode(Mode.IgnoreCaptures)(mbrDenot.matchesLoosely(impl, alwaysCompareTypes = true))) - .exists + def isImplemented(mbr: Symbol) = implementationOf(clazz, mbr).exists /** Filter out symbols from `syms` that are overridden by a symbol appearing later in the list. * Symbols that are not overridden are kept. */ @@ -1434,6 +1438,61 @@ object RefChecks { } } + /** For every abstract member `sym` of `clazz` that is only implemented indirectly by an inline + * method `impl` from an unrelated base class, synthesize a concrete override of `sym` in `clazz` + * whose body inline-expands a call to `impl`. + * Example case: + * + * ```scala + * trait Settings: + * inline def switch: Boolean = true + * + * trait Inner: + * def switch: Boolean + * def go: String = if switch then "Yes" else "No" + * + * object Outer extends Inner, Settings + * ``` + * + * Without this, `switch`'s inline body is erased entirely (it doesn't override anything from `Settings`'s own + * point of view), leaving `Outer` without any real implementation of `switch` and causing an + * `AbstractMethodError` at runtime. + */ + private def copyIndirectlyOverriddenInlineMethods(clazz: ClassSymbol, tree: Template)(using Context) = { + def retainedOverrideFor(clazz: ClassSymbol, sym: Symbol, impl: Symbol)(using Context): DefDef = + val newSym = impl.asTerm.copy( + owner = clazz, + name = sym.name.asTermName, + flags = (impl.flags &~ (Inline | Macro | Override | AbsOverride | Protected | Private)) + | Override | (sym.flags & Protected), + info = sym.info.asSeenFrom(clazz.thisType, sym.owner), + privateWithin = sym.privateWithin, + coord = clazz.coord + ).asTerm.entered + val result = DefDef(newSym, prefss => + atPhase(inliningPhase): + Inlines.inlineCall(This(clazz).select(impl).appliedToArgss(prefss).withSpan(clazz.span))(using ctx.withOwner(newSym))) + println(i"""--- retainedOverrideFor($clazz, $sym, $impl): + |${result.show} + |--- (raw tree) --- + |${result.toString}""") + result + + val indirectInlineOverrides = + for + bc <- clazz.baseClasses + sym <- bc.info.decls.toList + if sym.is(DeferredTerm) && !ignoreDeferred(clazz, sym) + impl = implementationOf(clazz, sym).toDenot(clazz.thisType).symbol + if impl.exists + && !impl.owner.flags.is(Flags.Inline) // let's not interfere with how inline traits handle things + && !impl.allOverriddenSymbols.contains(sym) + && impl.isInlineMethod + yield retainedOverrideFor(clazz, sym, impl) + + if indirectInlineOverrides.isEmpty then tree + else cpy.Template(tree)(body = tree.body ++ indirectInlineOverrides) + } } import RefChecks.* @@ -1464,7 +1523,13 @@ import RefChecks.* * Unlike in Scala 2.x not-private members keep their name. It is * up to the backend to find a unique expanded name for them. The * rationale to do name changes that late is that they are very fragile. - + * + * 5. It copies the implementation of inline methods that only indirectly override + * an abstract member (i.e. only when combined with an unrelated base class/trait in some other + * class), since such methods are otherwise erased entirely, leaving no implementation in the + * classfile. The implementation is added to the first class in which the indirect + * override becomes detectable. + * * todo: But RefChecks is not done yet. It's still a somewhat dirty port from the Scala 2 version. * todo: move untrivial logic to their own mini-phases */ @@ -1520,7 +1585,7 @@ class RefChecks extends MiniPhase { thisPhase => checkCompanionNameClashes(cls) checkAllOverrides(cls) checkImplicitNotFoundAnnotation.template(cls.classDenot) - tree + copyIndirectlyOverriddenInlineMethods(cls, tree) } catch { case ex: TypeError => report.error(ex, tree.srcPos) diff --git a/compiler/test/dotty/tools/backend/jvm/InlineBytecodeTests.scala b/compiler/test/dotty/tools/backend/jvm/InlineBytecodeTests.scala index bf348215c7fe..a28467f6d5d7 100644 --- a/compiler/test/dotty/tools/backend/jvm/InlineBytecodeTests.scala +++ b/compiler/test/dotty/tools/backend/jvm/InlineBytecodeTests.scala @@ -756,4 +756,38 @@ class InlineBytecodeTests extends DottyBytecodeTest { } } + @Test def i25091_indirectInlineOverride = { + val source = """trait Settings: + | inline def switch: Boolean = true + | + |trait Inner: + | def switch: Boolean + | inline def go1: String = inline if switch then "Yes" else "No" + | def go2: String = if switch then "Yes" else "No" + | + |class Outer extends Inner, Settings + | + |class Outer2 extends Outer + """.stripMargin + + checkBCode(source) { dir => + val cls = lookupClass(dir, "Outer.class") + val clsNode = loadClassNode(cls) + + val switchMethod = getMethod(clsNode, "switch") + val instructions = instructionsFromMethod(switchMethod) + val expected = List(Op(ICONST_1), Op(IRETURN)) + + assert(instructions == expected, + "indirectly overriding inline method `switch` was not retained with a concrete body in `Outer`\n" + + diffInstructions(instructions, expected)) + + val cls2 = lookupClass(dir, "Outer2.class") + val cls2Node = loadClassNode(cls2) + val switchInOuter2 = cls2Node.methods.asScala.find(_.name == "switch") + + assert(switchInOuter2.isEmpty, "method `switch` should not have been redundantly copied to `Outer2`") + } + } + } diff --git a/docs/_docs/reference/metaprogramming/inline.md b/docs/_docs/reference/metaprogramming/inline.md index ff975a3c87e6..3280ebddb929 100644 --- a/docs/_docs/reference/metaprogramming/inline.md +++ b/docs/_docs/reference/metaprogramming/inline.md @@ -191,6 +191,22 @@ Inline methods can override other non-inline methods. The rules are as follows: val a: A = B a.f // error: cannot inline f in A. ``` +4. Indirectly overridden inline methods, like: + ```scala + trait Settings: + inline def switch: Boolean = true + + trait Inner: + def switch: Boolean + def go: String = if switch then "Yes" else "No" + + object Outer extends Inner, Settings + ``` + are copied to the first class that is aware of the inline method at the classfile level. + This is because: + * inline defs that are not overriding any method at the point of implementation are erased. + * we might only be able to detect indirect overrides after the initial classlike with the inline method is already compiled (so we can't un-erase it then, and we have to include some reference to it in the classfile). + ### Relationship to `@inline` diff --git a/tests/run/25206.check b/tests/run/25206.check new file mode 100644 index 000000000000..e6ef6beaf17d --- /dev/null +++ b/tests/run/25206.check @@ -0,0 +1,3 @@ +Yes +Yes +true diff --git a/tests/run/25206.scala b/tests/run/25206.scala new file mode 100644 index 000000000000..b63b74d8563d --- /dev/null +++ b/tests/run/25206.scala @@ -0,0 +1,17 @@ +trait Settings: + inline def switch: Boolean = true + +trait Inner: + def switch: Boolean + inline def go1: String = inline if switch then "Yes" else "No" + def go2: String = if switch then "Yes" else "No" + +object Outer extends Inner, Settings: + def test1 = go1 + def test2 = go2 + +@main def main(): Unit = + println(Outer.test1) + println(Outer.test2) + val a: Inner = Outer + println(a.switch) From 0e45f9e2411a960e4a4843feb1bd64a483f3b633 Mon Sep 17 00:00:00 2001 From: Jan Chyb <48855024+jchyb@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:05:34 +0200 Subject: [PATCH 2/2] Move the inlining into typer, fixing issues with nested inlines --- .../src/dotty/tools/dotc/core/NameKinds.scala | 7 ++ .../dotty/tools/dotc/inlines/Inlines.scala | 25 +++++++ .../dotty/tools/dotc/transform/Erasure.scala | 48 ++++++++++++- .../dotty/tools/dotc/typer/RefChecks.scala | 69 ++----------------- .../src/dotty/tools/dotc/typer/Typer.scala | 40 +++++++++-- .../_docs/reference/metaprogramming/inline.md | 4 +- tests/run/25206-b.check | 1 + tests/run/25206-b.scala | 12 ++++ tests/run/25206-c.check | 4 ++ tests/run/25206-c/Main_4.scala | 8 +++ tests/run/25206-c/Override2_3.scala | 1 + tests/run/25206-c/Override_2.scala | 1 + tests/run/25206-c/Traits_1.scala | 6 ++ tests/run/25206.scala | 2 +- 14 files changed, 153 insertions(+), 75 deletions(-) create mode 100644 tests/run/25206-b.check create mode 100644 tests/run/25206-b.scala create mode 100644 tests/run/25206-c.check create mode 100644 tests/run/25206-c/Main_4.scala create mode 100644 tests/run/25206-c/Override2_3.scala create mode 100644 tests/run/25206-c/Override_2.scala create mode 100644 tests/run/25206-c/Traits_1.scala diff --git a/compiler/src/dotty/tools/dotc/core/NameKinds.scala b/compiler/src/dotty/tools/dotc/core/NameKinds.scala index c19ff61d2b97..61234e91f0f4 100644 --- a/compiler/src/dotty/tools/dotc/core/NameKinds.scala +++ b/compiler/src/dotty/tools/dotc/core/NameKinds.scala @@ -396,6 +396,13 @@ object NameKinds { case BothBounds extends AvoidNameKind(AVOIDBOTH, "(avoid)") val BodyRetainerName: SuffixNameKind = new SuffixNameKind(BODYRETAINER, "$retainedBody") + + // Built on a plain string to avoid using a tag byte and messing with TASTy, + // since those methods are built before the pickler. + val IndirectBodyRetainerSuffix: String = "$indirectRetainedBody" + def indirectRetainerName(name: TermName): TermName = + (name.toString + IndirectBodyRetainerSuffix).toTermName + val FieldName: SuffixNameKind = new SuffixNameKind(FIELD, "$$local") { override def mkString(underlying: TermName, info: ThisInfo) = underlying.toString } diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 42cb6442a269..c5a60d5f16e1 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -289,6 +289,31 @@ object Inlines: ref(meth).appliedToArgss(prefss).withSpan(mdef.rhs.span.startPos))( using ctx.withOwner(retainer))) .showing(i"retainer for $meth: $result", inlining) + + /** Similar to `bodyRetainer`, a method that keeps track of the body that is kept at runtime, + * except synthesised for indirect overrides, and put into a downstream class, e.g. in: + * + * class Foo: + * inline def bar = True + * class Bar: + * def bar + * def test = bar + * class Test extends Foo, Bar + * + * `bar$indirectRetainedBody` is added to `Test`, and rewritten to `bar` in erasure. + */ + def indirectBodyRetainer(sym: Symbol, impl: Symbol, cls: ClassSymbol)(using Context): DefDef = + val retainer = impl.asTerm.copy( + owner = cls, + name = NameKinds.indirectRetainerName(sym.name.asTermName), + flags = (impl.flags &~ (Inline | Macro | Override | AbsOverride)) | Private, + info = sym.info.asSeenFrom(cls.thisType, sym.owner), + coord = cls.coord + ).asTerm.entered + DefDef(retainer, prefss => + inlineCall( + This(cls).select(impl).appliedToArgss(prefss).withSpan(cls.span))( + using ctx.withOwner(retainer))) /** Replace `Inlined` node by a block that contains its bindings and expansion */ def dropInlined(inlined: Inlined)(using Context): Tree = diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index 1611d97f1675..98dc8da17e8f 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -14,7 +14,7 @@ import core.Names.* import core.StdNames.* import core.NameOps.* import core.Periods.currentStablePeriod -import core.NameKinds.{AdaptedClosureName, BodyRetainerName, DirectMethName} +import core.NameKinds.{AdaptedClosureName, BodyRetainerName, DirectMethName, IndirectBodyRetainerSuffix} import core.Scopes.newScopeWith import core.Decorators.* import core.Constants.* @@ -898,7 +898,8 @@ object Erasure { * parameter of type `[]Object`. */ override def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(using Context): Tree = - if sym.isEffectivelyErased || sym.name.is(BodyRetainerName) then + if sym.isEffectivelyErased || sym.name.is(BodyRetainerName) + || sym.name.toString.endsWith(IndirectBodyRetainerSuffix) then erasedDef(sym) else val restpe = if sym.isConstructor then defn.UnitType else sym.info.resultType @@ -1043,7 +1044,8 @@ object Erasure { override def typedStats(stats: List[untpd.Tree], exprOwner: Symbol)(using Context): (List[Tree], Context) = { // discard Imports first, since Bridges will use tree's symbol - val stats0 = addRetainedInlineBodies(stats.filter(!_.isInstanceOf[untpd.Import]))(using preErasureCtx) + val stats00 = stats.filter(!_.isInstanceOf[untpd.Import]) + val stats0 = addRetainedInlineBodies(implementIndirectlyOverriddenInlines(stats00))(using preErasureCtx) val stats1 = if (takesBridges(ctx.owner)) new Bridges(ctx.owner.asClass, erasurePhase).add(stats0) else stats0 @@ -1051,6 +1053,46 @@ object Erasure { (stats2.filterConserve(!_.isEmpty), finalCtx) } + /** For every stat that is a private `$indirectRetainedBody`-named helper, create the actual public, + * concrete override of the abstract member it corresponds to, splicing in the helper's + * already-fully-resolved body. Mirrors `addRetainedInlineBodies`, except the target method + * doesn't exist yet as a statement here and must be created, not just matched by name. + * + * The helper itself is subsequently mapped to the empty tree in `typedDefDef`, same as an + * ordinary `$retainedBody` retainer. + */ + private def implementIndirectlyOverriddenInlines(stats: List[untpd.Tree])(using Context): List[untpd.Tree] = + val newOverrides = stats.collect { + case stat: DefDef if stat.symbol.name.toString.endsWith(IndirectBodyRetainerSuffix) => + val helperSym = stat.symbol + val cls = helperSym.owner.asClass + val origName = helperSym.name.toString.stripSuffix(IndirectBodyRetainerSuffix).toTermName + val sym = + cls.baseClasses.iterator + .map(_.info.decl(origName).symbol) + .find(s => s.exists && s.is(Flags.Deferred)) + .getOrElse(NoSymbol) + if !sym.exists then + assert(false, i"illegal state - $stat retainer doesn't have a corresponding method") + else + val newSym = helperSym.asTerm.copy( + owner = cls, + name = origName, + flags = (helperSym.flags &~ Flags.Private) | Flags.Override | (sym.flags & Flags.Protected), + info = sym.info.asSeenFrom(cls.thisType, sym.owner), + privateWithin = sym.privateWithin, + coord = cls.coord + ).asTerm.entered + Some(DefDef(newSym, prefss => + val mapBody = TreeTypeMap( + oldOwners = helperSym :: Nil, + newOwners = newSym :: Nil, + substFrom = untpd.allParamSyms(stat), + substTo = prefss.flatten.map(_.symbol)) + mapBody.transform(stat.rhs))) + }.flatten + stats ++ newOverrides + /** Finally drops all (language-) imports in erasure. * Since some of the language imports change the subtyping, * we cannot check the trees before erasure. diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index fa74dae219c0..42e0b67dd17d 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -301,10 +301,10 @@ object RefChecks { } } - def ignoreDeferred(clazz: ClassSymbol, mbr: Symbol)(using Context): Boolean = + def ignoreDeferred(clazz: ClassSymbol, mbr: Symbol, checkJavaErasedOverriding: Boolean = true)(using Context): Boolean = mbr.isType || mbr.isSuperAccessor // not yet synthesized - || mbr.is(JavaDefined) && hasJavaErasedOverriding(clazz, mbr) + || checkJavaErasedOverriding && mbr.is(JavaDefined) && hasJavaErasedOverriding(clazz, mbr) || mbr.is(Tracked) // Tracked members correspond to existing val parameters, so they don't // count as deferred. The val parameter could not implement the tracked @@ -1438,61 +1438,6 @@ object RefChecks { } } - /** For every abstract member `sym` of `clazz` that is only implemented indirectly by an inline - * method `impl` from an unrelated base class, synthesize a concrete override of `sym` in `clazz` - * whose body inline-expands a call to `impl`. - * Example case: - * - * ```scala - * trait Settings: - * inline def switch: Boolean = true - * - * trait Inner: - * def switch: Boolean - * def go: String = if switch then "Yes" else "No" - * - * object Outer extends Inner, Settings - * ``` - * - * Without this, `switch`'s inline body is erased entirely (it doesn't override anything from `Settings`'s own - * point of view), leaving `Outer` without any real implementation of `switch` and causing an - * `AbstractMethodError` at runtime. - */ - private def copyIndirectlyOverriddenInlineMethods(clazz: ClassSymbol, tree: Template)(using Context) = { - def retainedOverrideFor(clazz: ClassSymbol, sym: Symbol, impl: Symbol)(using Context): DefDef = - val newSym = impl.asTerm.copy( - owner = clazz, - name = sym.name.asTermName, - flags = (impl.flags &~ (Inline | Macro | Override | AbsOverride | Protected | Private)) - | Override | (sym.flags & Protected), - info = sym.info.asSeenFrom(clazz.thisType, sym.owner), - privateWithin = sym.privateWithin, - coord = clazz.coord - ).asTerm.entered - val result = DefDef(newSym, prefss => - atPhase(inliningPhase): - Inlines.inlineCall(This(clazz).select(impl).appliedToArgss(prefss).withSpan(clazz.span))(using ctx.withOwner(newSym))) - println(i"""--- retainedOverrideFor($clazz, $sym, $impl): - |${result.show} - |--- (raw tree) --- - |${result.toString}""") - result - - val indirectInlineOverrides = - for - bc <- clazz.baseClasses - sym <- bc.info.decls.toList - if sym.is(DeferredTerm) && !ignoreDeferred(clazz, sym) - impl = implementationOf(clazz, sym).toDenot(clazz.thisType).symbol - if impl.exists - && !impl.owner.flags.is(Flags.Inline) // let's not interfere with how inline traits handle things - && !impl.allOverriddenSymbols.contains(sym) - && impl.isInlineMethod - yield retainedOverrideFor(clazz, sym, impl) - - if indirectInlineOverrides.isEmpty then tree - else cpy.Template(tree)(body = tree.body ++ indirectInlineOverrides) - } } import RefChecks.* @@ -1523,13 +1468,7 @@ import RefChecks.* * Unlike in Scala 2.x not-private members keep their name. It is * up to the backend to find a unique expanded name for them. The * rationale to do name changes that late is that they are very fragile. - * - * 5. It copies the implementation of inline methods that only indirectly override - * an abstract member (i.e. only when combined with an unrelated base class/trait in some other - * class), since such methods are otherwise erased entirely, leaving no implementation in the - * classfile. The implementation is added to the first class in which the indirect - * override becomes detectable. - * + * * todo: But RefChecks is not done yet. It's still a somewhat dirty port from the Scala 2 version. * todo: move untrivial logic to their own mini-phases */ @@ -1585,7 +1524,7 @@ class RefChecks extends MiniPhase { thisPhase => checkCompanionNameClashes(cls) checkAllOverrides(cls) checkImplicitNotFoundAnnotation.template(cls.classDenot) - copyIndirectlyOverriddenInlineMethods(cls, tree) + tree } catch { case ex: TypeError => report.error(ex, tree.srcPos) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 287c6addb5ba..687d09751966 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -3492,6 +3492,37 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer body ++ givenImpls end implementDeferredGivens + /** For an abstract member of `cls` that is only implemented indirectly, by an inline + * method from an unrelated base class (i.e. that method's own hierarchy doesn't + * directly override the abstract member), synthesize a private, non-inline helper + * in `cls` whose body inline-expands a call to that method. When the referenced inline + * method is erased, the helper will take it's place for runtime invocations. + */ + def addIndirectInlineRetainers(body: List[Tree]): List[Tree] = + if ctx.isAfterTyper || cls.isRefinementClass then body + else + def hasMatchingInlineMethod(cls: ClassSymbol, sym: Symbol) = + cls.baseClasses.tail.exists(_.info.decl(sym.name).symbol.isInlineMethod) + def alreadyHandledByBaseClass(cls: ClassSymbol, sym: Symbol, impl: Symbol) = + cls.baseClasses.tail.exists(bc => bc.derivesFrom(sym.owner) && bc.derivesFrom(impl.owner)) + val indirectBodyRetainers = + for + bc <- cls.baseClasses.tail + sym <- bc.info.decls.toList + if sym.is(DeferredTerm) && !sym.is(JavaDefined) + && hasMatchingInlineMethod(cls, sym) + && !RefChecks.ignoreDeferred(cls, sym, checkJavaErasedOverriding = false) + impl = RefChecks.implementationOf(cls, sym).toDenot(cls.thisType).symbol + if impl.exists + && !impl.owner.flags.is(Flags.Inline) // let's not interfere with inline traits + && !impl.allOverriddenSymbols.contains(sym) + && impl.isInlineMethod + && !alreadyHandledByBaseClass(cls, sym, impl) + yield Inlines.indirectBodyRetainer(sym, impl, cls) + + body ++ indirectBodyRetainers + end addIndirectInlineRetainers + ensureCorrectSuperClass() completeAnnotations(cdef, cls) val constr1 = typed(constr).asInstanceOf[DefDef] @@ -3522,10 +3553,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else { val dummy = localDummy(cls, impl) val body1 = - implementDeferredGivens( - addParentRefinements( - addAccessorDefs(cls, - typedStats(impl.body, dummy)(using ctx.inClassContext(self1.symbol))._1))) + addIndirectInlineRetainers( + implementDeferredGivens( + addParentRefinements( + addAccessorDefs(cls, + typedStats(impl.body, dummy)(using ctx.inClassContext(self1.symbol))._1)))) checkNoDoubleDeclaration(cls) val impl1 = cpy.Template(impl)(constr1, parents1, Nil, self1, body1) diff --git a/docs/_docs/reference/metaprogramming/inline.md b/docs/_docs/reference/metaprogramming/inline.md index 3280ebddb929..591bcca2af67 100644 --- a/docs/_docs/reference/metaprogramming/inline.md +++ b/docs/_docs/reference/metaprogramming/inline.md @@ -202,10 +202,10 @@ Inline methods can override other non-inline methods. The rules are as follows: object Outer extends Inner, Settings ``` - are copied to the first class that is aware of the inline method at the classfile level. + have their inline expanded body copied (not in TASTy, only at the backend level) to the first class that is aware of the override (`Outer` in the example above). This is because: * inline defs that are not overriding any method at the point of implementation are erased. - * we might only be able to detect indirect overrides after the initial classlike with the inline method is already compiled (so we can't un-erase it then, and we have to include some reference to it in the classfile). + * we might only be able to detect indirect overrides after the initial classlike with the inline method is already compiled (so we can't un-erase it then, and we have to include some reference to it in the classfile, copying makes the most sense). ### Relationship to `@inline` diff --git a/tests/run/25206-b.check b/tests/run/25206-b.check new file mode 100644 index 000000000000..dcd7a5d6d55b --- /dev/null +++ b/tests/run/25206-b.check @@ -0,0 +1 @@ +Yes diff --git a/tests/run/25206-b.scala b/tests/run/25206-b.scala new file mode 100644 index 000000000000..d8901d8b93dd --- /dev/null +++ b/tests/run/25206-b.scala @@ -0,0 +1,12 @@ +trait Settings: + inline def nestedInline = false + inline def switch: Boolean = !nestedInline + +trait Inner: + def switch: Boolean + def go: String = if switch then "Yes" else "No" + +object Outer extends Inner, Settings + +@main def Test() = + println(Outer.go) diff --git a/tests/run/25206-c.check b/tests/run/25206-c.check new file mode 100644 index 000000000000..85462e8f27f6 --- /dev/null +++ b/tests/run/25206-c.check @@ -0,0 +1,4 @@ +Yes +Yes +true +true diff --git a/tests/run/25206-c/Main_4.scala b/tests/run/25206-c/Main_4.scala new file mode 100644 index 000000000000..4cea5f77f424 --- /dev/null +++ b/tests/run/25206-c/Main_4.scala @@ -0,0 +1,8 @@ +@main def Test(): Unit = + println(new Outer().go) + println(new Outer2().go) + val outer: Inner = new Outer + val outer2: Inner = new Outer2 + println(outer.switch) + println(outer2.switch) + diff --git a/tests/run/25206-c/Override2_3.scala b/tests/run/25206-c/Override2_3.scala new file mode 100644 index 000000000000..fc88ac65fa91 --- /dev/null +++ b/tests/run/25206-c/Override2_3.scala @@ -0,0 +1 @@ +class Outer2 extends Outer diff --git a/tests/run/25206-c/Override_2.scala b/tests/run/25206-c/Override_2.scala new file mode 100644 index 000000000000..f9eb53b3f60e --- /dev/null +++ b/tests/run/25206-c/Override_2.scala @@ -0,0 +1 @@ +class Outer extends Inner, Settings diff --git a/tests/run/25206-c/Traits_1.scala b/tests/run/25206-c/Traits_1.scala new file mode 100644 index 000000000000..0dac7e913eba --- /dev/null +++ b/tests/run/25206-c/Traits_1.scala @@ -0,0 +1,6 @@ +trait Settings: + inline def switch: Boolean = true + +trait Inner: + def switch: Boolean + def go: String = if switch then "Yes" else "No" diff --git a/tests/run/25206.scala b/tests/run/25206.scala index b63b74d8563d..36313168a1d3 100644 --- a/tests/run/25206.scala +++ b/tests/run/25206.scala @@ -10,7 +10,7 @@ object Outer extends Inner, Settings: def test1 = go1 def test2 = go2 -@main def main(): Unit = +@main def Test(): Unit = println(Outer.test1) println(Outer.test2) val a: Inner = Outer