From baedc75e291a06195f91cc93bde35e43770e92a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Andres?= Date: Fri, 24 Feb 2023 16:44:35 +0100 Subject: [PATCH 001/254] inline trait implementation --- compiler/src/dotty/tools/dotc/Compiler.scala | 4 +- .../src/dotty/tools/dotc/core/Flags.scala | 5 +- .../src/dotty/tools/dotc/core/Phases.scala | 4 + .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../tools/dotc/core/SymDenotations.scala | 3 + .../tools/dotc/core/tasty/TreeUnpickler.scala | 14 + .../dotty/tools/dotc/inlines/Inliner.scala | 143 +++---- .../dotty/tools/dotc/inlines/Inlines.scala | 348 ++++++++++++++++-- .../tools/dotc/transform/Constructors.scala | 2 +- .../dotty/tools/dotc/transform/Inlining.scala | 2 +- .../dotty/tools/dotc/transform/Mixin.scala | 6 +- .../tools/dotc/transform/PickleQuotes.scala | 4 +- .../tools/dotc/transform/PostTyper.scala | 8 + .../dotc/transform/PruneInlineTraits.scala | 48 +++ .../transform/SpecializeInlineTraits.scala | 123 +++++++ .../dotty/tools/dotc/transform/Splicing.scala | 4 +- .../src/dotty/tools/dotc/typer/Checking.scala | 2 +- .../dotty/tools/dotc/typer/RefChecks.scala | 21 +- .../src/dotty/tools/dotc/typer/Typer.scala | 23 ++ .../pos/inline-trait-4-inner-class.scala | 20 + .../inline-trait-body-class-abstract.scala | 10 + .../pos/inline-trait-body-class-enum.scala | 6 + .../pos/inline-trait-body-class-object.scala | 6 + .../pos/inline-trait-body-trait-generic.scala | 6 + tests/neg/i2421.scala | 1 - tests/neg/inline-trait-body-class-case.scala | 5 + .../neg/inline-trait-body-class-generic.scala | 6 + .../neg/inline-trait-body-class-params.scala | 5 + ...inline-trait-body-override-def-final.scala | 5 + ...inline-trait-body-override-val-final.scala | 5 + .../neg/inline-trait-body-override-var.scala | 5 + ...ne-trait-body-private-name-collision.scala | 3 + .../neg/inline-trait-body-trait-inline.scala | 7 + .../inline-trait-body-trait-parameter.scala | 7 + ...ine-trait-body-trait-term-parameters.scala | 7 + tests/pos/inline-trait-1-simple-trait.scala | 36 ++ tests/pos/inline-trait-2-generic-trait.scala | 13 + tests/pos/inline-trait-3-trait-params.scala | 12 + .../inline-trait-3-trait-with-params.scala | 13 + tests/pos/inline-trait-4-no-inner-class.scala | 24 ++ .../pos/inline-trait-body-abstract-def.scala | 6 + ...rait-body-class-extends-inline-trait.scala | 11 + .../pos/inline-trait-body-class-sealed.scala | 7 + .../pos/inline-trait-body-class-simple.scala | 6 + .../inline-trait-body-def-context-bound.scala | 6 + ...inline-trait-body-def-curried-params.scala | 8 + ...line-trait-body-def-extension-method.scala | 6 + tests/pos/inline-trait-body-def-final.scala | 4 + ...ine-trait-body-def-generic-singleton.scala | 7 + tests/pos/inline-trait-body-def-generic.scala | 6 + .../pos/inline-trait-body-def-implicit.scala | 6 + ...nline-trait-body-def-inline-abstract.scala | 6 + ...ne-trait-body-def-inline-compiletime.scala | 9 + ...ne-trait-body-def-inline-transparent.scala | 5 + tests/pos/inline-trait-body-def-inline.scala | 5 + tests/pos/inline-trait-body-def-lambda.scala | 5 + .../pos/inline-trait-body-def-local-val.scala | 7 + tests/pos/inline-trait-body-def-params.scala | 7 + tests/pos/inline-trait-body-def-parens.scala | 5 + tests/pos/inline-trait-body-def-simple.scala | 5 + tests/pos/inline-trait-body-def-using.scala | 6 + tests/pos/inline-trait-body-lazy-val.scala | 5 + .../Macro.scala | 10 + .../Test.scala | 2 + .../pos/inline-trait-body-macro/Macro_1.scala | 10 + .../pos/inline-trait-body-macro/Test_2.scala | 1 + tests/pos/inline-trait-body-rhs-type.scala | 6 + tests/pos/inline-trait-body-setter.scala | 6 + .../pos/inline-trait-body-trait-simple.scala | 6 + tests/pos/inline-trait-body-type.scala | 5 + tests/pos/inline-trait-body-val-inline.scala | 5 + tests/pos/inline-trait-body-val.scala | 5 + ...-trait-inheritance-multiple-override.scala | 20 + .../inline-trait-inheritance-multiple.scala | 17 + ...tance-single-abstract-class-override.scala | 7 + ...it-inheritance-single-abstract-class.scala | 6 + ...t-inheritance-single-object-override.scala | 6 + ...line-trait-inheritance-single-object.scala | 5 + tests/pos/inline-trait-multiple-files/A.scala | 2 + tests/pos/inline-trait-multiple-files/B.scala | 1 + .../A_1.scala | 10 + .../B_2.scala | 3 + .../A_1.scala | 9 + .../B_2.scala | 3 + .../inline-trait-multiple-stages/A_1.scala | 2 + .../inline-trait-multiple-stages/B_2.scala | 1 + ...rait-signature-generic-context-bound.scala | 4 + ...rait-signature-generic-inferred-type.scala | 5 + ...ne-trait-signature-generic-invariant.scala | 4 + ...ne-trait-signature-generic-parameter.scala | 5 + ...it-signature-generic-refinement-type.scala | 9 + ...ne-trait-signature-generic-singleton.scala | 4 + ...-trait-signature-generic-type-bounds.scala | 4 + ...line-trait-signature-generic-variant.scala | 8 + ...-trait-signature-parameters-currying.scala | 4 + ...t-signature-parameters-default-value.scala | 4 + ...-trait-signature-parameters-implicit.scala | 4 + ...-signature-parameters-using-nameless.scala | 4 + ...ine-trait-signature-parameters-using.scala | 4 + ...ait-signature-parameters-val-private.scala | 3 + ...t-signature-parameters-val-protected.scala | 3 + ...nline-trait-signature-parameters-val.scala | 3 + ...nline-trait-signature-parameters-var.scala | 3 + .../inline-trait-signature-parentheses.scala | 4 + tests/pos/inline-trait-signature-sealed.scala | 4 + tests/pos/inline-trait-signature-simple.scala | 4 + .../inline-trait-usage-anonymous-class.scala | 4 + tests/pos/inline-trait-usage-extension.scala | 3 + tests/pos/inline-trait-usage-inner.scala | 5 + tests/pos/inline-trait-usage-param-type.scala | 3 + .../pos/inline-trait-usage-return-type.scala | 3 + tests/pos/inline-trait-usage-type-bound.scala | 3 + tests/pos/inline-trait-usage-type.scala | 3 + tests/run/inline-trait-body-lazy-val.scala | 9 + .../run/inline-trait-body-override-def.scala | 9 + .../run/inline-trait-body-override-val.check | 1 + .../run/inline-trait-body-override-val.scala | 9 + tests/run/inline-trait-body-statements.check | 8 + tests/run/inline-trait-body-statements.scala | 22 ++ tests/run/inline-trait-body-var.check | 4 + tests/run/inline-trait-body-var.scala | 15 + ...ait-inheritance-diamond-simple-trait.check | 4 + ...ait-inheritance-diamond-simple-trait.scala | 22 ++ .../inlinetraits.scala | 47 +++ .../normaltraits.scala | 47 +++ .../test.scala | 5 + ...trait-inheritance-inline-grandparent.check | 6 + ...trait-inheritance-inline-grandparent.scala | 33 ++ ...trait-signature-parameters-val-block.check | 4 + ...trait-signature-parameters-val-block.scala | 10 + ...nline-trait-signature-side-effects-1.check | 6 + ...nline-trait-signature-side-effects-1.scala | 27 ++ ...nline-trait-signature-side-effects-2.check | 6 + ...nline-trait-signature-side-effects-2.scala | 16 + 134 files changed, 1565 insertions(+), 109 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala create mode 100644 compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala create mode 100644 tests/disabled/pos/inline-trait-4-inner-class.scala create mode 100644 tests/disabled/pos/inline-trait-body-class-abstract.scala create mode 100644 tests/disabled/pos/inline-trait-body-class-enum.scala create mode 100644 tests/disabled/pos/inline-trait-body-class-object.scala create mode 100644 tests/disabled/pos/inline-trait-body-trait-generic.scala create mode 100644 tests/neg/inline-trait-body-class-case.scala create mode 100644 tests/neg/inline-trait-body-class-generic.scala create mode 100644 tests/neg/inline-trait-body-class-params.scala create mode 100644 tests/neg/inline-trait-body-override-def-final.scala create mode 100644 tests/neg/inline-trait-body-override-val-final.scala create mode 100644 tests/neg/inline-trait-body-override-var.scala create mode 100644 tests/neg/inline-trait-body-private-name-collision.scala create mode 100644 tests/neg/inline-trait-body-trait-inline.scala create mode 100644 tests/neg/inline-trait-body-trait-parameter.scala create mode 100644 tests/neg/inline-trait-body-trait-term-parameters.scala create mode 100644 tests/pos/inline-trait-1-simple-trait.scala create mode 100644 tests/pos/inline-trait-2-generic-trait.scala create mode 100644 tests/pos/inline-trait-3-trait-params.scala create mode 100644 tests/pos/inline-trait-3-trait-with-params.scala create mode 100644 tests/pos/inline-trait-4-no-inner-class.scala create mode 100644 tests/pos/inline-trait-body-abstract-def.scala create mode 100644 tests/pos/inline-trait-body-class-extends-inline-trait.scala create mode 100644 tests/pos/inline-trait-body-class-sealed.scala create mode 100644 tests/pos/inline-trait-body-class-simple.scala create mode 100644 tests/pos/inline-trait-body-def-context-bound.scala create mode 100644 tests/pos/inline-trait-body-def-curried-params.scala create mode 100644 tests/pos/inline-trait-body-def-extension-method.scala create mode 100644 tests/pos/inline-trait-body-def-final.scala create mode 100644 tests/pos/inline-trait-body-def-generic-singleton.scala create mode 100644 tests/pos/inline-trait-body-def-generic.scala create mode 100644 tests/pos/inline-trait-body-def-implicit.scala create mode 100644 tests/pos/inline-trait-body-def-inline-abstract.scala create mode 100644 tests/pos/inline-trait-body-def-inline-compiletime.scala create mode 100644 tests/pos/inline-trait-body-def-inline-transparent.scala create mode 100644 tests/pos/inline-trait-body-def-inline.scala create mode 100644 tests/pos/inline-trait-body-def-lambda.scala create mode 100644 tests/pos/inline-trait-body-def-local-val.scala create mode 100644 tests/pos/inline-trait-body-def-params.scala create mode 100644 tests/pos/inline-trait-body-def-parens.scala create mode 100644 tests/pos/inline-trait-body-def-simple.scala create mode 100644 tests/pos/inline-trait-body-def-using.scala create mode 100644 tests/pos/inline-trait-body-lazy-val.scala create mode 100644 tests/pos/inline-trait-body-macro-suspend/Macro.scala create mode 100644 tests/pos/inline-trait-body-macro-suspend/Test.scala create mode 100644 tests/pos/inline-trait-body-macro/Macro_1.scala create mode 100644 tests/pos/inline-trait-body-macro/Test_2.scala create mode 100644 tests/pos/inline-trait-body-rhs-type.scala create mode 100644 tests/pos/inline-trait-body-setter.scala create mode 100644 tests/pos/inline-trait-body-trait-simple.scala create mode 100644 tests/pos/inline-trait-body-type.scala create mode 100644 tests/pos/inline-trait-body-val-inline.scala create mode 100644 tests/pos/inline-trait-body-val.scala create mode 100644 tests/pos/inline-trait-inheritance-multiple-override.scala create mode 100644 tests/pos/inline-trait-inheritance-multiple.scala create mode 100644 tests/pos/inline-trait-inheritance-single-abstract-class-override.scala create mode 100644 tests/pos/inline-trait-inheritance-single-abstract-class.scala create mode 100644 tests/pos/inline-trait-inheritance-single-object-override.scala create mode 100644 tests/pos/inline-trait-inheritance-single-object.scala create mode 100644 tests/pos/inline-trait-multiple-files/A.scala create mode 100644 tests/pos/inline-trait-multiple-files/B.scala create mode 100644 tests/pos/inline-trait-multiple-stages-defs/A_1.scala create mode 100644 tests/pos/inline-trait-multiple-stages-defs/B_2.scala create mode 100644 tests/pos/inline-trait-multiple-stages-generic-defs/A_1.scala create mode 100644 tests/pos/inline-trait-multiple-stages-generic-defs/B_2.scala create mode 100644 tests/pos/inline-trait-multiple-stages/A_1.scala create mode 100644 tests/pos/inline-trait-multiple-stages/B_2.scala create mode 100644 tests/pos/inline-trait-signature-generic-context-bound.scala create mode 100644 tests/pos/inline-trait-signature-generic-inferred-type.scala create mode 100644 tests/pos/inline-trait-signature-generic-invariant.scala create mode 100644 tests/pos/inline-trait-signature-generic-parameter.scala create mode 100644 tests/pos/inline-trait-signature-generic-refinement-type.scala create mode 100644 tests/pos/inline-trait-signature-generic-singleton.scala create mode 100644 tests/pos/inline-trait-signature-generic-type-bounds.scala create mode 100644 tests/pos/inline-trait-signature-generic-variant.scala create mode 100644 tests/pos/inline-trait-signature-parameters-currying.scala create mode 100644 tests/pos/inline-trait-signature-parameters-default-value.scala create mode 100644 tests/pos/inline-trait-signature-parameters-implicit.scala create mode 100644 tests/pos/inline-trait-signature-parameters-using-nameless.scala create mode 100644 tests/pos/inline-trait-signature-parameters-using.scala create mode 100644 tests/pos/inline-trait-signature-parameters-val-private.scala create mode 100644 tests/pos/inline-trait-signature-parameters-val-protected.scala create mode 100644 tests/pos/inline-trait-signature-parameters-val.scala create mode 100644 tests/pos/inline-trait-signature-parameters-var.scala create mode 100644 tests/pos/inline-trait-signature-parentheses.scala create mode 100644 tests/pos/inline-trait-signature-sealed.scala create mode 100644 tests/pos/inline-trait-signature-simple.scala create mode 100644 tests/pos/inline-trait-usage-anonymous-class.scala create mode 100644 tests/pos/inline-trait-usage-extension.scala create mode 100644 tests/pos/inline-trait-usage-inner.scala create mode 100644 tests/pos/inline-trait-usage-param-type.scala create mode 100644 tests/pos/inline-trait-usage-return-type.scala create mode 100644 tests/pos/inline-trait-usage-type-bound.scala create mode 100644 tests/pos/inline-trait-usage-type.scala create mode 100644 tests/run/inline-trait-body-lazy-val.scala create mode 100644 tests/run/inline-trait-body-override-def.scala create mode 100644 tests/run/inline-trait-body-override-val.check create mode 100644 tests/run/inline-trait-body-override-val.scala create mode 100644 tests/run/inline-trait-body-statements.check create mode 100644 tests/run/inline-trait-body-statements.scala create mode 100644 tests/run/inline-trait-body-var.check create mode 100644 tests/run/inline-trait-body-var.scala create mode 100644 tests/run/inline-trait-inheritance-diamond-simple-trait.check create mode 100644 tests/run/inline-trait-inheritance-diamond-simple-trait.scala create mode 100644 tests/run/inline-trait-inheritance-inline-ancestors/inlinetraits.scala create mode 100644 tests/run/inline-trait-inheritance-inline-ancestors/normaltraits.scala create mode 100644 tests/run/inline-trait-inheritance-inline-ancestors/test.scala create mode 100644 tests/run/inline-trait-inheritance-inline-grandparent.check create mode 100644 tests/run/inline-trait-inheritance-inline-grandparent.scala create mode 100644 tests/run/inline-trait-signature-parameters-val-block.check create mode 100644 tests/run/inline-trait-signature-parameters-val-block.scala create mode 100644 tests/run/inline-trait-signature-side-effects-1.check create mode 100644 tests/run/inline-trait-signature-side-effects-1.scala create mode 100644 tests/run/inline-trait-signature-side-effects-2.check create mode 100644 tests/run/inline-trait-signature-side-effects-2.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index fadab10a4f3e..35dfe5b69233 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -50,6 +50,7 @@ class Compiler { protected def picklerPhases: List[List[Phase]] = List(new Pickler) :: // Generate TASTY info List(new sbt.ExtractAPI) :: // Sends a representation of the API of classes to sbt via callbacks + List(new SpecializeInlineTraits) :: // Inline the code of inline traits into their children List(new Inlining) :: // Inline and execute macros List(new PostInlining) :: // Add mirror support for inlined code List(new Staging) :: // Check staging levels and heal staged types @@ -95,7 +96,8 @@ class Compiler { new ExplicitSelf, // Make references to non-trivial self types explicit as casts new StringInterpolatorOpt, // Optimizes raw and s and f string interpolators by rewriting them to string concatenations or formats new DropBreaks) :: // Optimize local Break throws by rewriting them - List(new PruneErasedDefs, // Make erased symbols private + List(new PruneInlineTraits, // Remove right-hand side of definitions in inline traits + new PruneErasedDefs, // Drop erased definitions from scopes and simplify erased expressions new UninitializedDefs, // Replaces `compiletime.uninitialized` by `_` new InlinePatterns, // Remove placeholders of inlined patterns new VCInlineMethods, // Inlines calls to value class methods diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index af7455fa2994..3085db1a0043 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -453,13 +453,13 @@ object Flags { /** Flags representing source modifiers */ private val CommonSourceModifierFlags: FlagSet = - commonFlags(Private, Protected, Final, Case, Implicit, Given, Override, JavaStatic, Transparent, Erased) + commonFlags(Private, Protected, Final, Case, Implicit, Given, Override, JavaStatic, Transparent, Erased, Inline) val TypeSourceModifierFlags: FlagSet = CommonSourceModifierFlags.toTypeFlags | Abstract | Sealed | Opaque | Open | Into val TermSourceModifierFlags: FlagSet = - CommonSourceModifierFlags.toTermFlags | Inline | AbsOverride | Lazy | Tracked + CommonSourceModifierFlags.toTermFlags | AbsOverride | Lazy | Tracked /** Flags representing modifiers that can appear in trees */ val ModifierFlags: FlagSet = @@ -592,6 +592,7 @@ object Flags { val InlineOrProxy: FlagSet = Inline | InlineProxy // An inline method or inline argument proxy */ val InlineMethod: FlagSet = Inline | Method val InlineImplicitMethod: FlagSet = Implicit | InlineMethod + val InlineTrait: FlagSet = Inline | Trait val InlineParam: FlagSet = Inline | Param val InlineByNameProxy: FlagSet = InlineProxy | Method val JavaEnum: FlagSet = JavaDefined | Enum // A Java enum trait diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index b8522246eac8..0809ae3a5f93 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -238,6 +238,7 @@ object Phases { private var mySbtExtractAPIPhase: Phase = uninitialized private var myPicklerPhase: Phase = uninitialized private var mySetRootTreePhase: Phase = uninitialized + private var mySpecializeInlineTraitsPhase: Phase = uninitialized private var myInliningPhase: Phase = uninitialized private var myStagingPhase: Phase = uninitialized private var mySplicingPhase: Phase = uninitialized @@ -271,6 +272,7 @@ object Phases { final def sbtExtractAPIPhase: Phase = mySbtExtractAPIPhase final def picklerPhase: Phase = myPicklerPhase final def setRootTreePhase: Phase = mySetRootTreePhase + final def specializeInlineTraitsPhase: Phase = mySpecializeInlineTraitsPhase final def inliningPhase: Phase = myInliningPhase final def stagingPhase: Phase = myStagingPhase final def splicingPhase: Phase = mySplicingPhase @@ -304,6 +306,7 @@ object Phases { mySbtExtractAPIPhase = phaseOfClass(classOf[sbt.ExtractAPI]) mySetRootTreePhase = phaseOfClass(classOf[SetRootTree]) myPicklerPhase = phaseOfClass(classOf[Pickler]) + mySpecializeInlineTraitsPhase = phaseOfClass(classOf[SpecializeInlineTraits]) myInliningPhase = phaseOfClass(classOf[Inlining]) myStagingPhase = phaseOfClass(classOf[Staging]) mySplicingPhase = phaseOfClass(classOf[Splicing]) @@ -560,6 +563,7 @@ object Phases { def sbtExtractDependenciesPhase(using Context): Phase = ctx.base.sbtExtractDependenciesPhase def sbtExtractAPIPhase(using Context): Phase = ctx.base.sbtExtractAPIPhase def picklerPhase(using Context): Phase = ctx.base.picklerPhase + def specializeInlineTraitsPhase(using Context): Phase = ctx.base.specializeInlineTraitsPhase def inliningPhase(using Context): Phase = ctx.base.inliningPhase def stagingPhase(using Context): Phase = ctx.base.stagingPhase def splicingPhase(using Context): Phase = ctx.base.splicingPhase diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index b735db8866ac..c930982f6e62 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -26,6 +26,7 @@ object StdNames { inline val LOCALDUMMY_PREFIX = " + val ctx1 = localContext(cls)(using ctx0).addMode(Mode.ReadPositions) + inContext(sourceChangeContext(Addr(0))(using ctx1)) { + // avoids space leaks by not capturing the current context + + val fork = forkAt(statsStart) + val stats = fork.readIndexedStats(localDummy, end) + val inlinedMembers = (tparams ++ vparams ++ stats).filter(member => Inlines.isInlineableFromInlineTrait(cls, member)) + Block(inlinedMembers, unitLiteral).withSpan(cls.span) + } + }) NamerOps.addConstructorProxies(cls) NamerOps.addContextBoundCompanions(cls) setSpan(start, diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index 1ba7db1f2080..c3dfee250dd6 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala @@ -119,9 +119,10 @@ object Inliner: oldOwners: List[Symbol], newOwners: List[Symbol], substFrom: List[Symbol], - substTo: List[Symbol])(using Context) + substTo: List[Symbol], + val inlineCopier: TreeCopier)(using Context) extends TreeTypeMap( - typeMap, treeMap, oldOwners, newOwners, substFrom, substTo, InlineCopier()): + typeMap, treeMap, oldOwners, newOwners, substFrom, substTo, inlineCopier): override def transform(tree: Tree)(using Context): Tree = tree match @@ -147,7 +148,7 @@ object Inliner: newOwners: List[Symbol], substFrom: List[Symbol], substTo: List[Symbol])(using Context) = - new InlinerMap(typeMap, treeMap, oldOwners, newOwners, substFrom, substTo) + new InlinerMap(typeMap, treeMap, oldOwners, newOwners, substFrom, substTo, inlineCopier) override def transformInlined(tree: Inlined)(using Context) = if tree.inlinedFromOuterScope then @@ -337,7 +338,7 @@ class Inliner(val call: tpd.Tree)(using Context): private def classNestingLevel(cls: Symbol) = cls.ownersIterator.count(_.isClass) // Compute val-definitions for all this-proxies and append them to `bindingsBuf` - private def computeThisBindings() = { + protected def computeThisBindings() = { // All needed this-proxies, paired-with and sorted-by nesting depth of // the classes they represent (innermost first) val sortedProxies = thisProxy.toList @@ -504,7 +505,7 @@ class Inliner(val call: tpd.Tree)(using Context): else arg else arg - private def canElideThis(tpe: ThisType): Boolean = + protected def canElideThis(tpe: ThisType): Boolean = inlineCallPrefix.tpe == tpe && ctx.owner.isContainedIn(tpe.cls) || tpe.cls.isContainedIn(inlinedMethod) || tpe.cls.is(Package) @@ -553,7 +554,6 @@ class Inliner(val call: tpd.Tree)(using Context): if thisTypeProxyExists then mapBackToOpaques.typeMap(thisTypeUnpacker.typeMap(inlined.expansion.tpe)) else inlined.tpe - /** Populate `thisProxy` and `paramProxy` as follows: * * 1a. If given type refers to a static this, thisProxy binds it to corresponding global reference, @@ -609,7 +609,7 @@ class Inliner(val call: tpd.Tree)(using Context): * from its `originalOwner`, and, if it comes from outside the inlined method * itself, it has to be marked as an inlined argument. */ - private def integrate(tree: Tree, originalOwner: Symbol)(using Context): Tree = + protected def integrate(tree: Tree, originalOwner: Symbol)(using Context): Tree = // assertAllPositioned(tree) // debug tree.changeOwner(originalOwner, ctx.owner) @@ -621,9 +621,73 @@ class Inliner(val call: tpd.Tree)(using Context): val reducer = new InlineReducer(this) - /** The Inlined node representing the inlined call */ - def inlined(rhsToInline: tpd.Tree): (List[MemberDef], Tree) = + protected class InlinerTypeMap extends DeepTypeMap { + override def stopAt = + if opaqueProxies.isEmpty then StopAt.Static else StopAt.Package + def apply(t: Type) = t match { + case t: ThisType => thisProxy.get(t.cls).getOrElse(t) + case t: TypeRef => paramProxy.getOrElse(t, mapOver(t)) + case t: SingletonType => + if t.termSymbol.isAllOf(InlineParam) then apply(t.widenTermRefExpr) + else paramProxy.getOrElse(t, mapOver(t)) + case t => mapOver(t) + } + } + + protected class InlinerTreeMap extends (Tree => Tree) { + def apply(tree: Tree) = tree match { + case tree: This => + tree.tpe match { + case thistpe: ThisType => + thisProxy.get(thistpe.cls) match { + case Some(t) => + val thisRef = ref(t).withSpan(call.span) + inlinedFromOutside(thisRef)(tree.span) + case None => tree + } + case _ => tree + } + case tree: Ident => + /* Span of the argument. Used when the argument is inlined directly without a binding */ + def argSpan = + if (tree.name == nme.WILDCARD) tree.span // From type match + else if (tree.symbol.isTypeParam && tree.symbol.owner.isClass) tree.span // TODO is this the correct span? + else paramSpan(tree.name) + val inlinedCtx = ctx.withSource(inlinedMethod.topLevelClass.source) + paramProxy.get(tree.tpe) match { + case Some(t) if tree.isTerm && t.isSingleton => + val inlinedSingleton = singleton(t).withSpan(argSpan) + inlinedFromOutside(inlinedSingleton)(tree.span) + case Some(t) if tree.isType => + inlinedFromOutside(new InferredTypeTree().withType(t).withSpan(argSpan))(tree.span) + case _ => tree + } + case tree @ Select(qual: This, name) if tree.symbol.is(Private) && tree.symbol.isInlineMethod => + // This inline method refers to another (private) inline method (see tests/pos/i14042.scala). + // We insert upcast to access the private inline method once inlined. This makes the selection + // keep the symbol when re-typechecking in the InlineTyper. The method is inlined and hence no + // reference to a private method is kept at runtime. + cpy.Select(tree)(qual.asInstance(qual.tpe.widen), name) + + case tree => tree + } + + private def inlinedFromOutside(tree: Tree)(span: Span): Tree = + Inlined(EmptyTree, Nil, tree)(using ctx.withSource(inlinedMethod.topLevelClass.source)).withSpan(span) + } + + protected val inlinerTypeMap: InlinerTypeMap = InlinerTypeMap() + protected val inlinerTreeMap: InlinerTreeMap = InlinerTreeMap() + + protected def substFrom: List[Symbol] = Nil + protected def substTo: List[Symbol] = Nil + protected def inlineCopier: TreeCopier = InlineCopier() + + protected def inlineCtx(inlineTyper: InlineTyper)(using Context): Context = + inlineContext(Inlined(call, Nil, ref(defn.Predef_undefined))).fresh.setTyper(inlineTyper).setNewScope + /** The Inlined node representing the inlined call */ + def inlined(rhsToInline: tpd.Tree)(using Context): (List[MemberDef], Tree) = inlining.println(i"-----------------------\nInlining $call\nWith RHS $rhsToInline") def paramTypess(call: Tree, acc: List[List[Type]]): List[List[Type]] = call match @@ -665,69 +729,20 @@ class Inliner(val call: tpd.Tree)(using Context): val inlineTyper = new InlineTyper(ctx.reporter.errorCount) - val inlineCtx = inlineContext(Inlined(call, Nil, ref(defn.Predef_undefined))).fresh.setTyper(inlineTyper).setNewScope - - def inlinedFromOutside(tree: Tree)(span: Span): Tree = - Inlined(EmptyTree, Nil, tree)(using ctx.withSource(inlinedMethod.topLevelClass.source)).withSpan(span) + val inlineCtx = this.inlineCtx(inlineTyper) // A tree type map to prepare the inlined body for typechecked. // The translation maps references to `this` and parameters to // corresponding arguments or proxies on the type and term level. It also changes // the owner from the inlined method to the current owner. val inliner = new InlinerMap( - typeMap = - new DeepTypeMap { - override def stopAt = - if opaqueProxies.isEmpty then StopAt.Static else StopAt.Package - def apply(t: Type) = t match { - case t: ThisType => thisProxy.getOrElse(t.cls, t) - case t: TypeRef => paramProxy.getOrElse(t, mapOver(t)) - case t: SingletonType => - if t.termSymbol.isAllOf(InlineParam) then apply(t.widenTermRefExpr) - else paramProxy.getOrElse(t, mapOver(t)) - case t => mapOver(t) - } - }, - treeMap = { - case tree: This => - tree.tpe match { - case thistpe: ThisType => - thisProxy.get(thistpe.cls) match { - case Some(t) => - val thisRef = ref(t).withSpan(call.span) - inlinedFromOutside(thisRef)(tree.span) - case None => tree - } - case _ => tree - } - case tree: Ident => - /* Span of the argument. Used when the argument is inlined directly without a binding */ - def argSpan = - if (tree.name == nme.WILDCARD) tree.span // From type match - else if (tree.symbol.isTypeParam && tree.symbol.owner.isClass) tree.span // TODO is this the correct span? - else paramSpan(tree.name) - val inlinedCtx = ctx.withSource(inlinedMethod.topLevelClass.source) - paramProxy.get(tree.tpe) match { - case Some(t) if tree.isTerm && t.isSingleton => - val inlinedSingleton = singleton(t).withSpan(argSpan) - inlinedFromOutside(inlinedSingleton)(tree.span) - case Some(t) if tree.isType => - inlinedFromOutside(new InferredTypeTree().withType(t).withSpan(argSpan))(tree.span) - case _ => tree - } - case tree @ Select(qual: This, name) if tree.symbol.is(Private) && tree.symbol.isInlineMethod => - // This inline method refers to another (private) inline method (see tests/pos/i14042.scala). - // We insert upcast to access the private inline method once inlined. This makes the selection - // keep the symbol when re-typechecking in the InlineTyper. The method is inlined and hence no - // reference to a private method is kept at runtime. - cpy.Select(tree)(qual.asInstance(qual.tpe.widen), name) - - case tree => tree - }, + typeMap = inlinerTypeMap, + treeMap = inlinerTreeMap, oldOwners = inlinedMethod :: Nil, newOwners = ctx.owner :: Nil, - substFrom = Nil, - substTo = Nil + substFrom = substFrom, + substTo = substTo, + inlineCopier = inlineCopier )(using inlineCtx) inlining.println( diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index ba2ff4248ca7..d73fb3710572 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -3,8 +3,9 @@ package dotc package inlines import ast.*, core.* -import Flags.*, Symbols.*, Types.*, Decorators.*, Constants.*, Contexts.* -import StdNames.{tpnme, nme} +import Flags.*, Symbols.*, Types.*, Decorators.*, Constants.*, Contexts.*, TypeOps.* +import Names.Name +import StdNames.{str, tpnme, nme} import NameOps.* import typer.* import NameKinds.BodyRetainerName @@ -22,9 +23,10 @@ import cc.CleanupRetains import collection.mutable import reporting.{NotConstant, trace} -import util.Spans.Span +import util.Spans.{Span, spanCoord} import dotty.tools.dotc.core.Periods.PhaseId import dotty.tools.dotc.util.chaining.* +import NameOps.expandedName /** Support for querying inlineable methods and for inlining calls to such methods */ object Inlines: @@ -38,7 +40,7 @@ object Inlines: /** `sym` is an inline method with a known body to inline. */ def hasBodyToInline(sym: SymDenotation)(using Context): Boolean = - sym.isInlineMethod && sym.hasAnnotation(defn.BodyAnnot) + (sym.isInlineMethod || sym.isInlineTrait) && sym.hasAnnotation(defn.BodyAnnot) /** The body to inline for method `sym`, or `EmptyTree` if none exists. * @pre hasBodyToInline(sym) @@ -54,39 +56,78 @@ object Inlines: else EmptyTree + def defsToInline(traitSym: SymDenotation)(using Context): List[Tree] = + bodyToInline(traitSym) match + case Block(defs, _) if traitSym.isInlineTrait => defs + case _ => Nil + /** Are we in an inline method body? */ def inInlineMethod(using Context): Boolean = ctx.owner.ownersIterator.exists(_.isInlineMethod) + def inInlineContext(using Context): Boolean = + ctx.owner.ownersIterator.exists(sym => sym.isInlineMethod || sym.isInlineTrait) + /** Can a call to method `meth` be inlined? */ def isInlineable(meth: Symbol)(using Context): Boolean = - meth.is(Inline) && meth.hasAnnotation(defn.BodyAnnot) && !inInlineMethod + meth.isInlineMethod && meth.hasAnnotation(defn.BodyAnnot) && !inInlineMethod + + def isInlineableFromInlineTrait(inlinedTraitSym: ClassSymbol, member: tpd.Tree)(using Context): Boolean = + !(member.isInstanceOf[tpd.TypeDef] && inlinedTraitSym.typeParams.contains(member.symbol)) + && !member.symbol.isAllOf(Inline) + && !member.symbol.is(Deferred) /** Should call be inlined in this context? */ - def needsInlining(tree: Tree)(using Context): Boolean = tree match { - case Block(_, expr) => needsInlining(expr) - case _ => - def isUnapplyExpressionWithDummy: Boolean = - // The first step of typing an `unapply` consists in typing the call - // with a dummy argument (see Applications.typedUnApply). We delay the - // inlining of this call. - def rec(tree: Tree): Boolean = tree match - case Apply(_, ProtoTypes.dummyTreeOfType(_) :: Nil) => true - case Apply(fn, _) => rec(fn) - case _ => false - tree.symbol.name.isUnapplyName && rec(tree) - - isInlineable(tree.symbol) - && !tree.tpe.widenTermRefExpr.isInstanceOf[MethodOrPoly] - && StagingLevel.level == 0 + def needsInlining(tree: Tree)(using Context): Boolean = + def isInlineableInCtx = + StagingLevel.level == 0 && ( ctx.phase == Phases.inliningPhase || (ctx.phase == Phases.typerPhase && needsTransparentInlining(tree)) + || (ctx.phase == Phases.specializeInlineTraitsPhase && !tree.symbol.is(Macro)) ) && !ctx.typer.hasInliningErrors && !ctx.base.stopInlining - && !ctx.mode.is(Mode.NoInline) - && !isUnapplyExpressionWithDummy + && !ctx.owner.ownersIterator.exists(_.isInlineTrait) + + tree match + case Block(_, expr) => + needsInlining(expr) + case tdef @ TypeDef(_, impl: Template) => + !tdef.symbol.isInlineTrait && impl.parents.map(symbolFromParent).exists(_.isInlineTrait) && isInlineableInCtx + case _ => + def isUnapplyExpressionWithDummy: Boolean = + // The first step of typing an `unapply` consists in typing the call + // with a dummy argument (see Applications.typedUnApply). We delay the + // inlining of this call. + def rec(tree: Tree): Boolean = tree match + case Apply(_, ProtoTypes.dummyTreeOfType(_) :: Nil) => true + case Apply(fn, _) => rec(fn) + case _ => false + tree.symbol.name.isUnapplyName && rec(tree) + isInlineable(tree.symbol) && !tree.tpe.widenTermRefExpr.isInstanceOf[MethodOrPoly] && isInlineableInCtx && !ctx.mode.is(Mode.NoInline) && !isUnapplyExpressionWithDummy + + private[dotc] def symbolFromParent(parent: Tree)(using Context): Symbol = + if parent.symbol.isConstructor then parent.symbol.owner else parent.symbol + + private def inlineTraitAncestors(cls: TypeDef)(using Context): List[Tree] = cls match { + case tpd.TypeDef(_, tmpl: Template) => + val parentTrees: Map[Symbol, Tree] = tmpl.parents.map(par => symbolFromParent(par) -> par).toMap.filter(_._1.isInlineTrait) + val ancestors: List[ClassSymbol] = cls.tpe.baseClasses.filter(sym => sym.isInlineTrait && sym != cls.symbol) + ancestors.flatMap(ancestor => + def baseTree = + cls.tpe.baseType(ancestor) match + case AppliedType(tycon, targs) => + Some(AppliedTypeTree(TypeTree(tycon), targs.map(TypeTree(_)))) + case tref: TypeRef => + Some(Ident(tref)) + case baseTpe => + report.error(s"unknown base type ${baseTpe.show} for ancestor ${ancestor.show} of ${cls.symbol.show}") + None + parentTrees.get(ancestor).orElse(baseTree.map(_.withSpan(cls.span))) + ) + case _ => + Nil } private def needsTransparentInlining(tree: Tree)(using Context): Boolean = @@ -205,6 +246,26 @@ object Inlines: tree2 end inlineCall + def inlineParentInlineTraits(cls: Tree)(using Context): Tree = + cls match { + case cls @ tpd.TypeDef(_, impl: Template) => + val clsOverriddenSyms = cls.symbol.info.decls.toList.flatMap(_.allOverriddenSymbols).toSet + val newDefs = inContext(ctx.withOwner(cls.symbol)) { + inlineTraitAncestors(cls).foldLeft((List.empty[Tree], impl.body)){ + case ((inlineDefs, childDefs), parent) => + val parentTraitInliner = InlineParentTrait(parent) + val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) + val inlinedDefs1 = inlineDefs ::: parentTraitInliner.expandDefs(overriddenSymbols) + val childDefs1 = parentTraitInliner.adaptDefs(childDefs) // TODO do this outside of inlining: we need to adapt ALL references to inlined stuff + (inlinedDefs1, childDefs1) + } + } + val impl1 = cpy.Template(impl)(body = newDefs._1 ::: newDefs._2) + cpy.TypeDef(cls)(rhs = impl1) + case _ => + cls + } + /** Try to inline a pattern with an inline unapply method. Fail with error if the maximal * inline depth is exceeded. * @@ -636,4 +697,245 @@ object Inlines: // the opaque type itself. An example is in pos/opaque-inline1.scala. end expand end InlineCall + + private class InlineParentTrait(parent: tpd.Tree)(using Context) extends Inliner(parent): + import tpd._ + import Inlines.* + + private val parentSym = symbolFromParent(parent) + private val paramAccessorsMapper = ParamAccessorsMapper() + private val innerClassNewSyms: mutable.LinkedHashMap[Symbol, Symbol] = mutable.LinkedHashMap.empty + + private val childThisType = ctx.owner.thisType + private val childThisTree = This(ctx.owner.asClass).withSpan(parent.span) + + def expandDefs(overriddenDecls: Set[Symbol]): List[Tree] = + paramAccessorsMapper.registerParamValuesOf(parent) + val stats = Inlines.defsToInline(parentSym).filterNot(stat => overriddenDecls.contains(stat.symbol)) + stats.map{ + case member: MemberDef => Left((member, inlinedSym(member.symbol))) // Private symbols must be entered before the RHSs are inlined + case stat => Right(stat) + }.map{ + case Left((tree, inlinedSym)) => expandStat(tree, inlinedSym) + case Right(tree) => inlinedRhs(tree) + } + end expandDefs + + def adaptDefs(definitions: List[Tree]): List[Tree] = definitions.mapconserve(defsAdapter(_)) + + protected class InlineTraitTypeMap extends InlinerTypeMap { + override def apply(t: Type) = super.apply(t) match { + case t: ThisType if t.cls == parentSym => childThisType + case t => mapOver(t) + } + } + + protected class InlineTraitTreeMap extends InlinerTreeMap { + override def apply(tree: Tree) = super.apply(tree) match { + case tree: This if tree.symbol == parentSym => + Inlined(EmptyTree, Nil, childThisTree).withSpan(parent.span) + case tree: This => + tree.tpe match { + case thisTpe: ThisType if thisTpe.cls.isInlineTrait => + integrate(This(ctx.owner.asClass).withSpan(parent.span), thisTpe.cls) + case _ => + tree + } + case Select(qual, name) => + paramAccessorsMapper.getParamAccessorName(qual.symbol, name) match { + case Some(newName) => Select(this(qual), newName).withSpan(parent.span) + case None => Select(this(qual), name) + } + case tree => + tree + } + } + + override protected val inlinerTypeMap: InlinerTypeMap = InlineTraitTypeMap() + override protected val inlinerTreeMap: InlinerTreeMap = InlineTraitTreeMap() + + override protected def substFrom: List[Symbol] = innerClassNewSyms.keys.toList + override protected def substTo: List[Symbol] = innerClassNewSyms.values.toList + override protected def inlineCopier: tpd.TreeCopier = new TypedTreeCopier() { + // FIXME it feels weird... Is this correct? + override def Apply(tree: Tree)(fun: Tree, args: List[Tree])(using Context): Apply = + untpd.cpy.Apply(tree)(fun, args).withTypeUnchecked(tree.tpe) + } + + override protected def computeThisBindings(): Unit = () + override protected def canElideThis(tpe: ThisType): Boolean = true + + override protected def inlineCtx(inlineTyper: InlineTyper)(using Context): Context = + ctx.fresh.setTyper(inlineTyper).setNewScope + + extension (sym: Symbol) + private def isTermParamAccessor: Boolean = !sym.isType && sym.is(ParamAccessor) + + private def expandStat(stat: tpd.Tree, inlinedSym: Symbol)(using Context): tpd.Tree = stat match + case stat: ValDef => + inlinedValDef(stat, inlinedSym) + case stat: DefDef => + inlinedDefDef(stat, inlinedSym) + case stat @ TypeDef(_, _: Template) => + inlinedClassDef(stat, inlinedSym.asClass) + case stat: TypeDef => + inlinedTypeDef(stat, inlinedSym) + + private def inlinedSym(sym: Symbol, withoutFlags: FlagSet = EmptyFlags)(using Context): Symbol = + if sym.isClass then inlinedClassSym(sym.asClass, withoutFlags) else inlinedMemberSym(sym, withoutFlags) + + private def inlinedClassSym(sym: ClassSymbol, withoutFlags: FlagSet = EmptyFlags)(using Context): ClassSymbol = + sym.info match { + case clsInfo: ClassInfo => + val typeParams: List[Type] = sym.primaryConstructor.info match { + case poly: PolyType => poly.paramRefs + case _ => Nil + } + // Extend inner class from inline trait to preserve typing + val newParent = ctx.owner.thisType.select(sym).appliedTo(typeParams) + val inlinedSym = newClassSymbol( + ctx.owner, + sym.name, + (sym.flags | Synthetic) &~ withoutFlags, + newCls => { + val ClassInfo(prefix, _, parents, _, selfInfo) = inlinerTypeMap.mapClassInfo(clsInfo) + ClassInfo(prefix, newCls, parents :+ newParent, Scopes.newScope, selfInfo) // TODO fix selfInfo (what to use?) + }, + sym.privateWithin, + spanCoord(parent.span) + ) + innerClassNewSyms.put(sym, inlinedSym) + inlinedSym.entered + case _ => + report.error(s"Class symbol ${sym.show} does not have class info") + sym + } + + private def inlinedMemberSym(sym: Symbol, withoutFlags: FlagSet = EmptyFlags)(using Context): Symbol = + var name = sym.name + var flags = sym.flags | Synthetic + if sym.isTermParamAccessor then flags &~= ParamAccessor + if sym.is(Local) then + name = paramAccessorsMapper.registerNewName(sym) + else + flags |= Override + sym.copy( + owner = ctx.owner, + name = name, + flags = flags &~ withoutFlags, + info = inlinerTypeMap(sym.info), + coord = spanCoord(parent.span)).entered + + private def inlinedValDef(vdef: ValDef, inlinedSym: Symbol)(using Context): ValDef = + val rhs = + paramAccessorsMapper + .getParamAccessorRhs(vdef.symbol.owner, vdef.symbol.name) + .getOrElse(inlinedRhs(vdef, inlinedSym)) + tpd.ValDef(inlinedSym.asTerm, rhs).withSpan(parent.span) + + private def inlinedDefDef(ddef: DefDef, inlinedSym: Symbol)(using Context): DefDef = + val rhsFun: List[List[Tree]] => Tree = + if ddef.symbol.isSetter then + _ => unitLiteral + else + paramss => + val oldParamSyms = ddef.paramss.flatten.map(_.symbol) + val newParamSyms = paramss.flatten.map(_.symbol) + val ddef1 = cpy.DefDef(ddef)(rhs = ddef.rhs.subst(oldParamSyms, newParamSyms)) + inlinedRhs(ddef1, inlinedSym) + tpd.DefDef(inlinedSym.asTerm, rhsFun).withSpan(parent.span) + + private def inlinedPrimaryConstructorDefDef(ddef: DefDef)(using Context): DefDef = + // TODO check if symbol must be copied + val inlinedSym = inlinedMemberSym(ddef.symbol, withoutFlags = Override) + val constr = inlinedDefDef(ddef, inlinedSym) + cpy.DefDef(constr)(tpt = TypeTree(defn.UnitType), rhs = EmptyTree) + + private def inlinedClassDef(clsDef: TypeDef, inlinedCls: ClassSymbol)(using Context): Tree = + val TypeDef(_, tmpl: Template) = clsDef: @unchecked + val (constr, body) = inContext(ctx.withOwner(inlinedCls)) { + val inlinedConstr = inlinedPrimaryConstructorDefDef(tmpl.constr) + val inlinedTmpl = tmpl.body.map { + case stat: TypeDef if stat.symbol.isAllOf(PrivateLocal | Param) => + expandStat(stat, inlinedSym(stat.symbol, withoutFlags = Override)) + case stat => + expandStat(stat, inlinedSym(stat.symbol)) + } + (inlinedConstr, inlinedTmpl) + } + val clsDef1 = tpd.ClassDefWithParents(inlinedCls, constr, tmpl.parents, body) // TODO add correct parent tree + inlined(clsDef1)._2.withSpan(clsDef.span) + + private def inlinedTypeDef(tdef: TypeDef, inlinedSym: Symbol)(using Context): TypeDef = + tpd.TypeDef(inlinedSym.asType).withSpan(parent.span) + + private def inlinedRhs(vddef: ValOrDefDef, inlinedSym: Symbol)(using Context): Tree = + val rhs = vddef.rhs.changeOwner(vddef.symbol, inlinedSym) + inlinedRhs(rhs)(using ctx.withOwner(inlinedSym)) + + private def inlinedRhs(rhs: Tree)(using Context): Tree = + if rhs.isEmpty then + rhs + else + // TODO make version of inlined that does not return bindings? + Inlined(tpd.ref(parentSym), Nil, inlined(rhs)._2).withSpan(parent.span) + + private val defsAdapter = + val typeMap = new DeepTypeMap { + override def apply(tp: Type): Type = tp match { + case TypeRef(_, sym: Symbol) if innerClassNewSyms.contains(sym) => + TypeRef(childThisType, innerClassNewSyms(sym)) + case _ => + mapOver(tp) + } + } + def treeMap(tree: Tree) = tree match { + case ident: Ident if innerClassNewSyms.contains(ident.symbol) => + Ident(innerClassNewSyms(ident.symbol).namedType) + case tdef: TypeDef if tdef.symbol.isClass => + tdef.symbol.info = typeMap(tdef.symbol.info) + tdef + case tree => + tree + } + new TreeTypeMap( + typeMap = typeMap, + treeMap = treeMap, + substFrom = substFrom, + substTo = substTo, + ) + end defsAdapter + + private class ParamAccessorsMapper: + private val paramAccessorsTrees: mutable.Map[Symbol, Map[Name, Tree]] = mutable.Map.empty + private val paramAccessorsNewNames: mutable.Map[(Symbol, Name), Name] = mutable.Map.empty + + def registerParamValuesOf(parent: Tree): Unit = + def allArgs(tree: Tree, acc: Vector[List[Tree]]): List[List[Tree]] = tree match + case Apply(fun, args) => allArgs(fun, acc :+ args) + case TypeApply(fun, _) => allArgs(fun, acc) + case _ => acc.toList + def allParams(info: Type, acc: List[List[Name]]): List[List[Name]] = info match + case mt: MethodType => allParams(mt.resultType, mt.paramNames :: acc) + case pt: PolyType => allParams(pt.resultType, acc) + case _ => acc + val info = + if parent.symbol.isClass then parent.symbol.primaryConstructor.info + else parent.symbol.info + val paramAccessors = allParams(info, Nil).flatten.zip(allArgs(parent, Vector.empty).flatten).toMap + paramAccessorsTrees.put(symbolFromParent(parent), paramAccessors) + + def registerNewName(paramAccessorSym: Symbol): paramAccessorSym.ThisName = + val oldName = paramAccessorSym.name + val newName = oldName.expandedName(parentSym) + paramAccessorsNewNames.put((paramAccessorSym.owner, oldName), newName) + newName + + def getParamAccessorRhs(parent: Symbol, paramAccessorName: Name): Option[Tree] = + paramAccessorsTrees.get(parent).flatMap(_.get(paramAccessorName)) + + def getParamAccessorName(parent: Symbol, paramAccessorName: Name): Option[Name] = + paramAccessorsNewNames.get(parent, paramAccessorName) + end ParamAccessorsMapper + end InlineParentTrait end Inlines diff --git a/compiler/src/dotty/tools/dotc/transform/Constructors.scala b/compiler/src/dotty/tools/dotc/transform/Constructors.scala index b373565489f0..168e8a3e7fa0 100644 --- a/compiler/src/dotty/tools/dotc/transform/Constructors.scala +++ b/compiler/src/dotty/tools/dotc/transform/Constructors.scala @@ -231,7 +231,7 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase = constrStats += intoConstr(stat, sym) } else dropped += sym - case stat @ DefDef(name, _, tpt, _) if stat.symbol.isGetter && !stat.symbol.is(Lazy) => + case stat @ DefDef(name, _, tpt, _) if stat.symbol.isGetter && !stat.symbol.is(Lazy) && !stat.symbol.owner.isInlineTrait => val sym = stat.symbol assert(isRetained(sym), sym) if sym.isConstExprFinalVal then diff --git a/compiler/src/dotty/tools/dotc/transform/Inlining.scala b/compiler/src/dotty/tools/dotc/transform/Inlining.scala index 438374914ccb..53944e0c1532 100644 --- a/compiler/src/dotty/tools/dotc/transform/Inlining.scala +++ b/compiler/src/dotty/tools/dotc/transform/Inlining.scala @@ -81,7 +81,7 @@ class Inlining extends MacroTransform, IdentityDenotTransformer { new TreeTraverser { def traverse(tree: Tree)(using Context): Unit = tree match - case tree: RefTree if !Inlines.inInlineMethod && StagingLevel.level == 0 => + case tree: RefTree if !Inlines.inInlineContext && StagingLevel.level == 0 => assert(!tree.symbol.isInlineMethod, tree.show) case _ => traverseChildren(tree) diff --git a/compiler/src/dotty/tools/dotc/transform/Mixin.scala b/compiler/src/dotty/tools/dotc/transform/Mixin.scala index a9813ec90b1a..66605a5ba972 100644 --- a/compiler/src/dotty/tools/dotc/transform/Mixin.scala +++ b/compiler/src/dotty/tools/dotc/transform/Mixin.scala @@ -96,7 +96,7 @@ object Mixin { * def x_=(y: T) = () * * 4.5 (done in `mixinForwarders`) For every method - * ` def f[Ts](ps1)...(psN): U` in M` that needs to be disambiguated: + * ` def f[Ts](ps1)...(psN): U` in M that needs to be disambiguated: * * def f[Ts](ps1)...(psN): U = super[M].f[Ts](ps1)...(psN) * @@ -186,6 +186,7 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase => sym.isGetter && !wasOneOf(sym, DeferredOrLazy | ParamAccessor) && atPhase(thisPhase) { !sym.setter.exists } && !sym.isConstExprFinalVal + && !sym.owner.isInlineTrait private def makeTraitSetter(getter: TermSymbol)(using Context): Symbol = getter.copy( @@ -256,6 +257,7 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase => transformFollowingDeep(superRef(baseCls.primaryConstructor).appliedToNone) :: Nil def traitInits(mixin: ClassSymbol): List[Tree] = { + if mixin.isInlineTrait then return Nil val argsIt = superCallsAndArgs.get(mixin) match case Some((_, _, args)) => args.iterator case _ => Iterator.empty @@ -306,7 +308,7 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase => def setters(mixin: ClassSymbol): List[Tree] = val mixinSetters = mixin.info.decls.filter { sym => - sym.isSetter && (!wasOneOf(sym, Deferred) || sym.name.is(TraitSetterName)) + sym.isSetter && (!wasOneOf(sym, Deferred) || sym.name.is(TraitSetterName)) && !sym.owner.isInlineTrait } for (setter <- mixinSetters) yield transformFollowing(DefDef(mkForwarderSym(setter.asTerm), unitLiteral.withSpan(cls.span))) diff --git a/compiler/src/dotty/tools/dotc/transform/PickleQuotes.scala b/compiler/src/dotty/tools/dotc/transform/PickleQuotes.scala index 78d8c8b43a28..1cbd32183c83 100644 --- a/compiler/src/dotty/tools/dotc/transform/PickleQuotes.scala +++ b/compiler/src/dotty/tools/dotc/transform/PickleQuotes.scala @@ -80,9 +80,9 @@ class PickleQuotes extends MacroTransform { override def checkPostCondition(tree: Tree)(using Context): Unit = tree match case tree: Quote => - assert(Inlines.inInlineMethod) + assert(Inlines.inInlineContext) case tree: Splice => - assert(Inlines.inInlineMethod) + assert(Inlines.inInlineContext) case _ => override def run(using Context): Unit = diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 08409b2c1122..6f9444fb7a21 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -243,6 +243,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => tree match case tree: ValOrDefDef if !sym.is(Synthetic) => checkInferredWellFormed(tree.tpt) + if tree.symbol.owner.isInlineTrait then checkInlTraitPrivateMemberIsLocal(tree) if sym.is(Method) then if sym.isSetter then sym.keepAnnotationsCarrying(thisPhase, Set(defn.SetterMetaAnnot)) @@ -289,6 +290,9 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => => Checking.checkAppliedTypesIn(tree) case _ => + private def checkInlTraitPrivateMemberIsLocal(tree: Tree)(using Context): Unit = + if tree.symbol.owner.isInlineTrait && tree.symbol.isAllOf(Private, butNot = Local) then + report.error(em"implementation restriction: inline traits cannot have non-local private members", tree.srcPos) private def transformSelect(tree: Select, targs: List[Tree])(using Context): Tree = { val qual = tree.qualifier @@ -593,6 +597,8 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => val tree1 = cpy.DefDef(tree)(tpt = makeOverrideTypeDeclared(tree.symbol, tree.tpt)) processValOrDefDef(superAcc.wrapDefDef(tree1)(super.transform(tree1).asInstanceOf[DefDef])) case tree: TypeDef => + if tree.symbol.isInlineTrait then + ctx.compilationUnit.needsInlining = true // Transform inner classes to traits registerIfHasMacroAnnotations(tree) val sym = tree.symbol if (sym.isClass) @@ -604,6 +610,8 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => tree.rhs match case impl: Template => for parent <- impl.parents do + if Inlines.symbolFromParent(parent).isInlineTrait then + ctx.compilationUnit.needsInlining = true Checking.checkTraitInheritance(parent.tpe.classSymbol, sym.asClass, parent.srcPos) // Constructor parameters are in scope when typing a parent. // While they can safely appear in a parent tree, to preserve diff --git a/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala new file mode 100644 index 000000000000..5d9db954201a --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala @@ -0,0 +1,48 @@ +package dotty.tools.dotc +package transform + +import core._ +import Contexts._ +import DenotTransformers.SymTransformer +import Flags._ +import SymDenotations._ +import Symbols._ +import MegaPhase.MiniPhase +import ast.tpd + +class PruneInlineTraits extends MiniPhase with SymTransformer { thisTransform => + import tpd._ + import PruneInlineTraits._ + + override def phaseName: String = PruneInlineTraits.name + + override def description: String = PruneInlineTraits.description + + override def transformSym(sym: SymDenotation)(using Context): SymDenotation = + if isEraseable(sym) then sym.copySymDenotation(initFlags = sym.flags | Deferred) + else sym + + override def transformValDef(tree: ValDef)(using Context): ValDef = + if isEraseable(tree.symbol) then cpy.ValDef(tree)(rhs = EmptyTree) + else tree + + override def transformDefDef(tree: DefDef)(using Context): DefDef = + if isEraseable(tree.symbol) then cpy.DefDef(tree)(rhs = EmptyTree) + else tree + + private def isEraseable(sym: SymDenotation)(using Context): Boolean = + !sym.isType + && !sym.isConstructor + && !sym.is(Param) + && !sym.is(ParamAccessor) + && !sym.is(Private) + && !sym.isLocalDummy + && sym.owner.isInlineTrait +} + +object PruneInlineTraits { + import tpd._ + + val name: String = "pruneInlineTraits" + val description: String = "drop rhs definitions in inline traits" +} diff --git a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala new file mode 100644 index 000000000000..460b57cf5ab8 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala @@ -0,0 +1,123 @@ +package dotty.tools.dotc +package transform + +import core._ +import Flags._ +import Contexts._ +import Symbols._ +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.ast.Trees._ +import dotty.tools.dotc.quoted._ +import dotty.tools.dotc.inlines.Inlines +import dotty.tools.dotc.ast.TreeMapWithImplicits +import dotty.tools.dotc.core.DenotTransformers.SymTransformer +import dotty.tools.dotc.staging.StagingLevel +import dotty.tools.dotc.core.SymDenotations.SymDenotation +import dotty.tools.dotc.core.StdNames.{str, nme} +import dotty.tools.dotc.core.Types.* +import dotty.tools.dotc.core.Names.{Name, TermName} + +import scala.collection.mutable.ListBuffer + +class SpecializeInlineTraits extends MacroTransform, SymTransformer { + + import tpd._ + + override def phaseName: String = SpecializeInlineTraits.name + + override def description: String = SpecializeInlineTraits.description + + override def changesMembers: Boolean = true + + override def changesParents: Boolean = true + + override def run(using Context): Unit = + try super.run + catch case _: CompilationUnit.SuspendException => () + + override def newTransformer(using Context): Transformer = new Transformer { + override def transform(tree: Tree)(using Context): Tree = tree match { + case tree: TypeDef if tree.symbol.isInlineTrait => + transformInlineTrait(tree) + case tree: TypeDef if Inlines.needsInlining(tree) => + val tree1 = super.transform(tree).asInstanceOf[TypeDef] + if tree1.tpe.isError then tree1 + else if tree1.symbol.isInlineTrait then transformInlineTrait(tree1) + else Inlines.inlineParentInlineTraits(tree1) + case _ => super.transform(tree) + } + } + + override def transformSym(symd: SymDenotation)(using Context): SymDenotation = + if symd.isClass && symd.owner.isInlineTrait && !symd.is(Module) then + symd.copySymDenotation(name = SpecializeInlineTraits.newInnerClassName(symd.name), initFlags = (symd.flags &~ Final) | Trait) + else + symd + + override def checkPostCondition(tree: Tree)(using Context): Unit = + tree match { + // TODO check that things are inlined properly + case _ => + } + + private def transformInlineTrait(inlineTrait: TypeDef)(using Context): TypeDef = + val tpd.TypeDef(_, tmpl: Template) = inlineTrait: @unchecked + val body1 = tmpl.body.flatMap { + case innerClass: TypeDef if innerClass.symbol.isClass => + val newTrait = makeTraitFromInnerClass(innerClass) + val newType = makeTypeFromInnerClass(inlineTrait.symbol, innerClass, newTrait.symbol) + List(newTrait, newType) + case member: MemberDef => + List(member) + case _ => + // Remove non-memberdefs, as they are normally placed into $init() + Nil + } + val tmpl1 = cpy.Template(tmpl)(body = body1) + cpy.TypeDef(inlineTrait)(rhs = tmpl1) + end transformInlineTrait + + private def makeTraitFromInnerClass(innerClass: TypeDef)(using Context): TypeDef = + val TypeDef(name, tmpl: Template) = innerClass: @unchecked + val newInnerParents = tmpl.parents.mapConserve(ConcreteParentStripper.apply) + val tmpl1 = cpy.Template(tmpl)(parents = newInnerParents) // TODO .withType(???) + val newTrait = cpy.TypeDef(innerClass)(name = SpecializeInlineTraits.newInnerClassName(name), rhs = tmpl1) + newTrait.symbol.setFlag(Synthetic) + newTrait + end makeTraitFromInnerClass + + private def makeTypeFromInnerClass(parentSym: Symbol, innerClass: TypeDef, newTraitSym: Symbol)(using Context): TypeDef = + val upperBound = innerClass.symbol.primaryConstructor.info match { + case _: MethodType => + newTraitSym.typeRef + case poly: PolyType => + HKTypeLambda(poly.paramNames)(tl => poly.paramInfos, tl => newTraitSym.typeRef.appliedTo(tl.paramRefs.head)) + } + val newTypeSym = newSymbol( + owner = parentSym, + name = newTraitSym.name.asTypeName, + flags = innerClass.symbol.flags & (Private | Protected) | Synthetic, + info = TypeBounds.upper(upperBound), + privateWithin = innerClass.symbol.privateWithin, + coord = innerClass.symbol.coord, + nestingLevel = innerClass.symbol.nestingLevel, + ).asType + TypeDef(newTypeSym) + end makeTypeFromInnerClass + + private object ConcreteParentStripper extends TreeAccumulator[Tree] { + def apply(tree: Tree)(using Context): Tree = apply(tree, tree) + + override def apply(x: Tree, tree: Tree)(using Context): Tree = tree match { + case ident: Ident => ident + case tpt: TypeTree => tpt + case _ => foldOver(x, tree) + } + } +} + +object SpecializeInlineTraits: + val name: String = "specializeInlineTraits" + val description: String = "inline the code of inline traits and specialize calls to their members" + + private[transform] def newInnerClassName(name: Name): name.ThisName = name ++ str.INLINE_TRAIT_INNER_CLASS_SUFFIX diff --git a/compiler/src/dotty/tools/dotc/transform/Splicing.scala b/compiler/src/dotty/tools/dotc/transform/Splicing.scala index 1293d06cd18a..15310a17c72c 100644 --- a/compiler/src/dotty/tools/dotc/transform/Splicing.scala +++ b/compiler/src/dotty/tools/dotc/transform/Splicing.scala @@ -89,8 +89,8 @@ class Splicing extends MacroTransform: case tree: Quote => val body1 = QuoteTransformer().transform(tree.body)(using quoteContext) cpy.Quote(tree)(body1, tree.tags) - case tree: DefDef if tree.symbol.is(Inline) => - // Quotes in inlined methods are only pickled after they are inlined. + case _: DefDef | _: TypeDef if tree.symbol.is(Inline) => + // Quotes in inlined methods and traits are only pickled after they are inlined. tree case _ => super.transform(tree) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 503c88a5ff77..e208cd4a877a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -689,7 +689,7 @@ object Checking { if (sym.isConstructor && !sym.isPrimaryConstructor && sym.owner.is(Trait, butNot = JavaDefined)) val addendum = if ctx.settings.Ydebug.value then s" ${sym.owner.flagsString}" else "" fail(em"Traits cannot have secondary constructors$addendum") - checkApplicable(Inline, sym.isTerm && !sym.is(Module) && !sym.isMutableVarOrAccessor) + checkApplicable(Inline, sym.isTerm && !sym.is(Module) && !sym.isMutableVarOrAccessor || sym.is(Trait)) checkApplicable(Lazy, !sym.isOneOf(Method | Mutable)) if (sym.isType && !sym.isOneOf(Deferred | JavaDefined)) for (cls <- sym.allOverriddenSymbols.filter(_.isClass)) { diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 9c929cdee318..51b3b3271bd7 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -392,6 +392,8 @@ object RefChecks { * of class `clazz` are met. */ def checkOverride(member: Symbol, other: Symbol): Unit = + def isInlinedFromInlineTrait = other.owner.isAllOf(InlineTrait) && member.is(Synthetic) + def memberType(self: Type) = if (member.isClass) TypeAlias(member.typeRef.etaExpand) else self.memberInfo(member) @@ -477,12 +479,13 @@ object RefChecks { def overrideTargetNameError() = val otherTargetName = i"@targetName(${other.targetName})" - if member.hasTargetName(member.name) then - overrideError(i"misses a target name annotation $otherTargetName") - else if other.hasTargetName(other.name) then - overrideError(i"should not have a @targetName annotation since the overridden member hasn't one either") - else - overrideError(i"has a different target name annotation; it should be $otherTargetName") + if !isInlinedFromInlineTrait then + if member.hasTargetName(member.name) then + overrideError(i"misses a target name annotation $otherTargetName") + else if other.hasTargetName(other.name) then + overrideError(i"should not have a @targetName annotation since the overridden member hasn't one either") + else + overrideError(i"has a different target name annotation; it should be $otherTargetName") //Console.println(infoString(member) + " overrides " + infoString(other) + " in " + clazz);//DEBUG @@ -518,13 +521,13 @@ object RefChecks { // direct overrides were already checked on completion (see Checking.chckWellFormed) // the test here catches indirect overriddes between two inherited base types. overrideError("cannot be used here - class definitions cannot be overridden") - else if (other.isOpaqueAlias) + else if (other.isOpaqueAlias && !isInlinedFromInlineTrait) // direct overrides were already checked on completion (see Checking.chckWellFormed) // the test here catches indirect overriddes between two inherited base types. overrideError("cannot be used here - opaque type aliases cannot be overridden") else if (!other.is(Deferred) && member.isClass) overrideError("cannot be used here - classes can only override abstract types") - else if other.isEffectivelyFinal then // (1.2) + else if (other.isEffectivelyFinal && !isInlinedFromInlineTrait) then // (1.2) overrideError(i"cannot override final member ${other.showLocated}") else if (member.is(ExtensionMethod) && !other.is(ExtensionMethod)) // (1.3) overrideError("is an extension method, cannot override a normal method") @@ -567,7 +570,7 @@ object RefChecks { overrideError("needs `override` modifier") else if (other.is(AbsOverride) && other.isIncompleteIn(clazz) && !member.is(AbsOverride)) overrideError("needs `abstract override` modifiers") - else if isMarkedOverride(member) && other.isMutableVarOrAccessor then + else if isMarkedOverride(member) && other.isMutableVarOrAccessor && !isInlinedFromInlineTrait then overrideError("cannot override a mutable variable") else if isMarkedOverride(member) && !(member.owner.thisType.baseClasses.exists(_.isSubClass(other.owner))) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 4beb520b33a0..566cee9286db 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -3443,6 +3443,29 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer addAccessorDefs(cls, typedStats(impl.body, dummy)(using ctx.inClassContext(self1.symbol))._1))) + if !ctx.isAfterTyper && cls.isInlineTrait then + body1.map(_.symbol).filter(_.isInlineTrait).foreach(innerInlTrait => + report.error( + em"Implementation restriction: an inline trait cannot be defined inside of another inline trait", + innerInlTrait.srcPos + ) + ) + val membersToInline = body1.filter(member => Inlines.isInlineableFromInlineTrait(cls, member)) + membersToInline.foreach { + case tdef: TypeDef if tdef.symbol.isClass => + def rec(paramss: List[List[Symbol]]): Unit = paramss match { + case (param :: _) :: _ if param.isTerm => + report.error(em"Implementation restriction: inner classes inside inline traits cannot have term parameters", param.srcPos) + case _ :: paramss => + rec(paramss) + case _ => + } + rec(tdef.symbol.primaryConstructor.paramSymss) + case _ => + } + val wrappedMembersToInline = Block(membersToInline, unitLiteral).withSpan(cdef.span) + PrepareInlineable.registerInlineInfo(cls, wrappedMembersToInline) + checkNoDoubleDeclaration(cls) val impl1 = cpy.Template(impl)(constr1, parents1, Nil, self1, body1) .withType(dummy.termRef) diff --git a/tests/disabled/pos/inline-trait-4-inner-class.scala b/tests/disabled/pos/inline-trait-4-inner-class.scala new file mode 100644 index 000000000000..b3c25e75de4c --- /dev/null +++ b/tests/disabled/pos/inline-trait-4-inner-class.scala @@ -0,0 +1,20 @@ +inline trait Options[+T]: + sealed trait Option: + def get: T + def isEmpty: Boolean + + class Some(x: T) extends Option: + def get: T = x + def isEmpty: Boolean = false + + object None extends Option: + def get: T = throw new NoSuchElementException("None.get") + def isEmpty: Boolean = true +end Options + +object IntOptions extends Options[Int] +import IntOptions._ + +val o1: Option = Some(1) // specialized +val o2: Option = None +val x1: Int = o1.get // no unboxing diff --git a/tests/disabled/pos/inline-trait-body-class-abstract.scala b/tests/disabled/pos/inline-trait-body-class-abstract.scala new file mode 100644 index 000000000000..4704b324e26d --- /dev/null +++ b/tests/disabled/pos/inline-trait-body-class-abstract.scala @@ -0,0 +1,10 @@ +inline trait A: + class InnerA: + def foo(): Int + def bar = foo() + 1 + +class B extends A: + class InnerB extends InnerA: + def foo(): Int = -23 + + def f = InnerB().bar \ No newline at end of file diff --git a/tests/disabled/pos/inline-trait-body-class-enum.scala b/tests/disabled/pos/inline-trait-body-class-enum.scala new file mode 100644 index 000000000000..a114ff396067 --- /dev/null +++ b/tests/disabled/pos/inline-trait-body-class-enum.scala @@ -0,0 +1,6 @@ +inline trait A: + enum Inner: + case A, B, C + +class B extends A: + def f = Inner.B \ No newline at end of file diff --git a/tests/disabled/pos/inline-trait-body-class-object.scala b/tests/disabled/pos/inline-trait-body-class-object.scala new file mode 100644 index 000000000000..dc990c69573c --- /dev/null +++ b/tests/disabled/pos/inline-trait-body-class-object.scala @@ -0,0 +1,6 @@ +inline trait A[T]: + object Inner: + val x: T = ??? + +class B extends A[Int]: + def i: Int = Inner.x diff --git a/tests/disabled/pos/inline-trait-body-trait-generic.scala b/tests/disabled/pos/inline-trait-body-trait-generic.scala new file mode 100644 index 000000000000..999dd0c8c1ea --- /dev/null +++ b/tests/disabled/pos/inline-trait-body-trait-generic.scala @@ -0,0 +1,6 @@ +inline trait A[T]: + trait InnerA[U]: + def x: (T, U) = ??? + +class B extends A[Int]: + class InnerB extends InnerA[String] \ No newline at end of file diff --git a/tests/neg/i2421.scala b/tests/neg/i2421.scala index dc8e229f38f0..bcf3da6dbb36 100644 --- a/tests/neg/i2421.scala +++ b/tests/neg/i2421.scala @@ -1,7 +1,6 @@ inline object Foo // OK (error would be detected later, in PostTyper) inline class Bar // error: modifier(s) `inline' incompatible with type definition inline abstract class Baz // error: modifier(s) `inline' incompatible with type definition -inline trait Qux // error: modifier(s) `inline' incompatible with type definition object Quux { inline type T // error: modifier(s) `inline' incompatible with type definition diff --git a/tests/neg/inline-trait-body-class-case.scala b/tests/neg/inline-trait-body-class-case.scala new file mode 100644 index 000000000000..20a58fd4b267 --- /dev/null +++ b/tests/neg/inline-trait-body-class-case.scala @@ -0,0 +1,5 @@ +inline trait A: + case class Inner(val x: Int) // error + +class B extends A: + def f = Inner(17).x \ No newline at end of file diff --git a/tests/neg/inline-trait-body-class-generic.scala b/tests/neg/inline-trait-body-class-generic.scala new file mode 100644 index 000000000000..bd0fc5f3de42 --- /dev/null +++ b/tests/neg/inline-trait-body-class-generic.scala @@ -0,0 +1,6 @@ +inline trait A[T]: + class Inner[U](u: U): // error + val x: (T, U) = (???, u) + def f: (T, String) = Inner("U").x + +class B extends A[Int] \ No newline at end of file diff --git a/tests/neg/inline-trait-body-class-params.scala b/tests/neg/inline-trait-body-class-params.scala new file mode 100644 index 000000000000..e60da2536a69 --- /dev/null +++ b/tests/neg/inline-trait-body-class-params.scala @@ -0,0 +1,5 @@ +inline trait A: + class Inner(val x: Int) // error + +class B extends A: + def f = Inner(17).x \ No newline at end of file diff --git a/tests/neg/inline-trait-body-override-def-final.scala b/tests/neg/inline-trait-body-override-def-final.scala new file mode 100644 index 000000000000..c67540a09924 --- /dev/null +++ b/tests/neg/inline-trait-body-override-def-final.scala @@ -0,0 +1,5 @@ +inline trait A: + final def f(x: Int) = x + +class B extends A: + override final def f(x: Int) = x + 1 // error \ No newline at end of file diff --git a/tests/neg/inline-trait-body-override-val-final.scala b/tests/neg/inline-trait-body-override-val-final.scala new file mode 100644 index 000000000000..32d20c4b6696 --- /dev/null +++ b/tests/neg/inline-trait-body-override-val-final.scala @@ -0,0 +1,5 @@ +inline trait A: + final val x = 1 + +class B extends A: + override final val x = 2 // error \ No newline at end of file diff --git a/tests/neg/inline-trait-body-override-var.scala b/tests/neg/inline-trait-body-override-var.scala new file mode 100644 index 000000000000..4ad8b79db477 --- /dev/null +++ b/tests/neg/inline-trait-body-override-var.scala @@ -0,0 +1,5 @@ +inline trait A: + var x: Int = 1 + +class B extends A: + override var x = 2 // error diff --git a/tests/neg/inline-trait-body-private-name-collision.scala b/tests/neg/inline-trait-body-private-name-collision.scala new file mode 100644 index 000000000000..1e3d14480d82 --- /dev/null +++ b/tests/neg/inline-trait-body-private-name-collision.scala @@ -0,0 +1,3 @@ +inline trait A: + private val x: Int = 1 // error + def eq(o: A) = o.x == x diff --git a/tests/neg/inline-trait-body-trait-inline.scala b/tests/neg/inline-trait-body-trait-inline.scala new file mode 100644 index 000000000000..82144e4c9c4d --- /dev/null +++ b/tests/neg/inline-trait-body-trait-inline.scala @@ -0,0 +1,7 @@ +inline trait A[T]: + inline trait InnerA[U]: // error + val x: (T, U) = ??? + +class B extends A[Int]: + class InnerB extends InnerA[String] + def f: (Int, String) = InnerB().x \ No newline at end of file diff --git a/tests/neg/inline-trait-body-trait-parameter.scala b/tests/neg/inline-trait-body-trait-parameter.scala new file mode 100644 index 000000000000..ec11c5b9052e --- /dev/null +++ b/tests/neg/inline-trait-body-trait-parameter.scala @@ -0,0 +1,7 @@ +inline trait A[T]: + trait InnerAType[T >: Int <: AnyVal] + trait InnerATypes[T <: AnyVal, U <: T] + trait InnerATerm(i: Int) // error + trait InnerATerms(i: Int, j: Double) // error + trait InnerATermsCurried(i: Int, j: Double)(k: String) // error + trait InnerAAllCurried[T, U](i: T, j: U)(k: (T, U)) // error \ No newline at end of file diff --git a/tests/neg/inline-trait-body-trait-term-parameters.scala b/tests/neg/inline-trait-body-trait-term-parameters.scala new file mode 100644 index 000000000000..ef7c96baf6c4 --- /dev/null +++ b/tests/neg/inline-trait-body-trait-term-parameters.scala @@ -0,0 +1,7 @@ + +inline trait A[T]: + trait InnerA(t: T): // error + def x: T = t + +class B extends A[Int]: + class InnerB extends InnerA(???) \ No newline at end of file diff --git a/tests/pos/inline-trait-1-simple-trait.scala b/tests/pos/inline-trait-1-simple-trait.scala new file mode 100644 index 000000000000..5a58b7ea2d4c --- /dev/null +++ b/tests/pos/inline-trait-1-simple-trait.scala @@ -0,0 +1,36 @@ +inline trait A: + type X = String + + val x: Int = 3 + val y: Int = x + z + private val z: Int = 1 + + def f: Int = g + def f(x: Int): Int = x + def f[T](x: T): Int = 2 + + private def g = 1 + protected[this] def p = 123 + private[this] def pp = 123456 + + def xx: X = "foo".asInstanceOf[X] +end A + +class B extends A: + /* + override type X = String + + override val x: Int = 3 + override val y: Int = this.x.+(this.z) + + private[this] val z: Int = 1 + + override def f: Int = this.g + override def f(x: Int): Int = x + override def f[T](x: T): Int = 2 + + private[this] def g: Int = 1 + + override def xx: X = "foo".asInstanceOf[X] + */ +end B diff --git a/tests/pos/inline-trait-2-generic-trait.scala b/tests/pos/inline-trait-2-generic-trait.scala new file mode 100644 index 000000000000..0df576c9c124 --- /dev/null +++ b/tests/pos/inline-trait-2-generic-trait.scala @@ -0,0 +1,13 @@ +inline trait A[T]: + def f: T = f + def f(x: T): T = x + def f[U <: T](x: U, y: T): T = x +end A + +class B extends A[Int]: + /* + override def f: Int = this.f + override def f(x: Int): Int = x + override def f[U <: Int](x: U, y: Int): Int = x + */ +end B diff --git a/tests/pos/inline-trait-3-trait-params.scala b/tests/pos/inline-trait-3-trait-params.scala new file mode 100644 index 000000000000..e2119bf9757b --- /dev/null +++ b/tests/pos/inline-trait-3-trait-params.scala @@ -0,0 +1,12 @@ +inline trait A(a: Int): + def f: Int = a + def g(b: Int): Int = a + b +end A + +class B extends A(4): + /* + private val a: Int = 4 + override def f: Int = this.a + override def g(x: Int): Int = this.a.+(b) + */ +end B diff --git a/tests/pos/inline-trait-3-trait-with-params.scala b/tests/pos/inline-trait-3-trait-with-params.scala new file mode 100644 index 000000000000..a497ebbcb7c1 --- /dev/null +++ b/tests/pos/inline-trait-3-trait-with-params.scala @@ -0,0 +1,13 @@ +inline trait A[T](a: T): + def f: T = a + def f(x: T): T = x + def f[U <: T](x: U, y: T): T = x +end A + +class B extends A[Int](3): + /* + override def f: Int = ??? + override def f(x: Int): Int = ??? + override def f[U <: Int](x: U, y: Int): Int = ??? + */ +end B diff --git a/tests/pos/inline-trait-4-no-inner-class.scala b/tests/pos/inline-trait-4-no-inner-class.scala new file mode 100644 index 000000000000..063f8e654fe5 --- /dev/null +++ b/tests/pos/inline-trait-4-no-inner-class.scala @@ -0,0 +1,24 @@ +inline trait Option[+T]: + def get: T + def isEmpty: Boolean +end Option + +inline trait Some[+T](x: T) extends Option[T]: + def get: T = x + def isEmpty: Boolean = false +end Some + +inline trait None extends Option[Nothing]: + def get: Nothing = throw new NoSuchElementException("None.get") + def isEmpty: Boolean = true +end None + +sealed trait IntOption extends Option[Int] +class IntSome(i: Int) extends IntOption, Some[Int](i) +object IntNone extends IntOption, None + +val o1: IntOption = IntSome(1) // specialized +val o2: IntOption = IntNone +val o3: Some[Int] = IntSome(1) // non-specialized +val x1: Int = o1.get // no unboxing +val x3: Int = o3.get // unboxing diff --git a/tests/pos/inline-trait-body-abstract-def.scala b/tests/pos/inline-trait-body-abstract-def.scala new file mode 100644 index 000000000000..5b675a845236 --- /dev/null +++ b/tests/pos/inline-trait-body-abstract-def.scala @@ -0,0 +1,6 @@ +inline trait A[T]: + def x: T + +class B extends A[Int]: + def x = 1 + def f: Int = x \ No newline at end of file diff --git a/tests/pos/inline-trait-body-class-extends-inline-trait.scala b/tests/pos/inline-trait-body-class-extends-inline-trait.scala new file mode 100644 index 000000000000..1f1a3836fa98 --- /dev/null +++ b/tests/pos/inline-trait-body-class-extends-inline-trait.scala @@ -0,0 +1,11 @@ +inline trait A: + class Inner extends Trait[Int]: + val x = 1 + +inline trait Trait[T]: + def f(x: T): T = x + +class B extends A: + val inner = Inner() + def x = inner.x + def f = inner.f(x) \ No newline at end of file diff --git a/tests/pos/inline-trait-body-class-sealed.scala b/tests/pos/inline-trait-body-class-sealed.scala new file mode 100644 index 000000000000..ff28d164c617 --- /dev/null +++ b/tests/pos/inline-trait-body-class-sealed.scala @@ -0,0 +1,7 @@ +inline trait A: + sealed class InnerA: + val x = 1 + +class B extends A: + class InnerB extends InnerA + def f = InnerB().x \ No newline at end of file diff --git a/tests/pos/inline-trait-body-class-simple.scala b/tests/pos/inline-trait-body-class-simple.scala new file mode 100644 index 000000000000..56476b96b44b --- /dev/null +++ b/tests/pos/inline-trait-body-class-simple.scala @@ -0,0 +1,6 @@ +inline trait A: + class Inner: + val x = 1 + +class B extends A: + def f = Inner().x \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-context-bound.scala b/tests/pos/inline-trait-body-def-context-bound.scala new file mode 100644 index 000000000000..c932500b62c5 --- /dev/null +++ b/tests/pos/inline-trait-body-def-context-bound.scala @@ -0,0 +1,6 @@ +inline trait A: + given List[String] = "AAA" :: Nil + def foo[T: List](x: T): T = summon[List[T]].headOption.getOrElse(x) + +class B extends A: + def f = foo("BBB") \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-curried-params.scala b/tests/pos/inline-trait-body-def-curried-params.scala new file mode 100644 index 000000000000..d253aa035331 --- /dev/null +++ b/tests/pos/inline-trait-body-def-curried-params.scala @@ -0,0 +1,8 @@ +inline trait A: + def x(foo: Int)(bar: Int) = + foo + bar + + def y(foo: Int) = x(foo)(foo) + +class B extends A: + def f = x(1)(2) diff --git a/tests/pos/inline-trait-body-def-extension-method.scala b/tests/pos/inline-trait-body-def-extension-method.scala new file mode 100644 index 000000000000..77b39dc502aa --- /dev/null +++ b/tests/pos/inline-trait-body-def-extension-method.scala @@ -0,0 +1,6 @@ +inline trait A: + extension [T](x: T) + def foo[U](y: U)(z: T): (T, U, T) = (x, y, z) + +class B extends A: + def f = 1.foo("2")(3) \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-final.scala b/tests/pos/inline-trait-body-def-final.scala new file mode 100644 index 000000000000..18bdeed55a1f --- /dev/null +++ b/tests/pos/inline-trait-body-def-final.scala @@ -0,0 +1,4 @@ +inline trait A: + final def f(x: Int) = x + +class B extends A \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-generic-singleton.scala b/tests/pos/inline-trait-body-def-generic-singleton.scala new file mode 100644 index 000000000000..2862ff3b1e93 --- /dev/null +++ b/tests/pos/inline-trait-body-def-generic-singleton.scala @@ -0,0 +1,7 @@ +inline trait A: + def foo: 3 = 3 + def bar[T](x: T): T = x + +class B extends A: + def f: "A" = bar("A") + def g: 3 = foo \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-generic.scala b/tests/pos/inline-trait-body-def-generic.scala new file mode 100644 index 000000000000..75d932f2f04a --- /dev/null +++ b/tests/pos/inline-trait-body-def-generic.scala @@ -0,0 +1,6 @@ +inline trait A: + def foo[T] = 1 + def bar: [U <: A] => U => Int = [U <: A] => (x: U) => x.foo + +class B extends A: + def f = foo[String] + bar(this) \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-implicit.scala b/tests/pos/inline-trait-body-def-implicit.scala new file mode 100644 index 000000000000..51381b394cef --- /dev/null +++ b/tests/pos/inline-trait-body-def-implicit.scala @@ -0,0 +1,6 @@ +inline trait A: + implicit val x: String = "AAA" + def foo(implicit s: String): String = s + s + +class B extends A: + def f = foo \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-inline-abstract.scala b/tests/pos/inline-trait-body-def-inline-abstract.scala new file mode 100644 index 000000000000..c385c397a3f0 --- /dev/null +++ b/tests/pos/inline-trait-body-def-inline-abstract.scala @@ -0,0 +1,6 @@ +inline trait A: + inline def x: Int + +class B extends A: + inline def x = 1 + def f = x \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-inline-compiletime.scala b/tests/pos/inline-trait-body-def-inline-compiletime.scala new file mode 100644 index 000000000000..975bffc76ac2 --- /dev/null +++ b/tests/pos/inline-trait-body-def-inline-compiletime.scala @@ -0,0 +1,9 @@ +import scala.compiletime.* + +inline trait A: + inline def f[T <: String] = + inline if constValue[T] == "I consent" then "All is OK!" + else error("You must consent!") + +class B extends A: + val x = f["I consent"] \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-inline-transparent.scala b/tests/pos/inline-trait-body-def-inline-transparent.scala new file mode 100644 index 000000000000..7e48cdec1e0f --- /dev/null +++ b/tests/pos/inline-trait-body-def-inline-transparent.scala @@ -0,0 +1,5 @@ +inline trait A: + transparent inline def x = 1 + +class B extends A: + def f: 1 = x \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-inline.scala b/tests/pos/inline-trait-body-def-inline.scala new file mode 100644 index 000000000000..8b3751398365 --- /dev/null +++ b/tests/pos/inline-trait-body-def-inline.scala @@ -0,0 +1,5 @@ +inline trait A: + inline def x = 1 + +class B extends A: + def f = x \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-lambda.scala b/tests/pos/inline-trait-body-def-lambda.scala new file mode 100644 index 000000000000..4eec58d40749 --- /dev/null +++ b/tests/pos/inline-trait-body-def-lambda.scala @@ -0,0 +1,5 @@ +inline trait A: + def f = (i: Int) => i + +class B extends A: + def g = f \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-local-val.scala b/tests/pos/inline-trait-body-def-local-val.scala new file mode 100644 index 000000000000..c503432ec175 --- /dev/null +++ b/tests/pos/inline-trait-body-def-local-val.scala @@ -0,0 +1,7 @@ +inline trait A: + def f = + val foo = 1 + foo + +class B extends A: + def g = f \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-params.scala b/tests/pos/inline-trait-body-def-params.scala new file mode 100644 index 000000000000..79018db761a3 --- /dev/null +++ b/tests/pos/inline-trait-body-def-params.scala @@ -0,0 +1,7 @@ +inline trait A: + def x(foo: Int, bar: Unit) = + bar + foo + +class B extends A: + def f = x(1, ()) \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-parens.scala b/tests/pos/inline-trait-body-def-parens.scala new file mode 100644 index 000000000000..17c80e9d8ab7 --- /dev/null +++ b/tests/pos/inline-trait-body-def-parens.scala @@ -0,0 +1,5 @@ +inline trait A: + def x() = 1 + +class B extends A: + def f = x() \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-simple.scala b/tests/pos/inline-trait-body-def-simple.scala new file mode 100644 index 000000000000..edffe37168be --- /dev/null +++ b/tests/pos/inline-trait-body-def-simple.scala @@ -0,0 +1,5 @@ +inline trait A: + def x = 1 + +class B extends A: + def f = x \ No newline at end of file diff --git a/tests/pos/inline-trait-body-def-using.scala b/tests/pos/inline-trait-body-def-using.scala new file mode 100644 index 000000000000..1436962f6b48 --- /dev/null +++ b/tests/pos/inline-trait-body-def-using.scala @@ -0,0 +1,6 @@ +inline trait A: + given String = "AAA" + def foo(using s: String): String = s + s + +class B extends A: + def f = foo \ No newline at end of file diff --git a/tests/pos/inline-trait-body-lazy-val.scala b/tests/pos/inline-trait-body-lazy-val.scala new file mode 100644 index 000000000000..37ab5c249375 --- /dev/null +++ b/tests/pos/inline-trait-body-lazy-val.scala @@ -0,0 +1,5 @@ +inline trait A: + lazy val x = 1 + +class B extends A: + def f = x \ No newline at end of file diff --git a/tests/pos/inline-trait-body-macro-suspend/Macro.scala b/tests/pos/inline-trait-body-macro-suspend/Macro.scala new file mode 100644 index 000000000000..8aa252d8b7d0 --- /dev/null +++ b/tests/pos/inline-trait-body-macro-suspend/Macro.scala @@ -0,0 +1,10 @@ +import scala.quoted.* + +inline def foo(): Int = + ${fooImpl} + +def fooImpl(using Quotes): Expr[Int] = + '{3} + +inline trait A: + val i: Int = foo() diff --git a/tests/pos/inline-trait-body-macro-suspend/Test.scala b/tests/pos/inline-trait-body-macro-suspend/Test.scala new file mode 100644 index 000000000000..5079eecd497d --- /dev/null +++ b/tests/pos/inline-trait-body-macro-suspend/Test.scala @@ -0,0 +1,2 @@ +class B extends A: + def test = foo() \ No newline at end of file diff --git a/tests/pos/inline-trait-body-macro/Macro_1.scala b/tests/pos/inline-trait-body-macro/Macro_1.scala new file mode 100644 index 000000000000..8aa252d8b7d0 --- /dev/null +++ b/tests/pos/inline-trait-body-macro/Macro_1.scala @@ -0,0 +1,10 @@ +import scala.quoted.* + +inline def foo(): Int = + ${fooImpl} + +def fooImpl(using Quotes): Expr[Int] = + '{3} + +inline trait A: + val i: Int = foo() diff --git a/tests/pos/inline-trait-body-macro/Test_2.scala b/tests/pos/inline-trait-body-macro/Test_2.scala new file mode 100644 index 000000000000..26e47fd25fdb --- /dev/null +++ b/tests/pos/inline-trait-body-macro/Test_2.scala @@ -0,0 +1 @@ +class B extends A \ No newline at end of file diff --git a/tests/pos/inline-trait-body-rhs-type.scala b/tests/pos/inline-trait-body-rhs-type.scala new file mode 100644 index 000000000000..5b439ddf0c19 --- /dev/null +++ b/tests/pos/inline-trait-body-rhs-type.scala @@ -0,0 +1,6 @@ +inline trait A[T](x: T): + def f: T = x: T + def g: T = identity[T](x) + def h: this.type = this: this.type + +class B extends A("Hello") diff --git a/tests/pos/inline-trait-body-setter.scala b/tests/pos/inline-trait-body-setter.scala new file mode 100644 index 000000000000..f1fcf528da9f --- /dev/null +++ b/tests/pos/inline-trait-body-setter.scala @@ -0,0 +1,6 @@ +inline trait A: + def x = 1 + def x_= (x: Int) = ??? + var y = 1 + +class B extends A \ No newline at end of file diff --git a/tests/pos/inline-trait-body-trait-simple.scala b/tests/pos/inline-trait-body-trait-simple.scala new file mode 100644 index 000000000000..14d4bcf64563 --- /dev/null +++ b/tests/pos/inline-trait-body-trait-simple.scala @@ -0,0 +1,6 @@ +inline trait A[T]: + trait InnerA: + def x: T = ??? + +class B extends A[Int]: + class InnerB extends InnerA \ No newline at end of file diff --git a/tests/pos/inline-trait-body-type.scala b/tests/pos/inline-trait-body-type.scala new file mode 100644 index 000000000000..35cad5c81c99 --- /dev/null +++ b/tests/pos/inline-trait-body-type.scala @@ -0,0 +1,5 @@ +inline trait A[T]: + type U = String + +class B extends A[Int]: + def f: U = "ABD" \ No newline at end of file diff --git a/tests/pos/inline-trait-body-val-inline.scala b/tests/pos/inline-trait-body-val-inline.scala new file mode 100644 index 000000000000..b60a39cfb8fe --- /dev/null +++ b/tests/pos/inline-trait-body-val-inline.scala @@ -0,0 +1,5 @@ +inline trait A: + inline val x = 1 + +class B extends A: + def f = x \ No newline at end of file diff --git a/tests/pos/inline-trait-body-val.scala b/tests/pos/inline-trait-body-val.scala new file mode 100644 index 000000000000..be20419ffb9f --- /dev/null +++ b/tests/pos/inline-trait-body-val.scala @@ -0,0 +1,5 @@ +inline trait A: + val x = 1 + +class B extends A: + def f = x \ No newline at end of file diff --git a/tests/pos/inline-trait-inheritance-multiple-override.scala b/tests/pos/inline-trait-inheritance-multiple-override.scala new file mode 100644 index 000000000000..8a429deb5101 --- /dev/null +++ b/tests/pos/inline-trait-inheritance-multiple-override.scala @@ -0,0 +1,20 @@ +inline trait IT1: + def i: Int = 1 + def f[T](x: T): T = x + +inline trait IT2: + def j: String = "inline" + def g(x: Int): String = x.toString() + +trait T: + def k: List[Nothing] + def h(x: Int, y: Double): Double = x + y + +class C1 extends IT1, T: + override def i: Int = 123456 + def k = Nil + override def h(x: Int, y: Double): Double = 1.0 + +class C2 extends IT1, IT2: + override def i: Int = 567890 + override def f[T](x: T): T = ??? \ No newline at end of file diff --git a/tests/pos/inline-trait-inheritance-multiple.scala b/tests/pos/inline-trait-inheritance-multiple.scala new file mode 100644 index 000000000000..15013572f9b7 --- /dev/null +++ b/tests/pos/inline-trait-inheritance-multiple.scala @@ -0,0 +1,17 @@ +object InlineTraits: + inline trait IT1: + def i: Int = 1 + def f[T](x: T): T = x + + inline trait IT2: + def j: String = "inline" + def g(x: Int): String = x.toString() + +trait T: + def k: List[Nothing] + def h(x: Int, y: Double): Double = x + y + +class C1 extends InlineTraits.IT1, T: + def k = Nil + +class C2 extends InlineTraits.IT1, InlineTraits.IT2 \ No newline at end of file diff --git a/tests/pos/inline-trait-inheritance-single-abstract-class-override.scala b/tests/pos/inline-trait-inheritance-single-abstract-class-override.scala new file mode 100644 index 000000000000..7baaf3885d6b --- /dev/null +++ b/tests/pos/inline-trait-inheritance-single-abstract-class-override.scala @@ -0,0 +1,7 @@ +inline trait IT: + def i: Int = 1 + def f[T](x: T): T = x + +abstract class AC extends IT: + override def i: Int = 123456 + def j: Int diff --git a/tests/pos/inline-trait-inheritance-single-abstract-class.scala b/tests/pos/inline-trait-inheritance-single-abstract-class.scala new file mode 100644 index 000000000000..49243dfd56ec --- /dev/null +++ b/tests/pos/inline-trait-inheritance-single-abstract-class.scala @@ -0,0 +1,6 @@ +inline trait IT: + def i: Int = 1 + def f[T](x: T): T = x + +abstract class AC extends IT: + def j: Int diff --git a/tests/pos/inline-trait-inheritance-single-object-override.scala b/tests/pos/inline-trait-inheritance-single-object-override.scala new file mode 100644 index 000000000000..7e7726e0e8bb --- /dev/null +++ b/tests/pos/inline-trait-inheritance-single-object-override.scala @@ -0,0 +1,6 @@ +inline trait IT: + def i: Int = 1 + def f[T](x: T): T = x + +object O extends IT: + override def i: Int = 123456 diff --git a/tests/pos/inline-trait-inheritance-single-object.scala b/tests/pos/inline-trait-inheritance-single-object.scala new file mode 100644 index 000000000000..3916f2e3dc36 --- /dev/null +++ b/tests/pos/inline-trait-inheritance-single-object.scala @@ -0,0 +1,5 @@ +inline trait IT: + def i: Int = 1 + def f[T](x: T): T = x + +object O extends IT diff --git a/tests/pos/inline-trait-multiple-files/A.scala b/tests/pos/inline-trait-multiple-files/A.scala new file mode 100644 index 000000000000..a137403cdf53 --- /dev/null +++ b/tests/pos/inline-trait-multiple-files/A.scala @@ -0,0 +1,2 @@ +inline trait A: + val i: Int = 1 \ No newline at end of file diff --git a/tests/pos/inline-trait-multiple-files/B.scala b/tests/pos/inline-trait-multiple-files/B.scala new file mode 100644 index 000000000000..26e47fd25fdb --- /dev/null +++ b/tests/pos/inline-trait-multiple-files/B.scala @@ -0,0 +1 @@ +class B extends A \ No newline at end of file diff --git a/tests/pos/inline-trait-multiple-stages-defs/A_1.scala b/tests/pos/inline-trait-multiple-stages-defs/A_1.scala new file mode 100644 index 000000000000..97532bc38ae5 --- /dev/null +++ b/tests/pos/inline-trait-multiple-stages-defs/A_1.scala @@ -0,0 +1,10 @@ +inline trait A(x: Int): + def f: Int = 1 + def g(a: Int): Int = 2 + def h: Int + val i: Int = 3 + val j: Int + var k: Int = 4 + + inline val a = 5 + inline def b(a: Int): Int = 6 diff --git a/tests/pos/inline-trait-multiple-stages-defs/B_2.scala b/tests/pos/inline-trait-multiple-stages-defs/B_2.scala new file mode 100644 index 000000000000..b1a1d7d6af3d --- /dev/null +++ b/tests/pos/inline-trait-multiple-stages-defs/B_2.scala @@ -0,0 +1,3 @@ +class B extends A(10): + def h: Int = 11 + val j: Int = 12 diff --git a/tests/pos/inline-trait-multiple-stages-generic-defs/A_1.scala b/tests/pos/inline-trait-multiple-stages-generic-defs/A_1.scala new file mode 100644 index 000000000000..83e2f62dcd45 --- /dev/null +++ b/tests/pos/inline-trait-multiple-stages-generic-defs/A_1.scala @@ -0,0 +1,9 @@ +inline trait A[T](x: T): + def f: T = x + def g(a: T): T = a + def h: T + val i: T = x + val j: T + var k: T = x + + inline def b(a: T): T = x diff --git a/tests/pos/inline-trait-multiple-stages-generic-defs/B_2.scala b/tests/pos/inline-trait-multiple-stages-generic-defs/B_2.scala new file mode 100644 index 000000000000..b595626379b8 --- /dev/null +++ b/tests/pos/inline-trait-multiple-stages-generic-defs/B_2.scala @@ -0,0 +1,3 @@ +class B extends A[Int](10): + def h: Int = 11 + val j: Int = 12 diff --git a/tests/pos/inline-trait-multiple-stages/A_1.scala b/tests/pos/inline-trait-multiple-stages/A_1.scala new file mode 100644 index 000000000000..3596275f0498 --- /dev/null +++ b/tests/pos/inline-trait-multiple-stages/A_1.scala @@ -0,0 +1,2 @@ +inline trait A: + val i: Int = 1 diff --git a/tests/pos/inline-trait-multiple-stages/B_2.scala b/tests/pos/inline-trait-multiple-stages/B_2.scala new file mode 100644 index 000000000000..a18aec3dbe9b --- /dev/null +++ b/tests/pos/inline-trait-multiple-stages/B_2.scala @@ -0,0 +1 @@ +class B extends A diff --git a/tests/pos/inline-trait-signature-generic-context-bound.scala b/tests/pos/inline-trait-signature-generic-context-bound.scala new file mode 100644 index 000000000000..f3587911f168 --- /dev/null +++ b/tests/pos/inline-trait-signature-generic-context-bound.scala @@ -0,0 +1,4 @@ +inline trait A[T: List] + +given List[Int] = Nil +class B extends A \ No newline at end of file diff --git a/tests/pos/inline-trait-signature-generic-inferred-type.scala b/tests/pos/inline-trait-signature-generic-inferred-type.scala new file mode 100644 index 000000000000..275ed0c7ec5b --- /dev/null +++ b/tests/pos/inline-trait-signature-generic-inferred-type.scala @@ -0,0 +1,5 @@ +inline trait A[T](val x: T): + def f: T = x + +class B extends A(1): + val y: Int = f diff --git a/tests/pos/inline-trait-signature-generic-invariant.scala b/tests/pos/inline-trait-signature-generic-invariant.scala new file mode 100644 index 000000000000..cc20461e716e --- /dev/null +++ b/tests/pos/inline-trait-signature-generic-invariant.scala @@ -0,0 +1,4 @@ +inline trait A[T]: + def f(x: T): T = x + +class B extends A[Int] diff --git a/tests/pos/inline-trait-signature-generic-parameter.scala b/tests/pos/inline-trait-signature-generic-parameter.scala new file mode 100644 index 000000000000..174ceba06a8c --- /dev/null +++ b/tests/pos/inline-trait-signature-generic-parameter.scala @@ -0,0 +1,5 @@ +inline trait A[T](val x: T): + def f: T = x + +class B extends A[Int](1): + val y: Int = f diff --git a/tests/pos/inline-trait-signature-generic-refinement-type.scala b/tests/pos/inline-trait-signature-generic-refinement-type.scala new file mode 100644 index 000000000000..3683c55a4d02 --- /dev/null +++ b/tests/pos/inline-trait-signature-generic-refinement-type.scala @@ -0,0 +1,9 @@ +import reflect.Selectable.reflectiveSelectable + +class C[T](x: T): + def foo(): T = x + +inline trait A[T, U[T]](u: U[T]{ def foo(): T }): + def f: T = u.foo() + +class B extends A(C(1)) diff --git a/tests/pos/inline-trait-signature-generic-singleton.scala b/tests/pos/inline-trait-signature-generic-singleton.scala new file mode 100644 index 000000000000..c5d0cf2fcfb1 --- /dev/null +++ b/tests/pos/inline-trait-signature-generic-singleton.scala @@ -0,0 +1,4 @@ +inline trait A[T](x: T): + def f: T = x + +class B extends A[1](1) diff --git a/tests/pos/inline-trait-signature-generic-type-bounds.scala b/tests/pos/inline-trait-signature-generic-type-bounds.scala new file mode 100644 index 000000000000..9cf4a00c80d7 --- /dev/null +++ b/tests/pos/inline-trait-signature-generic-type-bounds.scala @@ -0,0 +1,4 @@ +inline trait A[T >: Int <: AnyVal]: + def f(x: T): T = x + +class B extends A[Int] diff --git a/tests/pos/inline-trait-signature-generic-variant.scala b/tests/pos/inline-trait-signature-generic-variant.scala new file mode 100644 index 000000000000..201a9254c47a --- /dev/null +++ b/tests/pos/inline-trait-signature-generic-variant.scala @@ -0,0 +1,8 @@ +inline trait Cov[+T]: + def f: T = ??? + +inline trait Contr[-T]: + def f(x: T) = ??? + +class A extends Cov[AnyVal] +class B extends Contr[Int] \ No newline at end of file diff --git a/tests/pos/inline-trait-signature-parameters-currying.scala b/tests/pos/inline-trait-signature-parameters-currying.scala new file mode 100644 index 000000000000..9d98e0596408 --- /dev/null +++ b/tests/pos/inline-trait-signature-parameters-currying.scala @@ -0,0 +1,4 @@ +inline trait A(i: Int)(j: Double): + def f: Double = i + j + +class B extends A(1)(1.0) diff --git a/tests/pos/inline-trait-signature-parameters-default-value.scala b/tests/pos/inline-trait-signature-parameters-default-value.scala new file mode 100644 index 000000000000..f76056ba70df --- /dev/null +++ b/tests/pos/inline-trait-signature-parameters-default-value.scala @@ -0,0 +1,4 @@ +inline trait A(val x: Int = 4) + +class B extends A(1) +class C extends A() \ No newline at end of file diff --git a/tests/pos/inline-trait-signature-parameters-implicit.scala b/tests/pos/inline-trait-signature-parameters-implicit.scala new file mode 100644 index 000000000000..df091c0cc7e6 --- /dev/null +++ b/tests/pos/inline-trait-signature-parameters-implicit.scala @@ -0,0 +1,4 @@ +inline trait A(implicit val imp: Int) + +implicit val x: Int = 1 +class B extends A \ No newline at end of file diff --git a/tests/pos/inline-trait-signature-parameters-using-nameless.scala b/tests/pos/inline-trait-signature-parameters-using-nameless.scala new file mode 100644 index 000000000000..1882b0348488 --- /dev/null +++ b/tests/pos/inline-trait-signature-parameters-using-nameless.scala @@ -0,0 +1,4 @@ +inline trait A(using Int) // error + +given x: Int = 1 +class B extends A \ No newline at end of file diff --git a/tests/pos/inline-trait-signature-parameters-using.scala b/tests/pos/inline-trait-signature-parameters-using.scala new file mode 100644 index 000000000000..5438359370f8 --- /dev/null +++ b/tests/pos/inline-trait-signature-parameters-using.scala @@ -0,0 +1,4 @@ +inline trait A(using usng: Int) + +given x: Int = 1 +class B extends A \ No newline at end of file diff --git a/tests/pos/inline-trait-signature-parameters-val-private.scala b/tests/pos/inline-trait-signature-parameters-val-private.scala new file mode 100644 index 000000000000..9306e0152f05 --- /dev/null +++ b/tests/pos/inline-trait-signature-parameters-val-private.scala @@ -0,0 +1,3 @@ +inline trait A(x: Int) + +class B extends A(1) \ No newline at end of file diff --git a/tests/pos/inline-trait-signature-parameters-val-protected.scala b/tests/pos/inline-trait-signature-parameters-val-protected.scala new file mode 100644 index 000000000000..4127d45e9002 --- /dev/null +++ b/tests/pos/inline-trait-signature-parameters-val-protected.scala @@ -0,0 +1,3 @@ +inline trait A(protected val x: Int) + +class B extends A(1) \ No newline at end of file diff --git a/tests/pos/inline-trait-signature-parameters-val.scala b/tests/pos/inline-trait-signature-parameters-val.scala new file mode 100644 index 000000000000..8372baa0b810 --- /dev/null +++ b/tests/pos/inline-trait-signature-parameters-val.scala @@ -0,0 +1,3 @@ +inline trait A(val x: Int) + +class B extends A(1) \ No newline at end of file diff --git a/tests/pos/inline-trait-signature-parameters-var.scala b/tests/pos/inline-trait-signature-parameters-var.scala new file mode 100644 index 000000000000..590d273f81b6 --- /dev/null +++ b/tests/pos/inline-trait-signature-parameters-var.scala @@ -0,0 +1,3 @@ +inline trait A(var x: Int) + +class B extends A(1) \ No newline at end of file diff --git a/tests/pos/inline-trait-signature-parentheses.scala b/tests/pos/inline-trait-signature-parentheses.scala new file mode 100644 index 000000000000..8e9c674edad6 --- /dev/null +++ b/tests/pos/inline-trait-signature-parentheses.scala @@ -0,0 +1,4 @@ +inline trait A(): + def i: Int = 1 + +class B extends A diff --git a/tests/pos/inline-trait-signature-sealed.scala b/tests/pos/inline-trait-signature-sealed.scala new file mode 100644 index 000000000000..db9d6a260ad3 --- /dev/null +++ b/tests/pos/inline-trait-signature-sealed.scala @@ -0,0 +1,4 @@ +inline sealed trait A: + final def f(x: Int) = x + +class B extends A \ No newline at end of file diff --git a/tests/pos/inline-trait-signature-simple.scala b/tests/pos/inline-trait-signature-simple.scala new file mode 100644 index 000000000000..6734a69a488c --- /dev/null +++ b/tests/pos/inline-trait-signature-simple.scala @@ -0,0 +1,4 @@ +inline trait A: + def i: Int = 1 + +class B extends A diff --git a/tests/pos/inline-trait-usage-anonymous-class.scala b/tests/pos/inline-trait-usage-anonymous-class.scala new file mode 100644 index 000000000000..c51dd05a6907 --- /dev/null +++ b/tests/pos/inline-trait-usage-anonymous-class.scala @@ -0,0 +1,4 @@ +inline trait A[T]: + val x: T = ??? + +val a = new A[Int] {} \ No newline at end of file diff --git a/tests/pos/inline-trait-usage-extension.scala b/tests/pos/inline-trait-usage-extension.scala new file mode 100644 index 000000000000..f9f5d87d2ceb --- /dev/null +++ b/tests/pos/inline-trait-usage-extension.scala @@ -0,0 +1,3 @@ +inline trait A + +class B extends A diff --git a/tests/pos/inline-trait-usage-inner.scala b/tests/pos/inline-trait-usage-inner.scala new file mode 100644 index 000000000000..3ed7869f0016 --- /dev/null +++ b/tests/pos/inline-trait-usage-inner.scala @@ -0,0 +1,5 @@ +object O: + inline trait A[T]: + def t: T = ??? + +class B extends O.A[Int] diff --git a/tests/pos/inline-trait-usage-param-type.scala b/tests/pos/inline-trait-usage-param-type.scala new file mode 100644 index 000000000000..e04f885f1e65 --- /dev/null +++ b/tests/pos/inline-trait-usage-param-type.scala @@ -0,0 +1,3 @@ +inline trait A + +def f(a: A) = ??? diff --git a/tests/pos/inline-trait-usage-return-type.scala b/tests/pos/inline-trait-usage-return-type.scala new file mode 100644 index 000000000000..e8015b065552 --- /dev/null +++ b/tests/pos/inline-trait-usage-return-type.scala @@ -0,0 +1,3 @@ +inline trait A + +def f: A = ??? diff --git a/tests/pos/inline-trait-usage-type-bound.scala b/tests/pos/inline-trait-usage-type-bound.scala new file mode 100644 index 000000000000..52b57f6acfa5 --- /dev/null +++ b/tests/pos/inline-trait-usage-type-bound.scala @@ -0,0 +1,3 @@ +inline trait A + +type T >: A <: A \ No newline at end of file diff --git a/tests/pos/inline-trait-usage-type.scala b/tests/pos/inline-trait-usage-type.scala new file mode 100644 index 000000000000..1c1ae110c367 --- /dev/null +++ b/tests/pos/inline-trait-usage-type.scala @@ -0,0 +1,3 @@ +inline trait A + +type T = List[A] \ No newline at end of file diff --git a/tests/run/inline-trait-body-lazy-val.scala b/tests/run/inline-trait-body-lazy-val.scala new file mode 100644 index 000000000000..68a9c9fd8697 --- /dev/null +++ b/tests/run/inline-trait-body-lazy-val.scala @@ -0,0 +1,9 @@ +inline trait A: + lazy val x = + throw new Exception + 1 + +class B extends A + +@main def Test: Unit = + val b = B() \ No newline at end of file diff --git a/tests/run/inline-trait-body-override-def.scala b/tests/run/inline-trait-body-override-def.scala new file mode 100644 index 000000000000..2ec53e02e669 --- /dev/null +++ b/tests/run/inline-trait-body-override-def.scala @@ -0,0 +1,9 @@ +inline trait A: + def foo: Unit = throw Exception("I should not be run!") + +class B extends A: + override def foo: Unit = () + +@main def Test = + val b = B() + b.foo diff --git a/tests/run/inline-trait-body-override-val.check b/tests/run/inline-trait-body-override-val.check new file mode 100644 index 000000000000..0cfbf08886fc --- /dev/null +++ b/tests/run/inline-trait-body-override-val.check @@ -0,0 +1 @@ +2 diff --git a/tests/run/inline-trait-body-override-val.scala b/tests/run/inline-trait-body-override-val.scala new file mode 100644 index 000000000000..ffd71ca0df56 --- /dev/null +++ b/tests/run/inline-trait-body-override-val.scala @@ -0,0 +1,9 @@ +inline trait A: + val x: Int = 1 + +class B extends A: + override val x = 2 + +@main def Test = + val b = B() + println(b.x) diff --git a/tests/run/inline-trait-body-statements.check b/tests/run/inline-trait-body-statements.check new file mode 100644 index 000000000000..73b256c35fd0 --- /dev/null +++ b/tests/run/inline-trait-body-statements.check @@ -0,0 +1,8 @@ +1 +foo +foo +1 +foo +bar +bar +bar diff --git a/tests/run/inline-trait-body-statements.scala b/tests/run/inline-trait-body-statements.scala new file mode 100644 index 000000000000..5da24022e99d --- /dev/null +++ b/tests/run/inline-trait-body-statements.scala @@ -0,0 +1,22 @@ +inline trait A[T]: + var x = 1 + def foo = + if x == 1 then println("1") + x = 1 - x + println("foo") + + foo + foo + foo + +class B extends A[Int]: + def bar = + if x == 1 then println("1") + println("bar") + + bar + bar + bar + +@main def Test = + B() \ No newline at end of file diff --git a/tests/run/inline-trait-body-var.check b/tests/run/inline-trait-body-var.check new file mode 100644 index 000000000000..94ebaf900161 --- /dev/null +++ b/tests/run/inline-trait-body-var.check @@ -0,0 +1,4 @@ +1 +2 +3 +4 diff --git a/tests/run/inline-trait-body-var.scala b/tests/run/inline-trait-body-var.scala new file mode 100644 index 000000000000..fda8fc3109b7 --- /dev/null +++ b/tests/run/inline-trait-body-var.scala @@ -0,0 +1,15 @@ +inline trait A: + var x = 1 + +class B extends A: + def f = + val old = x + x += 1 + old + +@main def Test = + val b = B() + println(b.f) + println(b.f) + println(b.f) + println(b.f) diff --git a/tests/run/inline-trait-inheritance-diamond-simple-trait.check b/tests/run/inline-trait-inheritance-diamond-simple-trait.check new file mode 100644 index 000000000000..adbdca39e3a4 --- /dev/null +++ b/tests/run/inline-trait-inheritance-diamond-simple-trait.check @@ -0,0 +1,4 @@ +1 +2 +999 +4 diff --git a/tests/run/inline-trait-inheritance-diamond-simple-trait.scala b/tests/run/inline-trait-inheritance-diamond-simple-trait.scala new file mode 100644 index 000000000000..0bb66f48b739 --- /dev/null +++ b/tests/run/inline-trait-inheritance-diamond-simple-trait.scala @@ -0,0 +1,22 @@ +trait TGP[T]: + def i: T + def f(x: T): T = x + +inline trait IT1[T](x: T) extends TGP[T]: + override def i: T = x + +inline trait IT2 extends TGP[Int]: + override def i: Int = 999 + def j: String = "inline" + def g(x: Int): String = x.toString() + +trait T extends TGP[Int] + +class C1 extends T, IT1[Int](1) +class C2 extends IT1[Int](2), T +class C3 extends IT1[Int](3), IT2 +class C4 extends IT2, IT1[Int](4) + +@main def Test: Unit = + for c <- List(C1(), C2(), C3(), C4()) + do println(c.i) \ No newline at end of file diff --git a/tests/run/inline-trait-inheritance-inline-ancestors/inlinetraits.scala b/tests/run/inline-trait-inheritance-inline-ancestors/inlinetraits.scala new file mode 100644 index 000000000000..e695ef3efd1a --- /dev/null +++ b/tests/run/inline-trait-inheritance-inline-ancestors/inlinetraits.scala @@ -0,0 +1,47 @@ +package inlinetraits + +val inlineValues: List[Int] = + val c = C() + List(c.zero, c.eleven, c.twelve, c.thirteen, c.twentyOne, c.twentyTwo, c.thirty) + +inline trait T0: + def zero: Int = 0 + def eleven: Int = 0 + def twelve: Int = 0 + def twentyOne: Int = 0 + def thirteen: Int = 0 + def twentyTwo: Int = 0 + def thirty: Int = 0 + +inline trait T11 extends T0: + override def eleven: Int = 11 + override def twelve: Int = 11 + override def twentyOne: Int = 11 + override def thirteen: Int = 11 + override def twentyTwo: Int = 11 + override def thirty: Int = 11 + +inline trait T12 extends T0: + override def twelve: Int = 12 + override def twentyOne: Int = 12 + override def thirteen: Int = 12 + override def twentyTwo: Int = 12 + override def thirty: Int = 12 + +inline trait T21 extends T11, T12: + override def twentyOne: Int = 21 + override def thirteen: Int = 21 + override def twentyTwo: Int = 21 + override def thirty: Int = 21 + +inline trait T13 extends T0: + override def thirteen: Int = 13 + override def twentyTwo: Int = 13 + override def thirty: Int = 13 + +inline trait T22 extends T12, T13: + override def twentyTwo: Int = 22 + override def thirty: Int = 22 + +class C extends T21, T22: + override def thirty: Int = 30 diff --git a/tests/run/inline-trait-inheritance-inline-ancestors/normaltraits.scala b/tests/run/inline-trait-inheritance-inline-ancestors/normaltraits.scala new file mode 100644 index 000000000000..ba128145c793 --- /dev/null +++ b/tests/run/inline-trait-inheritance-inline-ancestors/normaltraits.scala @@ -0,0 +1,47 @@ +package normaltraits + +val normalValues: List[Int] = + val c = C() + List(c.zero, c.eleven, c.twelve, c.thirteen, c.twentyOne, c.twentyTwo, c.thirty) + +trait T0: + def zero: Int = 0 + def eleven: Int = 0 + def twelve: Int = 0 + def twentyOne: Int = 0 + def thirteen: Int = 0 + def twentyTwo: Int = 0 + def thirty: Int = 0 + +trait T11 extends T0: + override def eleven: Int = 11 + override def twelve: Int = 11 + override def twentyOne: Int = 11 + override def thirteen: Int = 11 + override def twentyTwo: Int = 11 + override def thirty: Int = 11 + +trait T12 extends T0: + override def twelve: Int = 12 + override def twentyOne: Int = 12 + override def thirteen: Int = 12 + override def twentyTwo: Int = 12 + override def thirty: Int = 12 + +trait T21 extends T11, T12: + override def twentyOne: Int = 21 + override def thirteen: Int = 21 + override def twentyTwo: Int = 21 + override def thirty: Int = 21 + +trait T13 extends T0: + override def thirteen: Int = 13 + override def twentyTwo: Int = 13 + override def thirty: Int = 13 + +trait T22 extends T12, T13: + override def twentyTwo: Int = 22 + override def thirty: Int = 22 + +class C extends T21, T22: + override def thirty: Int = 30 \ No newline at end of file diff --git a/tests/run/inline-trait-inheritance-inline-ancestors/test.scala b/tests/run/inline-trait-inheritance-inline-ancestors/test.scala new file mode 100644 index 000000000000..5f7a9be7a41b --- /dev/null +++ b/tests/run/inline-trait-inheritance-inline-ancestors/test.scala @@ -0,0 +1,5 @@ +import normaltraits.normalValues +import inlinetraits.inlineValues + +@main def Test = + assert(normalValues == inlineValues) \ No newline at end of file diff --git a/tests/run/inline-trait-inheritance-inline-grandparent.check b/tests/run/inline-trait-inheritance-inline-grandparent.check new file mode 100644 index 000000000000..02a3fcae5b4e --- /dev/null +++ b/tests/run/inline-trait-inheritance-inline-grandparent.check @@ -0,0 +1,6 @@ +0 +(Test SimpleC,Hello) + +5678 +(Test C,Hello,9) +5678 diff --git a/tests/run/inline-trait-inheritance-inline-grandparent.scala b/tests/run/inline-trait-inheritance-inline-grandparent.scala new file mode 100644 index 000000000000..adc00b7b7bfe --- /dev/null +++ b/tests/run/inline-trait-inheritance-inline-grandparent.scala @@ -0,0 +1,33 @@ +package simpleGrandParent: + inline trait SimpleGrandParent[T]: + def foo(): Int = 0 + def foooo(): T = ??? + + inline trait SimpleParent[T, U](x: T, z: U) extends SimpleGrandParent[U]: + def bar(a: T) = (a, x) + + class SimpleC extends SimpleParent("Hello", 1234) + +package grandParentWithArgs: + inline trait GrandParent[T](val x: T, val y: T): + def foo(): T = x + def foooo(): T = y + + inline trait Parent[T, U](x: T, z: U) extends GrandParent[U]: + def bar(a: T) = (a, x, y) + + class C extends Parent("Hello", 1234), GrandParent(5678, 9) + +@main def Test = + import simpleGrandParent.SimpleC + import grandParentWithArgs.C + + val simpleC = SimpleC() + println(simpleC.foo()) + println(simpleC.bar("Test SimpleC")) + println + + val c = C() + println(c.foo()) + println(c.bar("Test C")) + println(c.x) \ No newline at end of file diff --git a/tests/run/inline-trait-signature-parameters-val-block.check b/tests/run/inline-trait-signature-parameters-val-block.check new file mode 100644 index 000000000000..063724ea6c43 --- /dev/null +++ b/tests/run/inline-trait-signature-parameters-val-block.check @@ -0,0 +1,4 @@ +I am a B! +1 +1 +1 diff --git a/tests/run/inline-trait-signature-parameters-val-block.scala b/tests/run/inline-trait-signature-parameters-val-block.scala new file mode 100644 index 000000000000..794a6071d073 --- /dev/null +++ b/tests/run/inline-trait-signature-parameters-val-block.scala @@ -0,0 +1,10 @@ +inline trait A(val x: Int) + +class B extends A({ println("I am a B!"); 1 }) + +@main() def Test: Unit = { + val b = B() + println(b.x) + println(b.x) + println(b.x) +} \ No newline at end of file diff --git a/tests/run/inline-trait-signature-side-effects-1.check b/tests/run/inline-trait-signature-side-effects-1.check new file mode 100644 index 000000000000..4df3c9a85d7f --- /dev/null +++ b/tests/run/inline-trait-signature-side-effects-1.check @@ -0,0 +1,6 @@ +0 +0 +1 +0 +1 +2 diff --git a/tests/run/inline-trait-signature-side-effects-1.scala b/tests/run/inline-trait-signature-side-effects-1.scala new file mode 100644 index 000000000000..61c348eb48d1 --- /dev/null +++ b/tests/run/inline-trait-signature-side-effects-1.scala @@ -0,0 +1,27 @@ +inline trait A(i: Int): + val x = i + val y = i + +class B(i: Int) extends A(i) + +@main def Test = + var c = 0 + + val b1 = new B({ + for i <- 0 to c + do println(i) + c += 1 + c + }) + val b2 = new B({ + for i <- 0 to c + do println(i) + c += 1 + c + }) + val b3 = new B({ + for i <- 0 to c + do println(i) + c += 1 + c + }) \ No newline at end of file diff --git a/tests/run/inline-trait-signature-side-effects-2.check b/tests/run/inline-trait-signature-side-effects-2.check new file mode 100644 index 000000000000..4df3c9a85d7f --- /dev/null +++ b/tests/run/inline-trait-signature-side-effects-2.check @@ -0,0 +1,6 @@ +0 +0 +1 +0 +1 +2 diff --git a/tests/run/inline-trait-signature-side-effects-2.scala b/tests/run/inline-trait-signature-side-effects-2.scala new file mode 100644 index 000000000000..19dcad0681a3 --- /dev/null +++ b/tests/run/inline-trait-signature-side-effects-2.scala @@ -0,0 +1,16 @@ +inline trait A(i: Int): + val x = i + val y = i + +class B(i: Int) extends A(i) + +@main def Test = + var c = 0 + + for _ <- 0 until 3 + do new B({ + for i <- 0 to c + do println(i) + c += 1 + c + }) \ No newline at end of file From 80b3d2ffea4df236da1b0ccb3129116c933f12a2 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 24 Feb 2026 16:49:34 +0100 Subject: [PATCH 002/254] Fix child reference mapping for nested class inside inline trait --- compiler/src/dotty/tools/dotc/inlines/Inlines.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index d73fb3710572..1536599e9ae0 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -823,7 +823,7 @@ object Inlines: owner = ctx.owner, name = name, flags = flags &~ withoutFlags, - info = inlinerTypeMap(sym.info), + info = inlinerTypeMap(sym.info).substSym(substFrom, substTo), coord = spanCoord(parent.span)).entered private def inlinedValDef(vdef: ValDef, inlinedSym: Symbol)(using Context): ValDef = @@ -880,7 +880,7 @@ object Inlines: // TODO make version of inlined that does not return bindings? Inlined(tpd.ref(parentSym), Nil, inlined(rhs)._2).withSpan(parent.span) - private val defsAdapter = + private def defsAdapter = val typeMap = new DeepTypeMap { override def apply(tp: Type): Type = tp match { case TypeRef(_, sym: Symbol) if innerClassNewSyms.contains(sym) => From 6aa569f864ee3d9516188ffa2dd5908eb6565f8b Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 24 Feb 2026 17:09:21 +0100 Subject: [PATCH 003/254] Add some more tests for inline traits --- tests/pos/inline-trait-body-class-return.scala | 7 +++++++ tests/pos/inline-trait-double-nested-class.scala | 13 +++++++++++++ tests/pos/inline-trait-pair-example.scala | 7 +++++++ tests/pos/inline-trait-parent-ref.scala | 7 +++++++ tests/pos/inline-trait-return-ref.scala | 5 +++++ 5 files changed, 39 insertions(+) create mode 100644 tests/pos/inline-trait-body-class-return.scala create mode 100644 tests/pos/inline-trait-double-nested-class.scala create mode 100644 tests/pos/inline-trait-pair-example.scala create mode 100644 tests/pos/inline-trait-parent-ref.scala create mode 100644 tests/pos/inline-trait-return-ref.scala diff --git a/tests/pos/inline-trait-body-class-return.scala b/tests/pos/inline-trait-body-class-return.scala new file mode 100644 index 000000000000..ac960c590b87 --- /dev/null +++ b/tests/pos/inline-trait-body-class-return.scala @@ -0,0 +1,7 @@ +inline trait A: + sealed class InnerA: + val x = 1 + def generate(x: Int) = InnerA() + +class B extends A: + val y = generate(7) diff --git a/tests/pos/inline-trait-double-nested-class.scala b/tests/pos/inline-trait-double-nested-class.scala new file mode 100644 index 000000000000..08c446eeeee0 --- /dev/null +++ b/tests/pos/inline-trait-double-nested-class.scala @@ -0,0 +1,13 @@ +inline trait A: + sealed class InnerA: + sealed class InnerInnerA: + val x = 1 + +class B extends A: + class InnerB extends InnerA { + class InnerInnerB extends InnerInnerA + } + def f = + val a = new InnerB() + val b = a.InnerInnerB() + b.x diff --git a/tests/pos/inline-trait-pair-example.scala b/tests/pos/inline-trait-pair-example.scala new file mode 100644 index 000000000000..733749aa516c --- /dev/null +++ b/tests/pos/inline-trait-pair-example.scala @@ -0,0 +1,7 @@ +package inlinetrait + +inline trait Pair[+T1, +T2](val _1: T1, val _2: T2) +class IntDoublePair(override val _1: Int, override val _2: Double) extends Pair[Int, Double](_1, _2) +class CharShortPair(override val _1: Char, override val _2: Short) extends Pair[Char, Short](_1, _2) + +def foo(i: IntDoublePair) = i._1 + i._2 diff --git a/tests/pos/inline-trait-parent-ref.scala b/tests/pos/inline-trait-parent-ref.scala new file mode 100644 index 000000000000..b43f750d9853 --- /dev/null +++ b/tests/pos/inline-trait-parent-ref.scala @@ -0,0 +1,7 @@ +inline trait A[T](val x: T): + def foo: T = x + +trait B extends A[Int]: + val y = 1 + +def h(x: B) = x.foo diff --git a/tests/pos/inline-trait-return-ref.scala b/tests/pos/inline-trait-return-ref.scala new file mode 100644 index 000000000000..3b04ac346fee --- /dev/null +++ b/tests/pos/inline-trait-return-ref.scala @@ -0,0 +1,5 @@ +inline trait A[T](val x: T): + def foo: T = x + +class B extends A[Int](1): + def bar: Int = foo From 6bf25676020f0f6f7071704604a13d20be9f19c8 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 24 Feb 2026 18:02:47 +0100 Subject: [PATCH 004/254] Add more inline-traits tests --- tests/pos/inline-trait-co-nested.scala | 9 +++++++++ .../pos/inline-trait-cross-reference-defined-after.scala | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 tests/pos/inline-trait-co-nested.scala create mode 100644 tests/pos/inline-trait-cross-reference-defined-after.scala diff --git a/tests/pos/inline-trait-co-nested.scala b/tests/pos/inline-trait-co-nested.scala new file mode 100644 index 000000000000..ac1e2a627b9c --- /dev/null +++ b/tests/pos/inline-trait-co-nested.scala @@ -0,0 +1,9 @@ +inline trait A: // At the moment this works with an ordinary trait but throws a TypeError with inline traits + sealed class InnerA: + val x = new InnerB + + sealed class InnerB: + val x = new InnerA + +class B extends A: + val y = 10 diff --git a/tests/pos/inline-trait-cross-reference-defined-after.scala b/tests/pos/inline-trait-cross-reference-defined-after.scala new file mode 100644 index 000000000000..43f87465d4ec --- /dev/null +++ b/tests/pos/inline-trait-cross-reference-defined-after.scala @@ -0,0 +1,9 @@ +inline trait A: // At the moment this works with an ordinary trait but throws a TypeError with inline traits + sealed class InnerA: + val x = new InnerB + + sealed class InnerB: + val x = 10 + +class B extends A: + val y = 10 From 421083f0c1e70c847bcdc4c22de0a5b4b0ae9058 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 24 Feb 2026 18:09:36 +0100 Subject: [PATCH 005/254] Add another test case for inline traits --- tests/pos/inline-trait-nested-class-outside-ref.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/pos/inline-trait-nested-class-outside-ref.scala diff --git a/tests/pos/inline-trait-nested-class-outside-ref.scala b/tests/pos/inline-trait-nested-class-outside-ref.scala new file mode 100644 index 000000000000..4d0c31e6f507 --- /dev/null +++ b/tests/pos/inline-trait-nested-class-outside-ref.scala @@ -0,0 +1,11 @@ +inline trait A: + class InnerA: + val x = 10 + +class B extends A: + def foo = 10 + +def x = + val b = B() + val c = b.InnerA() + c From a30d37063720b8e1b38dfe6c3fc07f96551191ab Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 25 Feb 2026 14:36:40 +0100 Subject: [PATCH 006/254] Add inline-trait-nested-class-parameter-passing.scala --- ...-trait-nested-class-parameter-passing.scala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/pos/inline-trait-nested-class-parameter-passing.scala diff --git a/tests/pos/inline-trait-nested-class-parameter-passing.scala b/tests/pos/inline-trait-nested-class-parameter-passing.scala new file mode 100644 index 000000000000..321d4c5ef692 --- /dev/null +++ b/tests/pos/inline-trait-nested-class-parameter-passing.scala @@ -0,0 +1,18 @@ +inline trait A: + sealed class InnerA: + val x = 1 + +class B extends A: + class InnerB extends InnerA: + override val x = 2 + +def foo(x: A#InnerA) = println(x.x) + +@main def main = + val a = new A() {} + val inner_a = a.InnerA() + foo(inner_a) + + val b = B() + val inner_b = b.InnerB() + foo(inner_b) From 9a33768df301f8152eb8273868f5a91cc2ccec3c Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 25 Feb 2026 14:53:14 +0100 Subject: [PATCH 007/254] Add inline trait parameter passing test case --- tests/pos/inline-trait-parameter-passing.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/pos/inline-trait-parameter-passing.scala diff --git a/tests/pos/inline-trait-parameter-passing.scala b/tests/pos/inline-trait-parameter-passing.scala new file mode 100644 index 000000000000..c5bba6b831b4 --- /dev/null +++ b/tests/pos/inline-trait-parameter-passing.scala @@ -0,0 +1,12 @@ +inline trait A: + def foo() = 10 + +class B extends A: + def bar() = 1000 + +def some_function(x: A) = + x.foo() + +@main def main = + val b = B() + some_function(b) From e7741223e008db99f380382083062a6101e256ca Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 26 Feb 2026 11:07:33 +0100 Subject: [PATCH 008/254] Fix inline-trait-parent-ref.scala --- tests/pos/inline-trait-parent-ref.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pos/inline-trait-parent-ref.scala b/tests/pos/inline-trait-parent-ref.scala index b43f750d9853..d200f205df51 100644 --- a/tests/pos/inline-trait-parent-ref.scala +++ b/tests/pos/inline-trait-parent-ref.scala @@ -1,7 +1,7 @@ -inline trait A[T](val x: T): +inline trait A[T](x: T): def foo: T = x -trait B extends A[Int]: +class B extends A[Int](15): val y = 1 def h(x: B) = x.foo From 7e54ced77d386cdbe74874fae205fb9670668045 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 26 Feb 2026 18:43:36 +0100 Subject: [PATCH 009/254] Add desugar --- .../inline-trait-specialised-desugar.scala | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/pos/inline-trait-specialised-desugar.scala diff --git a/tests/pos/inline-trait-specialised-desugar.scala b/tests/pos/inline-trait-specialised-desugar.scala new file mode 100644 index 000000000000..e317fd61096e --- /dev/null +++ b/tests/pos/inline-trait-specialised-desugar.scala @@ -0,0 +1,42 @@ +inline trait Iterator[T]: + def hasNext: Boolean + def next(): T + +// They do this: (with Specialized type class) +inline trait ArrayIterator[T](elems: Array[T]) extends Iterator[T]: + private var current = 0 + def hasNext: Boolean = current < elems.length + def next(): T = try elems(current) finally current += 1 + + +// We generate these: +inline trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator[Int] +class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator[Int](elems) + +// Inline traits does the magic of actually inlining the code and specialising from T to Int in that step. + + +// They do this: +// def foo(x: ArrayIterator[Int]): Int = x.next() +// We convert this to: +def foo(x: ArrayIterator$sp$Int): Int = x.next() +// As long as we generate this (i.e. "do the special erasure") before we run inline traits we should be fine because then the reference will be replaced. + + +// They do this: +// class MyClassA +// class MyClassB extends MyClassA, ArrayIterator[Int] + +// We convert this to: +class MyClassA +class MyClassB extends MyClassA, ArrayIterator$sp$Int + +@main def main = + val xs: Array[Int] = Array(1, 2, 3) + + // They do this: + // new ArrayIterator(xs) {} + + // We convert this to: + val ai = new ArrayIterator$impl$Int(xs) {} + println(ai.next()) From 90d6656bf9aff96cb2ecb08bce052330b1efc1b3 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 26 Feb 2026 18:51:27 +0100 Subject: [PATCH 010/254] Add inline trait trait inheritance --- tests/pos/inline-trait-trait-inheritance.scala | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests/pos/inline-trait-trait-inheritance.scala diff --git a/tests/pos/inline-trait-trait-inheritance.scala b/tests/pos/inline-trait-trait-inheritance.scala new file mode 100644 index 000000000000..176d544705fd --- /dev/null +++ b/tests/pos/inline-trait-trait-inheritance.scala @@ -0,0 +1,4 @@ +inline trait A(x: Int): + val y = x + +trait C extends A From 8b49ecfe2fca91f9f9db59470b1c752b40c9c55b Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 1 Mar 2026 19:17:15 +0100 Subject: [PATCH 011/254] Add first version of mapping symbols --- compiler/src/dotty/tools/dotc/Compiler.scala | 2 + .../src/dotty/tools/dotc/core/Contexts.scala | 13 ++ .../src/dotty/tools/dotc/core/Phases.scala | 4 + .../dotty/tools/dotc/inlines/Inlines.scala | 77 ++++--- .../ReplaceInlinedTraitSymbols.scala | 210 ++++++++++++++++++ .../transform/SpecializeInlineTraits.scala | 2 +- 6 files changed, 271 insertions(+), 37 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 35dfe5b69233..e1472586de4c 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -10,6 +10,7 @@ import transform.* import backend.jvm.GenBCode import localopt.{StringInterpolatorOpt, DropForMap} import semanticdb.ExtractSemanticDB.{ExtractSemanticInfo, AppendDiagnostics as AppendSemanticDiagnostics} +import dotty.tools.dotc.transform.ReplaceInlinedTraitSymbols /** The central class of the dotc compiler. The job of a compiler is to create * runs, which process given `phases` in a given `rootContext`. @@ -51,6 +52,7 @@ class Compiler { List(new Pickler) :: // Generate TASTY info List(new sbt.ExtractAPI) :: // Sends a representation of the API of classes to sbt via callbacks List(new SpecializeInlineTraits) :: // Inline the code of inline traits into their children + List(new ReplaceInlinedTraitSymbols) :: // Replace symbols referring to inline trait members with resulting inlined member symbols List(new Inlining) :: // Inline and execute macros List(new PostInlining) :: // Add mirror support for inlined code List(new Staging) :: // Check staging levels and heal staged types diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 5ac4c66fb61e..7179d8a5361d 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -43,6 +43,7 @@ import java.util.concurrent.atomic.AtomicInteger import java.nio.file.InvalidPathException import dotty.tools.dotc.coverage.Coverage import scala.annotation.tailrec +import dotty.tools.dotc.inlines.Inlines.InlineTraitState object Contexts { @@ -147,6 +148,7 @@ object Contexts { def typerState: TyperState def gadt: GadtConstraint = gadtState.gadt def gadtState: GadtState + def inlineTraitState: InlineTraitState def searchHistory: SearchHistory def source: SourceFile @@ -436,6 +438,7 @@ object Contexts { superOrThisCallContext(owner, constrCtx.scope) .setTyperState(typerState) .setGadtState(gadtState) + .setInlineTraitState(inlineTraitState) .fresh .setScope(this.scope) } @@ -595,6 +598,9 @@ object Contexts { private var _gadtState: GadtState = uninitialized final def gadtState: GadtState = _gadtState + + private var _inlineTraitState: InlineTraitState = uninitialized + final def inlineTraitState: InlineTraitState = _inlineTraitState private var _searchHistory: SearchHistory = uninitialized final def searchHistory: SearchHistory = _searchHistory @@ -620,6 +626,7 @@ object Contexts { _tree = origin.tree _scope = origin.scope _gadtState = origin.gadtState + _inlineTraitState = origin.inlineTraitState _searchHistory = origin.searchHistory _source = origin.source _moreProperties = origin.moreProperties @@ -683,6 +690,11 @@ object Contexts { def setFreshGADTBounds: this.type = setGadtState(gadtState.fresh) + def setInlineTraitState(inlineTraitState: InlineTraitState): this.type = + util.Stats.record("Context.setInlineTraitState") + this._inlineTraitState = inlineTraitState + this + def setSearchHistory(searchHistory: SearchHistory): this.type = util.Stats.record("Context.setSearchHistory") this._searchHistory = searchHistory @@ -777,6 +789,7 @@ object Contexts { .updated(profilerLoc, Profiler.NoOp) c._searchHistory = new SearchRoot c._gadtState = GadtState(GadtConstraint.empty) + c._inlineTraitState = InlineTraitState() c end FreshContext diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index 0809ae3a5f93..98df07575c2f 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -22,6 +22,7 @@ import ast.{tpd, untpd} import scala.annotation.internal.sharable import scala.util.control.NonFatal import scala.compiletime.uninitialized +import dotty.tools.dotc.transform.ReplaceInlinedTraitSymbols object Phases { @@ -239,6 +240,7 @@ object Phases { private var myPicklerPhase: Phase = uninitialized private var mySetRootTreePhase: Phase = uninitialized private var mySpecializeInlineTraitsPhase: Phase = uninitialized + private var myReplaceInlinedTraitSymbolsPhase: Phase = uninitialized private var myInliningPhase: Phase = uninitialized private var myStagingPhase: Phase = uninitialized private var mySplicingPhase: Phase = uninitialized @@ -273,6 +275,7 @@ object Phases { final def picklerPhase: Phase = myPicklerPhase final def setRootTreePhase: Phase = mySetRootTreePhase final def specializeInlineTraitsPhase: Phase = mySpecializeInlineTraitsPhase + final def replaceInlinedTraitSymbolsPhase: Phase = myReplaceInlinedTraitSymbolsPhase final def inliningPhase: Phase = myInliningPhase final def stagingPhase: Phase = myStagingPhase final def splicingPhase: Phase = mySplicingPhase @@ -307,6 +310,7 @@ object Phases { mySetRootTreePhase = phaseOfClass(classOf[SetRootTree]) myPicklerPhase = phaseOfClass(classOf[Pickler]) mySpecializeInlineTraitsPhase = phaseOfClass(classOf[SpecializeInlineTraits]) + myReplaceInlinedTraitSymbolsPhase = phaseOfClass(classOf[ReplaceInlinedTraitSymbols]) myInliningPhase = phaseOfClass(classOf[Inlining]) myStagingPhase = phaseOfClass(classOf[Staging]) mySplicingPhase = phaseOfClass(classOf[Splicing]) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 1536599e9ae0..a0f1faee5a29 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -256,8 +256,7 @@ object Inlines: val parentTraitInliner = InlineParentTrait(parent) val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) val inlinedDefs1 = inlineDefs ::: parentTraitInliner.expandDefs(overriddenSymbols) - val childDefs1 = parentTraitInliner.adaptDefs(childDefs) // TODO do this outside of inlining: we need to adapt ALL references to inlined stuff - (inlinedDefs1, childDefs1) + (inlinedDefs1, childDefs) } } val impl1 = cpy.Template(impl)(body = newDefs._1 ::: newDefs._2) @@ -704,7 +703,6 @@ object Inlines: private val parentSym = symbolFromParent(parent) private val paramAccessorsMapper = ParamAccessorsMapper() - private val innerClassNewSyms: mutable.LinkedHashMap[Symbol, Symbol] = mutable.LinkedHashMap.empty private val childThisType = ctx.owner.thisType private val childThisTree = This(ctx.owner.asClass).withSpan(parent.span) @@ -721,8 +719,6 @@ object Inlines: } end expandDefs - def adaptDefs(definitions: List[Tree]): List[Tree] = definitions.mapconserve(defsAdapter(_)) - protected class InlineTraitTypeMap extends InlinerTypeMap { override def apply(t: Type) = super.apply(t) match { case t: ThisType if t.cls == parentSym => childThisType @@ -754,8 +750,6 @@ object Inlines: override protected val inlinerTypeMap: InlinerTypeMap = InlineTraitTypeMap() override protected val inlinerTreeMap: InlinerTreeMap = InlineTraitTreeMap() - override protected def substFrom: List[Symbol] = innerClassNewSyms.keys.toList - override protected def substTo: List[Symbol] = innerClassNewSyms.values.toList override protected def inlineCopier: tpd.TreeCopier = new TypedTreeCopier() { // FIXME it feels weird... Is this correct? override def Apply(tree: Tree)(fun: Tree, args: List[Tree])(using Context): Apply = @@ -782,7 +776,9 @@ object Inlines: inlinedTypeDef(stat, inlinedSym) private def inlinedSym(sym: Symbol, withoutFlags: FlagSet = EmptyFlags)(using Context): Symbol = - if sym.isClass then inlinedClassSym(sym.asClass, withoutFlags) else inlinedMemberSym(sym, withoutFlags) + val newSym = if sym.isClass then inlinedClassSym(sym.asClass, withoutFlags) else inlinedMemberSym(sym, withoutFlags) + ctx.inlineTraitState.registerInlinedSymbol(sym, newSym, ctx.owner.thisType.widenDealias) + newSym private def inlinedClassSym(sym: ClassSymbol, withoutFlags: FlagSet = EmptyFlags)(using Context): ClassSymbol = sym.info match { @@ -804,7 +800,8 @@ object Inlines: sym.privateWithin, spanCoord(parent.span) ) - innerClassNewSyms.put(sym, inlinedSym) + // ctx.inlineTraitState.registerInlinedInnerClassSymbol(sym, inlinedSym, childThisType) + ctx.inlineTraitState.registerInlinedSymbol(sym, inlinedSym, childThisType.widenDealias) inlinedSym.entered case _ => report.error(s"Class symbol ${sym.show} does not have class info") @@ -823,7 +820,7 @@ object Inlines: owner = ctx.owner, name = name, flags = flags &~ withoutFlags, - info = inlinerTypeMap(sym.info).substSym(substFrom, substTo), + info = inlinerTypeMap(sym.info), // .substSym(substFrom, substTo), coord = spanCoord(parent.span)).entered private def inlinedValDef(vdef: ValDef, inlinedSym: Symbol)(using Context): ValDef = @@ -880,32 +877,6 @@ object Inlines: // TODO make version of inlined that does not return bindings? Inlined(tpd.ref(parentSym), Nil, inlined(rhs)._2).withSpan(parent.span) - private def defsAdapter = - val typeMap = new DeepTypeMap { - override def apply(tp: Type): Type = tp match { - case TypeRef(_, sym: Symbol) if innerClassNewSyms.contains(sym) => - TypeRef(childThisType, innerClassNewSyms(sym)) - case _ => - mapOver(tp) - } - } - def treeMap(tree: Tree) = tree match { - case ident: Ident if innerClassNewSyms.contains(ident.symbol) => - Ident(innerClassNewSyms(ident.symbol).namedType) - case tdef: TypeDef if tdef.symbol.isClass => - tdef.symbol.info = typeMap(tdef.symbol.info) - tdef - case tree => - tree - } - new TreeTypeMap( - typeMap = typeMap, - treeMap = treeMap, - substFrom = substFrom, - substTo = substTo, - ) - end defsAdapter - private class ParamAccessorsMapper: private val paramAccessorsTrees: mutable.Map[Symbol, Map[Name, Tree]] = mutable.Map.empty private val paramAccessorsNewNames: mutable.Map[(Symbol, Name), Name] = mutable.Map.empty @@ -938,4 +909,38 @@ object Inlines: paramAccessorsNewNames.get(parent, paramAccessorName) end ParamAccessorsMapper end InlineParentTrait + +// Block inline traits from having inner classes for now - gets annoying in terms of which symbols you should replace: + // consider class InnerB extends InnerA inside the same inline trait as class InnerA inside class A. + class InlineTraitState: + // Map representing all symbols we have inlined from inline traits, + // from the symbol in the parent trait, and the type of that child trait + // to the inlined symbol in the child trait. + private val inlinedTraitSymbols = mutable.HashMap[(Symbol, Type), Symbol]() + private val symbolsResultingFromInline = mutable.HashSet[Symbol]() + + // Map representing only inner class symbols that we have inlined from inline traits, + // from the symbol in the parent trait to the inlined symbol in the child trait, + // and the *ThisType* of the child trait. + // private val innerClassNewSyms = mutable.HashMap[Symbol, List[Symbol, Type]]() + + def registerInlinedSymbol(oldSym: Symbol, newSym: Symbol, childTrait: Type) = + inlinedTraitSymbols((oldSym, childTrait)) = newSym + symbolsResultingFromInline.add(newSym) + def lookupInlinedSymbol(oldSym: Symbol, childTrait: Type) = + inlinedTraitSymbols((oldSym, childTrait)) + def inlinedSymbolIsRegistered(oldSym: Symbol, childTrait: Type) = + inlinedTraitSymbols.contains((oldSym, childTrait)) + // def symbolResultsFromInlining(sym: Symbol) = + // symbolsResultingFromInline.contains(sym) + + // def registerInlinedInnerClassSymbol(oldSym: Symbol, newSym: Symbol, childThisType: Type) = + // innerClassNewSyms(oldSym) = (newSym, childThisType) + // def lookupInlinedInnerClassSymbol(oldSym: Symbol) = + // innerClassNewSyms(oldSym) + // def inlinedInnerClassSymbolIsRegistered(oldSym: Symbol) = + // innerClassNewSyms.contains(oldSym) + + end InlineTraitState + end Inlines diff --git a/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala b/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala new file mode 100644 index 000000000000..85372a61f7aa --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala @@ -0,0 +1,210 @@ +package dotty.tools.dotc +package transform + +import ast.*, core._ +import Flags._ +import Contexts._ +import Symbols._ +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.ast.Trees._ +import dotty.tools.dotc.quoted._ +import dotty.tools.dotc.inlines.Inlines +import dotty.tools.dotc.ast.TreeMapWithImplicits +import dotty.tools.dotc.core.DenotTransformers.SymTransformer +import dotty.tools.dotc.staging.StagingLevel +import dotty.tools.dotc.core.SymDenotations.SymDenotation +import dotty.tools.dotc.core.StdNames.{str, nme} +import dotty.tools.dotc.core.Types.* +import dotty.tools.dotc.core.Names.{Name, TermName} + +import scala.collection.mutable.ListBuffer + + +// WE don't use th child this type so we should probabyl be able to get away with only one symbol map. + +class ReplaceInlinedTraitSymbols extends MacroTransform: //, SymTransformer: + import tpd._ + import ast.tpd.* + + override def phaseName: String = ReplaceInlinedTraitSymbols.name + override def description: String = ReplaceInlinedTraitSymbols.description + override def changesMembers: Boolean = true + override def changesParents: Boolean = true + + private def symbolReplacer(using Context) = + + val typeMap = new DeepTypeMap { + override def apply(tp: Type): Type = tp match { // Deals exclusively with inner clases + case TypeRef(prefix: Type, sym: Symbol) => + val prefixType = prefix.widenDealias + if ctx.inlineTraitState.inlinedSymbolIsRegistered(sym, prefixType) then + val newSym = ctx.inlineTraitState.lookupInlinedSymbol(sym, prefixType) + TypeRef(prefixType, newSym) + else + mapOver(tp) + case _ => + mapOver(tp) + } + } + + def treeMap(tree: Tree) = tree match { + case sel: Select => + val qualType = sel.qualifier.tpe.widenDealias + if ctx.inlineTraitState.inlinedSymbolIsRegistered(sel.symbol, qualType) then + val newSym = ctx.inlineTraitState.lookupInlinedSymbol(sel.symbol, qualType) + if (sel.symbol.isTerm) + tree.withType(newSym.termRef) // This path seems good + else + tree.withType(newSym.typeRef) // Also deals with inner classes only + else + tree + case tdef: TypeDef if tdef.symbol.isClass => // maybe this belongs in the original inliner? Not clear exaclty what it does; does fire but only delegates to typemap + tdef.symbol.info = typeMap(tdef.symbol.info) + tdef + case tree => + tree + } + + new TreeTypeMap( + typeMap = typeMap, + treeMap = treeMap) + + // { + // override def transform(tree: Tree)(using Context): Tree = tree match { + // case cls @ tpd.TypeDef(_, impl: Template) => + // if (ctx.inlineTraitState.inlinedSymbolIsRegistered(cls.denot.symbol, ctx.ownd)) then + // // go recursively over the body only + // val impl1 = cpy.Template(impl)(body = super.transform(impl.body)) + // cpy.TypeDef(cls)(rhs = impl1) + // else + // // we can map over the whole thing + // super.transform(tree) + // case _ => super.transform(tree) + // } + // } + end symbolReplacer + +// Need to look at the receiver type when doing the replacement - only replace with members that match the receiver type. +// I think same for both Types and Terms - if you refer to it via the child you can specialise it otherwise no. + override def newTransformer(using Context): Transformer = new Transformer { + override def transform(tree: Tree)(using Context): Tree = + val state = ctx.inlineTraitState + symbolReplacer(tree) + } + + override def run(using Context): Unit = + try super.run + catch case _: CompilationUnit.SuspendException => () + + +object ReplaceInlinedTraitSymbols: + val name: String = "replaceInlinedTraitSymbols" + val description: String = "Replace symbols referring to inline trait members with resulting inlined member symbols" + + +// Try and break it with: + // calls inside and outside, inheritance inside and outside, reference to types inside and outisde, everything. + + + // val newDefs = inContext(ctx.withOwner(cls.symbol)) { + // inlineTraitAncestors(cls).foldLeft((List.empty[Tree], impl.body)){ + // case ((inlineDefs, childDefs), parent) => + // val parentTraitInliner = InlineParentTrait(parent) + // val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) + // val inlinedDefs1 = inlineDefs ::: parentTraitInliner.expandDefs(overriddenSymbols) + // (inlinedDefs1, childDefs) + // } + // } + // val impl1 = cpy.Template(impl)(body = newDefs._1 ::: newDefs._2) + // cpy.TypeDef(cls)(rhs = impl1) + + +// claim we do this at the select level and hope that someone already added selects everywhere where we need them including internal to a class /// + // override def transformIdent(Ident)(using Context): Tree = + // if ctx.owner + // case ident: Ident if ctx.inlineTraitState.inlinedSymbolIsRegistered(ident.symbol) => + // Ident(ctx.inlineTraitState.lookupInlinedSymbol(ident.symbol).namedType) + + // def adaptDefs(definitions: List[Tree]): List[Tree] = definitions.mapconserve(defsAdapter(_)) + + + // override def transformSelect(tree: Select)(using Context): Tree = + + +// override def transformSym(symd: SymDenotation)(using Context): SymDenotation = +// if symd.isClass && symd.owner.isInlineTrait && !symd.is(Module) then +// symd.copySymDenotation(name = SpecializeInlineTraits.newInnerClassName(symd.name), initFlags = (symd.flags &~ Final) | Trait) +// else +// symd + +// override def checkPostCondition(tree: Tree)(using Context): Unit = +// tree match { +// // TODO check that things are inlined properly +// case _ => +// } + +// private def transformInlineTrait(inlineTrait: TypeDef)(using Context): TypeDef = +// val tpd.TypeDef(_, tmpl: Template) = inlineTrait: @unchecked +// val body1 = tmpl.body.flatMap { +// case innerClass: TypeDef if innerClass.symbol.isClass => +// val newTrait = makeTraitFromInnerClass(innerClass) +// val newType = makeTypeFromInnerClass(inlineTrait.symbol, innerClass, newTrait.symbol) +// List(newTrait, newType) +// case member: MemberDef => +// List(member) +// case _ => +// // Remove non-memberdefs, as they are normally placed into $init() +// Nil +// } +// val tmpl1 = cpy.Template(tmpl)(body = body1) +// cpy.TypeDef(inlineTrait)(rhs = tmpl1) +// end transformInlineTrait + +// private def makeTraitFromInnerClass(innerClass: TypeDef)(using Context): TypeDef = +// val TypeDef(name, tmpl: Template) = innerClass: @unchecked +// val newInnerParents = tmpl.parents.mapConserve(ConcreteParentStripper.apply) +// val tmpl1 = cpy.Template(tmpl)(parents = newInnerParents) // TODO .withType(???) +// val newTrait = cpy.TypeDef(innerClass)(name = SpecializeInlineTraits.newInnerClassName(name), rhs = tmpl1) +// newTrait.symbol.setFlag(Synthetic) +// newTrait +// end makeTraitFromInnerClass + +// private def makeTypeFromInnerClass(parentSym: Symbol, innerClass: TypeDef, newTraitSym: Symbol)(using Context): TypeDef = +// val upperBound = innerClass.symbol.primaryConstructor.info match { +// case _: MethodType => +// newTraitSym.typeRef +// case poly: PolyType => +// HKTypeLambda(poly.paramNames)(tl => poly.paramInfos, tl => newTraitSym.typeRef.appliedTo(tl.paramRefs.head)) +// } +// val newTypeSym = newSymbol( +// owner = parentSym, +// name = newTraitSym.name.asTypeName, +// flags = innerClass.symbol.flags & (Private | Protected) | Synthetic, +// info = TypeBounds.upper(upperBound), +// privateWithin = innerClass.symbol.privateWithin, +// coord = innerClass.symbol.coord, +// nestingLevel = innerClass.symbol.nestingLevel, +// ).asType +// TypeDef(newTypeSym) +// end makeTypeFromInnerClass + +// private object ConcreteParentStripper extends TreeAccumulator[Tree] { +// def apply(tree: Tree)(using Context): Tree = apply(tree, tree) + +// override def apply(x: Tree, tree: Tree)(using Context): Tree = tree match { +// case ident: Ident => ident +// case tpt: TypeTree => tpt +// case _ => foldOver(x, tree) +// } +// } +// } + + + + // val sym = tree.symbol + // val qualTypeSym = tree.qualifier.tpe.widenDealias.typeSymbol + // if !(sym.isTerm && sym.owner.isClass) || sym.maybeOwner.eq(qualTypeSym) || !qualTypeSym.isClass then tree + // else + // val devirtualizedSym = sym.overriddenSymbol(qualTypeSym.asClass) + // if !devirtualizedSym.exists || sym.eq(devirtualizedSym) || devirtualizedSym.isAllOf(Mutable | JavaDefined) then tree + // else tree.withType(devirtualizedSym.termRef) diff --git a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala index 460b57cf5ab8..1d1490995174 100644 --- a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala @@ -118,6 +118,6 @@ class SpecializeInlineTraits extends MacroTransform, SymTransformer { object SpecializeInlineTraits: val name: String = "specializeInlineTraits" - val description: String = "inline the code of inline traits and specialize calls to their members" + val description: String = "inline the code of inline traits" private[transform] def newInnerClassName(name: Name): name.ThisName = name ++ str.INLINE_TRAIT_INNER_CLASS_SUFFIX From 9de5cb91fb6d7a0a823157eee074c982f1b5c02c Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 1 Mar 2026 20:03:27 +0100 Subject: [PATCH 012/254] Switch from TreeTypeMap to MiniPhase --- .../ReplaceInlinedTraitSymbols.scala | 135 ++++++++++-------- 1 file changed, 76 insertions(+), 59 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala b/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala index 85372a61f7aa..4ac9b0f32a36 100644 --- a/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala +++ b/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala @@ -18,11 +18,12 @@ import dotty.tools.dotc.core.Types.* import dotty.tools.dotc.core.Names.{Name, TermName} import scala.collection.mutable.ListBuffer +import dotty.tools.dotc.transform.MegaPhase.MiniPhase // WE don't use th child this type so we should probabyl be able to get away with only one symbol map. -class ReplaceInlinedTraitSymbols extends MacroTransform: //, SymTransformer: +class ReplaceInlinedTraitSymbols extends MiniPhase: //, SymTransformer: import tpd._ import ast.tpd.* @@ -31,70 +32,86 @@ class ReplaceInlinedTraitSymbols extends MacroTransform: //, SymTransformer: override def changesMembers: Boolean = true override def changesParents: Boolean = true - private def symbolReplacer(using Context) = + override def transformSelect(tree: Select)(using Context): Tree = + val qualType = tree.qualifier.tpe.widenDealias + if ctx.inlineTraitState.inlinedSymbolIsRegistered(tree.symbol, qualType) then + val newSym = ctx.inlineTraitState.lookupInlinedSymbol(tree.symbol, qualType) + assert(tree.symbol.isTerm) + tree.withType(newSym.termRef) // This path seems good + else + tree + + override def runsAfterGroupsOf: Set[String] = Set("specializeInlineTraits") + + + // if (tree.symbol.isTerm) + + + // private def symbolReplacer(using Context) = + + - val typeMap = new DeepTypeMap { - override def apply(tp: Type): Type = tp match { // Deals exclusively with inner clases - case TypeRef(prefix: Type, sym: Symbol) => - val prefixType = prefix.widenDealias - if ctx.inlineTraitState.inlinedSymbolIsRegistered(sym, prefixType) then - val newSym = ctx.inlineTraitState.lookupInlinedSymbol(sym, prefixType) - TypeRef(prefixType, newSym) - else - mapOver(tp) - case _ => - mapOver(tp) - } - } - - def treeMap(tree: Tree) = tree match { - case sel: Select => - val qualType = sel.qualifier.tpe.widenDealias - if ctx.inlineTraitState.inlinedSymbolIsRegistered(sel.symbol, qualType) then - val newSym = ctx.inlineTraitState.lookupInlinedSymbol(sel.symbol, qualType) - if (sel.symbol.isTerm) - tree.withType(newSym.termRef) // This path seems good - else - tree.withType(newSym.typeRef) // Also deals with inner classes only - else - tree - case tdef: TypeDef if tdef.symbol.isClass => // maybe this belongs in the original inliner? Not clear exaclty what it does; does fire but only delegates to typemap - tdef.symbol.info = typeMap(tdef.symbol.info) - tdef - case tree => - tree - } + // val typeMap = new DeepTypeMap { + // override def apply(tp: Type): Type = tp match { // Deals exclusively with inner clases + // // case TypeRef(prefix: Type, sym: Symbol) => + // // val prefixType = prefix.widenDealias + // // if ctx.inlineTraitState.inlinedSymbolIsRegistered(sym, prefixType) then + // // val newSym = ctx.inlineTraitState.lookupInlinedSymbol(sym, prefixType) + // // TypeRef(prefixType, newSym) + // // else + // // mapOver(tp) + // case _ => + // mapOver(tp) + // } + // } + + // def treeMap(tree: Tree) = tree match { + // // case sel: Select => + // // val qualType = sel.qualifier.tpe.widenDealias + // // if ctx.inlineTraitState.inlinedSymbolIsRegistered(sel.symbol, qualType) then + // // val newSym = ctx.inlineTraitState.lookupInlinedSymbol(sel.symbol, qualType) + // // if (sel.symbol.isTerm) + // // tree.withType(newSym.termRef) // This path seems good + // // else + // // tree.withType(newSym.typeRef) // Also deals with inner classes only + // // else + // // tree + // // case tdef: TypeDef if tdef.symbol.isClass => // maybe this belongs in the original inliner? Not clear exaclty what it does; does fire but only delegates to typemap + // // tdef.symbol.info = typeMap(tdef.symbol.info) + // // tdef + // case tree => + // tree + // } - new TreeTypeMap( - typeMap = typeMap, - treeMap = treeMap) + // new TreeTypeMap( + // typeMap = typeMap, + // treeMap = treeMap) - // { - // override def transform(tree: Tree)(using Context): Tree = tree match { - // case cls @ tpd.TypeDef(_, impl: Template) => - // if (ctx.inlineTraitState.inlinedSymbolIsRegistered(cls.denot.symbol, ctx.ownd)) then - // // go recursively over the body only - // val impl1 = cpy.Template(impl)(body = super.transform(impl.body)) - // cpy.TypeDef(cls)(rhs = impl1) - // else - // // we can map over the whole thing - // super.transform(tree) - // case _ => super.transform(tree) - // } - // } - end symbolReplacer + // // { + // // override def transform(tree: Tree)(using Context): Tree = tree match { + // // case cls @ tpd.TypeDef(_, impl: Template) => + // // if (ctx.inlineTraitState.inlinedSymbolIsRegistered(cls.denot.symbol, ctx.ownd)) then + // // // go recursively over the body only + // // val impl1 = cpy.Template(impl)(body = super.transform(impl.body)) + // // cpy.TypeDef(cls)(rhs = impl1) + // // else + // // // we can map over the whole thing + // // super.transform(tree) + // // case _ => super.transform(tree) + // // } + // // } + // end symbolReplacer // Need to look at the receiver type when doing the replacement - only replace with members that match the receiver type. // I think same for both Types and Terms - if you refer to it via the child you can specialise it otherwise no. - override def newTransformer(using Context): Transformer = new Transformer { - override def transform(tree: Tree)(using Context): Tree = - val state = ctx.inlineTraitState - symbolReplacer(tree) - } - - override def run(using Context): Unit = - try super.run - catch case _: CompilationUnit.SuspendException => () + // override def newTransformer(using Context): Transformer = new Transformer { + // override def transform(tree: Tree)(using Context): Tree = tree + // // symbolReplacer(tree) + // } + + // override def run(using Context): Unit = + // try super.run + // catch case _: CompilationUnit.SuspendException => () object ReplaceInlinedTraitSymbols: From 5e169bf2a56feb472dd07e34e26d90f6988ea5e7 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 1 Mar 2026 20:21:43 +0100 Subject: [PATCH 013/254] Remove some unneeded stuff --- .../src/dotty/tools/dotc/inlines/Inlines.scala | 18 ------------------ .../transform/ReplaceInlinedTraitSymbols.scala | 12 ++++-------- 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index a0f1faee5a29..bdd2bbacf815 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -910,36 +910,18 @@ object Inlines: end ParamAccessorsMapper end InlineParentTrait -// Block inline traits from having inner classes for now - gets annoying in terms of which symbols you should replace: - // consider class InnerB extends InnerA inside the same inline trait as class InnerA inside class A. class InlineTraitState: // Map representing all symbols we have inlined from inline traits, // from the symbol in the parent trait, and the type of that child trait // to the inlined symbol in the child trait. private val inlinedTraitSymbols = mutable.HashMap[(Symbol, Type), Symbol]() - private val symbolsResultingFromInline = mutable.HashSet[Symbol]() - - // Map representing only inner class symbols that we have inlined from inline traits, - // from the symbol in the parent trait to the inlined symbol in the child trait, - // and the *ThisType* of the child trait. - // private val innerClassNewSyms = mutable.HashMap[Symbol, List[Symbol, Type]]() def registerInlinedSymbol(oldSym: Symbol, newSym: Symbol, childTrait: Type) = inlinedTraitSymbols((oldSym, childTrait)) = newSym - symbolsResultingFromInline.add(newSym) def lookupInlinedSymbol(oldSym: Symbol, childTrait: Type) = inlinedTraitSymbols((oldSym, childTrait)) def inlinedSymbolIsRegistered(oldSym: Symbol, childTrait: Type) = inlinedTraitSymbols.contains((oldSym, childTrait)) - // def symbolResultsFromInlining(sym: Symbol) = - // symbolsResultingFromInline.contains(sym) - - // def registerInlinedInnerClassSymbol(oldSym: Symbol, newSym: Symbol, childThisType: Type) = - // innerClassNewSyms(oldSym) = (newSym, childThisType) - // def lookupInlinedInnerClassSymbol(oldSym: Symbol) = - // innerClassNewSyms(oldSym) - // def inlinedInnerClassSymbolIsRegistered(oldSym: Symbol) = - // innerClassNewSyms.contains(oldSym) end InlineTraitState diff --git a/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala b/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala index 4ac9b0f32a36..df63d782823b 100644 --- a/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala +++ b/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala @@ -20,12 +20,8 @@ import dotty.tools.dotc.core.Names.{Name, TermName} import scala.collection.mutable.ListBuffer import dotty.tools.dotc.transform.MegaPhase.MiniPhase - -// WE don't use th child this type so we should probabyl be able to get away with only one symbol map. - -class ReplaceInlinedTraitSymbols extends MiniPhase: //, SymTransformer: +class ReplaceInlinedTraitSymbols extends MiniPhase: import tpd._ - import ast.tpd.* override def phaseName: String = ReplaceInlinedTraitSymbols.name override def description: String = ReplaceInlinedTraitSymbols.description @@ -37,14 +33,14 @@ class ReplaceInlinedTraitSymbols extends MiniPhase: //, SymTransformer: if ctx.inlineTraitState.inlinedSymbolIsRegistered(tree.symbol, qualType) then val newSym = ctx.inlineTraitState.lookupInlinedSymbol(tree.symbol, qualType) assert(tree.symbol.isTerm) - tree.withType(newSym.termRef) // This path seems good + tree.withType(newSym.termRef) else tree override def runsAfterGroupsOf: Set[String] = Set("specializeInlineTraits") - - // if (tree.symbol.isTerm) + + // if (tree.symbol.isTerm) // private def symbolReplacer(using Context) = From 81105db84370530446c58fd8bc8a10a214c51343 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 2 Mar 2026 12:24:52 +0100 Subject: [PATCH 014/254] Add missing generic type --- tests/pos/inline-trait-specialised-desugar.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pos/inline-trait-specialised-desugar.scala b/tests/pos/inline-trait-specialised-desugar.scala index e317fd61096e..a758974d413f 100644 --- a/tests/pos/inline-trait-specialised-desugar.scala +++ b/tests/pos/inline-trait-specialised-desugar.scala @@ -35,7 +35,7 @@ class MyClassB extends MyClassA, ArrayIterator$sp$Int val xs: Array[Int] = Array(1, 2, 3) // They do this: - // new ArrayIterator(xs) {} + // new ArrayIterator[Int](xs) {} // We convert this to: val ai = new ArrayIterator$impl$Int(xs) {} From b026b25560724832f4f099a75c1e8ba2df403501 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 2 Mar 2026 13:28:31 +0100 Subject: [PATCH 015/254] Fix PureInterface and NoInits flags on inline traits --- .../src/dotty/tools/dotc/inlines/Inlines.scala | 15 +++++++++++++++ .../tools/dotc/transform/PruneInlineTraits.scala | 3 ++- tests/pos/inline-trait-trait-inheritance.scala | 2 ++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index bdd2bbacf815..6000b31191d4 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -246,6 +246,19 @@ object Inlines: tree2 end inlineCall + def updateFlagsFromInlinedParent(child: FlagSet, parent: FlagSet): FlagSet = + var updatedFlags = child + // Parent needs to be initialised so child must also as initialisers have been inlined + if (!parent.is(NoInits)) + updatedFlags &~= NoInits + + // Parent is impure; contaminates child as non-abstract methods have been inlined + if (!parent.is(PureInterface)) + updatedFlags &~= PureInterface + updatedFlags + + + def inlineParentInlineTraits(cls: Tree)(using Context): Tree = cls match { case cls @ tpd.TypeDef(_, impl: Template) => @@ -256,6 +269,8 @@ object Inlines: val parentTraitInliner = InlineParentTrait(parent) val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) val inlinedDefs1 = inlineDefs ::: parentTraitInliner.expandDefs(overriddenSymbols) + cls.symbol.flags = updateFlagsFromInlinedParent(cls.symbol.flags, parent.symbol.flags) + (inlinedDefs1, childDefs) } } diff --git a/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala index 5d9db954201a..e07a11838423 100644 --- a/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala @@ -20,6 +20,7 @@ class PruneInlineTraits extends MiniPhase with SymTransformer { thisTransform => override def transformSym(sym: SymDenotation)(using Context): SymDenotation = if isEraseable(sym) then sym.copySymDenotation(initFlags = sym.flags | Deferred) + else if sym.isInlineTrait then sym.copySymDenotation(initFlags = sym.flags | PureInterface | NoInits) else sym override def transformValDef(tree: ValDef)(using Context): ValDef = @@ -40,7 +41,7 @@ class PruneInlineTraits extends MiniPhase with SymTransformer { thisTransform => && sym.owner.isInlineTrait } -object PruneInlineTraits { +object PruneInlineTraits { import tpd._ val name: String = "pruneInlineTraits" diff --git a/tests/pos/inline-trait-trait-inheritance.scala b/tests/pos/inline-trait-trait-inheritance.scala index 176d544705fd..a381078c6cb7 100644 --- a/tests/pos/inline-trait-trait-inheritance.scala +++ b/tests/pos/inline-trait-trait-inheritance.scala @@ -2,3 +2,5 @@ inline trait A(x: Int): val y = x trait C extends A + +class D extends C, A(15) From 6a3ab504de83dca6c40c00a22f290f086f11217a Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 2 Mar 2026 13:36:12 +0100 Subject: [PATCH 016/254] Clean up unneeded comments in RepalceInlinedTraitSymbols.scala --- .../dotty/tools/dotc/inlines/Inlines.scala | 24 ++- .../ReplaceInlinedTraitSymbols.scala | 180 ------------------ 2 files changed, 16 insertions(+), 188 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 6000b31191d4..f0d6e26cf1ff 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -927,16 +927,24 @@ object Inlines: class InlineTraitState: // Map representing all symbols we have inlined from inline traits, - // from the symbol in the parent trait, and the type of that child trait - // to the inlined symbol in the child trait. + // from the symbol in the parent trait, and the type of the child class-like + // to the inlined symbol in that child class-like. + // E.g. inline trait A {def foo#1000}; trait B extends A {def foo#2000 // created by inlining} + // The map has (foo#1000, trait B) => foo#2000 private val inlinedTraitSymbols = mutable.HashMap[(Symbol, Type), Symbol]() - def registerInlinedSymbol(oldSym: Symbol, newSym: Symbol, childTrait: Type) = - inlinedTraitSymbols((oldSym, childTrait)) = newSym - def lookupInlinedSymbol(oldSym: Symbol, childTrait: Type) = - inlinedTraitSymbols((oldSym, childTrait)) - def inlinedSymbolIsRegistered(oldSym: Symbol, childTrait: Type) = - inlinedTraitSymbols.contains((oldSym, childTrait)) + // Record that we just inlined oldSym into childClasslike which created + // childClassLike.newSym + def registerInlinedSymbol(oldSym: Symbol, newSym: Symbol, childClasslike: Type) = + inlinedTraitSymbols((oldSym, childClasslike)) = newSym + + // Map (e.g.) B.foo#1000 into foo#2000 + def lookupInlinedSymbol(oldSym: Symbol, childClasslike: Type) = + inlinedTraitSymbols((oldSym, childClasslike)) + + // Check if oldSym has been inlined into childClasslike + def inlinedSymbolIsRegistered(oldSym: Symbol, childClasslike: Type) = + inlinedTraitSymbols.contains((oldSym, childClasslike)) end InlineTraitState diff --git a/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala b/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala index df63d782823b..0390ccc12882 100644 --- a/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala +++ b/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala @@ -38,186 +38,6 @@ class ReplaceInlinedTraitSymbols extends MiniPhase: tree override def runsAfterGroupsOf: Set[String] = Set("specializeInlineTraits") - - - // if (tree.symbol.isTerm) - - - // private def symbolReplacer(using Context) = - - - - // val typeMap = new DeepTypeMap { - // override def apply(tp: Type): Type = tp match { // Deals exclusively with inner clases - // // case TypeRef(prefix: Type, sym: Symbol) => - // // val prefixType = prefix.widenDealias - // // if ctx.inlineTraitState.inlinedSymbolIsRegistered(sym, prefixType) then - // // val newSym = ctx.inlineTraitState.lookupInlinedSymbol(sym, prefixType) - // // TypeRef(prefixType, newSym) - // // else - // // mapOver(tp) - // case _ => - // mapOver(tp) - // } - // } - - // def treeMap(tree: Tree) = tree match { - // // case sel: Select => - // // val qualType = sel.qualifier.tpe.widenDealias - // // if ctx.inlineTraitState.inlinedSymbolIsRegistered(sel.symbol, qualType) then - // // val newSym = ctx.inlineTraitState.lookupInlinedSymbol(sel.symbol, qualType) - // // if (sel.symbol.isTerm) - // // tree.withType(newSym.termRef) // This path seems good - // // else - // // tree.withType(newSym.typeRef) // Also deals with inner classes only - // // else - // // tree - // // case tdef: TypeDef if tdef.symbol.isClass => // maybe this belongs in the original inliner? Not clear exaclty what it does; does fire but only delegates to typemap - // // tdef.symbol.info = typeMap(tdef.symbol.info) - // // tdef - // case tree => - // tree - // } - - // new TreeTypeMap( - // typeMap = typeMap, - // treeMap = treeMap) - - // // { - // // override def transform(tree: Tree)(using Context): Tree = tree match { - // // case cls @ tpd.TypeDef(_, impl: Template) => - // // if (ctx.inlineTraitState.inlinedSymbolIsRegistered(cls.denot.symbol, ctx.ownd)) then - // // // go recursively over the body only - // // val impl1 = cpy.Template(impl)(body = super.transform(impl.body)) - // // cpy.TypeDef(cls)(rhs = impl1) - // // else - // // // we can map over the whole thing - // // super.transform(tree) - // // case _ => super.transform(tree) - // // } - // // } - // end symbolReplacer - -// Need to look at the receiver type when doing the replacement - only replace with members that match the receiver type. -// I think same for both Types and Terms - if you refer to it via the child you can specialise it otherwise no. - // override def newTransformer(using Context): Transformer = new Transformer { - // override def transform(tree: Tree)(using Context): Tree = tree - // // symbolReplacer(tree) - // } - - // override def run(using Context): Unit = - // try super.run - // catch case _: CompilationUnit.SuspendException => () - - object ReplaceInlinedTraitSymbols: val name: String = "replaceInlinedTraitSymbols" val description: String = "Replace symbols referring to inline trait members with resulting inlined member symbols" - - -// Try and break it with: - // calls inside and outside, inheritance inside and outside, reference to types inside and outisde, everything. - - - // val newDefs = inContext(ctx.withOwner(cls.symbol)) { - // inlineTraitAncestors(cls).foldLeft((List.empty[Tree], impl.body)){ - // case ((inlineDefs, childDefs), parent) => - // val parentTraitInliner = InlineParentTrait(parent) - // val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) - // val inlinedDefs1 = inlineDefs ::: parentTraitInliner.expandDefs(overriddenSymbols) - // (inlinedDefs1, childDefs) - // } - // } - // val impl1 = cpy.Template(impl)(body = newDefs._1 ::: newDefs._2) - // cpy.TypeDef(cls)(rhs = impl1) - - -// claim we do this at the select level and hope that someone already added selects everywhere where we need them including internal to a class /// - // override def transformIdent(Ident)(using Context): Tree = - // if ctx.owner - // case ident: Ident if ctx.inlineTraitState.inlinedSymbolIsRegistered(ident.symbol) => - // Ident(ctx.inlineTraitState.lookupInlinedSymbol(ident.symbol).namedType) - - // def adaptDefs(definitions: List[Tree]): List[Tree] = definitions.mapconserve(defsAdapter(_)) - - - // override def transformSelect(tree: Select)(using Context): Tree = - - -// override def transformSym(symd: SymDenotation)(using Context): SymDenotation = -// if symd.isClass && symd.owner.isInlineTrait && !symd.is(Module) then -// symd.copySymDenotation(name = SpecializeInlineTraits.newInnerClassName(symd.name), initFlags = (symd.flags &~ Final) | Trait) -// else -// symd - -// override def checkPostCondition(tree: Tree)(using Context): Unit = -// tree match { -// // TODO check that things are inlined properly -// case _ => -// } - -// private def transformInlineTrait(inlineTrait: TypeDef)(using Context): TypeDef = -// val tpd.TypeDef(_, tmpl: Template) = inlineTrait: @unchecked -// val body1 = tmpl.body.flatMap { -// case innerClass: TypeDef if innerClass.symbol.isClass => -// val newTrait = makeTraitFromInnerClass(innerClass) -// val newType = makeTypeFromInnerClass(inlineTrait.symbol, innerClass, newTrait.symbol) -// List(newTrait, newType) -// case member: MemberDef => -// List(member) -// case _ => -// // Remove non-memberdefs, as they are normally placed into $init() -// Nil -// } -// val tmpl1 = cpy.Template(tmpl)(body = body1) -// cpy.TypeDef(inlineTrait)(rhs = tmpl1) -// end transformInlineTrait - -// private def makeTraitFromInnerClass(innerClass: TypeDef)(using Context): TypeDef = -// val TypeDef(name, tmpl: Template) = innerClass: @unchecked -// val newInnerParents = tmpl.parents.mapConserve(ConcreteParentStripper.apply) -// val tmpl1 = cpy.Template(tmpl)(parents = newInnerParents) // TODO .withType(???) -// val newTrait = cpy.TypeDef(innerClass)(name = SpecializeInlineTraits.newInnerClassName(name), rhs = tmpl1) -// newTrait.symbol.setFlag(Synthetic) -// newTrait -// end makeTraitFromInnerClass - -// private def makeTypeFromInnerClass(parentSym: Symbol, innerClass: TypeDef, newTraitSym: Symbol)(using Context): TypeDef = -// val upperBound = innerClass.symbol.primaryConstructor.info match { -// case _: MethodType => -// newTraitSym.typeRef -// case poly: PolyType => -// HKTypeLambda(poly.paramNames)(tl => poly.paramInfos, tl => newTraitSym.typeRef.appliedTo(tl.paramRefs.head)) -// } -// val newTypeSym = newSymbol( -// owner = parentSym, -// name = newTraitSym.name.asTypeName, -// flags = innerClass.symbol.flags & (Private | Protected) | Synthetic, -// info = TypeBounds.upper(upperBound), -// privateWithin = innerClass.symbol.privateWithin, -// coord = innerClass.symbol.coord, -// nestingLevel = innerClass.symbol.nestingLevel, -// ).asType -// TypeDef(newTypeSym) -// end makeTypeFromInnerClass - -// private object ConcreteParentStripper extends TreeAccumulator[Tree] { -// def apply(tree: Tree)(using Context): Tree = apply(tree, tree) - -// override def apply(x: Tree, tree: Tree)(using Context): Tree = tree match { -// case ident: Ident => ident -// case tpt: TypeTree => tpt -// case _ => foldOver(x, tree) -// } -// } -// } - - - - // val sym = tree.symbol - // val qualTypeSym = tree.qualifier.tpe.widenDealias.typeSymbol - // if !(sym.isTerm && sym.owner.isClass) || sym.maybeOwner.eq(qualTypeSym) || !qualTypeSym.isClass then tree - // else - // val devirtualizedSym = sym.overriddenSymbol(qualTypeSym.asClass) - // if !devirtualizedSym.exists || sym.eq(devirtualizedSym) || devirtualizedSym.isAllOf(Mutable | JavaDefined) then tree - // else tree.withType(devirtualizedSym.termRef) From f9b1cd0c57fbb999cadc9c81100fe5f73aa05e4f Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 2 Mar 2026 17:38:39 +0100 Subject: [PATCH 017/254] Fix multiple file inline trait test --- compiler/src/dotty/tools/dotc/inlines/Inlines.scala | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index f0d6e26cf1ff..dcc26c143b44 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -256,8 +256,6 @@ object Inlines: if (!parent.is(PureInterface)) updatedFlags &~= PureInterface updatedFlags - - def inlineParentInlineTraits(cls: Tree)(using Context): Tree = cls match { @@ -753,9 +751,13 @@ object Inlines: tree } case Select(qual, name) => - paramAccessorsMapper.getParamAccessorName(qual.symbol, name) match { - case Some(newName) => Select(this(qual), newName).withSpan(parent.span) - case None => Select(this(qual), name) + inContext(ctx.withSource(tree.source)) { // Need to ensure we preserve the fact that this Select was inlined + // potentially from a different file. Recreating it discards that info. + // See: inline-trait-multiple-stages-generic-defs. + paramAccessorsMapper.getParamAccessorName(qual.symbol, name) match { + case Some(newName) => Select(this(qual), newName).withSpan(parent.span) + case None => Select(this(qual), name) + } } case tree => tree From f8f7dd579580ed375a35832fb8e27b502104b17f Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 2 Mar 2026 17:45:05 +0100 Subject: [PATCH 018/254] tidy --- compiler/src/dotty/tools/dotc/inlines/Inlines.scala | 2 +- compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index dcc26c143b44..4bdf3f10d41b 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -837,7 +837,7 @@ object Inlines: owner = ctx.owner, name = name, flags = flags &~ withoutFlags, - info = inlinerTypeMap(sym.info), // .substSym(substFrom, substTo), + info = inlinerTypeMap(sym.info), coord = spanCoord(parent.span)).entered private def inlinedValDef(vdef: ValDef, inlinedSym: Symbol)(using Context): ValDef = diff --git a/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala index e07a11838423..90083378e57d 100644 --- a/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala @@ -41,7 +41,7 @@ class PruneInlineTraits extends MiniPhase with SymTransformer { thisTransform => && sym.owner.isInlineTrait } -object PruneInlineTraits { +object PruneInlineTraits { import tpd._ val name: String = "pruneInlineTraits" From 3dcb26de997631b5d939c42b84cda7a9822c6ed9 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 3 Mar 2026 11:35:34 +0100 Subject: [PATCH 019/254] s->z --- ...lised-desugar.scala => inline-trait-specialized-desugar.scala} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/pos/{inline-trait-specialised-desugar.scala => inline-trait-specialized-desugar.scala} (100%) diff --git a/tests/pos/inline-trait-specialised-desugar.scala b/tests/pos/inline-trait-specialized-desugar.scala similarity index 100% rename from tests/pos/inline-trait-specialised-desugar.scala rename to tests/pos/inline-trait-specialized-desugar.scala From 9a74e01f0397f917b43f09d09e0e95c91d2e0c83 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 3 Mar 2026 11:35:56 +0100 Subject: [PATCH 020/254] Add test case for inline traits where generic type is a class --- tests/pos/inline-trait-class-type-generic.scala | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/pos/inline-trait-class-type-generic.scala diff --git a/tests/pos/inline-trait-class-type-generic.scala b/tests/pos/inline-trait-class-type-generic.scala new file mode 100644 index 000000000000..572e88c34ae1 --- /dev/null +++ b/tests/pos/inline-trait-class-type-generic.scala @@ -0,0 +1,9 @@ +object MyObject + +inline trait A[T](x: T): + def foo: T = x + +class B extends A[MyObject.type](MyObject): + val y = 1 + +def h(x: B) = x.foo From 0391b4e13bb4cf243d6c6ab3d0d803becf40b7ac Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 12 Mar 2026 16:47:48 +0100 Subject: [PATCH 021/254] Generate specialized interface traits (a bit roughly) --- compiler/src/dotty/tools/dotc/Compiler.scala | 1 + .../dotty/tools/dotc/core/Definitions.scala | 3 + .../src/dotty/tools/dotc/core/Phases.scala | 4 +- .../src/dotty/tools/dotc/core/StdNames.scala | 2 + .../transform/DesugarSpecializedTraits.scala | 418 ++++++++++++++++++ .../transform/SpecializeInlineTraits.scala | 2 + library/src/scala/Specialized.scala | 5 + .../inline-trait-object-not-primitive.scala | 9 + .../inline-trait-specialized-desugar.scala | 104 +++++ .../specialized-trait-vector-example.scala | 144 ++++++ tests/pos/specialized-traits-basic.scala | 56 +++ tests/pos/specialized-traits-strawman.scala | 141 ++++++ 12 files changed, 888 insertions(+), 1 deletion(-) create mode 100644 compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala create mode 100644 library/src/scala/Specialized.scala create mode 100644 tests/pos/inline-trait-object-not-primitive.scala create mode 100644 tests/pos/specialized-trait-vector-example.scala create mode 100644 tests/pos/specialized-traits-basic.scala create mode 100644 tests/pos/specialized-traits-strawman.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index e1472586de4c..9d7b63dd64e2 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -51,6 +51,7 @@ class Compiler { protected def picklerPhases: List[List[Phase]] = List(new Pickler) :: // Generate TASTY info List(new sbt.ExtractAPI) :: // Sends a representation of the API of classes to sbt via callbacks + List(new DesugarSpecializedTraits) :: // Processes the Specialized annotation List(new SpecializeInlineTraits) :: // Inline the code of inline traits into their children List(new ReplaceInlinedTraitSymbols) :: // Replace symbols referring to inline trait members with resulting inlined member symbols List(new Inlining) :: // Inline and execute macros diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 11e6e78a62b0..aa3ff1ba6d5f 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -771,6 +771,9 @@ class Definitions { @tu lazy val StringAddClass : ClassSymbol = requiredClass("scala.runtime.StringAdd") @tu lazy val StringAdd_+ : Symbol = StringAddClass.requiredMethod(nme.raw.PLUS) + @tu lazy val SpecializedBound : ClassSymbol = requiredClass("scala.Specialized") + @tu lazy val SpecializedBoundRef : TypeRef = SpecializedBound.typeRef + @tu lazy val StringContextClass: ClassSymbol = requiredClass("scala.StringContext") @tu lazy val StringContext_s : Symbol = StringContextClass.requiredMethod(nme.s) @tu lazy val StringContext_raw: Symbol = StringContextClass.requiredMethod(nme.raw_) diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index 98df07575c2f..bca50178f5e4 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -22,7 +22,6 @@ import ast.{tpd, untpd} import scala.annotation.internal.sharable import scala.util.control.NonFatal import scala.compiletime.uninitialized -import dotty.tools.dotc.transform.ReplaceInlinedTraitSymbols object Phases { @@ -241,6 +240,7 @@ object Phases { private var mySetRootTreePhase: Phase = uninitialized private var mySpecializeInlineTraitsPhase: Phase = uninitialized private var myReplaceInlinedTraitSymbolsPhase: Phase = uninitialized + private var myDesugarSpecializedTraitsPhase: Phase = uninitialized private var myInliningPhase: Phase = uninitialized private var myStagingPhase: Phase = uninitialized private var mySplicingPhase: Phase = uninitialized @@ -276,6 +276,7 @@ object Phases { final def setRootTreePhase: Phase = mySetRootTreePhase final def specializeInlineTraitsPhase: Phase = mySpecializeInlineTraitsPhase final def replaceInlinedTraitSymbolsPhase: Phase = myReplaceInlinedTraitSymbolsPhase + final def desugarSpecializedTraitsPhase: Phase = myDesugarSpecializedTraitsPhase final def inliningPhase: Phase = myInliningPhase final def stagingPhase: Phase = myStagingPhase final def splicingPhase: Phase = mySplicingPhase @@ -311,6 +312,7 @@ object Phases { myPicklerPhase = phaseOfClass(classOf[Pickler]) mySpecializeInlineTraitsPhase = phaseOfClass(classOf[SpecializeInlineTraits]) myReplaceInlinedTraitSymbolsPhase = phaseOfClass(classOf[ReplaceInlinedTraitSymbols]) + myDesugarSpecializedTraitsPhase = phaseOfClass(classOf[DesugarSpecializedTraits]) myInliningPhase = phaseOfClass(classOf[Inlining]) myStagingPhase = phaseOfClass(classOf[Staging]) mySplicingPhase = phaseOfClass(classOf[Splicing]) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index c930982f6e62..564cec1898d0 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -27,6 +27,8 @@ object StdNames { inline val ANON_CLASS = "$anon" inline val ANON_FUN = "$anonfun" inline val INLINE_TRAIT_INNER_CLASS_SUFFIX = "$trait" + inline val SPECIALIZED_TRAIT_SUFFIX = "$sp$" + inline val SPECIALIZED_TRAIT_TYPE_SEP = "$" inline val REPL_SESSION_LINE = "rs$line$" inline val REPL_ASSIGN_SUFFIX = "$assign" diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala new file mode 100644 index 000000000000..231d03e74310 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -0,0 +1,418 @@ +package dotty.tools.dotc.transform + +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.transform.MegaPhase.MiniPhase +import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.Decorators.i +import dotty.tools.dotc.{transform => Vec} +import dotty.tools.dotc.{transform => foo} +import dotty.tools.dotc.{transform => v} +import dotty.tools.dotc.core.Decorators.className +import dotty.tools.dotc.core.Symbols.{Symbol, ClassSymbol, newNormalizedClassSymbol} +import dotty.tools.dotc.CompilationUnit +import dotty.tools.dotc.core.StdNames.* +import dotty.tools.dotc.core.Types.* +import dotty.tools.dotc.core.Flags +import dotty.tools.dotc.core.Symbols.newClassSymbol +import dotty.tools.dotc.typer.ProtoTypes.instantiateWithTypeVars +import scala.Function.const +import dotty.tools.dotc.core.Names.TypeName +import dotty.tools.dotc.core.Symbols.TypeSymbol +import dotty.tools.dotc.core.Symbols._ +import dotty.tools.dotc.ast.untpd +import dotty.tools.dotc.core.Symbols.defn +import dotty.tools.dotc.core.Flags.EmptyFlags +import dotty.tools.dotc.ast.TreeTypeMap +import dotty.tools.dotc.core.Scopes.EmptyScope +import dotty.tools.dotc.core.StdNames.str.SPECIALIZED_TRAIT_SUFFIX +import Vec.DesugarSpecializedTraits.newSpecializedTraitName +import dotty.tools.dotc.core.Names.Name +import tpd._ + +class DesugarSpecializedTraits extends MacroTransform: + + override def phaseName: String = DesugarSpecializedTraits.name + override def description: String = DesugarSpecializedTraits.description + override def changesMembers: Boolean = false + override def changesParents: Boolean = true + // override def transformTemplate(tree: Template)(using Context): Tree = + // // println(s"template ${tree}") + // tree.deepFold() + // tree + + // override def transformTyped(tree: Typed)(using Context): Tree = ??? +// + // override def transformTypeApply(tree: TypeApply)(using Context): Tree = ??? + + // override def transformTypeTree(tree: TypeTree)(using Context): Tree = tree match { + + // } + // override def transformDefDef(tree: DefDef)(using Context): Tree = + // println(s"defdef ${tree}") + // if (tree.name.toString() == "foo") { + // val ValDef(v, , EmptyTree) = tree.paramss.head.head + // // DefDef(foo,List(List(ValDef(v,AppliedTypeTree(Ident(Vec),List(Ident(Int))),EmptyTree))),TypeTree[AppliedType(TypeRef(ThisType(TypeRef(NoPrefix,module class )),trait Vec),List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),class Int)))],Ident(v)) + // } + // tree + + // need some short circuit logic for if we've already processed this one. + + override def run(using Context): Unit = + try super.run + catch case _: CompilationUnit.SuspendException => () + + + override def newTransformer(using Context): Transformer = new Transformer { + + object SpecializedEvidence { + def unapply(tpe: Type)(using Context): Option[Type] = tpe match { + case AppliedType(tycon, List(tpeArg)) if tycon =:= ctx.definitions.SpecializedBoundRef => Some(tpeArg) + case _ => None + } + } + + private def newSpecializedTraitInterfaceTrait(specializedTrait: Ident, specializationMap: Map[Type, Tree]) = + println(specializationMap) + val specializedTraitSymbol = specializedTrait.denot.symbol + + val tm = new TypeMap: + def apply(t: Type) = specializationMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) + + val specialization = specializedTraitSymbol.typeParams.map(_.typeRef).map(specializationMap.applyOrElse(_, TypeTree(_))) + + val parents = defn.ObjectType + :: AppliedTypeTree(cpy.Ident(specializedTrait)(specializedTrait.name), specialization).tpe + :: specializedTrait.denot.info.parents.filterNot(_ eq defn.ObjectType).map(tm(_)) // parents of the original trait **but then specialized** + + + + + + val traitSymbol = newNormalizedClassSymbol( + specializedTraitSymbol.owner, + (newSpecializedTraitName(specializedTraitSymbol.name, specializationMap)).asTypeName, + Flags.Synthetic | Flags.Inline | Flags.Trait, + parents, + NoType, // TODO: Work out what to do about self types; for now just ban them + specializedTraitSymbol.privateWithin, + // compUnitInfo = specializedTraitSymbol.compUnitInfo // TODO: Do we need a compUnit info? + ) + + println(s"Owner of tpparam ${specializedTraitSymbol.typeParams.head.owner}") + + val old_type_params = specializedTraitSymbol.typeParams.filterNot(t => specializationMap.contains(t.typeRef)) + val tps = newTypeParams(traitSymbol, + old_type_params.map(_.name), + EmptyFlags, + targets => targets.map(t => specializedTraitSymbol.typeParams.find(_.name == t.name).get.info.bounds) + ) + tps.foreach(traitSymbol.enter(_, EmptyScope)) + println(i"Got new tps ${tps}") + + val tpMap: Map[Type, Type] = old_type_params.map(_.typeRef).zip(tps.map(_.typeRef)).toMap + val freshTypeVarMap = new TypeMap: + def apply(t: Type) = tpMap.applyOrElse(t, mapOver) + + println("the following is the tpMap") + println(tpMap) + + // val List(traitSymbol1) = mapSymbols(List(traitSymbol), ttmap) + + // val traitSymbol2 = traitSymbol1.asInstanceOf[ClassSymbol] + + // val init = newDefaultConstructor(traitSymbol) + // val tmpl = untpd.Template( + // DefDef(init), + // parents.map(TypeTree(_)), + // EmptyValDef, + // Nil + // ) + + + val traitSymbol2 = traitSymbol.subst(old_type_params, tps).asInstanceOf[ClassSymbol] + traitSymbol.info = + ClassInfo(traitSymbol.owner.thisType, traitSymbol, traitSymbol.info.parents.map(freshTypeVarMap(_)), traitSymbol.info.decls, traitSymbol.info.self) + + // .info.parents = + // denot.info = ClassInfo(owner.thisType, cls, parentTypes.map(_.dealias), decls, selfInfo) + + println("THESE ARE THE parents") + // println(traitSym, bol2.parentTypes) + traitSymbol2.entered //, tmpl) + + // println("GOT The following resulting parents") + // println(parents) + + + // // For the given type application of concreteTypes to specializedTraitSymbol, return a list of types + // // retaining the type variable from the trait definition if the type variable is not Specialized, + // // and the concrete type from the application if the type variable is Specialized. + // private def getSelectedSpecialization(specializedTraitSymbol: Symbol, concreteTypeTrees: List[Tree]): (List[Tree], Map[Type, Type]) = + + + + // println("HELLO") + // println(specializedTraitSymbol.typeParams.map(_.typeRef.symbol)) + // println(specializedTraitSymbol.paramSymss) + // println(specializedTypeVars) + // println(i"${}") + + // (List.empty, Map.empty) + + + + // specializedTraitSymbol.primaryConstructor.paramSymss match { + // case List(typeVars: List[Symbol], implicits: List[Symbol], params: List[Symbol]) => + // print("BUNGLING BAFFLING") + // print(i"${typeVars}") + // print(typeVars.map(_.owner)) // typeRef.symbol + // val concreteTypes = concreteTypeTrees.map(_.tpe) + + // val indicesWithSpecializedAnnotation = implicits.flatMap(sym => isSpecializedEvidence(sym.denot.info, typeVars)) + // val typeVarTypes: List[Type] = typeVars.map(_.typeRef) + + // val typeVarToConcreteTypeMap = Map.from(indicesWithSpecializedAnnotation.map(typeVarTypes.zip(concreteTypes)(_))) + // val typeVarToConcreteTypeMapTrees: Map[Symbol, Tree] = Map.from(indicesWithSpecializedAnnotation.map(typeVars.zip(concreteTypeTrees)(_))) + + + // (specializationTypeTrees, typeVarToConcreteTypeMap) + // case _ => (List.empty, Map.empty) + // } + + private def buildClassTree(originalTraitSymbol: Symbol, generatedTraitSymbol: ClassSymbol)(using Context) = { + val init = newDefaultConstructor(generatedTraitSymbol) + + // init.setParamss(List(generatedTraitSymbol.typeParams)) + + // val init = originalTraitSymbol.primaryConstructor.copy(owner = generatedTraitSymbol, + // flags = originalTraitSymbol.primaryConstructor.flags | Flags.Synthetic) + // .asInstanceOf[TermSymbol] + println(i"GOT PARAM NAMES OG ${originalTraitSymbol.primaryConstructor.info.paramNamess}") + println(i"GOT PARAM NAMES ${init.info.paramNamess}") + + // println(s"Got init2 constructor info ${init2.info}") + println(s"Got init constructor info ${init.info}") + + val rt = generatedTraitSymbol.typeRef.appliedTo(generatedTraitSymbol.typeParams.map(_.typeRef)) + def resultType(tpe: Type): Type = tpe match { + case mt @ MethodType(paramNames) => mt.derivedLambdaType(paramNames, mt.paramInfos, rt) + case pt : PolyType => pt.derivedLambdaType(pt.paramNames, pt.paramInfos, resultType(pt.resType)) + } + + init.info = resultType(init.info) + init.info = PolyType.fromParams(init.owner.typeParams, init.info) + + assert(originalTraitSymbol.primaryConstructor.rawParamss.length >= 2) // we know we at least have type params and evidences + val evidences = (originalTraitSymbol.primaryConstructor.rawParamss: @unchecked) match { + case List(_, _, evidences) => evidences + case List(_, evidences) => evidences + } + println(s"Evidences ${evidences.head.info}") + + // val tpMap: Map[Type, Type] = old_type_params.map(_.typeRef).zip(tps.map(_.typeRef)).toMap + // val typeParamMap = new TypeMap: + // def apply(t: Type) = tpMap.applyOrElse(t, mapOver. + + // Claim that we simply don't need to worry about the evidences because they will be dealt with when instantiating the parent + // and we always have the inheritance invariance that we discussed with Hamza. + // val newEvidences = evidences.collect(_.info match { + // case a@AppliedType(tycon, args) if (tycon =:= ctx.definitions.SpecializedBoundRef) => None + // case tpe => typeParamMap(tpe) + // }) + // println(s"Generated resulting evidences ${newEvidences}") + // println("Resulted in the following") + // println(init.paramSymss) + + // println(paramss) + + // val constrTps = + // newTypeParams(init, + // generatedTraitSymbol.typeParams.map(_.name), + // EmptyFlags, + // targets => targets.map(t => generatedTraitSymbol.typeParams.find(_.name == t.name).get.info.bounds) + // ) + + // List(List(type T, type S, type Q, type R, type D), List(val arr), List(val evidence$1, val evidence$2, val evidence$3, val evidence$4, val evidence$5, val evidence$6)) + +// List(constrTps) +// ,, generatedTraitSymbol.typeRef, EmptyTree/ +// paramss, generatedTraitSymbol.typeRef, EmptyTree + // init.paramS +// paramss, generatedTraitSymbol.typeRef, EmptyTree + + // Would it be easier just to copy and then remove instead of constructing from scratch? + // println(init.) + ClassDef(generatedTraitSymbol, DefDef(init.entered), Nil) + } + + override def transform(tree: Tree)(using Context): Tree = + tree match { + case pkg@PackageDef(pid, stats) => + val stats1 = collectNecessaryGeneratedSymbols(pkg).map(buildClassTree) + + // Template + println("GENERATED") + println(stats1) + println(stats) + + cpy.PackageDef(pkg)(pid, stats1 ++ stats) + } + + private def collectNecessaryGeneratedSymbols(tree: Tree)(using Context): List[(Symbol, ClassSymbol)] = + val result: List[(Symbol, ClassSymbol)] = tree.deepFold(List.empty)((found, tree) => tree match + // case New(AppliedTypeTree etc) -> need to output the impl class -> do we wantto generate that when we see Foo[Int] or not? + + case AppliedTypeTree(specializedTrait: Ident, concreteTypeTrees: List[Tree]) => + val specializedTraitSymbol = specializedTrait.denot.symbol + + val specializedTypeVars = specializedTraitSymbol.unforcedDecls.implicitDecls.collect(_.info match { case SpecializedEvidence(typeVar) => typeVar }).toSet + val specializationMap = specializedTraitSymbol.typeParams.map(_.typeRef.asInstanceOf[Type]).zip(concreteTypeTrees).toMap.filter((k, v) => specializedTypeVars(k)) + + if (specializationMap.nonEmpty) { + val specializedTraitInterfaceTraitSymbol = newSpecializedTraitInterfaceTrait(specializedTrait, specializationMap) + println(i"Got the following primary constructor {specializedTraitInterfaceTraitSymbol.primaryConstructor}") + + println("OG:") + println(specializedTraitSymbol.primaryConstructor.paramSymss) + (specializedTraitSymbol, specializedTraitInterfaceTraitSymbol) :: found + // specializedTraitInterfaceTraitSymbol.def + // ctx. + // TypeDef(specializedTraitInterfaceTraitSymbol) + } + else found + case tree: TypeDef => + found + case _ => found + ) + print(s"Got the following result ${result}") + result + + // generate the classes + // do the tree type map + } + + + // val impl1 = cpy.Template(impl)(body = newDefs._1 ::: newDefs._2) + // cpy.TypeDef(cls)(rhs = impl1) + + // override def newTransformer(using Context): Transformer = new Transformer { + // override def transform(tree: Tree)(using Context): Tree = tree match { + // case tree: TypeDef if tree.symbol.isInlineTrait => + // transformInlineTrait(tree) + // case tree: TypeDef if Inlines.needsInlining(tree) => + // val tree1 = super.transform(tree).asInstanceOf[TypeDef] + // if tree1.tpe.isError then tree1 + // else if tree1.symbol.isInlineTrait then transformInlineTrait(tree1) + // else Inlines.inlineParentInlineTraits(tree1) + // case _ => super.transform(tree) + // } + // } + + + + // And then apply this map twice; once to the completely generic [T, S, R, P, Q] to get specialization, and once (moyennant le fait que we need to apply .tpe to throw away the tree portion to produce the parent type map) + + + + +// how are we building the AST portion? + + + // val specialization = specializedTraitSymbol.typeParams.map(_.typeRef).zip(concreteTypeTrees).map(_ match { + // case (tpe, concreteTypeTree) if specializedTypeVars(tpe) => concreteTypeTree + // case (tpe, _) => TypeTree(tpe) + // }) + + + // val specializedTypeParamIndices = specializedTypeVars.map(specializedTraitSymbol.typeParams.indexOf(_)) + + // specializedTypeVars.zip(concreteTypeTrees) + + + + + // val specializationTypeTrees = typeVars.map(typeVar => typeVarToConcreteTypeMapTrees.applyOrElse(typeVar, const(TypeRef(NoPrefix, typeVar))))) // , + + + + + // val (selectedSpecialization, typeMap) = getSelectedSpecialization(specializedTraitSymbol, concreteTypes) + + + +// Todo: What happens with the name generation if we have Vec[Vec[T]] for example? + +// AppliedType(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),trait Specialized),List(TypeRef(NoPrefix,type T))) + + + +// traverse with a fold - how to dealw ith other compilation units? A new phase? +// set flags? + +object DesugarSpecializedTraits: + val name: String = "desugarSpecializedTraits" + val description: String = "Replaces traits having type parameters that have the Specialized annotation with specialized versions" + + // TODO: Check what happens here when we have a case where the types being specialized into are user defined instead of primitives or type vars. + private[transform] def newSpecializedTraitName(name: Name, specialization: Map[Type, Tree]) = + specialization.values.collect(t => t match { + case Ident(tpe) => tpe ++ str.SPECIALIZED_TRAIT_TYPE_SEP + }).fold(name ++ str.SPECIALIZED_TRAIT_SUFFIX)((n1, n2) => n1 ++ n2) + + //.flatten + // ++ + // specialization.map(_.description).concat //SPECIALIZED_TRAIT_TYPE_SEP + +// Cleanup +// Correctly generate names +// generate classes as well +// do we actually want to generate Iteratorsp$Int +// should we be worried about the results that we generate causing more stuff to be generated? +// figure out why we generate the T version. +// Try to see if we can do with only types and not trees +// Synthesise Specialized instances so that people can't do stupid stuff like Specialized[Array[T]]. type x = Specialized[Array[Array[Int]]] + +// Potentially we can just go through and find every place which needs one, do a direct replacement and spit it out directly into some kind of list buffer and then +// copy it out later + +// Need to test with one parameter that is specialized and one that isn't +// can we do + +// Probably (tree)typemap + +// 1. Figure out which specialisations we need to generate +// 2. Generate ArrayIterator$sp$Int and ArrayIterator$impl$Int wherever they live +// 3. Replace ArrayIterator[Int] with ArrayIterator$sp$Int +// 4. Replace new ArrayIterator[Int](xs) {} with new ArrayIterator$impl$Int(xs) {} +// 5. Somehow figure out the caching +// 6. Delete references to Specialized I guess + +// Synthesize Specialized[T] instances. + +// template [T#4477 >: scala#22.this.Nothing#1468 <: Any#462](using +// evidence$1#4478: #2.this.scala#21.Specialized#338[T#4477], +// evidence$2#4479: scala#22.this.package#123.Numeric#5766[T#4477]) extends +// Object#744 { +// T#4473 +// private[this] given val evidence$1#4474: +// #2.this.scala#21.Specialized#338[T#4473] +// private[this] given val evidence$2#4475: Numeric#5821[T#4473] +// } +// [[syntax tree + + +// template Template(DefDef(,List(List(TypeDef(T,TypeBoundsTree(TypeTree[TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Nothing)],TypeTree[TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Any)],EmptyTree))) +// , List(ValDef(evidence$1,AppliedTypeTree(Ident(Specialized),List(Ident(T))),EmptyTree), +//   ValDef(evidence$2,AppliedTypeTree(Ident(Numeric),List(Ident(T))),EmptyTree))),TypeTree[TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Unit)],EmptyTree),List(TypeTree[TypeRef(ThisType(TypeRef(NoPrefix,module class lang)),class Object)]),ValDef(_,EmptyTree,EmptyTree),List(TypeDef(T,TypeTree[TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Nothing),TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Any))]), ValDef(evidence$1,TypeTree[AppliedType(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),trait Specialized),List(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class )),trait Vec)),type T)))],EmptyTree), ValDef(evidence$2,TypeTree[AppliedType(TypeRef(TermRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),object math),trait Numeric),List(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class )),trait Vec)),type T)))],EmptyTree))) + +// defdef DefDef(foo,List(List(ValDef(v,AppliedTypeTree(Ident(Vec),List(Ident(Int))),EmptyTree))),TypeTree[AppliedType(TypeRef(ThisType(TypeRef(NoPrefix,module class )),trait Vec),List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),class Int)))],Ident(v)) + + +// class DesugarSpecializedTraits extends MacroTransform, SymTransformer: +// import tpd._ + +// override def phaseName: String = DesugarSpecializedTraits.name +// override def description: String = DesugarSpecializedTraits.description +// override def changesMembers: Boolean = false +// override def changesParents: Boolean = true + diff --git a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala index 1d1490995174..e1d2ea20cc69 100644 --- a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala @@ -31,6 +31,8 @@ class SpecializeInlineTraits extends MacroTransform, SymTransformer { override def changesParents: Boolean = true + override def runsAfter: Set[String] = Set("desugarSpecializedTraits") + override def run(using Context): Unit = try super.run catch case _: CompilationUnit.SuspendException => () diff --git a/library/src/scala/Specialized.scala b/library/src/scala/Specialized.scala new file mode 100644 index 000000000000..9b28b9eb5b14 --- /dev/null +++ b/library/src/scala/Specialized.scala @@ -0,0 +1,5 @@ +package scala + +trait Specialized[T] + +given [T]: Specialized[T] with {} diff --git a/tests/pos/inline-trait-object-not-primitive.scala b/tests/pos/inline-trait-object-not-primitive.scala new file mode 100644 index 000000000000..7f155af53b62 --- /dev/null +++ b/tests/pos/inline-trait-object-not-primitive.scala @@ -0,0 +1,9 @@ +object Hop + +inline trait A[T](x: T): + def foo: T = x + +class B extends A[Hop.type](Hop): + val y = 1 + +def h(x: B) = x.foo diff --git a/tests/pos/inline-trait-specialized-desugar.scala b/tests/pos/inline-trait-specialized-desugar.scala index a758974d413f..e912228c9bca 100644 --- a/tests/pos/inline-trait-specialized-desugar.scala +++ b/tests/pos/inline-trait-specialized-desugar.scala @@ -1,3 +1,4 @@ +// They do this: (with Specialized type class) inline trait Iterator[T]: def hasNext: Boolean def next(): T @@ -10,6 +11,7 @@ inline trait ArrayIterator[T](elems: Array[T]) extends Iterator[T]: // We generate these: +inline trait Iteratorsp$Int extends Iterator[Int] inline trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator[Int] class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator[Int](elems) @@ -40,3 +42,105 @@ class MyClassB extends MyClassA, ArrayIterator$sp$Int // We convert this to: val ai = new ArrayIterator$impl$Int(xs) {} println(ai.next()) + + + + // println(genericTrait.denot.info.appliedTo(genericTrait.denot.info.typeParams)) + // println(genericTrait.denot.info.widenDealias.) +// instantiateWithTypeVars + // instantiateWithTypeVars() + + // newNormalizedClassSymbol( + // genericTrait.owner, + // "CopiedSymbol", + // Flags.Synthetic | Flags.Inline | Flags.Trait, + // parents, + // NoType, // TODO: Work out what to do about self types; for now just ban them + // genericTrait.privateWithin, + // compUnitInfo = genericTrait.compUnitInfo + // ) + + + + // // Now add the constructor + + // selfInfo: Type = NoType, + // // Need to figure out how we leave some of the necessary generics for unspecialized params. + // ctx. + + + // genericTrait.copy( + // name= + // flags=genericTrait.flags | Flags.Synthetic, + + // ) + // newNormalizedClassSymbol() + // inline trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator[Int] + + + + // private def inlinedClassSym(sym: ClassSymbol, withoutFlags: FlagSet = EmptyFlags)(using Context): ClassSymbol = + // sym.info match { + // case clsInfo: ClassInfo => + // val typeParams: List[Type] = sym.primaryConstructor.info match { + // case poly: PolyType => poly.paramRefs + // case _ => Nil + // } + // // Extend inner class from inline trait to preserve typing + // val newParent = ctx.owner.thisType.select(sym).appliedTo(typeParams) + // val inlinedSym = ( + // ctx.owner, + // sym.name, + // (sym.flags | Synthetic) &~ withoutFlags, + // newCls => { + // val ClassInfo(prefix, _, parents, _, selfInfo) = inlinerTypeMap.mapClassInfo(clsInfo) + // ClassInfo(prefix, newCls, parents :+ newParent, Scopes.newScope, selfInfo) // TODO fix selfInfo (what to use?) + // }, + // sym.privateWithin, + // spanCoord(parent.span) + // ) + // // ctx.inlineTraitState.registerInlinedInnerClassSymbol(sym, inlinedSym, childThisType) + // ctx.inlineTraitState.registerInlinedSymbol(sym, inlinedSym, childThisType.widenDealias) + // inlinedSym.entered + // case _ => + // report.error(s"Class symbol ${sym.show} does not have class info") + // sym + // } + +// specializedTraitSymbol.copy( +// name="GeneratedSpecializedSymbol", + +// ) + + // Symbols.new + // def ( + // owner: Symbol, + // name: TypeName, + // flags: FlagSet, + // parentTypes: List[Type], + // selfInfo: Type = NoType, + // privateWithin: Symbol = NoSymbol, + // coord: Coord = NoCoord, + // compUnitInfo: CompilationUnitInfo | Null = null)(using Context): ClassSymbol = { + + + // def newClass(owner: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol = + // assert(parents.nonEmpty && !parents.head.typeSymbol.is(dotc.core.Flags.Trait), "First parent must be a class") + // val cls = dotc.core.Symbols.newNormalizedClassSymbol( + // owner, + // name.toTypeName, + // dotc.core.Flags.EmptyFlags, + // parents, + // selfType.getOrElse(Types.NoType), + // dotc.core.Symbols.NoSymbol) + // cls.enter(dotc.core.Symbols.newConstructor(cls, dotc.core.Flags.Synthetic, Nil, Nil)) + // for sym <- decls(cls) do cls.enter(sym) + // cls + + + // } + + // private def newSpecializedTraitImplementationClass() { + // // class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator[Int](elems) + + // } \ No newline at end of file diff --git a/tests/pos/specialized-trait-vector-example.scala b/tests/pos/specialized-trait-vector-example.scala new file mode 100644 index 000000000000..7b3aa003e6ee --- /dev/null +++ b/tests/pos/specialized-trait-vector-example.scala @@ -0,0 +1,144 @@ +inline trait Iterator[T: Specialized]: + def hasNext: Boolean + def next(): T + +// They do this: (with Specialized type class) +inline trait ArrayIterator[T: Specialized](elems: Array[T]) extends Iterator[T]: + private var current = 0 + def hasNext: Boolean = current < elems.length + def next(): T = try elems(current) finally current += 1 + + +// We should generate these: +// inline trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator[Int] +// class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator[Int](elems) + +// Inline traits does the magic of actually inlining the code and specialising from T to Int in that step. + + +// They do this: +def foo(x: ArrayIterator[Int]): Int = x.next() +// We convert this to: +// def foo(x: ArrayIterator$sp$Int): Int = x.next() +// As long as we generate this (i.e. "do the special erasure") before we run inline traits we should be fine because then the reference will be replaced. + + +// They do this: +// class MyClassA +// class MyClassB extends MyClassA, ArrayIterator[Int] + +// // We convert this to: +// class MyClassA +// class MyClassB extends MyClassA, ArrayIterator$sp$Int + +// @main def main = +// val xs: Array[Int] = Array(1, 2, 3) + +// // They do this: +// // new ArrayIterator[Int](xs) {} + +// // We convert this to: +// val ai = new ArrayIterator$impl$Int(xs) {} +// println(ai.next()) + + + + // println(genericTrait.denot.info.appliedTo(genericTrait.denot.info.typeParams)) + // println(genericTrait.denot.info.widenDealias.) +// instantiateWithTypeVars + // instantiateWithTypeVars() + + // newNormalizedClassSymbol( + // genericTrait.owner, + // "CopiedSymbol", + // Flags.Synthetic | Flags.Inline | Flags.Trait, + // parents, + // NoType, // TODO: Work out what to do about self types; for now just ban them + // genericTrait.privateWithin, + // compUnitInfo = genericTrait.compUnitInfo + // ) + + + + // // Now add the constructor + + // selfInfo: Type = NoType, + // // Need to figure out how we leave some of the necessary generics for unspecialized params. + // ctx. + + + // genericTrait.copy( + // name= + // flags=genericTrait.flags | Flags.Synthetic, + + // ) + // newNormalizedClassSymbol() + // inline trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator[Int] + + + + // private def inlinedClassSym(sym: ClassSymbol, withoutFlags: FlagSet = EmptyFlags)(using Context): ClassSymbol = + // sym.info match { + // case clsInfo: ClassInfo => + // val typeParams: List[Type] = sym.primaryConstructor.info match { + // case poly: PolyType => poly.paramRefs + // case _ => Nil + // } + // // Extend inner class from inline trait to preserve typing + // val newParent = ctx.owner.thisType.select(sym).appliedTo(typeParams) + // val inlinedSym = ( + // ctx.owner, + // sym.name, + // (sym.flags | Synthetic) &~ withoutFlags, + // newCls => { + // val ClassInfo(prefix, _, parents, _, selfInfo) = inlinerTypeMap.mapClassInfo(clsInfo) + // ClassInfo(prefix, newCls, parents :+ newParent, Scopes.newScope, selfInfo) // TODO fix selfInfo (what to use?) + // }, + // sym.privateWithin, + // spanCoord(parent.span) + // ) + // // ctx.inlineTraitState.registerInlinedInnerClassSymbol(sym, inlinedSym, childThisType) + // ctx.inlineTraitState.registerInlinedSymbol(sym, inlinedSym, childThisType.widenDealias) + // inlinedSym.entered + // case _ => + // report.error(s"Class symbol ${sym.show} does not have class info") + // sym + // } + +// specializedTraitSymbol.copy( +// name="GeneratedSpecializedSymbol", + +// ) + + // Symbols.new + // def ( + // owner: Symbol, + // name: TypeName, + // flags: FlagSet, + // parentTypes: List[Type], + // selfInfo: Type = NoType, + // privateWithin: Symbol = NoSymbol, + // coord: Coord = NoCoord, + // compUnitInfo: CompilationUnitInfo | Null = null)(using Context): ClassSymbol = { + + + // def newClass(owner: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol = + // assert(parents.nonEmpty && !parents.head.typeSymbol.is(dotc.core.Flags.Trait), "First parent must be a class") + // val cls = dotc.core.Symbols.newNormalizedClassSymbol( + // owner, + // name.toTypeName, + // dotc.core.Flags.EmptyFlags, + // parents, + // selfType.getOrElse(Types.NoType), + // dotc.core.Symbols.NoSymbol) + // cls.enter(dotc.core.Symbols.newConstructor(cls, dotc.core.Flags.Synthetic, Nil, Nil)) + // for sym <- decls(cls) do cls.enter(sym) + // cls + + + // } + + // private def newSpecializedTraitImplementationClass() { + // // class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator[Int](elems) + + // } \ No newline at end of file diff --git a/tests/pos/specialized-traits-basic.scala b/tests/pos/specialized-traits-basic.scala new file mode 100644 index 000000000000..ea2e1286db65 --- /dev/null +++ b/tests/pos/specialized-traits-basic.scala @@ -0,0 +1,56 @@ +trait A[T, R, Q] + +inline trait Vec[T: {Specialized, Numeric}, S <: Object, Q: Numeric, R: Specialized, D: {Numeric, Specialized}] extends A[S, Char, T] + +def foo(v: Vec[Int, String, Int, Int, Int]) = v + +def main() = + type x = Specialized[Array[Array[Int]]] + println("Hello, World!") + + // val x = new Vec[Int, String, Int, Int, Int]() {} + // foo(x) + +// Need to ban all of these but we will do that earlier I guess? +// Vec[Vec[Int]] hehe <- fine +// Vec[S, S[T]: Specialized] <- banned +// Vec[S, T[T]: Specialized] <- banned +// Vec[Array[T]: Specialized] <- banned + + +// Map(TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Nothing), +// TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Any)) -> TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),class Int)) + +// List(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class )),trait Vec)),type S), +// TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),class Char), +// TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class )),trait Vec)),type T) ))) + + +// Challenge: annotation is on the level of type params and not on the level of methods for example. Can have some without. + +// Got tp [T#825496893, S#825496893 <: Object#744, Q#825496893, R#825496893, D#825496893] +// #825496893 +// (using +// evidence$1#681088021: #2.this.scala#21.Specialized#338[T#825496893], +// evidence$2#681088021: Numeric#6014[T#825496893], +// evidence$3#681088021: Numeric#6014[Q#825496893], +// evidence$4#681088021: #2.this.scala#21.Specialized#338[R#825496893], +// evidence$5#681088021: Numeric#6014[D#825496893], evidence$6#681088021: +// #2.this.scala#21.Specialized#338[D#825496893]) +// (): +// #2299.this.Vec#4482[T#825496893, S#825496893, Q#825496893, +// R#825496893, D#825496893] + + +// need to test with explicit evidence. + + +// inline trait Vec[T: SomeTypeClass] +// + +// def foo = +// instance of typeclass SomeTypeClass[Int] +// new Vec[Int] \/ + +// new VecSp without the condition +// -> creates Vec[Int] \ No newline at end of file diff --git a/tests/pos/specialized-traits-strawman.scala b/tests/pos/specialized-traits-strawman.scala new file mode 100644 index 000000000000..27ec7ef110ff --- /dev/null +++ b/tests/pos/specialized-traits-strawman.scala @@ -0,0 +1,141 @@ +import language.experimental.erasedDefinitions + +// Source code + +/*inline*/ trait Iterator[T/*: Specialized*/]: + def hasNext: Boolean + def next(): T + +/*inline*/ trait ArrayIterator[T/*: Specialized*/](elems: Array[T]) extends Iterator[T]: + private var current = 0 + def hasNext: Boolean = current < elems.length + def next(): T = try elems(current) finally current += 1 + +/*inline*/ trait Iterable[T/*: Specialized*/]: + def iterator: Iterator[T] + def forall(f: T => Unit): Unit = + val it = iterator + while it.hasNext do f(it.next()) + +/*inline*/ trait Seq[T/*: Specialized*/](elems: Array[T]) +extends Iterable[T]: + def length: Int = elems.length + def apply(i: Int): T = elems(i) + def iterator: Iterator[T] = new ArrayIterator[T](elems) {} + +// Specialized trait APIs generated from specialization: + +trait Iterator_Int extends Iterator[Int]: + def hasNext: Boolean + def next(): Int + +trait ArrayIterator_Int extends ArrayIterator[Int], Iterator[Int] + +trait Iterable_Int extends Iterable[Int]: + def iterator: Iterator_Int + def forall(f: Int => Unit): Unit + +trait Seq_Int extends Seq[Int], Iterable[Int]: + def length: Int + def apply(i: Int): Int + +class ArrayIterator_Int$impl(elems: Array[Int]) extends ArrayIterator_Int + , ArrayIterator[Int](elems): // snd parent not needed in actual translation + private var current = 0 + override def hasNext: Boolean = + current < elems.length + override def next(): Int = + try elems(current) finally current += 1 + +class Seq_Int$impl(elems: Array[Int]) extends Seq_Int + , Seq[Int](elems): // snd parent not needed in actual translation + override def iterator: Iterator_Int = + new ArrayIterator_Int$impl(elems).asInstanceOf + // cast needed since the compiler does not not know that Iterable[Int] = Iterable_Int + // after erasure. No cast would be needed in the actual translation. + + override def forall(f: Int => Unit): Unit = + val it = iterator + while it.hasNext do f(it.next()) + override def length: Int = elems.length + override def apply(i: Int): Int = elems(i) + +// Inline trait APIs generated from inline traits transform: +object InlineTraitAPIs: + + trait Iterator[T]: + def hasNext: Boolean + def next(): T + + trait ArrayIterator[T] extends Iterator[T] + + trait Iterable[T]: + def iterator: Iterator[T] + def forall(f: T => Unit): Unit + + trait Seq[T] extends Iterable[T]: + def length: Int + def apply(i: Int): T +end InlineTraitAPIs + +/* Code after erasure: */ +object AfterErasure: + + trait Function1: + def apply(x: Any): Any + def apply_Int_Unit(x: Int): Unit + + trait Iterator: + def hasNext: Boolean + def next(): Any + + trait ArrayIterator extends Iterator + + trait Iterable: + def iterator: Iterator + def forall(f: Function1): Unit + + trait Seq: + def length: Int + def apply(i: Int): Any + + trait Iterator_Int extends Iterator: + def hasNext: Boolean + def next(): Int + + trait ArrayIterator_Int extends ArrayIterator, Iterator_Int + + trait Iterable_Int extends Iterable: + def iterator: Iterator_Int + def forall(f: Function1): Unit + + trait Seq_Int extends Seq, Iterable_Int: + def length: Int + def apply(i: Int): Int + + class ArrayIterator_Int$impl(elems: Array[Int]) extends ArrayIterator_Int: + private var current = 0 + override def hasNext: Boolean = + current < elems.length + override def next(): Int = + try elems(current) finally current += 1 + + /* Bridges: + override def next(): Any = Int.box(next()) + */ + end ArrayIterator_Int$impl + + class Seq_Int$impl(elems: Array[Int]) extends Seq_Int: + override def iterator: Iterator_Int = + new ArrayIterator_Int$impl(elems) + override def forall(f: Function1): Unit = + val it = iterator + while it.hasNext do f.apply_Int_Unit(it.next()) + override def length: Int = elems.length + override def apply(i: Int): Int = elems(i) + + /* Bridges: + override def iterator: Iterator = iterator + override def apply(i: Int): Any = Int.box(apply(i)) + */ + end Seq_Int$impl From bb3b2a4d0386e40f36d6500b1aa3eec7a41fa4dd Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 12 Mar 2026 17:18:23 +0100 Subject: [PATCH 022/254] Clean up --- .../transform/DesugarSpecializedTraits.scala | 292 +++--------------- .../inline-trait-specialized-desugar.scala | 102 ------ .../specialized-trait-vector-example.scala | 102 ------ tests/pos/specialized-traits-basic.scala | 48 --- 4 files changed, 39 insertions(+), 505 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 231d03e74310..adb12a9e0de5 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -14,7 +14,6 @@ import dotty.tools.dotc.core.StdNames.* import dotty.tools.dotc.core.Types.* import dotty.tools.dotc.core.Flags import dotty.tools.dotc.core.Symbols.newClassSymbol -import dotty.tools.dotc.typer.ProtoTypes.instantiateWithTypeVars import scala.Function.const import dotty.tools.dotc.core.Names.TypeName import dotty.tools.dotc.core.Symbols.TypeSymbol @@ -35,27 +34,6 @@ class DesugarSpecializedTraits extends MacroTransform: override def description: String = DesugarSpecializedTraits.description override def changesMembers: Boolean = false override def changesParents: Boolean = true - // override def transformTemplate(tree: Template)(using Context): Tree = - // // println(s"template ${tree}") - // tree.deepFold() - // tree - - // override def transformTyped(tree: Typed)(using Context): Tree = ??? -// - // override def transformTypeApply(tree: TypeApply)(using Context): Tree = ??? - - // override def transformTypeTree(tree: TypeTree)(using Context): Tree = tree match { - - // } - // override def transformDefDef(tree: DefDef)(using Context): Tree = - // println(s"defdef ${tree}") - // if (tree.name.toString() == "foo") { - // val ValDef(v, , EmptyTree) = tree.paramss.head.head - // // DefDef(foo,List(List(ValDef(v,AppliedTypeTree(Ident(Vec),List(Ident(Int))),EmptyTree))),TypeTree[AppliedType(TypeRef(ThisType(TypeRef(NoPrefix,module class )),trait Vec),List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),class Int)))],Ident(v)) - // } - // tree - - // need some short circuit logic for if we've already processed this one. override def run(using Context): Unit = try super.run @@ -72,22 +50,16 @@ class DesugarSpecializedTraits extends MacroTransform: } private def newSpecializedTraitInterfaceTrait(specializedTrait: Ident, specializationMap: Map[Type, Tree]) = - println(specializationMap) + // Define specialization we want to apply val specializedTraitSymbol = specializedTrait.denot.symbol - val tm = new TypeMap: def apply(t: Type) = specializationMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) - val specialization = specializedTraitSymbol.typeParams.map(_.typeRef).map(specializationMap.applyOrElse(_, TypeTree(_))) - + + // Create new trait val parents = defn.ObjectType - :: AppliedTypeTree(cpy.Ident(specializedTrait)(specializedTrait.name), specialization).tpe - :: specializedTrait.denot.info.parents.filterNot(_ eq defn.ObjectType).map(tm(_)) // parents of the original trait **but then specialized** - - - - - + :: AppliedTypeTree(cpy.Ident(specializedTrait)(specializedTrait.name), specialization).tpe // original trait; specialized + :: specializedTrait.denot.info.parents.filterNot(_ eq defn.ObjectType).map(tm(_)) // parents of the original trait, specialized val traitSymbol = newNormalizedClassSymbol( specializedTraitSymbol.owner, (newSpecializedTraitName(specializedTraitSymbol.name, specializationMap)).asTypeName, @@ -95,11 +67,10 @@ class DesugarSpecializedTraits extends MacroTransform: parents, NoType, // TODO: Work out what to do about self types; for now just ban them specializedTraitSymbol.privateWithin, - // compUnitInfo = specializedTraitSymbol.compUnitInfo // TODO: Do we need a compUnit info? + // TODO: Do we need a compUnit info? ) - println(s"Owner of tpparam ${specializedTraitSymbol.typeParams.head.owner}") - + // Create type parameters for new trait val old_type_params = specializedTraitSymbol.typeParams.filterNot(t => specializationMap.contains(t.typeRef)) val tps = newTypeParams(traitSymbol, old_type_params.map(_.name), @@ -107,98 +78,27 @@ class DesugarSpecializedTraits extends MacroTransform: targets => targets.map(t => specializedTraitSymbol.typeParams.find(_.name == t.name).get.info.bounds) ) tps.foreach(traitSymbol.enter(_, EmptyScope)) - println(i"Got new tps ${tps}") + + // Replace old type parameters that were copied from original trait with new ones + // inside the parents of the new trait val tpMap: Map[Type, Type] = old_type_params.map(_.typeRef).zip(tps.map(_.typeRef)).toMap val freshTypeVarMap = new TypeMap: def apply(t: Type) = tpMap.applyOrElse(t, mapOver) - - println("the following is the tpMap") - println(tpMap) - - // val List(traitSymbol1) = mapSymbols(List(traitSymbol), ttmap) - - // val traitSymbol2 = traitSymbol1.asInstanceOf[ClassSymbol] - - // val init = newDefaultConstructor(traitSymbol) - // val tmpl = untpd.Template( - // DefDef(init), - // parents.map(TypeTree(_)), - // EmptyValDef, - // Nil - // ) - - - val traitSymbol2 = traitSymbol.subst(old_type_params, tps).asInstanceOf[ClassSymbol] - traitSymbol.info = - ClassInfo(traitSymbol.owner.thisType, traitSymbol, traitSymbol.info.parents.map(freshTypeVarMap(_)), traitSymbol.info.decls, traitSymbol.info.self) - - // .info.parents = - // denot.info = ClassInfo(owner.thisType, cls, parentTypes.map(_.dealias), decls, selfInfo) - - println("THESE ARE THE parents") - // println(traitSym, bol2.parentTypes) - traitSymbol2.entered //, tmpl) - - // println("GOT The following resulting parents") - // println(parents) - - - // // For the given type application of concreteTypes to specializedTraitSymbol, return a list of types - // // retaining the type variable from the trait definition if the type variable is not Specialized, - // // and the concrete type from the application if the type variable is Specialized. - // private def getSelectedSpecialization(specializedTraitSymbol: Symbol, concreteTypeTrees: List[Tree]): (List[Tree], Map[Type, Type]) = - - - - // println("HELLO") - // println(specializedTraitSymbol.typeParams.map(_.typeRef.symbol)) - // println(specializedTraitSymbol.paramSymss) - // println(specializedTypeVars) - // println(i"${}") - - // (List.empty, Map.empty) - - - - // specializedTraitSymbol.primaryConstructor.paramSymss match { - // case List(typeVars: List[Symbol], implicits: List[Symbol], params: List[Symbol]) => - // print("BUNGLING BAFFLING") - // print(i"${typeVars}") - // print(typeVars.map(_.owner)) // typeRef.symbol - // val concreteTypes = concreteTypeTrees.map(_.tpe) - - // val indicesWithSpecializedAnnotation = implicits.flatMap(sym => isSpecializedEvidence(sym.denot.info, typeVars)) - // val typeVarTypes: List[Type] = typeVars.map(_.typeRef) - - // val typeVarToConcreteTypeMap = Map.from(indicesWithSpecializedAnnotation.map(typeVarTypes.zip(concreteTypes)(_))) - // val typeVarToConcreteTypeMapTrees: Map[Symbol, Tree] = Map.from(indicesWithSpecializedAnnotation.map(typeVars.zip(concreteTypeTrees)(_))) - - - // (specializationTypeTrees, typeVarToConcreteTypeMap) - // case _ => (List.empty, Map.empty) - // } + traitSymbol.info = ClassInfo(traitSymbol.owner.thisType, traitSymbol, traitSymbol.info.parents.map(freshTypeVarMap(_)), traitSymbol.info.decls, traitSymbol.info.self) + traitSymbol.entered private def buildClassTree(originalTraitSymbol: Symbol, generatedTraitSymbol: ClassSymbol)(using Context) = { val init = newDefaultConstructor(generatedTraitSymbol) - // init.setParamss(List(generatedTraitSymbol.typeParams)) - - // val init = originalTraitSymbol.primaryConstructor.copy(owner = generatedTraitSymbol, - // flags = originalTraitSymbol.primaryConstructor.flags | Flags.Synthetic) - // .asInstanceOf[TermSymbol] - println(i"GOT PARAM NAMES OG ${originalTraitSymbol.primaryConstructor.info.paramNamess}") - println(i"GOT PARAM NAMES ${init.info.paramNamess}") - - // println(s"Got init2 constructor info ${init2.info}") - println(s"Got init constructor info ${init.info}") - + // Fix constructor so that it: + // 1) Has correct generic type parameters + // 2) Returns the correct type corresponding to those type parameters applied to this trait val rt = generatedTraitSymbol.typeRef.appliedTo(generatedTraitSymbol.typeParams.map(_.typeRef)) def resultType(tpe: Type): Type = tpe match { case mt @ MethodType(paramNames) => mt.derivedLambdaType(paramNames, mt.paramInfos, rt) case pt : PolyType => pt.derivedLambdaType(pt.paramNames, pt.paramInfos, resultType(pt.resType)) } - init.info = resultType(init.info) init.info = PolyType.fromParams(init.owner.typeParams, init.info) @@ -207,53 +107,22 @@ class DesugarSpecializedTraits extends MacroTransform: case List(_, _, evidences) => evidences case List(_, evidences) => evidences } - println(s"Evidences ${evidences.head.info}") - - // val tpMap: Map[Type, Type] = old_type_params.map(_.typeRef).zip(tps.map(_.typeRef)).toMap - // val typeParamMap = new TypeMap: - // def apply(t: Type) = tpMap.applyOrElse(t, mapOver. - - // Claim that we simply don't need to worry about the evidences because they will be dealt with when instantiating the parent - // and we always have the inheritance invariance that we discussed with Hamza. - // val newEvidences = evidences.collect(_.info match { - // case a@AppliedType(tycon, args) if (tycon =:= ctx.definitions.SpecializedBoundRef) => None - // case tpe => typeParamMap(tpe) - // }) - // println(s"Generated resulting evidences ${newEvidences}") - // println("Resulted in the following") - // println(init.paramSymss) - - // println(paramss) - - // val constrTps = - // newTypeParams(init, - // generatedTraitSymbol.typeParams.map(_.name), - // EmptyFlags, - // targets => targets.map(t => generatedTraitSymbol.typeParams.find(_.name == t.name).get.info.bounds) - // ) - - // List(List(type T, type S, type Q, type R, type D), List(val arr), List(val evidence$1, val evidence$2, val evidence$3, val evidence$4, val evidence$5, val evidence$6)) - -// List(constrTps) -// ,, generatedTraitSymbol.typeRef, EmptyTree/ -// paramss, generatedTraitSymbol.typeRef, EmptyTree - // init.paramS -// paramss, generatedTraitSymbol.typeRef, EmptyTree - - // Would it be easier just to copy and then remove instead of constructing from scratch? - // println(init.) + + // TODO: Confirm that we don't need to worry about copying the evidence parameters over from the old constructor + // These should be dealt with when we instantiate the original trait as a parent of this one. Otherwise we should be + // able to copy them over, apply the specialization (keeping e.g. Numeric[Int] that arises from this) and + // pruning any that belong to Specialized. + ClassDef(generatedTraitSymbol, DefDef(init.entered), Nil) } override def transform(tree: Tree)(using Context): Tree = tree match { - case pkg@PackageDef(pid, stats) => + case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. val stats1 = collectNecessaryGeneratedSymbols(pkg).map(buildClassTree) - - // Template - println("GENERATED") - println(stats1) - println(stats) + + // Use the TreeTypeMap to replace instances (can we do this without accidentally replacing the definitions? I think it should be ok) + // val treeTypeMap = new TreeTypeMap() cpy.PackageDef(pkg)(pid, stats1 ++ stats) } @@ -270,98 +139,31 @@ class DesugarSpecializedTraits extends MacroTransform: if (specializationMap.nonEmpty) { val specializedTraitInterfaceTraitSymbol = newSpecializedTraitInterfaceTrait(specializedTrait, specializationMap) - println(i"Got the following primary constructor {specializedTraitInterfaceTraitSymbol.primaryConstructor}") - println("OG:") - println(specializedTraitSymbol.primaryConstructor.paramSymss) (specializedTraitSymbol, specializedTraitInterfaceTraitSymbol) :: found - // specializedTraitInterfaceTraitSymbol.def - // ctx. - // TypeDef(specializedTraitInterfaceTraitSymbol) } else found case tree: TypeDef => found case _ => found ) - print(s"Got the following result ${result}") result - // generate the classes - // do the tree type map } - // val impl1 = cpy.Template(impl)(body = newDefs._1 ::: newDefs._2) - // cpy.TypeDef(cls)(rhs = impl1) - - // override def newTransformer(using Context): Transformer = new Transformer { - // override def transform(tree: Tree)(using Context): Tree = tree match { - // case tree: TypeDef if tree.symbol.isInlineTrait => - // transformInlineTrait(tree) - // case tree: TypeDef if Inlines.needsInlining(tree) => - // val tree1 = super.transform(tree).asInstanceOf[TypeDef] - // if tree1.tpe.isError then tree1 - // else if tree1.symbol.isInlineTrait then transformInlineTrait(tree1) - // else Inlines.inlineParentInlineTraits(tree1) - // case _ => super.transform(tree) - // } - // } - - - - // And then apply this map twice; once to the completely generic [T, S, R, P, Q] to get specialization, and once (moyennant le fait que we need to apply .tpe to throw away the tree portion to produce the parent type map) - - - - -// how are we building the AST portion? - - - // val specialization = specializedTraitSymbol.typeParams.map(_.typeRef).zip(concreteTypeTrees).map(_ match { - // case (tpe, concreteTypeTree) if specializedTypeVars(tpe) => concreteTypeTree - // case (tpe, _) => TypeTree(tpe) - // }) - - - // val specializedTypeParamIndices = specializedTypeVars.map(specializedTraitSymbol.typeParams.indexOf(_)) - - // specializedTypeVars.zip(concreteTypeTrees) - - - - - // val specializationTypeTrees = typeVars.map(typeVar => typeVarToConcreteTypeMapTrees.applyOrElse(typeVar, const(TypeRef(NoPrefix, typeVar))))) // , - - - - - // val (selectedSpecialization, typeMap) = getSelectedSpecialization(specializedTraitSymbol, concreteTypes) - - - -// Todo: What happens with the name generation if we have Vec[Vec[T]] for example? - -// AppliedType(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),trait Specialized),List(TypeRef(NoPrefix,type T))) - - - -// traverse with a fold - how to dealw ith other compilation units? A new phase? -// set flags? - object DesugarSpecializedTraits: val name: String = "desugarSpecializedTraits" val description: String = "Replaces traits having type parameters that have the Specialized annotation with specialized versions" + // TODO: What happens with this name generation if we have Vec[Vec[T]] for example? We potentially don't have an Ident // TODO: Check what happens here when we have a case where the types being specialized into are user defined instead of primitives or type vars. private[transform] def newSpecializedTraitName(name: Name, specialization: Map[Type, Tree]) = specialization.values.collect(t => t match { case Ident(tpe) => tpe ++ str.SPECIALIZED_TRAIT_TYPE_SEP }).fold(name ++ str.SPECIALIZED_TRAIT_SUFFIX)((n1, n2) => n1 ++ n2) - //.flatten - // ++ - // specialization.map(_.description).concat //SPECIALIZED_TRAIT_TYPE_SEP + // Cleanup // Correctly generate names @@ -371,15 +173,16 @@ object DesugarSpecializedTraits: // figure out why we generate the T version. // Try to see if we can do with only types and not trees // Synthesise Specialized instances so that people can't do stupid stuff like Specialized[Array[T]]. type x = Specialized[Array[Array[Int]]] +// Set the Synthetic flags somewhere +// Cache / only generate once instead of multiple times. // Potentially we can just go through and find every place which needs one, do a direct replacement and spit it out directly into some kind of list buffer and then // copy it out later -// Need to test with one parameter that is specialized and one that isn't -// can we do - // Probably (tree)typemap +// Would it be better to just copy rather than creating everything from scratch? I think this is right + // 1. Figure out which specialisations we need to generate // 2. Generate ArrayIterator$sp$Int and ArrayIterator$impl$Int wherever they live // 3. Replace ArrayIterator[Int] with ArrayIterator$sp$Int @@ -388,31 +191,14 @@ object DesugarSpecializedTraits: // 6. Delete references to Specialized I guess // Synthesize Specialized[T] instances. +// TODO: Need to try with a bigger project with multiple packages later on to see if we get the behaviour that we are expecting to get in terms of the classes that we generate. -// template [T#4477 >: scala#22.this.Nothing#1468 <: Any#462](using -// evidence$1#4478: #2.this.scala#21.Specialized#338[T#4477], -// evidence$2#4479: scala#22.this.package#123.Numeric#5766[T#4477]) extends -// Object#744 { -// T#4473 -// private[this] given val evidence$1#4474: -// #2.this.scala#21.Specialized#338[T#4473] -// private[this] given val evidence$2#4475: Numeric#5821[T#4473] -// } -// [[syntax tree - - -// template Template(DefDef(,List(List(TypeDef(T,TypeBoundsTree(TypeTree[TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Nothing)],TypeTree[TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Any)],EmptyTree))) -// , List(ValDef(evidence$1,AppliedTypeTree(Ident(Specialized),List(Ident(T))),EmptyTree), -//   ValDef(evidence$2,AppliedTypeTree(Ident(Numeric),List(Ident(T))),EmptyTree))),TypeTree[TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Unit)],EmptyTree),List(TypeTree[TypeRef(ThisType(TypeRef(NoPrefix,module class lang)),class Object)]),ValDef(_,EmptyTree,EmptyTree),List(TypeDef(T,TypeTree[TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Nothing),TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Any))]), ValDef(evidence$1,TypeTree[AppliedType(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),trait Specialized),List(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class )),trait Vec)),type T)))],EmptyTree), ValDef(evidence$2,TypeTree[AppliedType(TypeRef(TermRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),object math),trait Numeric),List(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class )),trait Vec)),type T)))],EmptyTree))) - -// defdef DefDef(foo,List(List(ValDef(v,AppliedTypeTree(Ident(Vec),List(Ident(Int))),EmptyTree))),TypeTree[AppliedType(TypeRef(ThisType(TypeRef(NoPrefix,module class )),trait Vec),List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),class Int)))],Ident(v)) - - -// class DesugarSpecializedTraits extends MacroTransform, SymTransformer: -// import tpd._ +// Need to ban all of these but we will do that earlier I guess? +// Vec[Vec[Int]] hehe <- fine +// Vec[S, S[T]: Specialized] <- banned +// Vec[S, T[T]: Specialized] <- banned +// Vec[Array[T]: Specialized] <- banned -// override def phaseName: String = DesugarSpecializedTraits.name -// override def description: String = DesugarSpecializedTraits.description -// override def changesMembers: Boolean = false -// override def changesParents: Boolean = true +// need to test with explicit evidence / our own custom type classes + \ No newline at end of file diff --git a/tests/pos/inline-trait-specialized-desugar.scala b/tests/pos/inline-trait-specialized-desugar.scala index e912228c9bca..7e9327b7f643 100644 --- a/tests/pos/inline-trait-specialized-desugar.scala +++ b/tests/pos/inline-trait-specialized-desugar.scala @@ -42,105 +42,3 @@ class MyClassB extends MyClassA, ArrayIterator$sp$Int // We convert this to: val ai = new ArrayIterator$impl$Int(xs) {} println(ai.next()) - - - - // println(genericTrait.denot.info.appliedTo(genericTrait.denot.info.typeParams)) - // println(genericTrait.denot.info.widenDealias.) -// instantiateWithTypeVars - // instantiateWithTypeVars() - - // newNormalizedClassSymbol( - // genericTrait.owner, - // "CopiedSymbol", - // Flags.Synthetic | Flags.Inline | Flags.Trait, - // parents, - // NoType, // TODO: Work out what to do about self types; for now just ban them - // genericTrait.privateWithin, - // compUnitInfo = genericTrait.compUnitInfo - // ) - - - - // // Now add the constructor - - // selfInfo: Type = NoType, - // // Need to figure out how we leave some of the necessary generics for unspecialized params. - // ctx. - - - // genericTrait.copy( - // name= - // flags=genericTrait.flags | Flags.Synthetic, - - // ) - // newNormalizedClassSymbol() - // inline trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator[Int] - - - - // private def inlinedClassSym(sym: ClassSymbol, withoutFlags: FlagSet = EmptyFlags)(using Context): ClassSymbol = - // sym.info match { - // case clsInfo: ClassInfo => - // val typeParams: List[Type] = sym.primaryConstructor.info match { - // case poly: PolyType => poly.paramRefs - // case _ => Nil - // } - // // Extend inner class from inline trait to preserve typing - // val newParent = ctx.owner.thisType.select(sym).appliedTo(typeParams) - // val inlinedSym = ( - // ctx.owner, - // sym.name, - // (sym.flags | Synthetic) &~ withoutFlags, - // newCls => { - // val ClassInfo(prefix, _, parents, _, selfInfo) = inlinerTypeMap.mapClassInfo(clsInfo) - // ClassInfo(prefix, newCls, parents :+ newParent, Scopes.newScope, selfInfo) // TODO fix selfInfo (what to use?) - // }, - // sym.privateWithin, - // spanCoord(parent.span) - // ) - // // ctx.inlineTraitState.registerInlinedInnerClassSymbol(sym, inlinedSym, childThisType) - // ctx.inlineTraitState.registerInlinedSymbol(sym, inlinedSym, childThisType.widenDealias) - // inlinedSym.entered - // case _ => - // report.error(s"Class symbol ${sym.show} does not have class info") - // sym - // } - -// specializedTraitSymbol.copy( -// name="GeneratedSpecializedSymbol", - -// ) - - // Symbols.new - // def ( - // owner: Symbol, - // name: TypeName, - // flags: FlagSet, - // parentTypes: List[Type], - // selfInfo: Type = NoType, - // privateWithin: Symbol = NoSymbol, - // coord: Coord = NoCoord, - // compUnitInfo: CompilationUnitInfo | Null = null)(using Context): ClassSymbol = { - - - // def newClass(owner: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol = - // assert(parents.nonEmpty && !parents.head.typeSymbol.is(dotc.core.Flags.Trait), "First parent must be a class") - // val cls = dotc.core.Symbols.newNormalizedClassSymbol( - // owner, - // name.toTypeName, - // dotc.core.Flags.EmptyFlags, - // parents, - // selfType.getOrElse(Types.NoType), - // dotc.core.Symbols.NoSymbol) - // cls.enter(dotc.core.Symbols.newConstructor(cls, dotc.core.Flags.Synthetic, Nil, Nil)) - // for sym <- decls(cls) do cls.enter(sym) - // cls - - - // } - - // private def newSpecializedTraitImplementationClass() { - // // class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator[Int](elems) - - // } \ No newline at end of file diff --git a/tests/pos/specialized-trait-vector-example.scala b/tests/pos/specialized-trait-vector-example.scala index 7b3aa003e6ee..3a315481b3aa 100644 --- a/tests/pos/specialized-trait-vector-example.scala +++ b/tests/pos/specialized-trait-vector-example.scala @@ -40,105 +40,3 @@ def foo(x: ArrayIterator[Int]): Int = x.next() // // We convert this to: // val ai = new ArrayIterator$impl$Int(xs) {} // println(ai.next()) - - - - // println(genericTrait.denot.info.appliedTo(genericTrait.denot.info.typeParams)) - // println(genericTrait.denot.info.widenDealias.) -// instantiateWithTypeVars - // instantiateWithTypeVars() - - // newNormalizedClassSymbol( - // genericTrait.owner, - // "CopiedSymbol", - // Flags.Synthetic | Flags.Inline | Flags.Trait, - // parents, - // NoType, // TODO: Work out what to do about self types; for now just ban them - // genericTrait.privateWithin, - // compUnitInfo = genericTrait.compUnitInfo - // ) - - - - // // Now add the constructor - - // selfInfo: Type = NoType, - // // Need to figure out how we leave some of the necessary generics for unspecialized params. - // ctx. - - - // genericTrait.copy( - // name= - // flags=genericTrait.flags | Flags.Synthetic, - - // ) - // newNormalizedClassSymbol() - // inline trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator[Int] - - - - // private def inlinedClassSym(sym: ClassSymbol, withoutFlags: FlagSet = EmptyFlags)(using Context): ClassSymbol = - // sym.info match { - // case clsInfo: ClassInfo => - // val typeParams: List[Type] = sym.primaryConstructor.info match { - // case poly: PolyType => poly.paramRefs - // case _ => Nil - // } - // // Extend inner class from inline trait to preserve typing - // val newParent = ctx.owner.thisType.select(sym).appliedTo(typeParams) - // val inlinedSym = ( - // ctx.owner, - // sym.name, - // (sym.flags | Synthetic) &~ withoutFlags, - // newCls => { - // val ClassInfo(prefix, _, parents, _, selfInfo) = inlinerTypeMap.mapClassInfo(clsInfo) - // ClassInfo(prefix, newCls, parents :+ newParent, Scopes.newScope, selfInfo) // TODO fix selfInfo (what to use?) - // }, - // sym.privateWithin, - // spanCoord(parent.span) - // ) - // // ctx.inlineTraitState.registerInlinedInnerClassSymbol(sym, inlinedSym, childThisType) - // ctx.inlineTraitState.registerInlinedSymbol(sym, inlinedSym, childThisType.widenDealias) - // inlinedSym.entered - // case _ => - // report.error(s"Class symbol ${sym.show} does not have class info") - // sym - // } - -// specializedTraitSymbol.copy( -// name="GeneratedSpecializedSymbol", - -// ) - - // Symbols.new - // def ( - // owner: Symbol, - // name: TypeName, - // flags: FlagSet, - // parentTypes: List[Type], - // selfInfo: Type = NoType, - // privateWithin: Symbol = NoSymbol, - // coord: Coord = NoCoord, - // compUnitInfo: CompilationUnitInfo | Null = null)(using Context): ClassSymbol = { - - - // def newClass(owner: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol = - // assert(parents.nonEmpty && !parents.head.typeSymbol.is(dotc.core.Flags.Trait), "First parent must be a class") - // val cls = dotc.core.Symbols.newNormalizedClassSymbol( - // owner, - // name.toTypeName, - // dotc.core.Flags.EmptyFlags, - // parents, - // selfType.getOrElse(Types.NoType), - // dotc.core.Symbols.NoSymbol) - // cls.enter(dotc.core.Symbols.newConstructor(cls, dotc.core.Flags.Synthetic, Nil, Nil)) - // for sym <- decls(cls) do cls.enter(sym) - // cls - - - // } - - // private def newSpecializedTraitImplementationClass() { - // // class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator[Int](elems) - - // } \ No newline at end of file diff --git a/tests/pos/specialized-traits-basic.scala b/tests/pos/specialized-traits-basic.scala index ea2e1286db65..2905d1338ffd 100644 --- a/tests/pos/specialized-traits-basic.scala +++ b/tests/pos/specialized-traits-basic.scala @@ -5,52 +5,4 @@ inline trait Vec[T: {Specialized, Numeric}, S <: Object, Q: Numeric, R: Speciali def foo(v: Vec[Int, String, Int, Int, Int]) = v def main() = - type x = Specialized[Array[Array[Int]]] println("Hello, World!") - - // val x = new Vec[Int, String, Int, Int, Int]() {} - // foo(x) - -// Need to ban all of these but we will do that earlier I guess? -// Vec[Vec[Int]] hehe <- fine -// Vec[S, S[T]: Specialized] <- banned -// Vec[S, T[T]: Specialized] <- banned -// Vec[Array[T]: Specialized] <- banned - - -// Map(TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Nothing), -// TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Any)) -> TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),class Int)) - -// List(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class )),trait Vec)),type S), -// TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),class Char), -// TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class )),trait Vec)),type T) ))) - - -// Challenge: annotation is on the level of type params and not on the level of methods for example. Can have some without. - -// Got tp [T#825496893, S#825496893 <: Object#744, Q#825496893, R#825496893, D#825496893] -// #825496893 -// (using -// evidence$1#681088021: #2.this.scala#21.Specialized#338[T#825496893], -// evidence$2#681088021: Numeric#6014[T#825496893], -// evidence$3#681088021: Numeric#6014[Q#825496893], -// evidence$4#681088021: #2.this.scala#21.Specialized#338[R#825496893], -// evidence$5#681088021: Numeric#6014[D#825496893], evidence$6#681088021: -// #2.this.scala#21.Specialized#338[D#825496893]) -// (): -// #2299.this.Vec#4482[T#825496893, S#825496893, Q#825496893, -// R#825496893, D#825496893] - - -// need to test with explicit evidence. - - -// inline trait Vec[T: SomeTypeClass] -// - -// def foo = -// instance of typeclass SomeTypeClass[Int] -// new Vec[Int] \/ - -// new VecSp without the condition -// -> creates Vec[Int] \ No newline at end of file From 5be082272caad1a3eaa2d58b8295d5a4db780e13 Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 13 Mar 2026 12:20:22 +0100 Subject: [PATCH 023/254] Add specialized traits cache --- .../transform/DesugarSpecializedTraits.scala | 47 ++++++++++++------- tests/pos/specialized-traits-basic.scala | 1 + 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index adb12a9e0de5..8ca6bc12458d 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -27,6 +27,8 @@ import dotty.tools.dotc.core.StdNames.str.SPECIALIZED_TRAIT_SUFFIX import Vec.DesugarSpecializedTraits.newSpecializedTraitName import dotty.tools.dotc.core.Names.Name import tpd._ +import scala.collection.mutable + class DesugarSpecializedTraits extends MacroTransform: @@ -119,39 +121,32 @@ class DesugarSpecializedTraits extends MacroTransform: override def transform(tree: Tree)(using Context): Tree = tree match { case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. - val stats1 = collectNecessaryGeneratedSymbols(pkg).map(buildClassTree) + val stats1 = generateSpecializedTraitSymbols(pkg).map(buildClassTree) // Use the TreeTypeMap to replace instances (can we do this without accidentally replacing the definitions? I think it should be ok) - // val treeTypeMap = new TreeTypeMap() + // val treeTypeMap = new TreeTypeMap() cpy.PackageDef(pkg)(pid, stats1 ++ stats) } - private def collectNecessaryGeneratedSymbols(tree: Tree)(using Context): List[(Symbol, ClassSymbol)] = - val result: List[(Symbol, ClassSymbol)] = tree.deepFold(List.empty)((found, tree) => tree match + private def generateSpecializedTraitSymbols(tree: Tree)(using Context): List[(Symbol, ClassSymbol)] = + tree.deepFold(SpecializedTraitCache())((foundSpecializations, tree) => tree match // case New(AppliedTypeTree etc) -> need to output the impl class -> do we wantto generate that when we see Foo[Int] or not? case AppliedTypeTree(specializedTrait: Ident, concreteTypeTrees: List[Tree]) => val specializedTraitSymbol = specializedTrait.denot.symbol - val specializedTypeVars = specializedTraitSymbol.unforcedDecls.implicitDecls.collect(_.info match { case SpecializedEvidence(typeVar) => typeVar }).toSet val specializationMap = specializedTraitSymbol.typeParams.map(_.typeRef.asInstanceOf[Type]).zip(concreteTypeTrees).toMap.filter((k, v) => specializedTypeVars(k)) - if (specializationMap.nonEmpty) { - val specializedTraitInterfaceTraitSymbol = newSpecializedTraitInterfaceTrait(specializedTrait, specializationMap) - - (specializedTraitSymbol, specializedTraitInterfaceTraitSymbol) :: found + if (specializationMap.nonEmpty && !foundSpecializations.existsSpecialization(specializedTraitSymbol, specializationMap)) { + val newSpecializedTraitInterfaceTraitSymbol = newSpecializedTraitInterfaceTrait(specializedTrait, specializationMap) + foundSpecializations.addSpecialization(specializedTraitSymbol, specializationMap, newSpecializedTraitInterfaceTraitSymbol) } - else found - case tree: TypeDef => - found - case _ => found - ) - result - + else foundSpecializations + case _ => foundSpecializations + ).getSpecializations } - object DesugarSpecializedTraits: val name: String = "desugarSpecializedTraits" val description: String = "Replaces traits having type parameters that have the Specialized annotation with specialized versions" @@ -164,8 +159,23 @@ object DesugarSpecializedTraits: }).fold(name ++ str.SPECIALIZED_TRAIT_SUFFIX)((n1, n2) => n1 ++ n2) +class SpecializedTraitCache: + private val specializationMap: mutable.Map[(Symbol, Name), ClassSymbol] = mutable.Map.empty + + def existsSpecialization(traitSymbol: Symbol, specialization: Map[Type, Tree])(using Context) = + specializationMap.contains((traitSymbol, newSpecializedTraitName(traitSymbol.name, specialization))) + + def addSpecialization(traitSymbol: Symbol, specialization: Map[Type, Tree], specializedSymbol: ClassSymbol)(using Context): SpecializedTraitCache = { + specializationMap((traitSymbol, newSpecializedTraitName(traitSymbol.name, specialization))) = specializedSymbol + this + } + + def getSpecializations: List[(Symbol, ClassSymbol)] = specializationMap.toList.map((k, v) => (k._1, v)) + +end SpecializedTraitCache + -// Cleanup +// Need to somehow make my naming a lot more consistent as well. // Correctly generate names // generate classes as well // do we actually want to generate Iteratorsp$Int @@ -175,6 +185,7 @@ object DesugarSpecializedTraits: // Synthesise Specialized instances so that people can't do stupid stuff like Specialized[Array[T]]. type x = Specialized[Array[Array[Int]]] // Set the Synthetic flags somewhere // Cache / only generate once instead of multiple times. +// Ideally standardise on either specialization or specializationMap // Potentially we can just go through and find every place which needs one, do a direct replacement and spit it out directly into some kind of list buffer and then // copy it out later diff --git a/tests/pos/specialized-traits-basic.scala b/tests/pos/specialized-traits-basic.scala index 2905d1338ffd..1de7eb20c091 100644 --- a/tests/pos/specialized-traits-basic.scala +++ b/tests/pos/specialized-traits-basic.scala @@ -5,4 +5,5 @@ inline trait Vec[T: {Specialized, Numeric}, S <: Object, Q: Numeric, R: Speciali def foo(v: Vec[Int, String, Int, Int, Int]) = v def main() = + val a = new Vec[Int, String, Int, Int, Int]() {} println("Hello, World!") From da9b289205206d35103f683da8c8d381807cb34a Mon Sep 17 00:00:00 2001 From: Hamish Date: Sat, 14 Mar 2026 16:51:21 +0100 Subject: [PATCH 024/254] Save rejected method --- compiler/src/dotty/tools/dotc/ast/tpd.scala | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 23dde1139c03..1378f9e87f7a 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -331,7 +331,16 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { def ClassDef(cls: ClassSymbol, constr: DefDef, body: List[Tree], superArgs: List[Tree] = Nil, adaptVarargs: Boolean = false)(using Context): TypeDef = val firstParent :: otherParents = cls.info.parents: @unchecked + ClassDefWithParents(cls, constr, generateSuperClassRef(cls, firstParent, superArgs, adaptVarargs) :: otherParents.map(TypeTree(_)), body) + end ClassDef + + def ClassDefWithCustomTraitTrees(cls: ClassSymbol, constr: DefDef, body: List[Tree], + superArgs: List[Tree] = Nil, otherParents: List[Tree] = Nil, adaptVarargs: Boolean = false)(using Context): TypeDef = + val firstParent :: _ = cls.info.parents: @unchecked + ClassDefWithParents(cls, constr, generateSuperClassRef(cls, firstParent, superArgs, adaptVarargs) :: otherParents, body) + end ClassDefWithCustomTraitTrees + def generateSuperClassRef(cls: ClassSymbol, firstParent: Type, superArgs: List[Tree], adaptVarargs: Boolean)(using Context) = def adaptedSuperArgs(ctpe: Type): List[Tree] = ctpe match case ctpe: PolyType => adaptedSuperArgs(ctpe.instantiate(firstParent.argTypes)) @@ -351,9 +360,8 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { case constr :: Nil => constr case _ => assert(false, i"multiple applicable parent constructors of $firstParent for supercall arguments $superArgs") New(firstParent, parentConstr.asTerm, adaptedSuperArgs(parentConstr.info)) + superRef - ClassDefWithParents(cls, constr, superRef :: otherParents.map(TypeTree(_)), body) - end ClassDef def ClassDefWithParents(cls: ClassSymbol, constr: DefDef, parents: List[Tree], body: List[Tree])(using Context): TypeDef = { val selfType = From 5296cc9552ab21effe8c35e4720c990800dd2ce8 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sat, 14 Mar 2026 16:51:32 +0100 Subject: [PATCH 025/254] Revert "Save rejected method" This reverts commit da9b289205206d35103f683da8c8d381807cb34a. --- compiler/src/dotty/tools/dotc/ast/tpd.scala | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 1378f9e87f7a..23dde1139c03 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -331,16 +331,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { def ClassDef(cls: ClassSymbol, constr: DefDef, body: List[Tree], superArgs: List[Tree] = Nil, adaptVarargs: Boolean = false)(using Context): TypeDef = val firstParent :: otherParents = cls.info.parents: @unchecked - ClassDefWithParents(cls, constr, generateSuperClassRef(cls, firstParent, superArgs, adaptVarargs) :: otherParents.map(TypeTree(_)), body) - end ClassDef - - def ClassDefWithCustomTraitTrees(cls: ClassSymbol, constr: DefDef, body: List[Tree], - superArgs: List[Tree] = Nil, otherParents: List[Tree] = Nil, adaptVarargs: Boolean = false)(using Context): TypeDef = - val firstParent :: _ = cls.info.parents: @unchecked - ClassDefWithParents(cls, constr, generateSuperClassRef(cls, firstParent, superArgs, adaptVarargs) :: otherParents, body) - end ClassDefWithCustomTraitTrees - def generateSuperClassRef(cls: ClassSymbol, firstParent: Type, superArgs: List[Tree], adaptVarargs: Boolean)(using Context) = def adaptedSuperArgs(ctpe: Type): List[Tree] = ctpe match case ctpe: PolyType => adaptedSuperArgs(ctpe.instantiate(firstParent.argTypes)) @@ -360,8 +351,9 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { case constr :: Nil => constr case _ => assert(false, i"multiple applicable parent constructors of $firstParent for supercall arguments $superArgs") New(firstParent, parentConstr.asTerm, adaptedSuperArgs(parentConstr.info)) - superRef + ClassDefWithParents(cls, constr, superRef :: otherParents.map(TypeTree(_)), body) + end ClassDef def ClassDefWithParents(cls: ClassSymbol, constr: DefDef, parents: List[Tree], body: List[Tree])(using Context): TypeDef = { val selfType = From c18dc018c0c8feca8fb3bb972be5c7fa209e7636 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sat, 14 Mar 2026 16:53:10 +0100 Subject: [PATCH 026/254] Break out separate Specialized representation --- .../transform/DesugarSpecializedTraits.scala | 228 ++++++++++++------ 1 file changed, 159 insertions(+), 69 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 8ca6bc12458d..d59d54061e42 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -4,9 +4,6 @@ import dotty.tools.dotc.ast.tpd import dotty.tools.dotc.transform.MegaPhase.MiniPhase import dotty.tools.dotc.core.Contexts.* import dotty.tools.dotc.core.Decorators.i -import dotty.tools.dotc.{transform => Vec} -import dotty.tools.dotc.{transform => foo} -import dotty.tools.dotc.{transform => v} import dotty.tools.dotc.core.Decorators.className import dotty.tools.dotc.core.Symbols.{Symbol, ClassSymbol, newNormalizedClassSymbol} import dotty.tools.dotc.CompilationUnit @@ -24,7 +21,6 @@ import dotty.tools.dotc.core.Flags.EmptyFlags import dotty.tools.dotc.ast.TreeTypeMap import dotty.tools.dotc.core.Scopes.EmptyScope import dotty.tools.dotc.core.StdNames.str.SPECIALIZED_TRAIT_SUFFIX -import Vec.DesugarSpecializedTraits.newSpecializedTraitName import dotty.tools.dotc.core.Names.Name import tpd._ import scala.collection.mutable @@ -44,53 +40,46 @@ class DesugarSpecializedTraits extends MacroTransform: override def newTransformer(using Context): Transformer = new Transformer { - object SpecializedEvidence { - def unapply(tpe: Type)(using Context): Option[Type] = tpe match { - case AppliedType(tycon, List(tpeArg)) if tycon =:= ctx.definitions.SpecializedBoundRef => Some(tpeArg) - case _ => None - } - } - - private def newSpecializedTraitInterfaceTrait(specializedTrait: Ident, specializationMap: Map[Type, Tree]) = - // Define specialization we want to apply - val specializedTraitSymbol = specializedTrait.denot.symbol - val tm = new TypeMap: - def apply(t: Type) = specializationMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) - val specialization = specializedTraitSymbol.typeParams.map(_.typeRef).map(specializationMap.applyOrElse(_, TypeTree(_))) - + private def newSpecializedTraitInterfaceTrait(specialization: Specialization) = + val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. + def apply(t: Type) = specialization.specializedTypeParamsToTypeArgumentsMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) // TODO: IF we can do just types we can get rid fo this + // Create new trait + val att = AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization) + val v = att.symbol val parents = defn.ObjectType - :: AppliedTypeTree(cpy.Ident(specializedTrait)(specializedTrait.name), specialization).tpe // original trait; specialized - :: specializedTrait.denot.info.parents.filterNot(_ eq defn.ObjectType).map(tm(_)) // parents of the original trait, specialized + :: AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization).tpe // original trait, specialized + :: specialization.traitSymbol.denot.info.parents.filterNot(_ eq defn.ObjectType).map(tm(_)) // parents of the original trait, specialized + val traitSymbol = newNormalizedClassSymbol( - specializedTraitSymbol.owner, - (newSpecializedTraitName(specializedTraitSymbol.name, specializationMap)).asTypeName, - Flags.Synthetic | Flags.Inline | Flags.Trait, + specialization.traitSymbol.owner, + (DesugarSpecializedTraits.newSpecializedTraitName(specialization)).asTypeName, + Flags.Synthetic | Flags.Trait, parents, - NoType, // TODO: Work out what to do about self types; for now just ban them - specializedTraitSymbol.privateWithin, + NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? + specialization.traitSymbol.privateWithin, // TODO: Do we need a compUnit info? ) // Create type parameters for new trait - val old_type_params = specializedTraitSymbol.typeParams.filterNot(t => specializationMap.contains(t.typeRef)) val tps = newTypeParams(traitSymbol, - old_type_params.map(_.name), + specialization.unspecializedTypeParams.map(_.typeSymbol.name.asTypeName), EmptyFlags, - targets => targets.map(t => specializedTraitSymbol.typeParams.find(_.name == t.name).get.info.bounds) + targets => targets.map(t => specialization.traitSymbol.typeParams.find(_.name == t.name).get.info.bounds) ) tps.foreach(traitSymbol.enter(_, EmptyScope)) // Replace old type parameters that were copied from original trait with new ones - // inside the parents of the new trait - val tpMap: Map[Type, Type] = old_type_params.map(_.typeRef).zip(tps.map(_.typeRef)).toMap + // inside the parents of the new trait + val tpMap: Map[Type, Type] = specialization.unspecializedTypeParams.zip(tps.map(_.typeRef)).toMap val freshTypeVarMap = new TypeMap: def apply(t: Type) = tpMap.applyOrElse(t, mapOver) - traitSymbol.info = ClassInfo(traitSymbol.owner.thisType, traitSymbol, traitSymbol.info.parents.map(freshTypeVarMap(_)), traitSymbol.info.decls, traitSymbol.info.self) + traitSymbol.info = ClassInfo(traitSymbol.owner.thisType, traitSymbol, traitSymbol.info.parents.map(freshTypeVarMap(_)), traitSymbol.info.decls) // TODO: What happens if the creator of the specialized inline trait provides a self type? traitSymbol.entered - private def buildClassTree(originalTraitSymbol: Symbol, generatedTraitSymbol: ClassSymbol)(using Context) = { + private def buildClassTree(specialization: Specialization, generatedTraitSymbol: ClassSymbol)(using Context) = { + val originalTraitSymbol = specialization.traitSymbol val init = newDefaultConstructor(generatedTraitSymbol) // Fix constructor so that it: @@ -115,36 +104,80 @@ class DesugarSpecializedTraits extends MacroTransform: // able to copy them over, apply the specialization (keeping e.g. Numeric[Int] that arises from this) and // pruning any that belong to Specialized. - ClassDef(generatedTraitSymbol, DefDef(init.entered), Nil) - } + // TODO: Tidy this up and also figure out why we don't do this by default in the ClassDef? + val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. + def apply(t: Type) = specialization.specializedTypeParamsToTypeArgumentsMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) // TODO: IF we can do just types we can get rid fo this - override def transform(tree: Tree)(using Context): Tree = - tree match { - case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. - val stats1 = generateSpecializedTraitSymbols(pkg).map(buildClassTree) - - // Use the TreeTypeMap to replace instances (can we do this without accidentally replacing the definitions? I think it should be ok) - // val treeTypeMap = new TreeTypeMap() - cpy.PackageDef(pkg)(pid, stats1 ++ stats) - } - - private def generateSpecializedTraitSymbols(tree: Tree)(using Context): List[(Symbol, ClassSymbol)] = - tree.deepFold(SpecializedTraitCache())((foundSpecializations, tree) => tree match - // case New(AppliedTypeTree etc) -> need to output the impl class -> do we wantto generate that when we see Foo[Int] or not? + // specialization.traitSymbol.info. + // HACK: This doesn't work for if A is inline for example because in general it misses the type for A + // specialization.traitSymbol.denot.info.paren + - case AppliedTypeTree(specializedTrait: Ident, concreteTypeTrees: List[Tree]) => - val specializedTraitSymbol = specializedTrait.denot.symbol - val specializedTypeVars = specializedTraitSymbol.unforcedDecls.implicitDecls.collect(_.info match { case SpecializedEvidence(typeVar) => typeVar }).toSet - val specializationMap = specializedTraitSymbol.typeParams.map(_.typeRef.asInstanceOf[Type]).zip(concreteTypeTrees).toMap.filter((k, v) => specializedTypeVars(k)) + // val customParentTrees = + // AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization) // original trait, specialized + // :: specialization.traitSymbol.denot.info.parents.filterNot(_ eq defn.ObjectType).map(a => TypeTree(tm(a))) // parents of the original trait, specialized + + ClassDef(generatedTraitSymbol, DefDef(init.entered), Nil) + } - if (specializationMap.nonEmpty && !foundSpecializations.existsSpecialization(specializedTraitSymbol, specializationMap)) { - val newSpecializedTraitInterfaceTraitSymbol = newSpecializedTraitInterfaceTrait(specializedTrait, specializationMap) - foundSpecializations.addSpecialization(specializedTraitSymbol, specializationMap, newSpecializedTraitInterfaceTraitSymbol) + override def transform(tree: Tree)(using Context): Tree = tree match { + case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. + val specializedSymbols = generateSpecializedTraitSymbols(pkg) + val generatedTraitStats = specializedSymbols.getSpecializations.map(buildClassTree) + + // Use the TreeTypeMap to replace instances (can we do this without accidentally replacing the definitions? I think it should be ok) + val typeMap = new TypeMap: + def apply(t: Type) = t match { + case Specialization(spec) => + { + for (specializedSymbol <- specializedSymbols.get(spec)) + yield AppliedType(specializedSymbol.typeRef, spec.unspecializedTypeArgs.map(_.tpe)) + }.getOrElse(mapOver(t)) + case _ => mapOver(t) } - else foundSpecializations - case _ => foundSpecializations - ).getSpecializations + + def treeMap(tree: Tree): Tree = tree match { + case Apply(TypeApply(fun@Select(New(tpt), _init), args), ev) if fun.symbol.isConstructor => + val spec = Specialization(fun.symbol.owner, args) + // Note: We always drop the evidence params when creating these new specialized traits so we know that there are none, but we may need to revisit this if we decide we do want to copy the evidence parameters over + TypeApply(Select(New(treeMap(tpt)), _init), spec.unspecializedTypeArgs) + case Specialization(spec) => { + for (specializedSymbol <- specializedSymbols.get(spec)) + yield AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT + }.getOrElse(tree) + + case tree => tree + } + + val treeTypeMap = new TreeTypeMap(typeMap, treeMap) { + override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? + case dd@DefDef(name, paramss, tpt, preRhs) => + val transformedDef = super.transform(dd) + transformedDef.symbol.info = mapType(transformedDef.symbol.info) + transformedDef + case tree => super.transform(tree) + } + } + cpy.PackageDef(pkg)(pid, generatedTraitStats ++ stats.map(treeTypeMap(_))) // TODO: Do we also want to apply the map over generatedTraitStats?? + } + + private def generateSpecializedTraitSymbols(tree: Tree)(using Context): SpecializedTraitCache = + tree.deepFold(SpecializedTraitCache())((foundSpecs, tree) => tree match + // case New(something) => // if tycon.denot.symbol => // Is this fold going to be a problem? Or juist a good thing? Because we hit the child first + // // AppliedTypeTree(tycon: Ident, concreteTypeTrees: List[Tree]) + // // I guess ideally do this after already processing it down to the ArrayIterator$sp$Int then we just replace that with ArraytIterator + // println(s"Found something ${something}") + // foundSpecs + // case New(AppliedTypeTree etc) -> need to output the impl class -> do we wantto generate that when we see Foo[Int] or not? + + // TODO: In theory since we are going to apply the tree type map anyway we can surely just collect up the specialisations we need and then later generate the new symbols? + // I think that's slightly cleaner. + case Specialization(spec) if (spec.hasSpecializedParams && !foundSpecs.contains(spec)) => + val newSpecializedTraitInterfaceTraitSymbol = newSpecializedTraitInterfaceTrait(spec) + foundSpecs.add(spec, newSpecializedTraitInterfaceTraitSymbol) + case _ => foundSpecs + ) } object DesugarSpecializedTraits: @@ -153,28 +186,88 @@ object DesugarSpecializedTraits: // TODO: What happens with this name generation if we have Vec[Vec[T]] for example? We potentially don't have an Ident // TODO: Check what happens here when we have a case where the types being specialized into are user defined instead of primitives or type vars. - private[transform] def newSpecializedTraitName(name: Name, specialization: Map[Type, Tree]) = - specialization.values.collect(t => t match { + private[transform] def newSpecializedTraitName(specialization: Specialization)(using Context) = + specialization.specializedTypeArgs.collect(t => t match { case Ident(tpe) => tpe ++ str.SPECIALIZED_TRAIT_TYPE_SEP - }).fold(name ++ str.SPECIALIZED_TRAIT_SUFFIX)((n1, n2) => n1 ++ n2) + }).fold(specialization.traitSymbol.name ++ str.SPECIALIZED_TRAIT_SUFFIX)((n1, n2) => n1 ++ n2) +// TODO: Potentially we can just replace this with a map? class SpecializedTraitCache: - private val specializationMap: mutable.Map[(Symbol, Name), ClassSymbol] = mutable.Map.empty + private val specializationMap: mutable.Map[Specialization, ClassSymbol] = mutable.Map.empty - def existsSpecialization(traitSymbol: Symbol, specialization: Map[Type, Tree])(using Context) = - specializationMap.contains((traitSymbol, newSpecializedTraitName(traitSymbol.name, specialization))) + def contains(specialization: Specialization)(using Context) = + specializationMap.contains(specialization) - def addSpecialization(traitSymbol: Symbol, specialization: Map[Type, Tree], specializedSymbol: ClassSymbol)(using Context): SpecializedTraitCache = { - specializationMap((traitSymbol, newSpecializedTraitName(traitSymbol.name, specialization))) = specializedSymbol + def add(specialization: Specialization, specializedSymbol: ClassSymbol)(using Context): SpecializedTraitCache = { + specializationMap(specialization) = specializedSymbol this } - def getSpecializations: List[(Symbol, ClassSymbol)] = specializationMap.toList.map((k, v) => (k._1, v)) + def get(specialization: Specialization)(using Context) = specializationMap.get(specialization) + + def getSpecializations: List[(Specialization, ClassSymbol)] = specializationMap.toList end SpecializedTraitCache +/* Represents an application traitSymbol[typeArguments] */ +class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(using Context): // TODO: Can we get away with List[Type] + object SpecializedEvidence { + def unapply(tpe: Type)(using Context): Option[Type] = tpe match { + case AppliedType(tycon, List(tpeArg)) if tycon =:= ctx.definitions.SpecializedBoundRef => Some(tpeArg) + case _ => None + } + } + + val specializedTypeParams: List[Type] = traitSymbol.unforcedDecls.implicitDecls.collect(_.info match { case SpecializedEvidence(typeVar) => typeVar }) + + private val specializedTypeParamsSet = specializedTypeParams.toSet + private val paramToArgList = traitSymbol.typeParams.map(_.typeRef.asInstanceOf[Type]).zip(typeArguments) + + val unspecializedTypeParams: List[Type] = paramToArgList.filterNot((tParam, tArg) => specializedTypeParamsSet(tParam)).map(_._1) + val specializedTypeArgs: List[Tree] = paramToArgList.filter((tParam, tArg) => specializedTypeParamsSet(tParam)).map(_._2) + val unspecializedTypeArgs: List[Tree] = paramToArgList.filterNot((tParam, tArg) => specializedTypeParamsSet(tParam)).map(_._2) + + val specializedTypeParamsToTypeArgumentsMap: Map[Type, Tree] = paramToArgList.toMap.filter((k, v) => specializedTypeParamsSet(k)) + val specialization: List[Tree] = traitSymbol.typeParams.map(_.typeRef).map(specializedTypeParamsToTypeArgumentsMap.applyOrElse(_, TypeTree(_))) // TODO: Don't really like this name + + def hasSpecializedParams: Boolean = specializedTypeParams.nonEmpty + + // Note: We only care about the specialized arguments for equality; a specialization of Vec[A: Specialized, B] with B = Int and one + // with B = String can be considered to be the same as they use the same specialized trait + // TODO: I don't really like this logic being in Specialization because they are really different + // We should really put that logic in the SpecializedTraitCache because it's at that point that we treat them as the same. + override def equals(obj: Any): Boolean = + obj.isInstanceOf[Specialization] && obj.asInstanceOf[Specialization].traitSymbol == traitSymbol + && specializedTypeArgs.zip(obj.asInstanceOf[Specialization].specializedTypeArgs).forall((a1, a2) => a1.tpe =:= a2.tpe) + + override def hashCode(): Int = (traitSymbol, specializedTypeArgs.map(_.tpe)).hashCode() + +object Specialization: + def unapply(tpt: Tree)(using Context) = tpt match { + case AppliedTypeTree(specializedTrait: Ident, concreteTypeTrees: List[Tree]) => Some(Specialization(specializedTrait.denot.symbol, concreteTypeTrees)) + case _ => None + } + + def unapply(tpe: Type)(using Context) = tpe match { + case AppliedType(tycon: Type, args: List[Type]) => Some(Specialization(tycon.typeSymbol, args.map(TypeTree(_)))) + case _ => None + } + + +// Would be nice to define a Specialization class I think +// -> Map the specialized type params to Int etc +// -> Map the non-specialized type params to new type params +// -> Be a canonical representation so we can store that in a set +// -> Generate a name / string representation for use in new traits +// -> Get the specialized list to apply + + +// Do we really want the method definitions to live in the implementation classes or in the trait? +// Is proliferation of anonymous classes a problem? i.e. would we rather generate $impl$ classes and share them? We do generate one per time they use the class at the moment which is a lot + +// Need to make sure all my examples are up to date, consistent with what we do and what we want to do so that they are actually useful for the future. // Need to somehow make my naming a lot more consistent as well. // Correctly generate names // generate classes as well @@ -187,9 +280,6 @@ end SpecializedTraitCache // Cache / only generate once instead of multiple times. // Ideally standardise on either specialization or specializationMap -// Potentially we can just go through and find every place which needs one, do a direct replacement and spit it out directly into some kind of list buffer and then -// copy it out later - // Probably (tree)typemap // Would it be better to just copy rather than creating everything from scratch? I think this is right From 8c1fb7c9590918daad823ffac780b62acda46f96 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sat, 14 Mar 2026 16:53:39 +0100 Subject: [PATCH 027/254] Make inline traits robust to Trees without attached symbol, such as those that we generate --- compiler/src/dotty/tools/dotc/inlines/Inlines.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 4bdf3f10d41b..ed64f0723cc5 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -108,7 +108,7 @@ object Inlines: isInlineable(tree.symbol) && !tree.tpe.widenTermRefExpr.isInstanceOf[MethodOrPoly] && isInlineableInCtx && !ctx.mode.is(Mode.NoInline) && !isUnapplyExpressionWithDummy private[dotc] def symbolFromParent(parent: Tree)(using Context): Symbol = - if parent.symbol.isConstructor then parent.symbol.owner else parent.symbol + if parent.symbol.isConstructor then parent.symbol.owner else parent.tpe.typeSymbol private def inlineTraitAncestors(cls: TypeDef)(using Context): List[Tree] = cls match { case tpd.TypeDef(_, tmpl: Template) => From 063e4d2b99498daa1a7c558d7a2707fd79d6fc1a Mon Sep 17 00:00:00 2001 From: Hamish Date: Sat, 14 Mar 2026 16:56:14 +0100 Subject: [PATCH 028/254] Update comment --- .../tools/dotc/transform/DesugarSpecializedTraits.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index d59d54061e42..ad97bf1017e3 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -264,9 +264,11 @@ object Specialization: // -> Get the specialized list to apply -// Do we really want the method definitions to live in the implementation classes or in the trait? -// Is proliferation of anonymous classes a problem? i.e. would we rather generate $impl$ classes and share them? We do generate one per time they use the class at the moment which is a lot + +// Generate impl instead of generating anonymous classes every time to avoid insane code bloat + // Do we really want the method definitions to live in the implementation classes or in the trait?| + // I think in the trait is fine but note that this only actually saves any space if we don't use anonymous classes (because those copy parent members automatically it seems) // Need to make sure all my examples are up to date, consistent with what we do and what we want to do so that they are actually useful for the future. // Need to somehow make my naming a lot more consistent as well. // Correctly generate names From 7518cefc4439578d2e77b52517200c8337c7182c Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 15 Mar 2026 12:04:33 +0100 Subject: [PATCH 029/254] Save --- .../transform/DesugarSpecializedTraits.scala | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index ad97bf1017e3..47a7a0c6aa9c 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -108,12 +108,8 @@ class DesugarSpecializedTraits extends MacroTransform: val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. def apply(t: Type) = specialization.specializedTypeParamsToTypeArgumentsMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) // TODO: IF we can do just types we can get rid fo this - - // specialization.traitSymbol.info. // HACK: This doesn't work for if A is inline for example because in general it misses the type for A // specialization.traitSymbol.denot.info.paren - - // val customParentTrees = // AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization) // original trait, specialized // :: specialization.traitSymbol.denot.info.parents.filterNot(_ eq defn.ObjectType).map(a => TypeTree(tm(a))) // parents of the original trait, specialized @@ -144,7 +140,7 @@ class DesugarSpecializedTraits extends MacroTransform: TypeApply(Select(New(treeMap(tpt)), _init), spec.unspecializedTypeArgs) case Specialization(spec) => { for (specializedSymbol <- specializedSymbols.get(spec)) - yield AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT + yield AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? }.getOrElse(tree) case tree => tree @@ -162,20 +158,28 @@ class DesugarSpecializedTraits extends MacroTransform: cpy.PackageDef(pkg)(pid, generatedTraitStats ++ stats.map(treeTypeMap(_))) // TODO: Do we also want to apply the map over generatedTraitStats?? } + // TODO: Try with just generating new Foo(100) with no function to pass it to and no other references to Foo. this may not work because we might not + // correctly detect it. + private def generateSpecializedTraitSymbols(tree: Tree)(using Context): SpecializedTraitCache = tree.deepFold(SpecializedTraitCache())((foundSpecs, tree) => tree match - // case New(something) => // if tycon.denot.symbol => // Is this fold going to be a problem? Or juist a good thing? Because we hit the child first - // // AppliedTypeTree(tycon: Ident, concreteTypeTrees: List[Tree]) + case Typed(Apply(Select(New(cl),ctor),List()), t: TypeTree) => + Specialization.unapply(t).foreach( spec => + if (spec.hasSpecializedParams && !foundSpecs.contains(spec)) { + foundSpecs.add(spec, newSpecializedTraitInterfaceTrait(spec)) + } + foundSpecs.flagForImplementation(spec) // TODO: Need to think carefully about the behaviour when we are integrating libraries - should the library generate the implementation classes or the user? + // In any case we need to read back in either the $sp$ classes or the $impl$ traits to be able to work with them. + ) + foundSpecs + + // Is this fold going to be a problem? Or juist a good thing? Because we hit the child first // // I guess ideally do this after already processing it down to the ArrayIterator$sp$Int then we just replace that with ArraytIterator - // println(s"Found something ${something}") - // foundSpecs - // case New(AppliedTypeTree etc) -> need to output the impl class -> do we wantto generate that when we see Foo[Int] or not? // TODO: In theory since we are going to apply the tree type map anyway we can surely just collect up the specialisations we need and then later generate the new symbols? // I think that's slightly cleaner. case Specialization(spec) if (spec.hasSpecializedParams && !foundSpecs.contains(spec)) => - val newSpecializedTraitInterfaceTraitSymbol = newSpecializedTraitInterfaceTrait(spec) - foundSpecs.add(spec, newSpecializedTraitInterfaceTraitSymbol) + foundSpecs.add(spec, newSpecializedTraitInterfaceTrait(spec)) case _ => foundSpecs ) } @@ -192,9 +196,9 @@ object DesugarSpecializedTraits: }).fold(specialization.traitSymbol.name ++ str.SPECIALIZED_TRAIT_SUFFIX)((n1, n2) => n1 ++ n2) -// TODO: Potentially we can just replace this with a map? class SpecializedTraitCache: private val specializationMap: mutable.Map[Specialization, ClassSymbol] = mutable.Map.empty + private val flaggedForImplementation: mutable.Set[Specialization] = mutable.Set.empty def contains(specialization: Specialization)(using Context) = specializationMap.contains(specialization) @@ -208,6 +212,10 @@ class SpecializedTraitCache: def getSpecializations: List[(Specialization, ClassSymbol)] = specializationMap.toList + def flagForImplementation(spec: Specialization) = flaggedForImplementation.add(spec) + + def getSpecializationsForImplementation = flaggedForImplementation + end SpecializedTraitCache @@ -304,4 +312,4 @@ object Specialization: // need to test with explicit evidence / our own custom type classes - \ No newline at end of file +// TODO: Make sure name encoding is fully qualified - e.g. potential for conflicts if we define our own class Int. From c3a28b3d709c0814c487401ddf4c69a0468b497b Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 16 Mar 2026 12:21:07 +0100 Subject: [PATCH 030/254] Add impl and make work for specialized-traits-with-param.scala --- .../dotty/tools/dotc/ast/TreeTypeMap.scala | 7 +- .../dotty/tools/dotc/core/Definitions.scala | 5 +- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../transform/DesugarSpecializedTraits.scala | 181 +++++++++++++++--- .../dotty/tools/dotc/typer/Synthesizer.scala | 5 + library/src/scala/Specialized.scala | 4 +- tests/pos/specialized-traits-with-param.scala | 8 + 7 files changed, 174 insertions(+), 37 deletions(-) create mode 100644 tests/pos/specialized-traits-with-param.scala diff --git a/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala b/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala index dc8d9c700700..138130887937 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala @@ -98,7 +98,7 @@ class TreeTypeMap( override def transform(tree: Tree)(using Context): Tree = treeMap(tree) match { case impl @ Template(constr, _, self, _) => - val tmap = withMappedSyms(localSyms(impl :: self :: Nil)) + val tmap = withMappedSyms(localSyms(impl :: self :: Nil)) cpy.Template(impl)( constr = tmap.transformSub(constr), parents = impl.parents.mapconserve(transform), @@ -127,14 +127,15 @@ class TreeTypeMap( cpy.Block(blk)(stats1, expr1) case lit @ Literal(Constant(tpe: Type)) => cpy.Literal(lit)(Constant(mapType(tpe))) - case ddef @ DefDef(name, paramss, tpt, _) => + case ddef @ DefDef(name, paramss, tpt, _) => // Why are we not correctly mapping foo's return type? See Reached def def ... val (tmap1, paramss1) = transformAllParamss(paramss) val res = cpy.DefDef(ddef)(name, paramss1, tmap1.transform(tpt), tmap1.transform(ddef.rhs)) res.symbol.setParamssFromDefs(paramss1) res.symbol.transformAnnotations { case ann: BodyAnnotation => ann.derivedAnnotation(transform(ann.tree)) case ann => ann - } + } + // HERE? res case tdef @ LambdaTypeTree(tparams, body) => val (tmap1, tparams1) = transformDefs(tparams) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index aa3ff1ba6d5f..a0f089ffb96e 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -771,8 +771,9 @@ class Definitions { @tu lazy val StringAddClass : ClassSymbol = requiredClass("scala.runtime.StringAdd") @tu lazy val StringAdd_+ : Symbol = StringAddClass.requiredMethod(nme.raw.PLUS) - @tu lazy val SpecializedBound : ClassSymbol = requiredClass("scala.Specialized") - @tu lazy val SpecializedBoundRef : TypeRef = SpecializedBound.typeRef + @tu lazy val SpecializedClass : ClassSymbol = requiredClass("scala.Specialized") + @tu lazy val SpecializedModule: Symbol = SpecializedClass.companionModule + @tu lazy val SpecializedModule_apply: Symbol = SpecializedModule.requiredMethod(nme.apply) @tu lazy val StringContextClass: ClassSymbol = requiredClass("scala.StringContext") @tu lazy val StringContext_s : Symbol = StringContextClass.requiredMethod(nme.s) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 564cec1898d0..97448b2d0c20 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -28,6 +28,7 @@ object StdNames { inline val ANON_FUN = "$anonfun" inline val INLINE_TRAIT_INNER_CLASS_SUFFIX = "$trait" inline val SPECIALIZED_TRAIT_SUFFIX = "$sp$" + inline val SPECIALIZED_TRAIT_IMPL_SUFFIX = "$impl$" inline val SPECIALIZED_TRAIT_TYPE_SEP = "$" inline val REPL_SESSION_LINE = "rs$line$" diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 47a7a0c6aa9c..335d40ea5fea 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -24,6 +24,9 @@ import dotty.tools.dotc.core.StdNames.str.SPECIALIZED_TRAIT_SUFFIX import dotty.tools.dotc.core.Names.Name import tpd._ import scala.collection.mutable +import scala.annotation.unspecialized +import dotty.tools.dotc.typer.Synthesizer +import dotty.tools.dotc.typer.Typer class DesugarSpecializedTraits extends MacroTransform: @@ -45,8 +48,6 @@ class DesugarSpecializedTraits extends MacroTransform: def apply(t: Type) = specialization.specializedTypeParamsToTypeArgumentsMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) // TODO: IF we can do just types we can get rid fo this // Create new trait - val att = AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization) - val v = att.symbol val parents = defn.ObjectType :: AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization).tpe // original trait, specialized :: specialization.traitSymbol.denot.info.parents.filterNot(_ eq defn.ObjectType).map(tm(_)) // parents of the original trait, specialized @@ -78,8 +79,7 @@ class DesugarSpecializedTraits extends MacroTransform: traitSymbol.info = ClassInfo(traitSymbol.owner.thisType, traitSymbol, traitSymbol.info.parents.map(freshTypeVarMap(_)), traitSymbol.info.decls) // TODO: What happens if the creator of the specialized inline trait provides a self type? traitSymbol.entered - private def buildClassTree(specialization: Specialization, generatedTraitSymbol: ClassSymbol)(using Context) = { - val originalTraitSymbol = specialization.traitSymbol + private def buildSpTraitTree(specialization: Specialization, generatedTraitSymbol: ClassSymbol)(using Context) = { val init = newDefaultConstructor(generatedTraitSymbol) // Fix constructor so that it: @@ -93,47 +93,133 @@ class DesugarSpecializedTraits extends MacroTransform: init.info = resultType(init.info) init.info = PolyType.fromParams(init.owner.typeParams, init.info) - assert(originalTraitSymbol.primaryConstructor.rawParamss.length >= 2) // we know we at least have type params and evidences - val evidences = (originalTraitSymbol.primaryConstructor.rawParamss: @unchecked) match { - case List(_, _, evidences) => evidences - case List(_, evidences) => evidences - } + // TODO: Confirm that we don't need to worry about copying the evidence parameters over from the old constructor + // These should be dealt with when we instantiate the original trait as a parent of this one. Otherwise we should be + // able to copy them over, apply the specialization (keeping e.g. Numeric[Int] that arises from this) and + // pruning any that belong to Specialized. + + ClassDef(generatedTraitSymbol, DefDef(init.entered), Nil) + } + + // TODO: Do we want to share some code with the newSpecializedInterfaceTrait and buildSpTraitTree? + // TODO: Standardise a bit so that we either generate the symbols and later the classes or not. + private def buildImplClassTree(specialization: Specialization, generatedTraitSymbol: ClassSymbol)(using Context) = { + // Create new class + val parents = List(defn.ObjectType, + generatedTraitSymbol.typeRef, // new specialized trait + AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization).tpe) // original trait, specialized + + val classSymbol = newNormalizedClassSymbol( + specialization.traitSymbol.owner, + (DesugarSpecializedTraits.newImplementationClassName(specialization)).asTypeName, + Flags.Synthetic, + parents, + NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? + specialization.traitSymbol.privateWithin, + // TODO: Do we need a compUnit info? + ) + + val t = specialization.traitSymbol.primaryConstructor.localReturnType + + val init = newDefaultConstructor(classSymbol) + init.setParamss( + List(specialization.traitSymbol.primaryConstructor.rawParamss(1).map(param => param.copy(info = specialization.constructorParamToArgumentTypeMap(param.info)))) // only the value params + ) + init.info = MethodType(specialization.traitSymbol.primaryConstructor.rawParamss(1).map(_.name.asTermName), + specialization.specialization.map(_.tpe), + classSymbol.typeRef) + + + // Fix constructor so that it: + // 1) Has correct generic type parameters + // 2) Returns the correct type corresponding to those type parameters applied to this trait + // val rt = generatedTraitSymbol.typeRef.appliedTo(generatedTraitSymbol.typeParams.map(_.typeRef)) + // def resultType(tpe: Type): Type = tpe match { + // case mt @ MethodType(paramNames) => mt.derivedLambdaType(paramNames, mt.paramInfos, rt) + // // case pt : PolyType => pt.derivedLambdaType(pt.paramNames, pt.paramInfos, resultType(pt.resType)) + // } + // val z = init.info + // init.info = resultType(init.info) + // init.info = PolyType.fromParams(init.owner.typeParams, init.info) // TODO: Confirm that we don't need to worry about copying the evidence parameters over from the old constructor // These should be dealt with when we instantiate the original trait as a parent of this one. Otherwise we should be // able to copy them over, apply the specialization (keeping e.g. Numeric[Int] that arises from this) and // pruning any that belong to Specialized. - // TODO: Tidy this up and also figure out why we don't do this by default in the ClassDef? - val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. - def apply(t: Type) = specialization.specializedTypeParamsToTypeArgumentsMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) // TODO: IF we can do just types we can get rid fo this - // HACK: This doesn't work for if A is inline for example because in general it misses the type for A - // specialization.traitSymbol.denot.info.paren - // val customParentTrees = - // AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization) // original trait, specialized - // :: specialization.traitSymbol.denot.info.parents.filterNot(_ eq defn.ObjectType).map(a => TypeTree(tm(a))) // parents of the original trait, specialized + // TODO: Clean adn robust - ClassDef(generatedTraitSymbol, DefDef(init.entered), Nil) - } + val field = specialization.traitSymbol.findMember(init.paramSymss.head.head.name, specialization.traitSymbol.typeRef, Flags.EmptyFlags, Flags.EmptyFlags).symbol.copy( + owner = classSymbol, + info = init.paramSymss.head.head.info + ) + + // TODO: probably just copy the whole class symbol to get all the params + + val v = init.paramSymss.head(0).info + + + val classDef = ClassDefWithParents( + classSymbol, + DefDef(init.asTerm.entered), + List( + New(classSymbol.info.parents.head, classSymbol.info.parents.head.classSymbol.primaryConstructor.asTerm, Nil), + New(classSymbol.info.parents(1), classSymbol.info.parents(1).classSymbol.primaryConstructor.asTerm, Nil), + New(parents(2), classSymbol.info.parents(2).classSymbol.primaryConstructor.asTerm, // TODO: Check for other constructors + + List(ref(field)) + // init.paramSymss.head.map(ref(_)) + + ).appliedTo( + TypeApply(ref(defn.SpecializedModule_apply), List(TypeTree(init.paramSymss.head(0).info))) + )), + Nil) + // println("HALLO MATE") + // println(classDef) + (classDef, classSymbol) + } override def transform(tree: Tree)(using Context): Tree = tree match { case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. + println(pkg) val specializedSymbols = generateSpecializedTraitSymbols(pkg) - val generatedTraitStats = specializedSymbols.getSpecializations.map(buildClassTree) + val generatedTraitStats = specializedSymbols.getSpecializations.map(buildSpTraitTree) + /// TODO : Make this proper + val (generatedClassStats, classSymbols) = specializedSymbols.getSpecializationsForImplementation.map(buildImplClassTree).unzip + val classSymbolMap = specializedSymbols.getSpecializations.map(_._1).zip(classSymbols).toMap + + // Use the TreeTypeMap to replace instances (can we do this without accidentally replacing the definitions? I think it should be ok) val typeMap = new TypeMap: - def apply(t: Type) = t match { + def apply(t: Type) = { + // println(t) + t match { case Specialization(spec) => { for (specializedSymbol <- specializedSymbols.get(spec)) - yield AppliedType(specializedSymbol.typeRef, spec.unspecializedTypeArgs.map(_.tpe)) + yield + spec.toTypeWithNewSymbol(specializedSymbol) }.getOrElse(mapOver(t)) case _ => mapOver(t) } + } def treeMap(tree: Tree): Tree = tree match { + // To some extent would need to match this anyway - could we do something to the parent? + // Say this is fine for now/ + case Block(List(TypeDef(anon, Template(_, parentCalls: List[Tree], _, _))), + Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => + parentCalls(1) match { // only allowed to extend Object and our specialized trait + case Apply(Apply(tpe, ctorArgs), _ev) => + val spec = Specialization.unapply(t.tpe).get + val x = classSymbolMap + val y = specializedSymbols.getSpecializations + Typed(Apply(Select(New(ref(classSymbolMap(spec))),ctor), ctorArgs), t) + case _ => tree + } + case Apply(TypeApply(fun@Select(New(tpt), _init), args), ev) if fun.symbol.isConstructor => val spec = Specialization(fun.symbol.owner, args) // Note: We always drop the evidence params when creating these new specialized traits so we know that there are none, but we may need to revisit this if we decide we do want to copy the evidence parameters over @@ -149,22 +235,31 @@ class DesugarSpecializedTraits extends MacroTransform: val treeTypeMap = new TreeTypeMap(typeMap, treeMap) { override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? case dd@DefDef(name, paramss, tpt, preRhs) => + // println(dd) val transformedDef = super.transform(dd) transformedDef.symbol.info = mapType(transformedDef.symbol.info) transformedDef case tree => super.transform(tree) } } - cpy.PackageDef(pkg)(pid, generatedTraitStats ++ stats.map(treeTypeMap(_))) // TODO: Do we also want to apply the map over generatedTraitStats?? + cpy.PackageDef(pkg)(pid, generatedTraitStats ++ generatedClassStats ++ stats.map(treeTypeMap(_))) // TODO: Do we also want to apply the map over generatedTraitStats?? } // TODO: Try with just generating new Foo(100) with no function to pass it to and no other references to Foo. this may not work because we might not // correctly detect it. + // TODO : Is it not better to just delete the Specialized? + private def generateSpecializedTraitSymbols(tree: Tree)(using Context): SpecializedTraitCache = tree.deepFold(SpecializedTraitCache())((foundSpecs, tree) => tree match - case Typed(Apply(Select(New(cl),ctor),List()), t: TypeTree) => - Specialization.unapply(t).foreach( spec => + // case Typed(Apply(Select(New(anon),ctor),List()), t: TypeTree) => + // val z = anon.symbol + // val f = anon.symbol.isAnonymousClass + // foundSpecs + case Typed(Apply(Select(New(anon),ctor),List()), t: TypeTree) if anon.symbol.isAnonymousClass => + val maybeSpec = Specialization.unapply(t.tpe) + + maybeSpec.foreach( spec => if (spec.hasSpecializedParams && !foundSpecs.contains(spec)) { foundSpecs.add(spec, newSpecializedTraitInterfaceTrait(spec)) } @@ -190,10 +285,16 @@ object DesugarSpecializedTraits: // TODO: What happens with this name generation if we have Vec[Vec[T]] for example? We potentially don't have an Ident // TODO: Check what happens here when we have a case where the types being specialized into are user defined instead of primitives or type vars. - private[transform] def newSpecializedTraitName(specialization: Specialization)(using Context) = + private def generateName(specialization: Specialization, suffix: String)(using Context) = specialization.specializedTypeArgs.collect(t => t match { case Ident(tpe) => tpe ++ str.SPECIALIZED_TRAIT_TYPE_SEP - }).fold(specialization.traitSymbol.name ++ str.SPECIALIZED_TRAIT_SUFFIX)((n1, n2) => n1 ++ n2) + }).fold(specialization.traitSymbol.name ++ suffix)((n1, n2) => n1 ++ n2) + + private[transform] def newSpecializedTraitName(specialization: Specialization)(using Context) = + generateName(specialization, str.SPECIALIZED_TRAIT_SUFFIX) + + private[transform] def newImplementationClassName(specialization: Specialization)(using Context) = + generateName(specialization, str.SPECIALIZED_TRAIT_IMPL_SUFFIX) class SpecializedTraitCache: @@ -214,7 +315,7 @@ class SpecializedTraitCache: def flagForImplementation(spec: Specialization) = flaggedForImplementation.add(spec) - def getSpecializationsForImplementation = flaggedForImplementation + def getSpecializationsForImplementation = flaggedForImplementation.toList.map(spec => (spec, specializationMap(spec))) end SpecializedTraitCache @@ -223,7 +324,7 @@ end SpecializedTraitCache class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(using Context): // TODO: Can we get away with List[Type] object SpecializedEvidence { def unapply(tpe: Type)(using Context): Option[Type] = tpe match { - case AppliedType(tycon, List(tpeArg)) if tycon =:= ctx.definitions.SpecializedBoundRef => Some(tpeArg) + case AppliedType(tycon, List(tpeArg)) if tycon =:= ctx.definitions.SpecializedClass.typeRef => Some(tpeArg) case _ => None } } @@ -239,9 +340,20 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(usi val specializedTypeParamsToTypeArgumentsMap: Map[Type, Tree] = paramToArgList.toMap.filter((k, v) => specializedTypeParamsSet(k)) val specialization: List[Tree] = traitSymbol.typeParams.map(_.typeRef).map(specializedTypeParamsToTypeArgumentsMap.applyOrElse(_, TypeTree(_))) // TODO: Don't really like this name - + // val constructorParamToArgumentTypeMap: Map[Type, Type] = traitSymbol.primaryConstructor.typeParams.zip(paramToArgList).filter((constrParam, paramArg) => specializedTypeParamsSet(paramArg._1)).map((constrParam, paramArg) => (constrParam.typeRef, paramArg._1)).toMap + + // TODO: Potentially can get this out of the specialization.specialization directly given we make the same assumption about one primary constructor and param ordering. + def constructorParamToArgumentTypeMap: Map[Type, Type] = + traitSymbol.primaryConstructor.rawParamss.head.map(_.typeRef).zip(typeArguments.map(_.tpe)).toMap + def hasSpecializedParams: Boolean = specializedTypeParams.nonEmpty + def toTypeWithNewSymbol(newSymbol: Symbol) = unspecializedTypeArgs match { + case head :: next => AppliedType(newSymbol.typeRef, unspecializedTypeArgs.map(_.tpe)) + case Nil => newSymbol.typeRef + } + + // Note: We only care about the specialized arguments for equality; a specialization of Vec[A: Specialized, B] with B = Int and one // with B = String can be considered to be the same as they use the same specialized trait // TODO: I don't really like this logic being in Specialization because they are really different @@ -272,6 +384,9 @@ object Specialization: // -> Get the specialized list to apply +// TODO: Fix name generation which doesn't work if the tpye isn't provided explicitly + + // Generate impl instead of generating anonymous classes every time to avoid insane code bloat @@ -290,6 +405,8 @@ object Specialization: // Cache / only generate once instead of multiple times. // Ideally standardise on either specialization or specializationMap +// TODO: Think carefully about use of primaryConstructor and the other appropriateConstructors call or whatever it was. + // Probably (tree)typemap // Would it be better to just copy rather than creating everything from scratch? I think this is right @@ -311,5 +428,9 @@ object Specialization: // Vec[Array[T]: Specialized] <- banned +// TODO: Prune the generated anonymous classes. + + // need to test with explicit evidence / our own custom type classes // TODO: Make sure name encoding is fully qualified - e.g. potential for conflicts if we define our own class Int. + // // TODO: check that we have a single type var only diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index 7bfda4e4b5fa..5311ab938e6c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -673,6 +673,10 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): val synthesizedMirror: SpecialHandler = (formal, span) => orElse(synthesizedProductMirror(formal, span), synthesizedSumMirror(formal, span)) + val synthesizedSpecialized: SpecialHandler = (formal, span) => formal match { // TODO: Is this exhaustive? + case AppliedType(tycon, arg :: Nil) => withNoErrors(TypeApply(ref(defn.SpecializedModule_apply), TypeTree(arg) :: Nil)) + } + private def escapeJavaArray(tp: Type)(using Context): Type = tp match case JavaArrayType(elemTp) => defn.ArrayOf(escapeJavaArray(elemTp)) case _ => tp @@ -800,6 +804,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): defn.OptManifestClass -> synthesizedOptManifest, defn.SingletonClass -> synthesizedSingleton, defn.PreciseClass -> synthesizedPrecise, + defn.SpecializedClass -> synthesizedSpecialized ) def tryAll(formal: Type, span: Span)(using Context): TreeWithErrors = diff --git a/library/src/scala/Specialized.scala b/library/src/scala/Specialized.scala index 9b28b9eb5b14..078a2a3a376e 100644 --- a/library/src/scala/Specialized.scala +++ b/library/src/scala/Specialized.scala @@ -1,5 +1,5 @@ package scala trait Specialized[T] - -given [T]: Specialized[T] with {} +object Specialized: + def apply[T] = new Specialized[T] {} diff --git a/tests/pos/specialized-traits-with-param.scala b/tests/pos/specialized-traits-with-param.scala new file mode 100644 index 000000000000..bbb4a57d0167 --- /dev/null +++ b/tests/pos/specialized-traits-with-param.scala @@ -0,0 +1,8 @@ +inline trait Foo[T: Specialized](x: T): + def foo = x + +def f(b: Foo[Int]) = 37 + b.foo + +@main def main = + val x = new Foo[Int](42) {} + f(x) From d7578a0d847175f7a6f9e00dc48b0925e0c32c85 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 16 Mar 2026 12:32:03 +0100 Subject: [PATCH 031/254] Add a couple more inline trait tests --- tests/pos/inline-trait-extends-non-inline.scala | 6 ++++++ tests/pos/inline-trait-inheritance-same-name.scala | 11 +++++++++++ 2 files changed, 17 insertions(+) create mode 100644 tests/pos/inline-trait-extends-non-inline.scala create mode 100644 tests/pos/inline-trait-inheritance-same-name.scala diff --git a/tests/pos/inline-trait-extends-non-inline.scala b/tests/pos/inline-trait-extends-non-inline.scala new file mode 100644 index 000000000000..f66746b7dfeb --- /dev/null +++ b/tests/pos/inline-trait-extends-non-inline.scala @@ -0,0 +1,6 @@ +trait A: + def foo = "Hello World" + +inline trait B extends A + +trait C extends B diff --git a/tests/pos/inline-trait-inheritance-same-name.scala b/tests/pos/inline-trait-inheritance-same-name.scala new file mode 100644 index 000000000000..b3aa1b2bb7d2 --- /dev/null +++ b/tests/pos/inline-trait-inheritance-same-name.scala @@ -0,0 +1,11 @@ +inline trait A: + def foo = "Hello World" + +inline trait B: + def foo = "Bonjour" + +class C extends A, B + +def main = + val x = C() + println(x.foo) From 8d5dd496ec348e9ce8a339e41bda4cb8e59c8b82 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 17 Mar 2026 12:35:16 +0100 Subject: [PATCH 032/254] Fix inline traits mixin error with indirect inheritance --- .../src/dotty/tools/dotc/transform/Mixin.scala | 8 ++++++++ ...nline-trait-param-shadows-parent-indirect.scala | 14 ++++++++++++++ tests/run/inline-trait-param-shadows-parent.scala | 9 +++++++++ 3 files changed, 31 insertions(+) create mode 100644 tests/run/inline-trait-param-shadows-parent-indirect.scala create mode 100644 tests/run/inline-trait-param-shadows-parent.scala diff --git a/compiler/src/dotty/tools/dotc/transform/Mixin.scala b/compiler/src/dotty/tools/dotc/transform/Mixin.scala index 66605a5ba972..037dda9b07e7 100644 --- a/compiler/src/dotty/tools/dotc/transform/Mixin.scala +++ b/compiler/src/dotty/tools/dotc/transform/Mixin.scala @@ -273,11 +273,19 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase => cls.srcPos) EmptyTree + // See: tests/run/inline-trait-param-shadows-parent.scala and tests/run/inline-trait-param-shadows-parent-indirect.scala + // We need this even though we also have "if mixin.isInlineTrait then return Nil" because getters can come + // from traits that inherit inline traits as well as from inline traits themselves. + def fromInlineTraitInlining(getter: Symbol): Boolean = mixin.parentSyms.exists( + parentSym => parentSym.isInlineTrait && parentSym.info.decls.exists(d => d.name == getter.name) + ) + for getter <- mixin.info.decls.toList if getter.isGetter && !wasOneOf(getter, Deferred) && !getter.isConstExprFinalVal + && !fromInlineTraitInlining(getter) yield if (isInImplementingClass(getter) || getter.name.is(ExpandedName)) { val rhs = diff --git a/tests/run/inline-trait-param-shadows-parent-indirect.scala b/tests/run/inline-trait-param-shadows-parent-indirect.scala new file mode 100644 index 000000000000..b5e20f0e4766 --- /dev/null +++ b/tests/run/inline-trait-param-shadows-parent-indirect.scala @@ -0,0 +1,14 @@ +inline trait A[T](x: T): + def y = x +trait B extends A[Int] +trait D extends A[Int] +trait E extends B +trait F extends D +class C extends E, F +// A[Int](4) + + +object Test: + def main(args: Array[String]): Unit = + val z = new C + println("Testing") diff --git a/tests/run/inline-trait-param-shadows-parent.scala b/tests/run/inline-trait-param-shadows-parent.scala new file mode 100644 index 000000000000..c7aaaeeb8a4c --- /dev/null +++ b/tests/run/inline-trait-param-shadows-parent.scala @@ -0,0 +1,9 @@ +inline trait A[T](x: T): + def y = x +trait B extends A[Int] +class C extends B, A[Int](4) + +object Test: + def main(args: Array[String]): Unit = + val z = new C + println("Testing") From 00e7fa314b33527b1b1b7f89336a738df0997139 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 17 Mar 2026 12:37:28 +0100 Subject: [PATCH 033/254] Add first specialized traits run test! --- tests/run/specialized-trait-maths.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/run/specialized-trait-maths.scala diff --git a/tests/run/specialized-trait-maths.scala b/tests/run/specialized-trait-maths.scala new file mode 100644 index 000000000000..f2b47201c741 --- /dev/null +++ b/tests/run/specialized-trait-maths.scala @@ -0,0 +1,11 @@ +inline trait Foo[T: Specialized](x: T): + def foo = x + +def f(b: Foo[Int]) = 37 + b.foo + + object Test: + def main(args: Array[String]): Unit = { + val x = new Foo[Int](42) {} + val y = f(x) + assert(y == 79) + } From 3c3b63daa91154c536abbf369cbf0bdc9840f0ad Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 17 Mar 2026 12:38:55 +0100 Subject: [PATCH 034/254] Clean up test --- tests/run/inline-trait-param-shadows-parent-indirect.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/run/inline-trait-param-shadows-parent-indirect.scala b/tests/run/inline-trait-param-shadows-parent-indirect.scala index b5e20f0e4766..f5106f0ee039 100644 --- a/tests/run/inline-trait-param-shadows-parent-indirect.scala +++ b/tests/run/inline-trait-param-shadows-parent-indirect.scala @@ -1,12 +1,14 @@ inline trait A[T](x: T): def y = x + trait B extends A[Int] trait D extends A[Int] + +// These two are not really necessary for the case but add to the "indirectness" trait E extends B trait F extends D -class C extends E, F -// A[Int](4) +class C extends E, F object Test: def main(args: Array[String]): Unit = From 169daa5b47be334c18c17338f1fea9accb5e5e00 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 17 Mar 2026 16:40:25 +0100 Subject: [PATCH 035/254] Make name clearer --- compiler/src/dotty/tools/dotc/transform/Mixin.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/Mixin.scala b/compiler/src/dotty/tools/dotc/transform/Mixin.scala index 037dda9b07e7..9c1e0c9ad563 100644 --- a/compiler/src/dotty/tools/dotc/transform/Mixin.scala +++ b/compiler/src/dotty/tools/dotc/transform/Mixin.scala @@ -276,7 +276,7 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase => // See: tests/run/inline-trait-param-shadows-parent.scala and tests/run/inline-trait-param-shadows-parent-indirect.scala // We need this even though we also have "if mixin.isInlineTrait then return Nil" because getters can come // from traits that inherit inline traits as well as from inline traits themselves. - def fromInlineTraitInlining(getter: Symbol): Boolean = mixin.parentSyms.exists( + def isFromInlineTraitInlining(getter: Symbol): Boolean = mixin.parentSyms.exists( parentSym => parentSym.isInlineTrait && parentSym.info.decls.exists(d => d.name == getter.name) ) @@ -285,7 +285,7 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase => if getter.isGetter && !wasOneOf(getter, Deferred) && !getter.isConstExprFinalVal - && !fromInlineTraitInlining(getter) + && !isFromInlineTraitInlining(getter) yield if (isInImplementingClass(getter) || getter.name.is(ExpandedName)) { val rhs = From b13a884a25ce9fc830097bb176f814f1e6466eac Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 17 Mar 2026 16:43:36 +0100 Subject: [PATCH 036/254] Fix incorrect owner bug --- .../transform/DesugarSpecializedTraits.scala | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 335d40ea5fea..d1ee3262270f 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -122,9 +122,13 @@ class DesugarSpecializedTraits extends MacroTransform: val t = specialization.traitSymbol.primaryConstructor.localReturnType val init = newDefaultConstructor(classSymbol) + val valueParams = specialization.traitSymbol.primaryConstructor.rawParamss(1).map(param => param.copy(owner = init, info = specialization.constructorParamToArgumentTypeMap(param.info))) init.setParamss( - List(specialization.traitSymbol.primaryConstructor.rawParamss(1).map(param => param.copy(info = specialization.constructorParamToArgumentTypeMap(param.info)))) // only the value params + List(valueParams) // only the value params ) + val paramAccessors = valueParams.map(_.copy(owner = classSymbol, flags= Flags.LocalParamAccessor)) // , + paramAccessors.foreach(classSymbol.enter(_)) + init.info = MethodType(specialization.traitSymbol.primaryConstructor.rawParamss(1).map(_.name.asTermName), specialization.specialization.map(_.tpe), classSymbol.typeRef) @@ -149,11 +153,6 @@ class DesugarSpecializedTraits extends MacroTransform: // TODO: Clean adn robust - - val field = specialization.traitSymbol.findMember(init.paramSymss.head.head.name, specialization.traitSymbol.typeRef, Flags.EmptyFlags, Flags.EmptyFlags).symbol.copy( - owner = classSymbol, - info = init.paramSymss.head.head.info - ) // TODO: probably just copy the whole class symbol to get all the params @@ -168,19 +167,19 @@ class DesugarSpecializedTraits extends MacroTransform: New(classSymbol.info.parents(1), classSymbol.info.parents(1).classSymbol.primaryConstructor.asTerm, Nil), New(parents(2), classSymbol.info.parents(2).classSymbol.primaryConstructor.asTerm, // TODO: Check for other constructors - List(ref(field)) - // init.paramSymss.head.map(ref(_)) - + paramAccessors.map(ref(_)) ).appliedTo( TypeApply(ref(defn.SpecializedModule_apply), List(TypeTree(init.paramSymss.head(0).info))) )), - Nil) + paramAccessors.map(sym => tpd.ValDef(sym.asTerm)) // .withFlags(Flags.LocalParamAccessor).withType(sym.info) + ) // println("HALLO MATE") // println(classDef) (classDef, classSymbol) } - override def transform(tree: Tree)(using Context): Tree = tree match { + override def transform(tree: Tree)(using Context): Tree = tree + match { case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. println(pkg) val specializedSymbols = generateSpecializedTraitSymbols(pkg) From 4b253a62fd4302bd9a9c71b4c9da10ae7da9c5d7 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 17 Mar 2026 17:28:15 +0100 Subject: [PATCH 037/254] Tidy --- .../transform/DesugarSpecializedTraits.scala | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index d1ee3262270f..df5433bc8cbf 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -173,15 +173,12 @@ class DesugarSpecializedTraits extends MacroTransform: )), paramAccessors.map(sym => tpd.ValDef(sym.asTerm)) // .withFlags(Flags.LocalParamAccessor).withType(sym.info) ) - // println("HALLO MATE") - // println(classDef) (classDef, classSymbol) } override def transform(tree: Tree)(using Context): Tree = tree match { case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. - println(pkg) val specializedSymbols = generateSpecializedTraitSymbols(pkg) val generatedTraitStats = specializedSymbols.getSpecializations.map(buildSpTraitTree) @@ -192,9 +189,7 @@ class DesugarSpecializedTraits extends MacroTransform: // Use the TreeTypeMap to replace instances (can we do this without accidentally replacing the definitions? I think it should be ok) val typeMap = new TypeMap: - def apply(t: Type) = { - // println(t) - t match { + def apply(t: Type) = t match { case Specialization(spec) => { for (specializedSymbol <- specializedSymbols.get(spec)) @@ -203,11 +198,9 @@ class DesugarSpecializedTraits extends MacroTransform: }.getOrElse(mapOver(t)) case _ => mapOver(t) } - } def treeMap(tree: Tree): Tree = tree match { - // To some extent would need to match this anyway - could we do something to the parent? - // Say this is fine for now/ + // Replace (anonymous class version of) new Foo[Int] {} with new Foo$impl$Int.asInstanceOf[Foo$sp$Int] case Block(List(TypeDef(anon, Template(_, parentCalls: List[Tree], _, _))), Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => parentCalls(1) match { // only allowed to extend Object and our specialized trait @@ -219,10 +212,17 @@ class DesugarSpecializedTraits extends MacroTransform: case _ => tree } + // Replace class Bar extends Foo[Int](params) with class Bar extends Foo$sp$Int(params) + // Note: We always drop the evidence params when creating these new specialized traits so we know that there are none, but we may need to revisit this if we decide we do want to copy the evidence parameters over case Apply(TypeApply(fun@Select(New(tpt), _init), args), ev) if fun.symbol.isConstructor => val spec = Specialization(fun.symbol.owner, args) - // Note: We always drop the evidence params when creating these new specialized traits so we know that there are none, but we may need to revisit this if we decide we do want to copy the evidence parameters over - TypeApply(Select(New(treeMap(tpt)), _init), spec.unspecializedTypeArgs) + val tt = specializedSymbols + { + for (specializedSymbol <- specializedSymbols.get(spec)) + yield TypeApply(Select(New(ref(specializedSymbol)), _init), spec.unspecializedTypeArgs) + }.getOrElse(tree) + + // Replace AppliedTypeTree instances in code case Specialization(spec) => { for (specializedSymbol <- specializedSymbols.get(spec)) yield AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? @@ -234,7 +234,6 @@ class DesugarSpecializedTraits extends MacroTransform: val treeTypeMap = new TreeTypeMap(typeMap, treeMap) { override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? case dd@DefDef(name, paramss, tpt, preRhs) => - // println(dd) val transformedDef = super.transform(dd) transformedDef.symbol.info = mapType(transformedDef.symbol.info) transformedDef From 4aa4a5fbcf8056e23b4a4b73f59ab68d7ce33fcd Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 18 Mar 2026 15:54:28 +0100 Subject: [PATCH 038/254] Fix parameter passing and create pathological-context-bounds case --- .../transform/DesugarSpecializedTraits.scala | 143 ++++++++++++------ tests/pos/specialized-traits-basic.scala | 9 -- ...ed-trait-pathological-context-bounds.scala | 14 ++ 3 files changed, 111 insertions(+), 55 deletions(-) delete mode 100644 tests/pos/specialized-traits-basic.scala create mode 100644 tests/run/specialized-trait-pathological-context-bounds.scala diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index df5433bc8cbf..ed070d3aa368 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -27,6 +27,8 @@ import scala.collection.mutable import scala.annotation.unspecialized import dotty.tools.dotc.typer.Synthesizer import dotty.tools.dotc.typer.Typer +import dotty.tools.dotc.core.NameKinds +import dotty.tools.dotc.core.Flags.GivenOrImplicit class DesugarSpecializedTraits extends MacroTransform: @@ -35,6 +37,7 @@ class DesugarSpecializedTraits extends MacroTransform: override def description: String = DesugarSpecializedTraits.description override def changesMembers: Boolean = false override def changesParents: Boolean = true + override def allowsImplicitSearch: Boolean = true override def run(using Context): Unit = try super.run @@ -104,10 +107,13 @@ class DesugarSpecializedTraits extends MacroTransform: // TODO: Do we want to share some code with the newSpecializedInterfaceTrait and buildSpTraitTree? // TODO: Standardise a bit so that we either generate the symbols and later the classes or not. private def buildImplClassTree(specialization: Specialization, generatedTraitSymbol: ClassSymbol)(using Context) = { + // Create new class - val parents = List(defn.ObjectType, - generatedTraitSymbol.typeRef, // new specialized trait - AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization).tpe) // original trait, specialized + val objectParent = defn.ObjectType + val traitSpParent = generatedTraitSymbol.typeRef.appliedTo(specialization.unspecializedTypeArgs.map(_.tpe)) + val originalTraitSpecializedParent = AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.typeArguments).tpe + + val parents = List(objectParent, traitSpParent, originalTraitSpecializedParent) val classSymbol = newNormalizedClassSymbol( specialization.traitSymbol.owner, @@ -119,29 +125,49 @@ class DesugarSpecializedTraits extends MacroTransform: // TODO: Do we need a compUnit info? ) - val t = specialization.traitSymbol.primaryConstructor.localReturnType val init = newDefaultConstructor(classSymbol) - val valueParams = specialization.traitSymbol.primaryConstructor.rawParamss(1).map(param => param.copy(owner = init, info = specialization.constructorParamToArgumentTypeMap(param.info))) - init.setParamss( - List(valueParams) // only the value params - ) - val paramAccessors = valueParams.map(_.copy(owner = classSymbol, flags= Flags.LocalParamAccessor)) // , - paramAccessors.foreach(classSymbol.enter(_)) + + // TODO: Share this with where we copied it from + // def isSyntheticEvidence(sym: Symbol) = + // println(sym.name) + // println(NameKinds.ContextBoundParamName.separator) + // println(sym.name.show.startsWith(NameKinds.ContextBoundParamName.separator)) + // println(sym.flags.isOneOf(GivenOrImplicit)) + // sym.name.show.startsWith(NameKinds.ContextBoundParamName.separator) && sym.flags.isOneOf(Flags.GivenOrImplicit) + + val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. + def apply(t: Type) = specialization.constructorParamToArgumentTypeMap.view.applyOrElse(t, mapOver) // TODO: IF we can do just types we can get rid fo this + + val tm2 = new TypeMap: + def apply(t: Type) = t match { + case Specialization(spec) if spec.traitSymbol eq specialization.traitSymbol => + classSymbol.typeRef + case _ => mapOver(t) + } + + val nonTypeParams = specialization.traitSymbol.primaryConstructor.rawParamss.tail + val valueParams = nonTypeParams.map(_.map(param => param.copy(owner = init, info = tm(param.info)))) // .map(_.filterNot(isSyntheticEvidence) + + init.setParamss(valueParams) + + val paramAccessors = valueParams.map(params => params.map(_.copy(owner = classSymbol, flags= Flags.LocalParamAccessor))) + paramAccessors.foreach(_.foreach(classSymbol.enter(_))) - init.info = MethodType(specialization.traitSymbol.primaryConstructor.rawParamss(1).map(_.name.asTermName), - specialization.specialization.map(_.tpe), - classSymbol.typeRef) + init.info = tm2(specialization.traitSymbol.primaryConstructor.info.appliedTo(specialization.typeArguments.map(_.tpe))) + + // MethodType(valueParams.head.map(_.name.asTermName), // specialization.traitSymbol.primaryConstructor.rawParamss(1).map(_.name.asTermName), // TOOD: Multiple param sets? + // specialization.specialization.map(_.tpe), + // classSymbol.typeRef) + + val v = specialization.traitSymbol.primaryConstructor.info + val w = specialization.traitSymbol.primaryConstructor.info.appliedTo(specialization.typeArguments.map(_.tpe)) + val z = init.info // Fix constructor so that it: // 1) Has correct generic type parameters // 2) Returns the correct type corresponding to those type parameters applied to this trait - // val rt = generatedTraitSymbol.typeRef.appliedTo(generatedTraitSymbol.typeParams.map(_.typeRef)) - // def resultType(tpe: Type): Type = tpe match { - // case mt @ MethodType(paramNames) => mt.derivedLambdaType(paramNames, mt.paramInfos, rt) - // // case pt : PolyType => pt.derivedLambdaType(pt.paramNames, pt.paramInfos, resultType(pt.resType)) - // } // val z = init.info // init.info = resultType(init.info) // init.info = PolyType.fromParams(init.owner.typeParams, init.info) @@ -151,27 +177,43 @@ class DesugarSpecializedTraits extends MacroTransform: // able to copy them over, apply the specialization (keeping e.g. Numeric[Int] that arises from this) and // pruning any that belong to Specialized. + // val synth = Typer(ctx.nestingLevel + 1) - // TODO: Clean adn robust + // val z = specialization.traitSymbol.primaryConstructor + // val y = specialization.traitSymbol.primaryConstructor.paramSymss - // TODO: probably just copy the whole class symbol to get all the params - val v = init.paramSymss.head(0).info + // val v = specialization.traitSymbol.primaryConstructor.rawParamss.last.map(ev => (tm(ev.info), ev.span)._1) + // val w = specialization.traitSymbol.primaryConstructor.rawParamss.last.map(ev => synth.inferImplicitArg(tm(ev.info), ev.span)) + /** new C(args), calling given constructor `constr` of C */ + // def New(tp: Type, constr: TermSymbol, args: List[Tree])(using Context): Apply = { + // val targs = tp.argTypes + // val tycon = tp.typeConstructor + // New(tycon) + // .select(TermRef(tycon, constr)) + // .appliedToTypes(targs) + // .appliedToTermArgs(args) + // } + // TODO: Clean adn robust val classDef = ClassDefWithParents( classSymbol, DefDef(init.asTerm.entered), List( - New(classSymbol.info.parents.head, classSymbol.info.parents.head.classSymbol.primaryConstructor.asTerm, Nil), - New(classSymbol.info.parents(1), classSymbol.info.parents(1).classSymbol.primaryConstructor.asTerm, Nil), - New(parents(2), classSymbol.info.parents(2).classSymbol.primaryConstructor.asTerm, // TODO: Check for other constructors - - paramAccessors.map(ref(_)) - ).appliedTo( - TypeApply(ref(defn.SpecializedModule_apply), List(TypeTree(init.paramSymss.head(0).info))) - )), - paramAccessors.map(sym => tpd.ValDef(sym.asTerm)) // .withFlags(Flags.LocalParamAccessor).withType(sym.info) + New(objectParent, objectParent.classSymbol.primaryConstructor.asTerm, Nil), + New(traitSpParent, traitSpParent.classSymbol.primaryConstructor.asTerm, Nil), + New(originalTraitSpecializedParent.typeConstructor) + .select(TermRef(originalTraitSpecializedParent.typeConstructor, specialization.traitSymbol.primaryConstructor.asTerm)) // TODO: Check for other constructors + .appliedToTypes(originalTraitSpecializedParent.argTypes) + .appliedToArgss(paramAccessors.map(_.map(ref))) + ), + //.appliedTo + // .appliedToArgs( + // specialization.traitSymbol.primaryConstructor.rawParamss.last.map(ev => synth.inferImplicitArg(tm(ev.info), ev.span)) // Can potentially move some of this into the Specialization + // // TypeApply(ref(defn.SpecializedModule_apply), List(TypeTree(init.paramSymss.head(0).info))) + // )), + paramAccessors.flatMap(syms => syms.map(sym => tpd.ValDef(sym.asTerm))) // .withFlags(Flags.LocalParamAccessor).withType(sym.info) ) (classDef, classSymbol) } @@ -193,8 +235,7 @@ class DesugarSpecializedTraits extends MacroTransform: case Specialization(spec) => { for (specializedSymbol <- specializedSymbols.get(spec)) - yield - spec.toTypeWithNewSymbol(specializedSymbol) + yield specializedSymbol.typeRef.appliedTo(spec.unspecializedTypeArgs.map(_.tpe)) }.getOrElse(mapOver(t)) case _ => mapOver(t) } @@ -204,24 +245,24 @@ class DesugarSpecializedTraits extends MacroTransform: case Block(List(TypeDef(anon, Template(_, parentCalls: List[Tree], _, _))), Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => parentCalls(1) match { // only allowed to extend Object and our specialized trait - case Apply(Apply(tpe, ctorArgs), _ev) => + case Apply(Apply(tpe, ctorArgs), ev) => val spec = Specialization.unapply(t.tpe).get val x = classSymbolMap val y = specializedSymbols.getSpecializations - Typed(Apply(Select(New(ref(classSymbolMap(spec))),ctor), ctorArgs), t) + Typed(Apply(Apply(Select(New(ref(classSymbolMap(spec))),ctor), ctorArgs), ev), t) case _ => tree } // Replace class Bar extends Foo[Int](params) with class Bar extends Foo$sp$Int(params) // Note: We always drop the evidence params when creating these new specialized traits so we know that there are none, but we may need to revisit this if we decide we do want to copy the evidence parameters over - case Apply(TypeApply(fun@Select(New(tpt), _init), args), ev) if fun.symbol.isConstructor => + case Apply(TypeApply(fun@Select(New(tpt), init), args), ev) if fun.symbol.isConstructor => val spec = Specialization(fun.symbol.owner, args) - val tt = specializedSymbols - { + val r = { for (specializedSymbol <- specializedSymbols.get(spec)) - yield TypeApply(Select(New(ref(specializedSymbol)), _init), spec.unspecializedTypeArgs) + yield New(ref(specializedSymbol)).select(init).appliedToTypeTrees(spec.unspecializedTypeArgs) }.getOrElse(tree) - + r + // Replace AppliedTypeTree instances in code case Specialization(spec) => { for (specializedSymbol <- specializedSymbols.get(spec)) @@ -240,6 +281,7 @@ class DesugarSpecializedTraits extends MacroTransform: case tree => super.transform(tree) } } + val res = stats.map(treeTypeMap(_)) cpy.PackageDef(pkg)(pid, generatedTraitStats ++ generatedClassStats ++ stats.map(treeTypeMap(_))) // TODO: Do we also want to apply the map over generatedTraitStats?? } @@ -346,12 +388,6 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(usi def hasSpecializedParams: Boolean = specializedTypeParams.nonEmpty - def toTypeWithNewSymbol(newSymbol: Symbol) = unspecializedTypeArgs match { - case head :: next => AppliedType(newSymbol.typeRef, unspecializedTypeArgs.map(_.tpe)) - case Nil => newSymbol.typeRef - } - - // Note: We only care about the specialized arguments for equality; a specialization of Vec[A: Specialized, B] with B = Int and one // with B = String can be considered to be the same as they use the same specialized trait // TODO: I don't really like this logic being in Specialization because they are really different @@ -360,7 +396,8 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(usi obj.isInstanceOf[Specialization] && obj.asInstanceOf[Specialization].traitSymbol == traitSymbol && specializedTypeArgs.zip(obj.asInstanceOf[Specialization].specializedTypeArgs).forall((a1, a2) => a1.tpe =:= a2.tpe) - override def hashCode(): Int = (traitSymbol, specializedTypeArgs.map(_.tpe)).hashCode() + override def hashCode(): Int = + (traitSymbol, specializedTypeArgs.map(_.tpe.widen.dealias.show)).hashCode() // TODO: Consider not using show for this for performance reasons (correctness also?) object Specialization: def unapply(tpt: Tree)(using Context) = tpt match { @@ -432,3 +469,17 @@ object Specialization: // need to test with explicit evidence / our own custom type classes // TODO: Make sure name encoding is fully qualified - e.g. potential for conflicts if we define our own class Int. // // TODO: check that we have a single type var only + +// trait Vec$Sp[S] extends Vec[S, Int, Int, Int, Int] +// inline trait Two[S: Specialized] extends Vec$sp[S] +// does mean that any methods in the original trait lose their specialization - maybe we /should/ make the generated traits inline? +// hmm but we can't do that because we need the methods called on the Vec$Sp trait to be the specialized ones - that is really important. +// Could potentially copy over the inline based on whether Two is inline or not? Needs some thought. + + +// In the case of foo[S](a: Vec[S, Int, Int, Int, Int]) I think we ideally do want this because we should be able to get speed gains by accessing the specialized members + +// Should we allow these? I think they are all fine +// inline trait Two[S: Specialized] extends Vec[S, Int, Int, Int, Int] +// inline trait Two[S] extends Vec[S, Int, Int, Int, Int] // Maybe worth warning? Perhaps behind an extra flag +// trait Two[S] extends Vec[S, Int, Int, Int, Int] \ No newline at end of file diff --git a/tests/pos/specialized-traits-basic.scala b/tests/pos/specialized-traits-basic.scala deleted file mode 100644 index 1de7eb20c091..000000000000 --- a/tests/pos/specialized-traits-basic.scala +++ /dev/null @@ -1,9 +0,0 @@ -trait A[T, R, Q] - -inline trait Vec[T: {Specialized, Numeric}, S <: Object, Q: Numeric, R: Specialized, D: {Numeric, Specialized}] extends A[S, Char, T] - -def foo(v: Vec[Int, String, Int, Int, Int]) = v - -def main() = - val a = new Vec[Int, String, Int, Int, Int]() {} - println("Hello, World!") diff --git a/tests/run/specialized-trait-pathological-context-bounds.scala b/tests/run/specialized-trait-pathological-context-bounds.scala new file mode 100644 index 000000000000..477ba8324d14 --- /dev/null +++ b/tests/run/specialized-trait-pathological-context-bounds.scala @@ -0,0 +1,14 @@ +trait A[T, R, Q] + +inline trait Trait[T: {Specialized, Numeric}, S <: Object, Q: Numeric, R: Specialized, D: {Numeric, Specialized}](a: Int) extends A[S, Char, T] { + def bar = "Buna saira" +} + +def foo(v: Trait[Int, String, Int, Int, Int]) = v + +object Test: + def main(args: Array[String]): Unit = { + val a = new Trait[Int, String, Int, Int, Int](5) {} + assert(foo(a).bar == "Buna saira") + } + From d7282844c335b40e9bd7c56aa6ed76ff825572b1 Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 18 Mar 2026 20:09:39 +0100 Subject: [PATCH 039/254] Fix some bugs --- .../dotty/tools/dotc/inlines/Inliner.scala | 10 +- .../transform/DesugarSpecializedTraits.scala | 191 +++++++----------- 2 files changed, 84 insertions(+), 117 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index c3dfee250dd6..2dd1b0b30d16 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala @@ -735,11 +735,17 @@ class Inliner(val call: tpd.Tree)(using Context): // The translation maps references to `this` and parameters to // corresponding arguments or proxies on the type and term level. It also changes // the owner from the inlined method to the current owner. + + // TODO: This gets around the fact that inline traits doesn't define inlinedMethod correctly but maybe there is a better + // way. + val oldOwners = if (inlinedMethod.exists) then inlinedMethod :: Nil else Nil + val newOwners = if (inlinedMethod.exists) then ctx.owner :: Nil else Nil + val inliner = new InlinerMap( typeMap = inlinerTypeMap, treeMap = inlinerTreeMap, - oldOwners = inlinedMethod :: Nil, - newOwners = ctx.owner :: Nil, + oldOwners = oldOwners, + newOwners = newOwners, substFrom = substFrom, substTo = substTo, inlineCopier = inlineCopier diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index ed070d3aa368..a04638d31ccf 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -37,7 +37,6 @@ class DesugarSpecializedTraits extends MacroTransform: override def description: String = DesugarSpecializedTraits.description override def changesMembers: Boolean = false override def changesParents: Boolean = true - override def allowsImplicitSearch: Boolean = true override def run(using Context): Unit = try super.run @@ -57,7 +56,7 @@ class DesugarSpecializedTraits extends MacroTransform: val traitSymbol = newNormalizedClassSymbol( specialization.traitSymbol.owner, - (DesugarSpecializedTraits.newSpecializedTraitName(specialization)).asTypeName, + DesugarSpecializedTraits.newSpecializedTraitName(specialization), Flags.Synthetic | Flags.Trait, parents, NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? @@ -117,7 +116,7 @@ class DesugarSpecializedTraits extends MacroTransform: val classSymbol = newNormalizedClassSymbol( specialization.traitSymbol.owner, - (DesugarSpecializedTraits.newImplementationClassName(specialization)).asTypeName, + DesugarSpecializedTraits.newImplementationClassName(specialization), Flags.Synthetic, parents, NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? @@ -156,46 +155,6 @@ class DesugarSpecializedTraits extends MacroTransform: init.info = tm2(specialization.traitSymbol.primaryConstructor.info.appliedTo(specialization.typeArguments.map(_.tpe))) - // MethodType(valueParams.head.map(_.name.asTermName), // specialization.traitSymbol.primaryConstructor.rawParamss(1).map(_.name.asTermName), // TOOD: Multiple param sets? - // specialization.specialization.map(_.tpe), - // classSymbol.typeRef) - - - val v = specialization.traitSymbol.primaryConstructor.info - val w = specialization.traitSymbol.primaryConstructor.info.appliedTo(specialization.typeArguments.map(_.tpe)) - val z = init.info - - // Fix constructor so that it: - // 1) Has correct generic type parameters - // 2) Returns the correct type corresponding to those type parameters applied to this trait - // val z = init.info - // init.info = resultType(init.info) - // init.info = PolyType.fromParams(init.owner.typeParams, init.info) - - // TODO: Confirm that we don't need to worry about copying the evidence parameters over from the old constructor - // These should be dealt with when we instantiate the original trait as a parent of this one. Otherwise we should be - // able to copy them over, apply the specialization (keeping e.g. Numeric[Int] that arises from this) and - // pruning any that belong to Specialized. - - // val synth = Typer(ctx.nestingLevel + 1) - - // val z = specialization.traitSymbol.primaryConstructor - // val y = specialization.traitSymbol.primaryConstructor.paramSymss - - - // val v = specialization.traitSymbol.primaryConstructor.rawParamss.last.map(ev => (tm(ev.info), ev.span)._1) - // val w = specialization.traitSymbol.primaryConstructor.rawParamss.last.map(ev => synth.inferImplicitArg(tm(ev.info), ev.span)) - - /** new C(args), calling given constructor `constr` of C */ - // def New(tp: Type, constr: TermSymbol, args: List[Tree])(using Context): Apply = { - // val targs = tp.argTypes - // val tycon = tp.typeConstructor - // New(tycon) - // .select(TermRef(tycon, constr)) - // .appliedToTypes(targs) - // .appliedToTermArgs(args) - // } - // TODO: Clean adn robust val classDef = ClassDefWithParents( classSymbol, @@ -208,82 +167,81 @@ class DesugarSpecializedTraits extends MacroTransform: .appliedToTypes(originalTraitSpecializedParent.argTypes) .appliedToArgss(paramAccessors.map(_.map(ref))) ), - //.appliedTo - // .appliedToArgs( - // specialization.traitSymbol.primaryConstructor.rawParamss.last.map(ev => synth.inferImplicitArg(tm(ev.info), ev.span)) // Can potentially move some of this into the Specialization - // // TypeApply(ref(defn.SpecializedModule_apply), List(TypeTree(init.paramSymss.head(0).info))) - // )), paramAccessors.flatMap(syms => syms.map(sym => tpd.ValDef(sym.asTerm))) // .withFlags(Flags.LocalParamAccessor).withType(sym.info) ) (classDef, classSymbol) } - override def transform(tree: Tree)(using Context): Tree = tree - match { - case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. - val specializedSymbols = generateSpecializedTraitSymbols(pkg) - val generatedTraitStats = specializedSymbols.getSpecializations.map(buildSpTraitTree) - - /// TODO : Make this proper - val (generatedClassStats, classSymbols) = specializedSymbols.getSpecializationsForImplementation.map(buildImplClassTree).unzip - val classSymbolMap = specializedSymbols.getSpecializations.map(_._1).zip(classSymbols).toMap - + override def transform(tree: Tree)(using Context): Tree = + val r = tree match { + case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. + val specializedSymbols = generateSpecializedTraitSymbols(pkg) + + // TODO: Make consistent in terms of when we generate the symbols vs the definitions + val generatedTraitStats = specializedSymbols.getSpecializations.map(buildSpTraitTree) + + val (generatedClassStats, classSymbols) = specializedSymbols.getSpecializationsForImplementation.map(buildImplClassTree).unzip + val implMap = specializedSymbols.getSpecializationsForImplementation.map(_._1).zip(classSymbols).toMap + + + // Use the TreeTypeMap to replace instances (can we do this without accidentally replacing the definitions? I think it should be ok) + val typeMap = new TypeMap: + def apply(t: Type) = t match { + case Specialization(spec) => + { + for (specializedSymbol <- specializedSymbols.get(spec)) + yield specializedSymbol.typeRef.appliedTo(spec.unspecializedTypeArgs.map(_.tpe)) + }.getOrElse(mapOver(t)) + case _ => mapOver(t) + } - // Use the TreeTypeMap to replace instances (can we do this without accidentally replacing the definitions? I think it should be ok) - val typeMap = new TypeMap: - def apply(t: Type) = t match { - case Specialization(spec) => - { + def treeMap(tree: Tree): Tree = tree match { + // Replace (anonymous class version of) new Foo[Int] {} with new Foo$impl$Int.asInstanceOf[Foo$sp$Int] + case Block(List(TypeDef(anon, Template(_, parentCalls: List[Tree], _, _))), + Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => + parentCalls(1) match { // only allowed to extend Object and our specialized trait + case Apply(Apply(tpe, ctorArgs), ev) => + val spec = Specialization.unapply(t.tpe).get + println(spec.traitSymbol) + println(tree) + println(Thread.currentThread.getStackTrace().toList) + val specializedMap = implMap + Typed(Apply(Apply(Select(New(ref(implMap(spec))),ctor), ctorArgs), ev), t) + case _ => tree + } + + // Replace class Bar extends Foo[Int](params) with class Bar extends Foo$sp$Int(params) + // Note: We always drop the evidence params when creating these new specialized traits so we know that there are none, but we may need to revisit this if we decide we do want to copy the evidence parameters over + case Apply(TypeApply(fun@Select(New(tpt), init), args), ev) if fun.symbol.isConstructor => + val spec = Specialization(fun.symbol.owner, args) + val r = { for (specializedSymbol <- specializedSymbols.get(spec)) - yield specializedSymbol.typeRef.appliedTo(spec.unspecializedTypeArgs.map(_.tpe)) - }.getOrElse(mapOver(t)) - case _ => mapOver(t) - } - - def treeMap(tree: Tree): Tree = tree match { - // Replace (anonymous class version of) new Foo[Int] {} with new Foo$impl$Int.asInstanceOf[Foo$sp$Int] - case Block(List(TypeDef(anon, Template(_, parentCalls: List[Tree], _, _))), - Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => - parentCalls(1) match { // only allowed to extend Object and our specialized trait - case Apply(Apply(tpe, ctorArgs), ev) => - val spec = Specialization.unapply(t.tpe).get - val x = classSymbolMap - val y = specializedSymbols.getSpecializations - Typed(Apply(Apply(Select(New(ref(classSymbolMap(spec))),ctor), ctorArgs), ev), t) - case _ => tree - } + yield New(ref(specializedSymbol)).select(init).appliedToTypeTrees(spec.unspecializedTypeArgs) + }.getOrElse(tree) + r - // Replace class Bar extends Foo[Int](params) with class Bar extends Foo$sp$Int(params) - // Note: We always drop the evidence params when creating these new specialized traits so we know that there are none, but we may need to revisit this if we decide we do want to copy the evidence parameters over - case Apply(TypeApply(fun@Select(New(tpt), init), args), ev) if fun.symbol.isConstructor => - val spec = Specialization(fun.symbol.owner, args) - val r = { + // Replace AppliedTypeTree instances in code + case Specialization(spec) => { for (specializedSymbol <- specializedSymbols.get(spec)) - yield New(ref(specializedSymbol)).select(init).appliedToTypeTrees(spec.unspecializedTypeArgs) + yield AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? }.getOrElse(tree) - r - - // Replace AppliedTypeTree instances in code - case Specialization(spec) => { - for (specializedSymbol <- specializedSymbols.get(spec)) - yield AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? - }.getOrElse(tree) - case tree => tree - } - - val treeTypeMap = new TreeTypeMap(typeMap, treeMap) { - override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? - case dd@DefDef(name, paramss, tpt, preRhs) => - val transformedDef = super.transform(dd) - transformedDef.symbol.info = mapType(transformedDef.symbol.info) - transformedDef - case tree => super.transform(tree) + case tree => tree } - } - val res = stats.map(treeTypeMap(_)) - cpy.PackageDef(pkg)(pid, generatedTraitStats ++ generatedClassStats ++ stats.map(treeTypeMap(_))) // TODO: Do we also want to apply the map over generatedTraitStats?? - } + + val treeTypeMap = new TreeTypeMap(typeMap, treeMap) { + override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? + case dd@DefDef(name, paramss, tpt, preRhs) => + val transformedDef = super.transform(dd) + transformedDef.symbol.info = mapType(transformedDef.symbol.info) + transformedDef + case tree => super.transform(tree) + } + } + cpy.PackageDef(pkg)(pid, generatedTraitStats ++ generatedClassStats ++ stats.map(treeTypeMap(_))) // TODO: Do we also want to apply the map over generatedTraitStats?? + } + println(r) + r // TODO: Try with just generating new Foo(100) with no function to pass it to and no other references to Foo. this may not work because we might not // correctly detect it. @@ -313,7 +271,7 @@ class DesugarSpecializedTraits extends MacroTransform: // TODO: In theory since we are going to apply the tree type map anyway we can surely just collect up the specialisations we need and then later generate the new symbols? // I think that's slightly cleaner. - case Specialization(spec) if (spec.hasSpecializedParams && !foundSpecs.contains(spec)) => + case Specialization(spec) if (spec.isSpecialized && !foundSpecs.contains(spec)) => foundSpecs.add(spec, newSpecializedTraitInterfaceTrait(spec)) case _ => foundSpecs ) @@ -325,15 +283,13 @@ object DesugarSpecializedTraits: // TODO: What happens with this name generation if we have Vec[Vec[T]] for example? We potentially don't have an Ident // TODO: Check what happens here when we have a case where the types being specialized into are user defined instead of primitives or type vars. - private def generateName(specialization: Specialization, suffix: String)(using Context) = - specialization.specializedTypeArgs.collect(t => t match { - case Ident(tpe) => tpe ++ str.SPECIALIZED_TRAIT_TYPE_SEP - }).fold(specialization.traitSymbol.name ++ suffix)((n1, n2) => n1 ++ n2) + private def generateName(specialization: Specialization, suffix: String)(using Context) = // TODO: Probably don't use show + specialization.specializedTypeArgs.collect(t => t.tpe.show ++ str.SPECIALIZED_TRAIT_TYPE_SEP).foldLeft((specialization.traitSymbol.name ++ suffix).asTypeName)((n1, n2) => n1 ++ n2) - private[transform] def newSpecializedTraitName(specialization: Specialization)(using Context) = + private[transform] def newSpecializedTraitName(specialization: Specialization)(using Context): TypeName = generateName(specialization, str.SPECIALIZED_TRAIT_SUFFIX) - private[transform] def newImplementationClassName(specialization: Specialization)(using Context) = + private[transform] def newImplementationClassName(specialization: Specialization)(using Context): TypeName = generateName(specialization, str.SPECIALIZED_TRAIT_IMPL_SUFFIX) @@ -388,6 +344,10 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(usi def hasSpecializedParams: Boolean = specializedTypeParams.nonEmpty + // If inline trait Foo[T] has a method taking another Foo[T] there's no point specializing the reference + // since the resulting sp$T$ would be the same as the starting trait. + def isSpecialized: Boolean = hasSpecializedParams && !(typeArguments.zip(traitSymbol.typeParams).forall(_ .tpe =:= _.typeRef)) + // Note: We only care about the specialized arguments for equality; a specialization of Vec[A: Specialized, B] with B = Int and one // with B = String can be considered to be the same as they use the same specialized trait // TODO: I don't really like this logic being in Specialization because they are really different @@ -482,4 +442,5 @@ object Specialization: // Should we allow these? I think they are all fine // inline trait Two[S: Specialized] extends Vec[S, Int, Int, Int, Int] // inline trait Two[S] extends Vec[S, Int, Int, Int, Int] // Maybe worth warning? Perhaps behind an extra flag -// trait Two[S] extends Vec[S, Int, Int, Int, Int] \ No newline at end of file +// trait Two[S] extends Vec[S, Int, Int, Int, Int] +// TODO: We want a self reference case where Vec[T] has some method that takes a Vec[Int] for example. \ No newline at end of file From c13ec24091fff52dc799766de90353342055c193 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 19 Mar 2026 13:32:49 +0100 Subject: [PATCH 040/254] Also delete private members from inline traits in case they are marked inline --- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../dotc/transform/PruneInlineTraits.scala | 25 ++++++++++++++----- ...it-private-nested-inline-must-delete.scala | 5 ++++ 3 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 tests/pos/inline-trait-private-nested-inline-must-delete.scala diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 97448b2d0c20..43e56b6fe387 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -27,6 +27,7 @@ object StdNames { inline val ANON_CLASS = "$anon" inline val ANON_FUN = "$anonfun" inline val INLINE_TRAIT_INNER_CLASS_SUFFIX = "$trait" + inline val INLINE_TRAIT_ERASED_PRIVATE_SUFFIX = "$inline_trait_erased_private" inline val SPECIALIZED_TRAIT_SUFFIX = "$sp$" inline val SPECIALIZED_TRAIT_IMPL_SUFFIX = "$impl$" inline val SPECIALIZED_TRAIT_TYPE_SEP = "$" diff --git a/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala index 90083378e57d..7d4b271ac05b 100644 --- a/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala @@ -9,6 +9,7 @@ import SymDenotations._ import Symbols._ import MegaPhase.MiniPhase import ast.tpd +import dotty.tools.dotc.core.StdNames.str class PruneInlineTraits extends MiniPhase with SymTransformer { thisTransform => import tpd._ @@ -19,16 +20,17 @@ class PruneInlineTraits extends MiniPhase with SymTransformer { thisTransform => override def description: String = PruneInlineTraits.description override def transformSym(sym: SymDenotation)(using Context): SymDenotation = - if isEraseable(sym) then sym.copySymDenotation(initFlags = sym.flags | Deferred) + if isDeletable(sym) then sym.copySymDenotation(initFlags = (sym.flags ^ Private) | Deferred | Protected, name = sym.name ++ str.INLINE_TRAIT_ERASED_PRIVATE_SUFFIX) + else if isEraseable(sym) then sym.copySymDenotation(initFlags = sym.flags | Deferred) else if sym.isInlineTrait then sym.copySymDenotation(initFlags = sym.flags | PureInterface | NoInits) else sym - - override def transformValDef(tree: ValDef)(using Context): ValDef = - if isEraseable(tree.symbol) then cpy.ValDef(tree)(rhs = EmptyTree) + + override def transformValDef(tree: ValDef)(using Context): Tree = + if isDeletable(tree.symbol) || isEraseable(tree.symbol) then cpy.ValDef(tree)(rhs = EmptyTree) else tree - override def transformDefDef(tree: DefDef)(using Context): DefDef = - if isEraseable(tree.symbol) then cpy.DefDef(tree)(rhs = EmptyTree) + override def transformDefDef(tree: DefDef)(using Context): Tree = + if isDeletable(tree.symbol) || isEraseable(tree.symbol) then cpy.DefDef(tree)(rhs = EmptyTree) else tree private def isEraseable(sym: SymDenotation)(using Context): Boolean = @@ -39,6 +41,17 @@ class PruneInlineTraits extends MiniPhase with SymTransformer { thisTransform => && !sym.is(Private) && !sym.isLocalDummy && sym.owner.isInlineTrait + + // We also must erase private symbols because they can contain problematic defintions such + // as inline functions which need to be inlined (see tests/pos/inline-trait-private-nested-inline-must-delete.scala) + // It's hard to delete the actual symbol and we can't leave it private and deferred/with no definition + // Thus we settle for making it protected, deferred (no definition) and giving it a mangled name/ + private def isDeletable(sym: SymDenotation)(using Context): Boolean = + !sym.isType + && sym.is(Private) + && sym.owner.isInlineTrait + && !sym.is(Param) + && !sym.is(ParamAccessor) } object PruneInlineTraits { diff --git a/tests/pos/inline-trait-private-nested-inline-must-delete.scala b/tests/pos/inline-trait-private-nested-inline-must-delete.scala new file mode 100644 index 000000000000..5528543aee4d --- /dev/null +++ b/tests/pos/inline-trait-private-nested-inline-must-delete.scala @@ -0,0 +1,5 @@ +inline trait A[T: Numeric]: + private val num = summon[Numeric[T]] + private val x = 1 + +class B extends A[Float] From ae18157341cad9a96e4418a83ab00fe6039d0845 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 22 Mar 2026 13:48:05 +0100 Subject: [PATCH 041/254] Add dot product run test --- ...specialized-trait-vector-dot-product.scala | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/run/specialized-trait-vector-dot-product.scala diff --git a/tests/run/specialized-trait-vector-dot-product.scala b/tests/run/specialized-trait-vector-dot-product.scala new file mode 100644 index 000000000000..935f82210106 --- /dev/null +++ b/tests/run/specialized-trait-vector-dot-product.scala @@ -0,0 +1,22 @@ +inline trait Vec[T: {Specialized, Numeric}](elems: Array[T]): + private val num = summon[Numeric[T]] + private val x = 1 + + def length = elems.length + + def apply(i: Int): T = elems(i) + + def scalarProduct(other: Vec[T]): T = + require(this.length == other.length) + var result = num.fromInt(0) + for i <- 0 until length do + result = num.plus(result, num.times(this(i), other(i))) + result + +object Test: + def main(args: Array[String]) = + val x = new Vec[Int](Array(1, 2, 3, 4, 5)) {} + val y = new Vec[Int](Array(3, 4, 5, 6, 7)) {} + val z = x.scalarProduct(y) + assert(z == 85) + \ No newline at end of file From 4a7e3bd4658a9b99c06e59844ec1024a2204db46 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 22 Mar 2026 13:53:10 +0100 Subject: [PATCH 042/254] Add specialized-traits-inheritance.scala --- tests/pos/specialized-traits-inheritance.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/pos/specialized-traits-inheritance.scala diff --git a/tests/pos/specialized-traits-inheritance.scala b/tests/pos/specialized-traits-inheritance.scala new file mode 100644 index 000000000000..75de30f0dc31 --- /dev/null +++ b/tests/pos/specialized-traits-inheritance.scala @@ -0,0 +1,10 @@ +inline trait Foo[T: Specialized](x: T): + def foo = x + +class Bar extends Foo(10) + +def f(b: Foo[Int]) = println(s"We found the following value of foo ${b.foo}") + +@main def main = + val x = Bar() + f(x) From f038a321cdc8a1549f4842c541f32eba233c78c9 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 22 Mar 2026 13:58:17 +0100 Subject: [PATCH 043/254] Add inline-trait-private-evidence test --- tests/pos/inline-trait-private-evidence.scala | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/pos/inline-trait-private-evidence.scala diff --git a/tests/pos/inline-trait-private-evidence.scala b/tests/pos/inline-trait-private-evidence.scala new file mode 100644 index 000000000000..8cd7a5112a17 --- /dev/null +++ b/tests/pos/inline-trait-private-evidence.scala @@ -0,0 +1,13 @@ +/* + This is a special case for inline traits because the summon method is defined inline. We don't evaluate it until after + we've inlined it into child traits, but this means it can hang around in the original inline trait, which is a problem because + it throws a compiler error "method should have been inlined but was not". We need to make sure we prune it out, which initially + was missing for private fields (since it wasn't clear that this was necessary, but it is). +*/ + +inline trait A[T: Numeric]: + private val v = summon[Numeric[T]] + +inline trait B[T: Numeric] extends A[T] + +class C extends B[Int], A[Int] From 4f39f0ab4e9b43f458e47d55eb32c22dab25f6ae Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 22 Mar 2026 14:02:16 +0100 Subject: [PATCH 044/254] Uncontroversial bug fixes --- .../dotc/transform/DesugarSpecializedTraits.scala | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index a04638d31ccf..d69af8e82f23 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -126,14 +126,6 @@ class DesugarSpecializedTraits extends MacroTransform: val init = newDefaultConstructor(classSymbol) - - // TODO: Share this with where we copied it from - // def isSyntheticEvidence(sym: Symbol) = - // println(sym.name) - // println(NameKinds.ContextBoundParamName.separator) - // println(sym.name.show.startsWith(NameKinds.ContextBoundParamName.separator)) - // println(sym.flags.isOneOf(GivenOrImplicit)) - // sym.name.show.startsWith(NameKinds.ContextBoundParamName.separator) && sym.flags.isOneOf(Flags.GivenOrImplicit) val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. def apply(t: Type) = specialization.constructorParamToArgumentTypeMap.view.applyOrElse(t, mapOver) // TODO: IF we can do just types we can get rid fo this @@ -360,12 +352,13 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(usi (traitSymbol, specializedTypeArgs.map(_.tpe.widen.dealias.show)).hashCode() // TODO: Consider not using show for this for performance reasons (correctness also?) object Specialization: - def unapply(tpt: Tree)(using Context) = tpt match { + def unapply(tpt: Tree)(using Context): Option[Specialization] = tpt match { case AppliedTypeTree(specializedTrait: Ident, concreteTypeTrees: List[Tree]) => Some(Specialization(specializedTrait.denot.symbol, concreteTypeTrees)) + case t: TypeTree => Specialization.unapply(t.tpe) case _ => None } - def unapply(tpe: Type)(using Context) = tpe match { + def unapply(tpe: Type)(using Context): Option[Specialization] = tpe match { case AppliedType(tycon: Type, args: List[Type]) => Some(Specialization(tycon.typeSymbol, args.map(TypeTree(_)))) case _ => None } From 94dee859b2d3c8b0885876d5aeefbda0f53b4d11 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 22 Mar 2026 14:09:04 +0100 Subject: [PATCH 045/254] Do transform nested inlines --- compiler/src/dotty/tools/dotc/inlines/Inliner.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index 2dd1b0b30d16..19a68fdbbd11 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala @@ -154,7 +154,9 @@ object Inliner: if tree.inlinedFromOuterScope then tree.expansion match case expansion: TypeTree => expansion - case _ => tree + // TODO: Check if this is a problem; previously tree. We do this because of Inlined(Inlined(EmptyTree, Select)) blocks conversion of the Select + // and this causes a problem when we have inline methods inlined into inline traits (e.g. summon in tests/run/specialized-trait-vector-dot-product.scala) + case _ => super.transformInlined(tree) else super.transformInlined(tree) end InlinerMap @@ -829,7 +831,6 @@ class Inliner(val call: tpd.Tree)(using Context): if (inlinedMethod == defn.Compiletime_error) issueError() addInlinedTrees(treeSize(finalExpansion)) - (finalBindings, finalExpansion) } end inlined From e5ff7297fa975ba0220be2f961f6f2045b436b5e Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 22 Mar 2026 14:10:56 +0100 Subject: [PATCH 046/254] Allow inlining into inline traits (we prune out the definitions afterwards) --- compiler/src/dotty/tools/dotc/inlines/Inlines.scala | 5 +++-- .../dotty/tools/dotc/transform/SpecializeInlineTraits.scala | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index ed64f0723cc5..10faa6e4f980 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -88,13 +88,14 @@ object Inlines: ) && !ctx.typer.hasInliningErrors && !ctx.base.stopInlining - && !ctx.owner.ownersIterator.exists(_.isInlineTrait) + // && !ctx.owner.ownersIterator.exists(_.isInlineTrait) tree match case Block(_, expr) => needsInlining(expr) case tdef @ TypeDef(_, impl: Template) => - !tdef.symbol.isInlineTrait && impl.parents.map(symbolFromParent).exists(_.isInlineTrait) && isInlineableInCtx + // !tdef.symbol.isInlineTrait && + impl.parents.map(symbolFromParent).exists(_.isInlineTrait) && isInlineableInCtx case _ => def isUnapplyExpressionWithDummy: Boolean = // The first step of typing an `unapply` consists in typing the call diff --git a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala index e1d2ea20cc69..3092349e19b8 100644 --- a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala @@ -40,7 +40,8 @@ class SpecializeInlineTraits extends MacroTransform, SymTransformer { override def newTransformer(using Context): Transformer = new Transformer { override def transform(tree: Tree)(using Context): Tree = tree match { case tree: TypeDef if tree.symbol.isInlineTrait => - transformInlineTrait(tree) + val tree1 = transformInlineTrait(tree) + if Inlines.needsInlining(tree1) then Inlines.inlineParentInlineTraits(tree1) else tree1 case tree: TypeDef if Inlines.needsInlining(tree) => val tree1 = super.transform(tree).asInstanceOf[TypeDef] if tree1.tpe.isError then tree1 From 931b61e48879927b7396cfc952244ff7a8ba97e0 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 22 Mar 2026 14:11:54 +0100 Subject: [PATCH 047/254] Specialize member types after inlining (especially num evidence params) --- compiler/src/dotty/tools/dotc/inlines/Inlines.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 10faa6e4f980..a2a1a16d2e1c 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -846,6 +846,9 @@ object Inlines: paramAccessorsMapper .getParamAccessorRhs(vdef.symbol.owner, vdef.symbol.name) .getOrElse(inlinedRhs(vdef, inlinedSym)) + // TODO: We might only need to do this to evidence params but tbh I can't see much harm in applying it when we want to? + if (rhs.tpe.exists) + inlinedSym.info = rhs.tpe tpd.ValDef(inlinedSym.asTerm, rhs).withSpan(parent.span) private def inlinedDefDef(ddef: DefDef, inlinedSym: Symbol)(using Context): DefDef = From 98b7dc6ab77e76bbc0cb715e083f82f44412bad8 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 22 Mar 2026 14:14:07 +0100 Subject: [PATCH 048/254] Add some important TODO comments --- .../dotc/transform/DesugarSpecializedTraits.scala | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index d69af8e82f23..b75ada4dc2ff 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -436,4 +436,15 @@ object Specialization: // inline trait Two[S: Specialized] extends Vec[S, Int, Int, Int, Int] // inline trait Two[S] extends Vec[S, Int, Int, Int, Int] // Maybe worth warning? Perhaps behind an extra flag // trait Two[S] extends Vec[S, Int, Int, Int, Int] -// TODO: We want a self reference case where Vec[T] has some method that takes a Vec[Int] for example. \ No newline at end of file +// TODO: We want a self reference case where Vec[T] has some method that takes a Vec[Int] for example. +// TODO: Fix broken "inline" tests +// TODO: Only specialize if there is some material increase in specialization - I think only if at least one new parameter gets fully specialized +// Maybe it is better to not allow partial specializations -- we can think about that. +// TODO: Need to add the rule that we need to directly extend inline traits to pass parameters in the same way as for normal traits +// See inline-trait-param-no-shadow.scala + +// could ban normal traits from mixing in inline traits on the basis that +// it's a very unlikely usecase and would fix the mixin problem - although +// we could just try and fix the mixin problem properly. + +// TODO: Create a proper benchmark \ No newline at end of file From ab075b2c583f3b78bb47533fa2d4c4886d96c615 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 22 Mar 2026 14:14:16 +0100 Subject: [PATCH 049/254] Make generated traits inline --- .../dotty/tools/dotc/transform/DesugarSpecializedTraits.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index b75ada4dc2ff..ebd9594fb555 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -57,7 +57,7 @@ class DesugarSpecializedTraits extends MacroTransform: val traitSymbol = newNormalizedClassSymbol( specialization.traitSymbol.owner, DesugarSpecializedTraits.newSpecializedTraitName(specialization), - Flags.Synthetic | Flags.Trait, + Flags.Synthetic | Flags.Trait | Flags.Inline, parents, NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? specialization.traitSymbol.privateWithin, From c4b1e8a3f5cc015434682724373cf91349b333d0 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 22 Mar 2026 14:43:33 +0100 Subject: [PATCH 050/254] Add potential trait test --- tests/run/inline-trait-param-no-shadow.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/run/inline-trait-param-no-shadow.scala diff --git a/tests/run/inline-trait-param-no-shadow.scala b/tests/run/inline-trait-param-no-shadow.scala new file mode 100644 index 000000000000..7f469b831e15 --- /dev/null +++ b/tests/run/inline-trait-param-no-shadow.scala @@ -0,0 +1,10 @@ +// TODO: Decide if we want to allow this or not (might ban trait extends inline trait pattern) + +inline trait A[T](x: T): + val y = x +trait B extends A[Int] +class C extends B + +object Test: + def main(args: Array[String]): Unit = + val z = new C From ccc709ab76a0ef26266adb564321e1c39a5b1e51 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 22 Mar 2026 14:45:42 +0100 Subject: [PATCH 051/254] Rename specialized-trait-vector-example to specialized-trait-collections-example --- ...a => specialized-trait-collections-example.scala} | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) rename tests/pos/{specialized-trait-vector-example.scala => specialized-trait-collections-example.scala} (73%) diff --git a/tests/pos/specialized-trait-vector-example.scala b/tests/pos/specialized-trait-collections-example.scala similarity index 73% rename from tests/pos/specialized-trait-vector-example.scala rename to tests/pos/specialized-trait-collections-example.scala index 3a315481b3aa..de41d305a88b 100644 --- a/tests/pos/specialized-trait-vector-example.scala +++ b/tests/pos/specialized-trait-collections-example.scala @@ -8,20 +8,17 @@ inline trait ArrayIterator[T: Specialized](elems: Array[T]) extends Iterator[T]: def hasNext: Boolean = current < elems.length def next(): T = try elems(current) finally current += 1 - // We should generate these: -// inline trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator[Int] +// trait Iteratorsp$Int extends Iterator[Int] +// trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator[Int] // class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator[Int](elems) -// Inline traits does the magic of actually inlining the code and specialising from T to Int in that step. - // They do this: def foo(x: ArrayIterator[Int]): Int = x.next() -// We convert this to: +// We should convert this to: // def foo(x: ArrayIterator$sp$Int): Int = x.next() -// As long as we generate this (i.e. "do the special erasure") before we run inline traits we should be fine because then the reference will be replaced. - +// Check that the call to next() should be a specialized call and not have boxing - can compare to without specialized to see the impact. // They do this: // class MyClassA @@ -40,3 +37,4 @@ def foo(x: ArrayIterator[Int]): Int = x.next() // // We convert this to: // val ai = new ArrayIterator$impl$Int(xs) {} // println(ai.next()) +100 \ No newline at end of file From 1fc3b8e5b68d4aab36651a217361fe4154f5692f Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 22 Mar 2026 14:46:53 +0100 Subject: [PATCH 052/254] Working up to benchmark but ugly --- compiler/src/dotty/tools/dotc/Compiler.scala | 3 + .../transform/DesugarSpecializedTraits.scala | 65 ++++-- .../dotty/tools/dotc/transform/Mixin.scala | 32 ++- .../tools/dotc/transform/TreeChecker.scala | 10 +- .../inline-trait-specialized-desugar.scala | 40 +++- .../specialized-traits-unspecialized.scala | 187 ++++++++++++++++++ tests/run/benchmark-jmh-compatible.scala | 89 +++++++++ tests/run/benchmark-manual.scala | 99 ++++++++++ 8 files changed, 502 insertions(+), 23 deletions(-) create mode 100644 tests/pos/specialized-traits-unspecialized.scala create mode 100644 tests/run/benchmark-jmh-compatible.scala create mode 100644 tests/run/benchmark-manual.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 9d7b63dd64e2..9ecc2f83de1f 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -51,6 +51,9 @@ class Compiler { protected def picklerPhases: List[List[Phase]] = List(new Pickler) :: // Generate TASTY info List(new sbt.ExtractAPI) :: // Sends a representation of the API of classes to sbt via callbacks + //TODO: Don't try this at home obviously + List(new DesugarSpecializedTraits) :: // Processes the Specialized annotation + List(new SpecializeInlineTraits) :: // Inline the code of inline traits into their children List(new DesugarSpecializedTraits) :: // Processes the Specialized annotation List(new SpecializeInlineTraits) :: // Inline the code of inline traits into their children List(new ReplaceInlinedTraitSymbols) :: // Replace symbols referring to inline trait members with resulting inlined member symbols diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index ebd9594fb555..364b1ce00b85 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -29,6 +29,7 @@ import dotty.tools.dotc.typer.Synthesizer import dotty.tools.dotc.typer.Typer import dotty.tools.dotc.core.NameKinds import dotty.tools.dotc.core.Flags.GivenOrImplicit +import dotty.tools.dotc.core.NameKinds.ContextBoundParamName class DesugarSpecializedTraits extends MacroTransform: @@ -37,6 +38,7 @@ class DesugarSpecializedTraits extends MacroTransform: override def description: String = DesugarSpecializedTraits.description override def changesMembers: Boolean = false override def changesParents: Boolean = true + override def allowsImplicitSearch: Boolean = true override def run(using Context): Unit = try super.run @@ -126,7 +128,7 @@ class DesugarSpecializedTraits extends MacroTransform: val init = newDefaultConstructor(classSymbol) - + val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. def apply(t: Type) = specialization.constructorParamToArgumentTypeMap.view.applyOrElse(t, mapOver) // TODO: IF we can do just types we can get rid fo this @@ -142,10 +144,23 @@ class DesugarSpecializedTraits extends MacroTransform: init.setParamss(valueParams) - val paramAccessors = valueParams.map(params => params.map(_.copy(owner = classSymbol, flags= Flags.LocalParamAccessor))) - paramAccessors.foreach(_.foreach(classSymbol.enter(_))) + val paramAccessorss = valueParams.map(params => params.map(_.copy(owner = classSymbol, flags= Flags.LocalParamAccessor))) + paramAccessorss.foreach(_.foreach(classSymbol.enter(_))) init.info = tm2(specialization.traitSymbol.primaryConstructor.info.appliedTo(specialization.typeArguments.map(_.tpe))) + + val typer = Typer(ctx.nestingLevel + 1) // TODO: actually get these from the user. + + val newParamss = + specialization.traitSymbol.primaryConstructor.paramSymss.tail.zip(paramAccessorss.map(_.map(ref))) // skip the type params + .map((paramSyms, paramAccessors) => + paramSyms.zip(paramAccessors).map( + (paramSym, accessor) => + if paramSym.name.asTermName.is(ContextBoundParamName) + then typer.implicitArgTree(tm(paramSym.info), paramSym.span) // TODO: Fix spans throughout + else accessor + ) + ) // TODO: Clean adn robust val classDef = ClassDefWithParents( @@ -157,21 +172,38 @@ class DesugarSpecializedTraits extends MacroTransform: New(originalTraitSpecializedParent.typeConstructor) .select(TermRef(originalTraitSpecializedParent.typeConstructor, specialization.traitSymbol.primaryConstructor.asTerm)) // TODO: Check for other constructors .appliedToTypes(originalTraitSpecializedParent.argTypes) - .appliedToArgss(paramAccessors.map(_.map(ref))) + // .appliedToArgss(paramAccessors.map(_.map(ref))) + .appliedToArgss(newParamss) + + // TODO: What about potential custom typeclass instances? How do we balance that with generating another version of the class every time? Probably just generate the basic version and then let them apply their own version want (based on some kind of hashing). Then we generate a whole new impl class / or anon class which is still specialised to their instances that they provided, at the time that we see it? + // To be honest if our assumption is that we aren't very often going to do anything weird we can just always generate the class at the point of use, with the evidences specialized (but only if we don't ahve that one already - i.e. effectively consider the evidences as part of the name) ), - paramAccessors.flatMap(syms => syms.map(sym => tpd.ValDef(sym.asTerm))) // .withFlags(Flags.LocalParamAccessor).withType(sym.info) + // Put into body of class + paramAccessorss.flatMap(syms => syms.map(sym => tpd.ValDef(sym.asTerm))) // .withFlags(Flags.LocalParamAccessor).withType(sym.info) ) (classDef, classSymbol) } - override def transform(tree: Tree)(using Context): Tree = - val r = tree match { + override def transform(tree: Tree)(using Context): Tree = tree + match { case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. - val specializedSymbols = generateSpecializedTraitSymbols(pkg) + // HACK/TODO: Remove + // In the future we want to cache these on the class path + val existing = pkg.stats.flatMap({ + case t@TypeDef(name, rhs) if name.toString().contains("$sp$") => Some((name.toTypeName.toString, t.symbol.asClass)) + case _ => None + }).toMap + + val specializedSymbols = generateSpecializedTraitSymbols(pkg, existing) + // TODO: Make consistent in terms of when we generate the symbols vs the definitions - val generatedTraitStats = specializedSymbols.getSpecializations.map(buildSpTraitTree) - + val generatedTraitStats = + if (specializedSymbols.getSpecializations.size != existing.size) // TODO: bin + specializedSymbols.getSpecializations.map(buildSpTraitTree) + else + List() + val (generatedClassStats, classSymbols) = specializedSymbols.getSpecializationsForImplementation.map(buildImplClassTree).unzip val implMap = specializedSymbols.getSpecializationsForImplementation.map(_._1).zip(classSymbols).toMap @@ -194,9 +226,6 @@ class DesugarSpecializedTraits extends MacroTransform: parentCalls(1) match { // only allowed to extend Object and our specialized trait case Apply(Apply(tpe, ctorArgs), ev) => val spec = Specialization.unapply(t.tpe).get - println(spec.traitSymbol) - println(tree) - println(Thread.currentThread.getStackTrace().toList) val specializedMap = implMap Typed(Apply(Apply(Select(New(ref(implMap(spec))),ctor), ctorArgs), ev), t) case _ => tree @@ -227,20 +256,20 @@ class DesugarSpecializedTraits extends MacroTransform: val transformedDef = super.transform(dd) transformedDef.symbol.info = mapType(transformedDef.symbol.info) transformedDef + case impl@Template(constr, preParentsOrDerived, self, _) => // TODO/HACK: Remove with existing + cpy.Template(impl)(body = impl.body.map(super.transform(_))) case tree => super.transform(tree) } } cpy.PackageDef(pkg)(pid, generatedTraitStats ++ generatedClassStats ++ stats.map(treeTypeMap(_))) // TODO: Do we also want to apply the map over generatedTraitStats?? } - println(r) - r // TODO: Try with just generating new Foo(100) with no function to pass it to and no other references to Foo. this may not work because we might not // correctly detect it. // TODO : Is it not better to just delete the Specialized? - private def generateSpecializedTraitSymbols(tree: Tree)(using Context): SpecializedTraitCache = + private def generateSpecializedTraitSymbols(tree: Tree, existing: Map[String, ClassSymbol])(using Context): SpecializedTraitCache = // HACK/TODO: Remove existing tree.deepFold(SpecializedTraitCache())((foundSpecs, tree) => tree match // case Typed(Apply(Select(New(anon),ctor),List()), t: TypeTree) => // val z = anon.symbol @@ -264,7 +293,9 @@ class DesugarSpecializedTraits extends MacroTransform: // TODO: In theory since we are going to apply the tree type map anyway we can surely just collect up the specialisations we need and then later generate the new symbols? // I think that's slightly cleaner. case Specialization(spec) if (spec.isSpecialized && !foundSpecs.contains(spec)) => - foundSpecs.add(spec, newSpecializedTraitInterfaceTrait(spec)) + if (existing.contains(DesugarSpecializedTraits.newSpecializedTraitName(spec).toString)) + then foundSpecs.add(spec, existing(DesugarSpecializedTraits.newSpecializedTraitName(spec).toString)) // TODO: Bin this obviosuly + else foundSpecs.add(spec, newSpecializedTraitInterfaceTrait(spec)) case _ => foundSpecs ) } diff --git a/compiler/src/dotty/tools/dotc/transform/Mixin.scala b/compiler/src/dotty/tools/dotc/transform/Mixin.scala index 9c1e0c9ad563..9707f1ce199d 100644 --- a/compiler/src/dotty/tools/dotc/transform/Mixin.scala +++ b/compiler/src/dotty/tools/dotc/transform/Mixin.scala @@ -142,6 +142,24 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase => override def transformSym(sym: SymDenotation)(using Context): SymDenotation = def ownerIsTrait: Boolean = was(sym.owner, Trait, butNot = JavaDefined) + // // See: tests/run/inline-trait-param-shadows-parent.scala and tests/run/inline-trait-param-shadows-parent-indirect.scala + // // We need this even though we also have "if mixin.isInlineTrait then return Nil" because getters can come + // // from traits that inherit inline traits as well as from inline traits themselves. + // // TODO: Do we not only want to do this if we get the symbol from somewhere else in the child trait as well? + // // i.e. need to know that the trait is mixed in either directly or indirectly as another parent as well. + // // Maybe just say that ordinary traits cannot inherit from inline traits? That would probably fix it. + // def isFromInlineTraitInlining(getter: Symbol): Boolean = + // val y = mixin.parentSyms + + // val x = mixin.parentSyms.map( + // parentSym => parentSym.info.decls//.exists(d => d.name == getter.name || getter.name == d.name.expandedName(parentSym)) + // ) + // mixin.parentSyms.exists( + // parentSym => parentSym.isInlineTrait && parentSym.info.decls.exists(d => { + // d.name == getter.name || getter.name ++ str.INLINE_TRAIT_ERASED_PRIVATE_SUFFIX == d.name.expandedName(parentSym)}) + // ) + + if (sym.is(Accessor, butNot = Deferred) && ownerIsTrait) { val sym1 = if (sym.is(Lazy) || sym.symbol.isConstExprFinalVal) sym @@ -276,8 +294,18 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase => // See: tests/run/inline-trait-param-shadows-parent.scala and tests/run/inline-trait-param-shadows-parent-indirect.scala // We need this even though we also have "if mixin.isInlineTrait then return Nil" because getters can come // from traits that inherit inline traits as well as from inline traits themselves. - def isFromInlineTraitInlining(getter: Symbol): Boolean = mixin.parentSyms.exists( - parentSym => parentSym.isInlineTrait && parentSym.info.decls.exists(d => d.name == getter.name) + // TODO: Do we not only want to do this if we get the symbol from somewhere else in the child trait as well? + // i.e. need to know that the trait is mixed in either directly or indirectly as another parent as well. + // Maybe just say that ordinary traits cannot inherit from inline traits? That would probably fix it. + def isFromInlineTraitInlining(getter: Symbol): Boolean = + val y = mixin.parentSyms + + val x = mixin.parentSyms.map( + parentSym => parentSym.info.decls//.exists(d => d.name == getter.name || getter.name == d.name.expandedName(parentSym)) + ) + mixin.parentSyms.exists( + parentSym => parentSym.isInlineTrait && parentSym.info.decls.exists(d => { + d.name == getter.name || getter.name ++ str.INLINE_TRAIT_ERASED_PRIVATE_SUFFIX == d.name.expandedName(parentSym)}) ) for diff --git a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala index cbc10b53abcf..ed56777a6ed8 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -603,8 +603,14 @@ object TreeChecker { def isNonMagicalMember(x: Symbol) = !x.isValueClassConvertMethod && !x.name.is(DocArtifactName) && - !(ctx.phase.id >= genBCodePhase.id && x.name == str.MODULE_INSTANCE_FIELD.toTermName) - + !(ctx.phase.id >= genBCodePhase.id && x.name == str.MODULE_INSTANCE_FIELD.toTermName) && + !(x.owner.isInlineTrait) + // ^TODO: Not sure if this is strictly necessary but it seems to help in some cases because + // we can generate undefined private members in inline traits - we might need to think about + // this a bit more because I think it's only fine if you assume that you can't directly instantiate + // inline traits, whereas we might want to allow that (aside from specialisation) . + + val decls = cls.classInfo.decls.toList.toSet.filter(isNonMagicalMember) val defined = impl.body.map(_.symbol) diff --git a/tests/pos/inline-trait-specialized-desugar.scala b/tests/pos/inline-trait-specialized-desugar.scala index 7e9327b7f643..c54840c5fef2 100644 --- a/tests/pos/inline-trait-specialized-desugar.scala +++ b/tests/pos/inline-trait-specialized-desugar.scala @@ -11,10 +11,15 @@ inline trait ArrayIterator[T](elems: Array[T]) extends Iterator[T]: // We generate these: -inline trait Iteratorsp$Int extends Iterator[Int] -inline trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator[Int] +trait Iteratorsp$Int extends Iterator[Int] +trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator$sp$Int class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator[Int](elems) +// We could keep the signatures in the sp trait and then put the implementations in the impl class +// This would still require the modification to inline traits to inline all the way down, but also pruning at every step +// + + // Inline traits does the magic of actually inlining the code and specialising from T to Int in that step. @@ -25,6 +30,11 @@ def foo(x: ArrayIterator$sp$Int): Int = x.next() // As long as we generate this (i.e. "do the special erasure") before we run inline traits we should be fine because then the reference will be replaced. +// trait Foo$sp$Int +// + +// what if they do already some kind of with clause + // They do this: // class MyClassA // class MyClassB extends MyClassA, ArrayIterator[Int] @@ -39,6 +49,32 @@ class MyClassB extends MyClassA, ArrayIterator$sp$Int // They do this: // new ArrayIterator[Int](xs) {} + // new ArrayIterator$sp$Int with ArrayIterator[Int] (xs) {} + // We convert this to: val ai = new ArrayIterator$impl$Int(xs) {} + + + + println(ai.next()) + + +// Concerns: + // Specializing the "arr" field + // - Avoid boxing for internal values like `result` will that actually get done? - maybe through the last point "calls will be inlined" +// - The superclass of `C` is a top class, or `C` itself is a top class. +// Drop all specialized trait parameters of A +// - adds `A[S]` as first parent trait, +// - _also_ adds all parents of `A` in their specialized forms, +// - contains all specialized declarations of `A`. + +// and yet "second parent is not needed" +// - repeats the value parameters of trait `A`, +// - extends `A[S]`. + +// If we can manage to get rid of the inheritance there that could be helpful in terms of avoiding multiple values +// BUT: generate a version which is with just inline traits that has this problem as well. +// Need to deal with the caching at some point +// These implementation classes are type correct as long as we inject the knowledge that a specialization trait +// like `Seq$sp$Int` is equal to its parameterized version `Seq[Int]` diff --git a/tests/pos/specialized-traits-unspecialized.scala b/tests/pos/specialized-traits-unspecialized.scala new file mode 100644 index 000000000000..aed576cd0947 --- /dev/null +++ b/tests/pos/specialized-traits-unspecialized.scala @@ -0,0 +1,187 @@ +// (1) +inline trait A[T: Specialized, D: Specialized]: + def foo: T + def bar: D +inline trait B[S] extends A[S, Int] +trait C extends B[Char] + +// expands to: +trait A$sp$S$Int$[S] extends A[S, Int]: + def foo: S + def bar: Int +inline trait B[S] extends A$sp$S$Int[S] +trait C extends B[Char] +// so the foo can never be specialized, even if we specialize on S: +trait A$sp$S$Int$[S] extends A[S, Int]: + def foo: S + def bar: Int +inline trait B[S: Specialized] extends A$sp$S$Int[S] +trait B$sp$Char extends B[Char] +trait C extends B[Char] + + +inline trait A$sp$S$Int$[S] extends A[S, Int]: +inline trait B[S: Specialized] extends A$sp$S$Int[S] +inline trait B$sp$Char extends B[Char] +trait C extends B[Char] + def foo: S + def bar: Int + + +def fun[S](x: A$sp$S$Int[S]) + x.bar + + +def fun(x: B[Char]) + x.foo + +// (2) If we change the rules so that we can generate inline traits for $sp$: +inline trait A$sp$S$Int$[S] extends A[S, Int]: + def foo: S + def bar: Int +inline trait B[S] extends A$sp$S$Int[S]: + def foo: S + def bar: Int +trait C extends B[Char] + def foo: Char + def bar: Int + +// (3) And furthermore if we have Specialized on the B[S]: +inline trait A$sp$S$Int$[S] extends A[S, Int]: + def foo: S + def bar: Int +inline trait B[S: Specialized] extends A$sp$S$Int[S]: + def foo: S + def bar: Int +inline trait B$sp$Char$ extends B[Char]: + def foo: Char + def bar: Int +trait C extends B$sp$Char$ + def foo: Char + def bar: Int + +// And then I would argue that there could be value in adding a warning for dropping the Specialized qualifier in case (2) + + +// Should we be worried about code bloat due to inlining every time? I don't think so. +// We just need a rule to decide which method is selected. +inline trait A: + def foo = "Hello, World" + +inline trait B extends A: + over + def bar = "Boo" + +inline trait C extends A: + def baz = "baz" + +inline trait D extends A, B, C + +// Result: +inline trait A: + def foo#1 = "Hello, World" + +inline trait B extends A: + def foo#2 = "Hello, World" + def bar#1 = "Boo" + +// inline trait C extends A: +// def foo#3 = "Hello, World" +// def baz#1 = "baz" + +inline trait D extends A, B, C: + def foo#1 = "Hello, World" + def bar#1 = "Boo" + def baz#1 = "baz" + + +// At the moment, the following is rejected +inline trait A: + def foo = "Hello World" + +inline trait B: + def foo = "Bonjour" + +class C extends A, B: + def foo = "Bonjour2" + +def main = + val x = C() + println(x.foo) + +// and this is also rejected without the override modifier, but allowed with: +trait A: + def foo = "Hello World" + +trait B extends A: + def foo = "Bonjour" + +class C extends A, B + +def main = + val x = C() + println(x.foo) + +// while the following is allowed (and we take the value from the second trait i.e. B) +inline trait A: + def foo = "Hello World" + +inline trait B: + def foo = "Bonjour" + +class C extends A, B + +def main = + val x = C() + println(x.foo) +// I think this is fine and necessary if we want to make the resulting traits inline, because we need +// to extend from multiple inline traits sharing members. To be honest the behaviour will be more like the +// override case because they come from the same inheritance hierarchy anyway. + +// Alternative approach: +// (1) Erase S, concerned that this won't type correctly / will get a missing cast +// Also this is just not giving us the maximum amount of efficiency gain that we could get. +inline trait A[T: Specialized, D: Specialized]: + def foo: T + def bar: D +inline trait B[S] extends A[S, Int] +trait C extends B + + +trait A$sp$Any$Int extends A[Any, Int]: + def foo: Any + def bar: Int +inline trait B[S] extends A$sp$Any$Int +trait C extends B[Char] + +// (2) +// Don't erase S but just don't care about the loss of specialization +trait A$sp$S$Int extends A[S, Int]: + def foo: S + def bar: Int +inline trait B[S] extends A$sp$S$Int[S] +trait C extends B[Char] + + + +// This one is also kind of a massive problem.... +inline trait Spec[S: Specialized] + +inline trait A[T] + def x(y: Spec[T]) + +class B extends A[Char] + +// inline trait Spec2[W: Specialized] +// inline trait Spec[S: Specialized] +// def z(y: Spec2[S]) +// inline trait A[T] +// def x(y: Spec[T]) +// class B extends A[Char] + +// 1. Specialization does nothing because no materially specialized instances +// 2. Inlining generates reference to Spec[Char] which is materially specialized +// 3. Specialization generates Spec$sp$Char class +// 4. Inlining fills this class up which generates reference to Spec2[Char] +// 5. Specialization generates Spec2$sp$Char class +// 6. Inlining again and done. \ No newline at end of file diff --git a/tests/run/benchmark-jmh-compatible.scala b/tests/run/benchmark-jmh-compatible.scala new file mode 100644 index 000000000000..a97aa94d933c --- /dev/null +++ b/tests/run/benchmark-jmh-compatible.scala @@ -0,0 +1,89 @@ +package SpecializedTraitsBenchmark + +// This trait isn't really necessary but just wanted to check that the trait virtual call wasn't +// slowing us down in ours relative to the manual version which it's not. +trait VecT: + def length: Int + def apply(i: Int): Int + def scalarProduct(other: VecT): Int + +class Vec(elems: Array[Int]) extends VecT: + private val num = summon[Numeric[Int]] + private val x = 1 + + def length = elems.length + + def apply(i: Int): Int = elems(i) + + def scalarProduct(other: VecT): Int = + require(this.length == other.length) + var result = num.fromInt(0) + for i <- 0 until length do + result = num.plus(result, num.times(this(i), other(i))) + result + +class VecGeneric[T: Numeric](elems: Array[T]): + private val num = summon[Numeric[T]] + private val x = 1 + + def length = elems.length + + def apply(i: Int): T = elems(i) + + def scalarProduct(other: VecGeneric[T]): T = + require(this.length == other.length) + var result = num.fromInt(0) + for i <- 0 until length do + result = num.plus(result, num.times(this(i), other(i))) + result + +inline trait VecSpec[T: {Specialized, Numeric}](elems: Array[T]): + private val num = summon[Numeric[T]] + private val x = 1 + + def length = elems.length + + def apply(i: Int): T = elems(i) + + def scalarProduct(other: VecSpec[T]): T = + require(this.length == other.length) + var result = num.fromInt(0) + for i <- 0 until length do + result = num.plus(result, num.times(this(i), other(i))) + result + + +object TestBench: + val v1 = Array.fill(100_000_000) {math.round(math.random.floatValue * 4)} + val v2 = Array.fill(100_000_000) {math.round(math.random.floatValue * 4)} + + val a1 = Vec(v1) + val b1 = Vec(v2) + + val a2 = VecGeneric[Int](v1) + val b2 = VecGeneric[Int](v2) + + val a3 = new VecSpec[Int](v1) {} + val b3 = new VecSpec[Int](v2) {} + + def benchManuallySpec = + val result = a1.scalarProduct(b1) + println(s"Got ${result}") + + def benchGeneric = + val result2 = a2.scalarProduct(b2) + println(s"Got ${result2}") + + def benchOurs = + val result = a3.scalarProduct(b3) + println(s"Got ${result}") + + +// scala-cli --power package --assembly --preamble=false tests/run/benchmark-jmh-compatible.scala -S 3.8.3-RC1-bin-SNAPSHOT-nonbootstrapped --verbose +// mv benchmark-jmh-compatible.jar ../bench/test/benchmark.jar + +// In benchmark project intellij +// mvn install:install-file -Dfile=benchmark.jar -DgroupId=benchmark -DartifactId=benchmark -Dversion=1.0 -Dpackaging=jar +// mvn clean compile +// Invalidate Caches and Restart +// Run benchmarks! diff --git a/tests/run/benchmark-manual.scala b/tests/run/benchmark-manual.scala new file mode 100644 index 000000000000..82bc9f6850b8 --- /dev/null +++ b/tests/run/benchmark-manual.scala @@ -0,0 +1,99 @@ +trait VecT: + def length: Int + def apply(i: Int): Int + def scalarProduct(other: VecT): Int + +class Vec(elems: Array[Int]) extends VecT: + private val num = summon[Numeric[Int]] + private val x = 1 + + def length = elems.length + + def apply(i: Int): Int = elems(i) + + def scalarProduct(other: VecT): Int = + require(this.length == other.length) + var result = num.fromInt(0) + for i <- 0 until length do + result = num.plus(result, num.times(this(i), other(i))) + result + +class VecGeneric[T: Numeric](elems: Array[T]): + private val num = summon[Numeric[T]] + private val x = 1 + + def length = elems.length + + def apply(i: Int): T = elems(i) + + def scalarProduct(other: VecGeneric[T]): T = + require(this.length == other.length) + var result = num.fromInt(0) + for i <- 0 until length do + result = num.plus(result, num.times(this(i), other(i))) + result + +inline trait VecSpec[T: {Specialized, Numeric}](elems: Array[T]): + private val num = summon[Numeric[T]] + private val x = 1 + + def length = elems.length + + def apply(i: Int): T = elems(i) + + def scalarProduct(other: VecSpec[T]): T = + require(this.length == other.length) + var result = num.fromInt(0) + for i <- 0 until length do + result = num.plus(result, num.times(this(i), other(i))) + result + +@main def main = + val v1 = Array.fill(100_000_000) {math.round(math.random.floatValue * 4)} + val v2 = Array.fill(100_000_000) {math.round(math.random.floatValue * 4)} + + // println("------------ Ours ------------") + // val a4 = new VecSpec[Int](v1) {} + // val b4 = new VecSpec[Int](v2) {} + // val start4 = System.nanoTime() + // val result4 = a4.scalarProduct(b4) + // println(s"Got ${result4}") + // val end4 = System.nanoTime() + // println(s"Took ${(end4 - start4).toFloat / 1_000_000_000.toFloat} seconds") + + // println("------------ Generic ------------") + // val a2 = VecGeneric[Int](v1) + // val b2 = VecGeneric[Int](v2) + // val start2 = System.nanoTime() + // val result2 = a2.scalarProduct(b2) + // println(s"Got ${result2}") + // val end2 = System.nanoTime() + // println(s"Took ${(end2 - start2).toFloat / 1_000_000_000.toFloat} seconds") + + println("------------ Manually Specialized ------------") + val a1 = Vec(v1) + val b1 = Vec(v2) + val start1 = System.nanoTime() + val result1 = a1.scalarProduct(b1) + println(s"Got ${result1}") + val end1 = System.nanoTime() + println(s"Took ${(end1 - start1).toFloat / 1_000_000_000.toFloat} seconds") + + // println("------------ Ours ------------") + // val a3 = new VecSpec[Int](v1) {} + // val b3 = new VecSpec[Int](v2) {} + // val start3 = System.nanoTime() + // val result3 = a3.scalarProduct(b3) + // println(s"Got ${result3}") + // val end3 = System.nanoTime() + // println(s"Took ${(end3 - start3).toFloat / 1_000_000_000.toFloat} seconds") + +// Think that the Generic case is really messing with the JIT +// When we put it in we get very inconsistent results where usually the second Ours +// is 0.7 or 1.0 instead of 0.3, but sometimes it's the Manually Specialized that gets messed up. +// If on the other hand we add -XInt and reduce to 1 million instead of 10 million the problem goes away. +// If we run each case independently we get the expected results. + +// For comparison (because why not) +// We get comparable results to C++ with the manually specialized and ours versions +// BUT if you put on -Ofast then C++ destroys us. From eaa657cdbd78724e07501ef95beceb58ffcbe785 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 22 Mar 2026 20:57:08 +0100 Subject: [PATCH 053/254] Add inline traits desugar explanation doc --- docs/_docs/internals/inline-traits.md | 228 ++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 docs/_docs/internals/inline-traits.md diff --git a/docs/_docs/internals/inline-traits.md b/docs/_docs/internals/inline-traits.md new file mode 100644 index 000000000000..28df61a57fd6 --- /dev/null +++ b/docs/_docs/internals/inline-traits.md @@ -0,0 +1,228 @@ +# Inline Traits + +## Motivation +Inline traits is a new attempt to solve the specialization problem for the JVM in a more convenient way than the `@specialized` annotation +from Scala 2. It works alongside `Specialized` traits, the latter being detailed in an accompanying document. + +The problem is as follows: due to the JVM's (lack of) support for generics, generic type parameters are erased by the compiler: +```scala +class A[T](val x: T) + +class C: + val w1 = A[Int](1) + val w2 = A[Int](2) + val w3 = A[Int](w1.x + w2.x) +``` +Is converted to: +```scala +class A: + val x: Object +class C() extends Object() { + def w1(): A = new A(Int.box(1)) + def w2(): A = new A(Int.box(2)) + def w3(): A = new A(Int.box(Int.unbox(this.w1().x()).+(Int.unbox(this.w2().x())))) +} +``` +Thus type `T` is converted to `Object`, and so every use of `A[Int]()` must first build an `Object` containing the `Int` we want to pass (*boxing*), +in order to be able to call `A(x: Object)`. There is no `A(x: Int)`. When referencing `x` on an `A` we get an `Object` back, which we have to *unbox* +(extract from the wrapping `Object`). + +This object creation and deletion is very slow. We desire a way to avoid this by generating specialized instances of classes which +use primitive types instead of `Object`. These can be used in situations where the (un)boxing overhead is likely to be high. + +## Solution +An inline trait is defined just like a normal trait, but with an `inline` modifier. + +Inline traits may be extended by objects, classes or other inline traits, *but not by ordinary traits*. + +The following is an example of the use of an `inline trait`. + +```scala +inline trait A[T](val x: T): + def foo: T = x + +class B extends A[Int](1) +``` +Let the term *inline trait* refer to traits such as `A` above, and let *inline receiver* refer to class-likes +we inline into, such as `B` above. + +When an inline trait is inherited by an object, class or another inline trait, all its contents are inlined, and adapted +to the context of the inline receiver. In particular this means that: +- references to type parameters of the inline trait are specialized to the type arguments provided during extension +- `this` calls are updated to refer to the inline receiver. + +Inline traits are themselves translated to pure interfaces. However their bodies are of course retained in Tasty files; this +enables us to inline them into inline receivers that exist in different compilation units (for example when an inline recevier +in user code extends an inline trait from a library). + +The example above generates: +```scala +// Inline trait converted to pure interface +inline trait A[T](x: T): + val x: T + def foo: T + +// Extending class now contains inlined body +// with references to T specialized to Int. +// this.x refers to B.x +class B extends A[Int](1): + override val x: Int = 1 + override def foo: Int = this.x +``` + +With multiple inline traits: + +```scala +inline trait A[T](val x: T): + def foo: T = x + +inline trait B extends A[Int] +class C extends B, A[Int](1) +``` + +```scala +inline trait A[T](x: T): + val x: T + def foo: T + +inline trait B extends A[Int]: + override def x: Int + override def foo: Int + +class C extends B, A[Int](1): + override def x = 1 + override def foo = x +``` +While it is not immediately obvious why permitting inline traits to be inlined into other inline traits is useful (we could simply +inline everything into the first class/object in the hierarchy, it becomes advantageous when we bring in the `Specialized` annotation; see the accompanying document). + + +Furthermore: +- References to members of inline traits accessed on inline receivers point to the inlined version, to ensure we avoid unnecessary boxing: +```scala +inline trait A[T](val x: T): + def foo#1: T + +class B extends A[Int](1) + def foo#2: T = x + +def fun(x: B) = + x.foo // points to foo#2 +``` +- Inline traits may define private members, and these are handled specially: + - Private fields in the inline trait are inlined as private fields with a mangled name in the inline receiver. This ensures they do not collide with privates inherited from other inline traits. + - The private fields are then no longer accessible in the inline trait, as it is transformed into a pure interface. We can't however easily delete them; therefore they are name-mangled and converted to protected to allow them to exist without a definition. + +```scala +inline trait A(b: Boolean): + private val x: Int = 1 + def foo(): Int = if b then x + 1 else 0 + +class B extends A(true) +``` +Is conveted to: + +```scala +inline trait A(b: Boolean): + protected val x$inline_trait_erased_private#4481: Int + def foo(): Int + +class B extends A(true): + private val A$$b: Boolean = true + private val A$$x: Int = 1 + override def foo(): Int = if this.A$$b then this.A$$x.+(1) else 0 +``` +- An inline receiver may mix in multiple inline traits with colliding member names. In this case the latest extended trait prevails. In the following example calling `foo` on an instance of `C` will return "Bonjour". +```scala +inline trait A: + def foo = "Hello World" + +inline trait B: + def foo = "Bonjour" + +class C extends A, B +``` +- Inlined members of inline traits are typed with the type of the right hand side resulting from inlining. This is particularly important for typeclass instances: +```scala +inline trait A[T: Numeric]: + private val v: Numeric[T] = summon[Numeric[T]] + +class B extends A[Int] +``` +Is converted to: +```scala +inline trait A[T: Numeric]: + private val v: Numeric[T] = summon[Numeric[T]] + +class B extends A[Int]: + private given val A$$evidence$1: scala.math.Numeric.IntIsIntegral.type = scala.math.Numeric.IntIsIntegral + private val A$$v: scala.math.Numeric.IntIsIntegral = this.A$$evidence$1 +``` +This means that references to `v.fromInt()`, `v.add()` etc are optimised and avoid boxing. + +## Benefits of inline traits +We can now do the following with no boxing and unboxing: +```scala +inline trait A[T](val x: T) +class IntA(x: Int) extends A[Int](x) + +class C: + val w1 = IntA(1) + val w2 = IntA(2) + val w3 = IntA(w1.x + w2.x) +``` + +Inline traits avoid all of the pain points of Scala 2 specialization. They can do more than primitive specialization since they also specialize on value parameters and reference types. This helps avoid megamorphic dispatch. Inline traits also profit from all the optimizations available for inline methods, including inline matches, summonFrom, embedded splices. Indeed the analogy of inline traits and inline methods is strong: inline calls correspond to supercalls in extending classes and objects, inline parameters are inline parameters of the trait, inline traits can have nested inline matches, etc. + +Inline trait expansions are only generated on demand when a class or object extends an inline trait. This avoids the up-front cost and code explosion due to creating specialized copies which might never be needed. + +## Shortcoming of inline traits +Compared to full specialization, inline traits have one shortcoming, namely that interfaces are not specialized. For example: + +```scala +inline trait Foo[T](x: T): + def foo = x + +class Bar extends Foo[Int](42) + +def f(b: Foo[Int]) = 37 + b.foo + +@main def main = + val x = Bar() + f(x) +``` + +In this code the call to `b.foo` will refer to the version of `foo` typed `foo: T` which becomes `foo: Object` during erasure, becasue we accessed `foo` on +an object of declared type `Foo` (even though `b`'s actual runtime type is `Bar`). This will in turn call a bridge method which means the `foo: Int` method will be called, but unnecessary boxing and unboxing will be added: + +```scala +inline trait Foo + def foo#1(): Object + +class Bar extends Foo[Int](42): + override def foo#2(): Int = 42 + override def foo#3(): Object = Int.box(this.foo()) + +def f(b: Foo): Int = 37 + Int.unbox(b.foo#1()) // virtual call to foo#1 resolves to bridge method foo#3, which in turn calls actual method foo#2, with boxing. +``` + +This problem is addressed via `Specialized` traits; see the accompanying document on those. + +## Interaction with other language features + +| Language feature | Is currently supported inside inline traits? | +|--------------------------|----------------------------------------------| +| Methods | ✅ | +| `val` / `var` Properties | ✅ | +| `type`s | ✅ | +| Inner classes | ❌ | +| Opaque types | ❌ | +| Self types | ❌ + +## Internal Note regarding versions of inline traits +This behaviour is the same as that in Timothée's thesis except for the following points: + - We now allow inline traits to be inlined directly into other inline traits as well as objects and classes. + - We now do replacement of member accesses to point to the inlined versions throughout the whole code, not just in the bodies of inner classes + - He allows inline traits to contain inner classes in principle, however in practice they don't work which is why we ban them. + - We specialize types of member accesses on e.g. Numeric + - He in principle allows traits to extend inline traits although it doesn't work that well; we think we probably want to forbid this. From 070caea52469946bef402f5df006e182701f295b Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 22 Mar 2026 21:45:00 +0100 Subject: [PATCH 054/254] Add todos --- .../dotc/transform/DesugarSpecializedTraits.scala | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 364b1ce00b85..4fb16314e691 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -478,4 +478,12 @@ object Specialization: // it's a very unlikely usecase and would fix the mixin problem - although // we could just try and fix the mixin problem properly. -// TODO: Create a proper benchmark \ No newline at end of file +// TODO: Create a proper benchmark +// TOOD: probably need to do this from Timothée private members are not renamed, only private parameter accessors are; +// this needs to be changed so that all overridable private members are renamed +// Implement postphase checks for inline traits +// TODO: Don't synthesize specialized instances for random generic types probably - as Hamza said we want to be able to control the specialization +// TODO: If we are to ban trait extends inline trait then need to fix some tests. +// TODO: Check that when we extend parents we actually do so in the specialized forms. +// TODO: Fix specialized-trait-collections-example.scala +// TODO: Test extensively with inline methods and inline traits. From a6e54bd48c0e6f29d9de00f4d2744aea1e1ccb41 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 22 Mar 2026 21:45:27 +0100 Subject: [PATCH 055/254] Add specialized traits md --- docs/_docs/internals/specialized-traits.md | 422 +++++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 docs/_docs/internals/specialized-traits.md diff --git a/docs/_docs/internals/specialized-traits.md b/docs/_docs/internals/specialized-traits.md new file mode 100644 index 000000000000..07b859acb6b4 --- /dev/null +++ b/docs/_docs/internals/specialized-traits.md @@ -0,0 +1,422 @@ +# Specialized Traits and Classes + +Specialization is one of the few remaining desirable features from Scala 2 that are still missing in Scala 3. We could try to port the Scala 2 scheme, which would be non-trivial since the implementation is quite complex. But that scheme is problematic enough to suggest that we also look for alternatives. A possible alternative is described here. It is meant to complement the [proposal on inline traits](https://github.com/lampepfl/dotty/issues/15532). That proposal also contains a more detailed critique of Scala 2 specialization. +The parts in that proposal that mention a proposed new specialization design should be ignored; they are superseded by the proposal here. + +The main problem of Scala-2 specialization is code bloat. We have to pro-actively generate up to 11 copies of functions and classes when they have a specialized type parameter, and this grows exponentially with the number of such type parameters. Miniboxing tries to reduce the number under the exponent from ~10 to 3 or 4, but it has problems dealing with arrays. + +Languages like C++, Rust, Go, D, or Zig avoid the proactive generation of all possible specializations by monomorphizing the whole program. This means we only need to generate a specialized version of a function or class if it is actually used in the program. On the other hand, a global monomorphization can lead itself to code bloat and long compile times. It is also a problematic choice for binary APIs. + +This note discusses a different scheme to get specialization for Scala 3, which is somewhat between Scala 2's selective specialization and full monomorphization. As in Scala 2, specialized type parameters are tagged explicitly (but not with an annotation). But as for monomorphization, specializations are only generated if a specialized type is referenced in the program. To make this work efficiently, we need a way to transport information about possible specialization types through generic code (full monomorphization does not need that since it eliminates all generic code). + +We do that using a type class `Specialized` that is typically used as a context bound on a type parameter of some class. It indicates that we want to create specialized versions of that class where the type parameter is instantiated to the type argument. The specialized versions offer optimization opportunities compared to the generic class. + +## Example + +As a first example, consider a `Vec` trait for vectors over a numeric type. +```scala +import scala.math.Numeric + +inline trait Vec[T: {Specialized, Numeric}](elems: Array[T]): + + def length = elems.length + + def apply(i: Int): T = elems(i) + + def scalarProduct(other: Vec[T]): T = + require(this.length == other.length) + var result = num.fromInt(0) + for i <- 0 until length do + result = num.plus(result, num.times(this(i), other(i))) + result + +object Vec: + inline def apply[T: Specialized](elems: Array[T]) = new Vec[T](elems) {} +end Vec +``` +The idea is that we want to specialize vectors on the type parameter `T` in order to get important efficiency gains, including the following: + + - Use an array `arr` specialized to the actual element instead of a fully generic array that has to be accessed via reflection + - Avoid boxing for internal values like `result` + - Avoid boxing in the API for values like the result of `scalarProduct` + - Specialize on the concrete `Numeric` class instance for `T`, so that calls to `num`'s methods have static targets and can be inlined. + +## Terminology and Restrictions + +A _specialized trait_ is an inline trait that has at least one `Specialized` context bound. + +A specialized context bound (or its expansion to a context parameter) is only allowed for +type parameters of inline methods and inline traits. Regular methods or traits or classes +cannot take `Specialized[T]` parameters. Hence, the only way to create a specialized trait is using an anonymous class instance, like in the `Vec.apply` method above. What's more, +we require that each such anonymous class instance + + - can extend only a single specialized trait, + - cannot mix in further classes or traits, and + - cannot contain member definitions. + +So each such class instance is of the form `new A[Ts](ps1)...(psN) {}` where +`A` is a specialized trait and the type parameters `Ts` and term parameters `ps1, ,,, psN` can also be absent. + +The restrictions ensure that each time we create an instance of a specialized trait we know statically the classes of all `Specialized` type arguments. This enables us to implement the following expansion scheme: + + +## Expansion of Specialized Traits + +A type instance of a specialized trait such as `Vec[Tp]` has a special erasure, which depends on the specializing supertype of `Tp`. + +**Definition**: A _simple class type_ is a reference to a static class that does not have type parameters. References to traits and references containing non-static prefixes or refinements are excluded. + +**Definition**: A _top class_ is one of `Any`, `AnyVal`, or `Object`. + +**Definition**: The _specializing supertype_ `SpecType(Tp)` of a type `Tp` is the smallest simple class type `C` such that + + - `Tp` is a subtype of `C` + - The superclass of `C` is a top class, or `C` itself is a top class. + +The _erasure_ of `Vec[Tp]` where `SpecType(Tp) = C` is: + + - If `C` is one of the top classes `Any` or `AnyRef` or `AnyVal`, the usual erased trait `Vec`. + - If `C` is some other class, a new specialized instance trait with a name of the form `Vec$sp$TN`, + where `$sp$` is a fixed specialization marker and `TN` is an encoding of the fully qualified name of `C`. + +If there is more than one specialized type parameter, the specialized instance trait will reflect in its name all specializing supertypes of such type parameters in sequence. + +An anonymous class instance creation like `new Vec[T](elems) {}` expands to +an instance creation `new Vec$impl$TN(elems)` of a new _specialized instance class_ +named `Vec$impl$TN`. Here, `Vec$sp$TN` is the erasure of `Vec[T]` and the class name derives from that trait name by replacing `$sp` with `$impl$`. + + +The specialized instance traits are created on demand the first time they are mentioned in a type. For example, here is the definition of the specialized instance `Vec$sp$Int` for `Vec[Int]`: + +```scala +inline trait Vec$sp$Int extends Vec[Int]: + def length: Int + def apply(x: Int): Int + def scalarProduct(other: Vec[T]): Int +``` + +In general a specialized instance trait that specializes an inline trait `A[T]` with a specialization type `S`: + + - drops all specialized trait parameters of `A`, + - adds `A[S]` as first parent trait, + - _also_ adds all parents of `A` *in their specialized forms*, + - contains all specialized declarations of `A`. + - Is inline + - Maintains type parameters for type params not marked with `Specialized` in the original trait, and also for specializations to `T: Specialized` e.g. in the case of partial specializations. + +A specialized instance class for an inline trait `A` at specialized argument `S` + + - repeats the value parameters of inline trait `A`, + - extends `A[S]` with these parameters + - extends the corresponding $sp$ trait + +For example, here is the specialized instance class for `Vec` at `Int`: + +```scala +class Vec$impl$Int(elems: Array[T]) extends Vec[Int](elems), Vec$sp$Int +``` + +After inlining `Vec[Int]` the expanded class looks like this: +```scala +class Vec$impl$Int(elems: Array[Int])(using Numeric[Int]) extends Vec[Int](elems), Vec$sp$Int: + + def length: Int = elems.length + def apply(i: Int): Int = elems(i) + + def scalarProduct(other: Vec[Int]): Int = + require(this.length == other.length) + var result = num.fromInt(0) + for i <- 0 until length do + result = num.plus(result, num.times(this(i), other(i))) + result +``` + +More examples of expansions are shown in the case study below. + +## Caching of Specialized Traits and Classes + +To avoid redundant repeated code generation of the same traits and classes, specialized instance traits and classes are cached. The compiler will put their tasty and classfile artifacts in a special directory +on the class path. Each artifact will contain in an annotation a hash of the contents of the trait from which the instance was derived. Before creating a new specialized instance, the compiler will consult this directory to see whether an instance with the given name exists and whether its hash matches. In that case, the artifacts can be re-used. + +## The `Specialized` Type Class + +The `Specialized` Type Class is erased at runtime. Instances +of `Specialized[T]` are created automatically for types that do not contain type variables. + +## A Larger Case Study + +As an example of a hierarchy of specialized traits, consider the following small group of specialized collection traits: + +```scala +inline trait Iterator[T: Specialized]: + def hasNext: Boolean + def next(): T + +inline trait ArrayIterator[T: Specialized](elems: Array[T]) extends Iterator[T]: + private var current = 0 + def hasNext: Boolean = current < elems.length + def next(): T = try elems(current) finally current += 1 + +inline trait Iterable[T: Specialized]: + def iterator: Iterator[T] + def forall(f: T => Unit): Unit = + val it = iterator + while it.hasNext do f(it.next()) + +inline trait Seq[T: Specialized](elems: Array[T]) extends Iterable[T]: + def length: Int = elems.length + def apply(i: Int): T = elems(i) + def iterator: Iterator[T] = new ArrayIterator[T](elems) {} +``` + +This generates the following instance traits: + +```scala +inline trait Iterator$sp$Int extends Iterator[Int]: + def hasNext: Boolean + def next(): Int + +inline trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator$sp$Int + +inline trait Iterable$sp$Int extends Iterable[Int]: + def iterator: Iterator$sp$Int + def forall(f: Int => Unit): Unit + +inline trait Seq$sp$Int extends Seq[Int], Iterable$sp$Int: + def length: Int + def apply(i: Int): Int +``` +Note that these traits repeat the parent types of their corresponding inline traits. For instance, `ArrayIterator$sp$Int` extends `ArrayIterator[Int]` as well as its parent `Iterator[Int]`. After erasure, the definition of +`ArrayIterator$sp$Int` becomes +```scala +inline trait ArrayIterator$sp$Int extends ArrayIterator, Iterator$sp$Int +``` +Hence, the erased `trait ArrayIterator$sp$Int` extends the general `ArrayIterator` trait as well as the specialized `Iterator$sp$Int` parent trait, which is what we want. + +The specialized implementation classes for `ArrayIterator` and `Seq` are as follows: +```scala +class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator[Int](elems): + private var current = 0 + override def hasNext: Boolean = + current < elems.length + override def next(): Int = + try elems(current) finally current += 1 + +class Seq$impl$Int(elems: Array[Int]) extends Seq$sp$Int, Seq[Int]: + override def iterator: Iterator$sp$Int = new ArrayIterator$impl$Int(elems) + + override def forall(f: Int => Unit): Unit = + val it = iterator + while it.hasNext do f(it.next()) + override def length: Int = elems.length + override def apply(i: Int): Int = elems(i) +``` +These implementation classes are type correct as long as we inject the knowledge that a specialization trait +like `Seq$sp$Int` is equal to its parameterized version `Seq[Int]`. This equality holds once types are erased. +Before that we either have to assume it, or insert some casts, as shown in the test file +`tests/pos/specialized-traits-strawman.scala`. + +After erasure, the implementation traits and classes look like this: + +```scala + inline trait Iterator$sp$Int extends Iterator: + def hasNext: Boolean + def next(): Int + + inline trait ArrayIterator$sp$Int extends ArrayIterator, Iterator$sp$Int + + inline trait Iterable$sp$Int extends Iterable: + def iterator: Iterator$sp$Int + def forall(f: Function1): Unit + + inline trait Seq$sp$Int extends Seq, Iterable$sp$Int: + def length: Int + def apply(i: Int): Int + + class ArrayIterator$impl$Int(elems: Int[]) extends ArrayIterator$sp$Int, ArrayIterator: // second parent seems redundant but was needed for correct param passing + private var current = 0 + override def hasNext: Boolean = + current < elems.length + override def next(): Int = + try elems(current) finally current += 1 + + /* Bridges: + override def next(): Object = Int.box(next()) + */ + end ArrayIterator$impl$Int + + class Seq$impl$Int(elems: Int[]) extends Seq$sp$Int, Seq: + override def iterator: Iterator$sp$Int = + new ArrayIterator$impl$Int(elems) + override def forall(f: Function1): Unit = + val it = iterator + while it.hasNext do f.apply$mcVI$sp(it.next()) + override def length: Int = elems.length + override def apply(i: Int): Int = elems(i) + + /* Bridges: + override def iterator: Iterator = iterator + override def apply(i: Int): Object = Int.box(apply(i)) + */ + end Seq$impl$Int +``` +Here, `f.apply$mcVI$sp` is the specialized apply method of `Function1` at type `Int => Unit`. +This method is generated by Scala 2's function specialization which is also adopted by Scala 3. + +The example shows that indeed all code is properly specialized with no need for box or unbox operations. + + +## Evaluation + +The described scheme is surprisingly simple. All the heavy lifting is done by inline traits. Adding specialization on top requires little more than arranging for a cache of specialized instances. + +The scheme requires explicit monomorphization through inline methods and inline traits. One point to investigate further is how convenient and expressive code adhering to that restriction can be. If we take specialized collections as +an example, if we want the result of `map` to be specialized, we have to define `map` as an inline method: +```scala +package collection.immutable.faster +inline trait Vector[+A: Specialized](elems: A*): + ... + inline def map[B: Specialized](f: A => B): Vector[B] = + new Vector[B](elems.map(f)) +``` +There's precedent for this in Kotlin where the majority of higher-order collection methods are declared inline, in this case in order to allow specialization for suspendability. So the restriction does not look like a blocker. + +## Going Further: Improve Existing Class Hierarchies + +We have shown that we can formulate an alternative version of a collection-like class hierarchy that is fully specialized. But can we retro-fit this idea even to existing collections? The direct approach would +clearly not work since an existing collection like `Vector[T]` can be created from anywhere whereas a specialized collection can be created only in a monomorphic context where we know the type instance of `T`. So specialized +collections come with a tax in expressiveness which pays for their superior performance. + +But it turns out we can gain a lot of flexibility with three additional tweaks to the language and compiler. + +### 1. Adapt Overloading to Specialization + +More flexibility could be gained if we allowed method overloading between specialized inline methods and normal methods with matching type signatures. For instance, the `Vector` implementation above seriously restricts `map` by requiring that its `B` type parameter is also `Specialized`. Thus `map` cannot be used to map a specialized collection to another collection if the result element type is not statically known. But we could alleviate the problem by allowing a second, overloaded `map` operation like this: +```scala + def map[B](f: A => B): collection.immutable.Vector[B] = + new collection.immutable.Vector[B](elems.map(f)) +``` +The second implementation of `map` will return an unspecialized vector if +the new element type is not statically known. If overloads like this were allowed, they could be resolved by picking the specialized inline version if +a `Specialized` instance can be synthesized for the actual type argument, and picking the unspecialized version otherwise. + +We can do even better if we allow some additions of the existing collections. In that case, we can add definitions like the inline `map` above to the original collections. +That means, whenever we have a collection `xs` with a type such as `Vector[A]` and a function `f` with a statically known result type `B`, then `xs.map(f)` returns a specialized collection. So we can get specialized collections out of normal collections as long as the element type of the created collection is statically known. + +This can be generalized. In particular, all `apply` methods of `Vector` should be split into methods taking specialized types and unrestricted methods. For instance: +```scala +object Vector: + def apply[T](xs: T*): Vector[T] = ... + inline def apply[T: Specialized](xs: T*): faster.Vector[T] = ... +``` +The same holds for all collection methods such as `map` that return a new collection of a different element type. + +### 2. Automate the Boilerplate with `specializedBy` + +The described scheme would entail some amount of code duplication. We could automate this with a new annotation that is put on a class and states that the class has a specialized variant. Example: +```scala +@specializedBy[faster.Vector] class Vector[+T] ... +``` +If a class carries such an annotation the specialized inline functions described above could be added automatically. + +### 3. Optimize Use Sites by Path Splitting + +One remaining problem is that specialization is a compile-time operation. Without putting in additional work, we cannot immediately exploit the situation where a runtime type is a specialized collection but the static type is unspecialized. For instance, consider this use of `Vector`: + +```scala +def sumElems(xs: Vector[Int]): Int = + var i = 0 + var sum = 0 + while i < xs.length do + sum += xs(i) + i += 1 + sum +``` +Here, the problem is that, even though we know that `xs` is a `Vector` of `Int`, we cannot deduce that has been specialized to a `faster.Vector[Int]`. Therefore, `xs(i)` goes through the `apply` method of `Vector`. If the runtime class of `Vector` is indeed specialized this would box the `Int` element to `Object` in a bridge method and unbox it again to `Int` at the call site. This could lose a lot of performance, unless the JVM manages to optimize the box/unbox pair away (so far, experience shows that the JVM is not very good at this). The performance could be even worse than working with an unspecialized `Vector` where elements are held in boxed form so they don't have to be boxed each time they are accessed. + + +Of course, we can narrow the type of `sumElems` to +```scala +def sumElems(xs: faster.Vector[Int]): Int +``` +but that would make it less generally usable. Another alternative is to optimize `sumElems` by path splitting. We could detect at runtime whether +`xs` is a `faster.Vector` and optimize the code if it is. For instance, like this: +```scala +def sumElems(xs: Vector[Int]): Int = + val faster: faster.Vector[Int] | Null = xs match + case xs: faster.Vector[_] => xs + case _ => null + var i = 0 + var sum = 0 + while i < xs.length do + sum += (if faster != null then faster(i) else xs(i)) + i += 1 + sum +``` +That would avoid the boxing at the cost of a type test in the computation of `faster` and a null test in the call of `apply`. The type test would be amortized over possibly many calls in the loop. We could do even better by generating a bit more code, splitting the whole loop: +```scala +def sumElems(xs: Vector[Int]): Int = + val faster: faster.Vector[Int] | Null = xs match + case xs: faster.Vector[_] => xs + case _ => null + var i = 0 + var sum = 0 + if faster != null then + while i < xs.length do + sum += faster(i) + i += 1 + else + while i < xs.length do + sum += xs(i) + i += 1 + sum +``` +The example has shown that one can write code over possibly specialized collections that is both general and highly performant. But it does require a lot of hand-written boiler-plate. + +The boilerplate could be generated automatically by an optimization phase in the compiler. Essentially, when compiling methods that take parameters whose type is a class annotated with `specializedBy`, we can do the path splitting automatically in an optimization step. The optimization would first analyze the body of the method to decide which path splitting strategy to apply. + +We believe the three tweaks we have outlined could overcome most of the performance penalties imposed by existing unspecialized class hierarchies like collections, making their performance comparable to languages that use global monomorphization. + +### Specializing Tuples + +The same optimizations can also avoid boxing for tuple elements, and with it extractor-based pattern matching. Scala 3 does not currently specialize tuples at all. Scala 2 specializes pairs but not tuples of higher arity. But it uses a scheme quite different from the one proposed here. + +Scala 2 pre-generates pair classes for all combinations of primitive types and Object. Each pair class inherits or implements access methods for all primitive types and Object. This allows to +arrange it so that access always goes through a specialized method that does not involve boxing. No path splitting is needed to achieve that. On the other hand, the exponentially growing amount of code that needs to be generated restricts the scheme to pairs only. Also, specialization is not done for reference types, access to fields of (say) `String` type still need a cast from `Object` to `String`. + +We could adopt the Scala 2 specialization scheme for pairs. This is not hard, since no new classes need to be generated, we simply re-use the Scala 2 classes. Then the new specialization scheme would apply to tuples of higher arities. Or we forego Scala 2 specialization altogether and specialize all tuples with the new scheme. + +The situation with functions is a bit different. Here, Scala 2 specializes functions with up to two parameters, and Scala 3 re-uses these specializations. +Going beyond that requires some adaptations since functions are not implemented as classes but as lambdas that are directly supported by the JVM. So Scala 3 specialization would have to be extended to the definition of these lambdas. + +## Going Further: Hand-written Specializations + +Additional improvements could be gained if we allowed the programmer to pick their own implementations for specialized class instances. For example, +we could have a +```scala +inline trait HashMap[K: Specialized, +V: Specialized] ... +``` +and an optimized sub-trait +```scala +inline trait IntHashMap[+V: Specialized] extends HashMap[Int, V] ... +``` +The implementation in `IntHashMap` could exploit that fact that the key type `K` is known to be `Int` to pick a more performant algorithm, for instance. + +It would be great if we could use `IntHashMap` each time a specialized HashMap such as `HashMap$sp$Int$String` is referred to or created. In other words, `IntHashMap` should act as a drop-in replacement for `HashMap$sp$Int$String` that is selected automatically. A detailed proposal for this is left for future work. + +## Summary + +This proposal + + 1. _Inline traits._ With them one can create specialized modules and classes, but no specialization on type parameters is possible. Inline traits also enable new patterns for meta programming. + 2. _Specialized traits and classes._ With them one can create class hierarchies that can require and exploit statically known type parameters. + 3. _Specialized overloads and path splitting_. With these additions one can create structures that can take advantage of statically known type parameters when they are available while still working for other type parameters as well. They also allow retro-fitting specializaton to existing libraries. + 4. _Hand-written specializations_. They allow to make user-defined algorithmic optimizations based on statically known type parameters. + + + + + + + +# the following things are banned... From c857661f1d087679c8f7614cd82aa42c20e0a9e3 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 23 Mar 2026 17:24:48 +0100 Subject: [PATCH 056/254] Replace manual benchmark with scala-cli --jmh compatible benchmark --- tests/run/benchmark-jmh-compatible.scala | 89 ------------------ tests/run/benchmark-manual.scala | 99 -------------------- tests/run/specialized-traits-benchmark.scala | 84 +++++++++++++++++ 3 files changed, 84 insertions(+), 188 deletions(-) delete mode 100644 tests/run/benchmark-jmh-compatible.scala delete mode 100644 tests/run/benchmark-manual.scala create mode 100644 tests/run/specialized-traits-benchmark.scala diff --git a/tests/run/benchmark-jmh-compatible.scala b/tests/run/benchmark-jmh-compatible.scala deleted file mode 100644 index a97aa94d933c..000000000000 --- a/tests/run/benchmark-jmh-compatible.scala +++ /dev/null @@ -1,89 +0,0 @@ -package SpecializedTraitsBenchmark - -// This trait isn't really necessary but just wanted to check that the trait virtual call wasn't -// slowing us down in ours relative to the manual version which it's not. -trait VecT: - def length: Int - def apply(i: Int): Int - def scalarProduct(other: VecT): Int - -class Vec(elems: Array[Int]) extends VecT: - private val num = summon[Numeric[Int]] - private val x = 1 - - def length = elems.length - - def apply(i: Int): Int = elems(i) - - def scalarProduct(other: VecT): Int = - require(this.length == other.length) - var result = num.fromInt(0) - for i <- 0 until length do - result = num.plus(result, num.times(this(i), other(i))) - result - -class VecGeneric[T: Numeric](elems: Array[T]): - private val num = summon[Numeric[T]] - private val x = 1 - - def length = elems.length - - def apply(i: Int): T = elems(i) - - def scalarProduct(other: VecGeneric[T]): T = - require(this.length == other.length) - var result = num.fromInt(0) - for i <- 0 until length do - result = num.plus(result, num.times(this(i), other(i))) - result - -inline trait VecSpec[T: {Specialized, Numeric}](elems: Array[T]): - private val num = summon[Numeric[T]] - private val x = 1 - - def length = elems.length - - def apply(i: Int): T = elems(i) - - def scalarProduct(other: VecSpec[T]): T = - require(this.length == other.length) - var result = num.fromInt(0) - for i <- 0 until length do - result = num.plus(result, num.times(this(i), other(i))) - result - - -object TestBench: - val v1 = Array.fill(100_000_000) {math.round(math.random.floatValue * 4)} - val v2 = Array.fill(100_000_000) {math.round(math.random.floatValue * 4)} - - val a1 = Vec(v1) - val b1 = Vec(v2) - - val a2 = VecGeneric[Int](v1) - val b2 = VecGeneric[Int](v2) - - val a3 = new VecSpec[Int](v1) {} - val b3 = new VecSpec[Int](v2) {} - - def benchManuallySpec = - val result = a1.scalarProduct(b1) - println(s"Got ${result}") - - def benchGeneric = - val result2 = a2.scalarProduct(b2) - println(s"Got ${result2}") - - def benchOurs = - val result = a3.scalarProduct(b3) - println(s"Got ${result}") - - -// scala-cli --power package --assembly --preamble=false tests/run/benchmark-jmh-compatible.scala -S 3.8.3-RC1-bin-SNAPSHOT-nonbootstrapped --verbose -// mv benchmark-jmh-compatible.jar ../bench/test/benchmark.jar - -// In benchmark project intellij -// mvn install:install-file -Dfile=benchmark.jar -DgroupId=benchmark -DartifactId=benchmark -Dversion=1.0 -Dpackaging=jar -// mvn clean compile -// Invalidate Caches and Restart -// Run benchmarks! diff --git a/tests/run/benchmark-manual.scala b/tests/run/benchmark-manual.scala deleted file mode 100644 index 82bc9f6850b8..000000000000 --- a/tests/run/benchmark-manual.scala +++ /dev/null @@ -1,99 +0,0 @@ -trait VecT: - def length: Int - def apply(i: Int): Int - def scalarProduct(other: VecT): Int - -class Vec(elems: Array[Int]) extends VecT: - private val num = summon[Numeric[Int]] - private val x = 1 - - def length = elems.length - - def apply(i: Int): Int = elems(i) - - def scalarProduct(other: VecT): Int = - require(this.length == other.length) - var result = num.fromInt(0) - for i <- 0 until length do - result = num.plus(result, num.times(this(i), other(i))) - result - -class VecGeneric[T: Numeric](elems: Array[T]): - private val num = summon[Numeric[T]] - private val x = 1 - - def length = elems.length - - def apply(i: Int): T = elems(i) - - def scalarProduct(other: VecGeneric[T]): T = - require(this.length == other.length) - var result = num.fromInt(0) - for i <- 0 until length do - result = num.plus(result, num.times(this(i), other(i))) - result - -inline trait VecSpec[T: {Specialized, Numeric}](elems: Array[T]): - private val num = summon[Numeric[T]] - private val x = 1 - - def length = elems.length - - def apply(i: Int): T = elems(i) - - def scalarProduct(other: VecSpec[T]): T = - require(this.length == other.length) - var result = num.fromInt(0) - for i <- 0 until length do - result = num.plus(result, num.times(this(i), other(i))) - result - -@main def main = - val v1 = Array.fill(100_000_000) {math.round(math.random.floatValue * 4)} - val v2 = Array.fill(100_000_000) {math.round(math.random.floatValue * 4)} - - // println("------------ Ours ------------") - // val a4 = new VecSpec[Int](v1) {} - // val b4 = new VecSpec[Int](v2) {} - // val start4 = System.nanoTime() - // val result4 = a4.scalarProduct(b4) - // println(s"Got ${result4}") - // val end4 = System.nanoTime() - // println(s"Took ${(end4 - start4).toFloat / 1_000_000_000.toFloat} seconds") - - // println("------------ Generic ------------") - // val a2 = VecGeneric[Int](v1) - // val b2 = VecGeneric[Int](v2) - // val start2 = System.nanoTime() - // val result2 = a2.scalarProduct(b2) - // println(s"Got ${result2}") - // val end2 = System.nanoTime() - // println(s"Took ${(end2 - start2).toFloat / 1_000_000_000.toFloat} seconds") - - println("------------ Manually Specialized ------------") - val a1 = Vec(v1) - val b1 = Vec(v2) - val start1 = System.nanoTime() - val result1 = a1.scalarProduct(b1) - println(s"Got ${result1}") - val end1 = System.nanoTime() - println(s"Took ${(end1 - start1).toFloat / 1_000_000_000.toFloat} seconds") - - // println("------------ Ours ------------") - // val a3 = new VecSpec[Int](v1) {} - // val b3 = new VecSpec[Int](v2) {} - // val start3 = System.nanoTime() - // val result3 = a3.scalarProduct(b3) - // println(s"Got ${result3}") - // val end3 = System.nanoTime() - // println(s"Took ${(end3 - start3).toFloat / 1_000_000_000.toFloat} seconds") - -// Think that the Generic case is really messing with the JIT -// When we put it in we get very inconsistent results where usually the second Ours -// is 0.7 or 1.0 instead of 0.3, but sometimes it's the Manually Specialized that gets messed up. -// If on the other hand we add -XInt and reduce to 1 million instead of 10 million the problem goes away. -// If we run each case independently we get the expected results. - -// For comparison (because why not) -// We get comparable results to C++ with the manually specialized and ours versions -// BUT if you put on -Ofast then C++ destroys us. diff --git a/tests/run/specialized-traits-benchmark.scala b/tests/run/specialized-traits-benchmark.scala new file mode 100644 index 000000000000..1a7452e31e60 --- /dev/null +++ b/tests/run/specialized-traits-benchmark.scala @@ -0,0 +1,84 @@ +// Run with: scala-cli --power --jmh dotty/tests/run/specialized-traits-benchmark.scala +// May have to run it again if you get a class not found error from scala-cli first time. +//> using scala 3.8.3-RC1-bin-SNAPSHOT-nonbootstrapped + +package bench + +import org.openjdk.jmh.annotations.* +import java.util.concurrent.TimeUnit + +class VecManual(elems: Array[Int]): + private val num = summon[Numeric[Int]] + private val x = 1 + + def length = elems.length + + def apply(i: Int): Int = elems(i) + + def scalarProduct(other: VecManual): Int = + require(this.length == other.length) + var result = num.fromInt(0) + for i <- 0 until length do + result = num.plus(result, num.times(this(i), other(i))) + result + +class VecGeneric[T: Numeric](elems: Array[T]): + private val num = summon[Numeric[T]] + private val x = 1 + + def length = elems.length + + def apply(i: Int): T = elems(i) + + def scalarProduct(other: VecGeneric[T]): T = + require(this.length == other.length) + var result = num.fromInt(0) + for i <- 0 until length do + result = num.plus(result, num.times(this(i), other(i))) + result + +inline trait VecSpecialized[T: {Specialized, Numeric}](elems: Array[T]): + private val num = summon[Numeric[T]] + private val x = 1 + + def length = elems.length + + def apply(i: Int): T = elems(i) + + def scalarProduct(other: VecSpecialized[T]): T = + require(this.length == other.length) + var result = num.fromInt(0) + for i <- 0 until length do + result = num.plus(result, num.times(this(i), other(i))) + result + +@State(Scope.Benchmark) +class Arrays: + var arr1 = Array.fill(100_000_000) {math.round(math.random().floatValue * 4)} + var arr2 = Array.fill(100_000_000) {math.round(math.random().floatValue * 4)} + val target = arr1.zip(arr2).map((x, y) => x * y).fold(0)(_ + _) + +@State(Scope.Benchmark) +@BenchmarkMode(Array(Mode.AverageTime)) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 15, time = 100, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +class VecBench: + @Benchmark + def manual(arr: Arrays) = + val x = VecManual(arr.arr1) + val y = VecManual(arr.arr2) + assert(x.scalarProduct(y) == arr.target) + + @Benchmark + def generic(arr: Arrays) = + val x = VecGeneric[Int](arr.arr1) + val y = VecGeneric[Int](arr.arr2) + assert(x.scalarProduct(y) == arr.target) + + @Benchmark + def specialized(arr: Arrays) = + val x = new VecSpecialized[Int](arr.arr1) {} + val y = new VecSpecialized[Int](arr.arr2) {} + assert(x.scalarProduct(y) == arr.target) From 10b8d8498553e69d2f2e52983da4fdf3de413bb8 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 23 Mar 2026 17:37:06 +0100 Subject: [PATCH 057/254] Move benchmark to a better location --- .../dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename tests/run/specialized-traits-benchmark.scala => bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala (92%) diff --git a/tests/run/specialized-traits-benchmark.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala similarity index 92% rename from tests/run/specialized-traits-benchmark.scala rename to bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala index 1a7452e31e60..bc8356e39cc9 100644 --- a/tests/run/specialized-traits-benchmark.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala @@ -1,8 +1,9 @@ // Run with: scala-cli --power --jmh dotty/tests/run/specialized-traits-benchmark.scala -// May have to run it again if you get a class not found error from scala-cli first time. +// May have to run it again / delete .scala-build and rerun if you get a class not found +// error from scala-cli first time - the --jmh flag is still experimental. //> using scala 3.8.3-RC1-bin-SNAPSHOT-nonbootstrapped -package bench +package dotty.tools.benchmarks import org.openjdk.jmh.annotations.* import java.util.concurrent.TimeUnit From 85aeeaf49520e468f0c22978ef64e36e901dd337 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 23 Mar 2026 18:30:09 +0100 Subject: [PATCH 058/254] Add run test which checks if correct method is called by reflection / stack trace --- ...aits-check-specialized-method-called.scala | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/run/specialized-traits-check-specialized-method-called.scala diff --git a/tests/run/specialized-traits-check-specialized-method-called.scala b/tests/run/specialized-traits-check-specialized-method-called.scala new file mode 100644 index 000000000000..9b5c153427a4 --- /dev/null +++ b/tests/run/specialized-traits-check-specialized-method-called.scala @@ -0,0 +1,24 @@ +// Check we actually call the specialized method that we should be calling +// We can't easily check the return type, but we can check that we are calling a method +// on Foo$impl$Int$ directly, with no bridge methods in between (this means it's the correct +// method without boxing / unboxing). + +inline trait Foo[T: Specialized](x: T): + def foo = + val stackTrace = Thread.currentThread.getStackTrace() + + // No bridge methods; we call foo directly + assert(stackTrace.toList.tail.takeWhile(call => call.getMethodName().startsWith("foo")).length == 1) + + // We call this method on the correct impl class + assert(Thread.currentThread.getStackTrace()(1).getClassName() == "Foo$impl$Int$") + x + +def f(b: Foo[Int]) = + 37 + b.foo + +object Test: + def main(args: Array[String]): Unit = { + val x = new Foo[Int](42) {} + f(x) + } From baf2e01b1674d32f45c135bfe1f1c4bbc404c201 Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 25 Mar 2026 18:38:55 +0100 Subject: [PATCH 059/254] Move inline traits to run before specialized traits, loop specialized where necessary, tidy --- compiler/src/dotty/tools/dotc/Compiler.scala | 21 +- .../dotty/tools/dotc/inlines/Inlines.scala | 20 +- .../transform/DesugarSpecializedTraits.scala | 341 ++++++++++-------- .../ReplaceInlinedTraitSymbols.scala | 1 + .../transform/SpecializeInlineTraits.scala | 77 ++-- tests/pos/inline-trait-anonymous-class.scala | 10 + ...ining-causes-implementation-required.scala | 14 + ...trait-inlining-causes-specialization.scala | 9 + tests/pos/specialized-trait-simplest.scala | 4 + 9 files changed, 292 insertions(+), 205 deletions(-) create mode 100644 tests/pos/inline-trait-anonymous-class.scala create mode 100644 tests/pos/specialized-trait-inlining-causes-implementation-required.scala create mode 100644 tests/pos/specialized-trait-inlining-causes-specialization.scala create mode 100644 tests/pos/specialized-trait-simplest.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 9ecc2f83de1f..395c131c1455 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -49,19 +49,16 @@ class Compiler { /** Phases dealing with TASTY tree pickling and unpickling */ protected def picklerPhases: List[List[Phase]] = - List(new Pickler) :: // Generate TASTY info - List(new sbt.ExtractAPI) :: // Sends a representation of the API of classes to sbt via callbacks - //TODO: Don't try this at home obviously - List(new DesugarSpecializedTraits) :: // Processes the Specialized annotation - List(new SpecializeInlineTraits) :: // Inline the code of inline traits into their children - List(new DesugarSpecializedTraits) :: // Processes the Specialized annotation - List(new SpecializeInlineTraits) :: // Inline the code of inline traits into their children + List(new Pickler) :: // Generate TASTY info + List(new sbt.ExtractAPI) :: // Sends a representation of the API of classes to sbt via callbacks + List(new SpecializeInlineTraits) :: // Inline the code of inline traits into their children + List(new DesugarSpecializedTraits) :: // Process the Specialized annotation List(new ReplaceInlinedTraitSymbols) :: // Replace symbols referring to inline trait members with resulting inlined member symbols - List(new Inlining) :: // Inline and execute macros - List(new PostInlining) :: // Add mirror support for inlined code - List(new Staging) :: // Check staging levels and heal staged types - List(new Splicing) :: // Replace level 1 splices with holes - List(new PickleQuotes) :: // Turn quoted trees into explicit run-time data structures + List(new Inlining) :: // Inline and execute macros + List(new PostInlining) :: // Add mirror support for inlined code + List(new Staging) :: // Check staging levels and heal staged types + List(new Splicing) :: // Replace level 1 splices with holes + List(new PickleQuotes) :: // Turn quoted trees into explicit run-time data structures Nil /** Phases dealing with the transformation from pickled trees to backend trees */ diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index a2a1a16d2e1c..239c7651be53 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -247,7 +247,7 @@ object Inlines: tree2 end inlineCall - def updateFlagsFromInlinedParent(child: FlagSet, parent: FlagSet): FlagSet = + private def updateFlagsFromInlinedParent(child: FlagSet, parent: FlagSet): FlagSet = var updatedFlags = child // Parent needs to be initialised so child must also as initialisers have been inlined if (!parent.is(NoInits)) @@ -258,6 +258,24 @@ object Inlines: updatedFlags &~= PureInterface updatedFlags + + def transformInlineTrait(inlineTrait: TypeDef)(using Context): TypeDef = + val tpd.TypeDef(_, tmpl: Template) = inlineTrait: @unchecked + val body1 = tmpl.body.flatMap { + // case innerClass: TypeDef if innerClass.symbol.isClass => + // val newTrait = makeTraitFromInnerClass(innerClass) + // val newType = makeTypeFromInnerClass(inlineTrait.symbol, innerClass, newTrait.symbol) + // List(newTrait, newType) + case member: MemberDef => + List(member) + case _ => + // Remove non-memberdefs, as they are normally placed into $init() + Nil + } + val tmpl1 = cpy.Template(tmpl)(body = body1) + cpy.TypeDef(inlineTrait)(rhs = tmpl1) + end transformInlineTrait + def inlineParentInlineTraits(cls: Tree)(using Context): Tree = cls match { case cls @ tpd.TypeDef(_, impl: Template) => diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 4fb16314e691..e6df1f045ed0 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -30,7 +30,8 @@ import dotty.tools.dotc.typer.Typer import dotty.tools.dotc.core.NameKinds import dotty.tools.dotc.core.Flags.GivenOrImplicit import dotty.tools.dotc.core.NameKinds.ContextBoundParamName - +import dotty.tools.dotc.inlines.Inlines +import dotty.tools.dotc.util.Spans.Span class DesugarSpecializedTraits extends MacroTransform: @@ -38,6 +39,7 @@ class DesugarSpecializedTraits extends MacroTransform: override def description: String = DesugarSpecializedTraits.description override def changesMembers: Boolean = false override def changesParents: Boolean = true + override def runsAfter: Set[String] = Set("specializeInlineTraits") override def allowsImplicitSearch: Boolean = true override def run(using Context): Unit = @@ -47,7 +49,7 @@ class DesugarSpecializedTraits extends MacroTransform: override def newTransformer(using Context): Transformer = new Transformer { - private def newSpecializedTraitInterfaceTrait(specialization: Specialization) = + private def newInterfaceTrait(specialization: Specialization) = val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. def apply(t: Type) = specialization.specializedTypeParamsToTypeArgumentsMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) // TODO: IF we can do just types we can get rid fo this @@ -83,13 +85,13 @@ class DesugarSpecializedTraits extends MacroTransform: traitSymbol.info = ClassInfo(traitSymbol.owner.thisType, traitSymbol, traitSymbol.info.parents.map(freshTypeVarMap(_)), traitSymbol.info.decls) // TODO: What happens if the creator of the specialized inline trait provides a self type? traitSymbol.entered - private def buildSpTraitTree(specialization: Specialization, generatedTraitSymbol: ClassSymbol)(using Context) = { - val init = newDefaultConstructor(generatedTraitSymbol) + private def buildInterfaceTraitTree(interfaceSymbol: ClassSymbol)(using Context) = { + val init = newDefaultConstructor(interfaceSymbol) // Fix constructor so that it: // 1) Has correct generic type parameters // 2) Returns the correct type corresponding to those type parameters applied to this trait - val rt = generatedTraitSymbol.typeRef.appliedTo(generatedTraitSymbol.typeParams.map(_.typeRef)) + val rt = interfaceSymbol.typeRef.appliedTo(interfaceSymbol.typeParams.map(_.typeRef)) def resultType(tpe: Type): Type = tpe match { case mt @ MethodType(paramNames) => mt.derivedLambdaType(paramNames, mt.paramInfos, rt) case pt : PolyType => pt.derivedLambdaType(pt.paramNames, pt.paramInfos, resultType(pt.resType)) @@ -102,21 +104,20 @@ class DesugarSpecializedTraits extends MacroTransform: // able to copy them over, apply the specialization (keeping e.g. Numeric[Int] that arises from this) and // pruning any that belong to Specialized. - ClassDef(generatedTraitSymbol, DefDef(init.entered), Nil) + ClassDef(interfaceSymbol, DefDef(init.entered), Nil) } - // TODO: Do we want to share some code with the newSpecializedInterfaceTrait and buildSpTraitTree? - // TODO: Standardise a bit so that we either generate the symbols and later the classes or not. - private def buildImplClassTree(specialization: Specialization, generatedTraitSymbol: ClassSymbol)(using Context) = { - - // Create new class + private def generateImplementationClassParents(specialization: Specialization, interfaceSymbol: ClassSymbol) = val objectParent = defn.ObjectType - val traitSpParent = generatedTraitSymbol.typeRef.appliedTo(specialization.unspecializedTypeArgs.map(_.tpe)) + val traitSpParent = interfaceSymbol.typeRef.appliedTo(specialization.unspecializedTypeArgs.map(_.tpe)) val originalTraitSpecializedParent = AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.typeArguments).tpe + (objectParent, traitSpParent, originalTraitSpecializedParent) + private def newImplementationClass(specialization: Specialization, interfaceSymbol: ClassSymbol) = + val (objectParent, traitSpParent, originalTraitSpecializedParent) = generateImplementationClassParents(specialization, interfaceSymbol) val parents = List(objectParent, traitSpParent, originalTraitSpecializedParent) - val classSymbol = newNormalizedClassSymbol( + newNormalizedClassSymbol( specialization.traitSymbol.owner, DesugarSpecializedTraits.newImplementationClassName(specialization), Flags.Synthetic, @@ -124,9 +125,12 @@ class DesugarSpecializedTraits extends MacroTransform: NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? specialization.traitSymbol.privateWithin, // TODO: Do we need a compUnit info? - ) - + ).entered + // TODO: Do we want to share some code with the newSpecializedInterfaceTrait and buildInterfaceTraitTree? + // TODO: Standardise a bit so that we either generate the symbols and later the classes or not. + private def buildImplementationClassTree(specialization: Specialization, interfaceSymbol: ClassSymbol, classSymbol: ClassSymbol)(using Context) = { + val (objectParent, traitSpParent, originalTraitSpecializedParent) = generateImplementationClassParents(specialization, interfaceSymbol) val init = newDefaultConstructor(classSymbol) val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. @@ -162,6 +166,8 @@ class DesugarSpecializedTraits extends MacroTransform: ) ) + val newParams1 = if (newParamss.length == 1) then newParamss ++ List(List()) else newParamss + // TODO: Clean adn robust val classDef = ClassDefWithParents( classSymbol, @@ -173,7 +179,7 @@ class DesugarSpecializedTraits extends MacroTransform: .select(TermRef(originalTraitSpecializedParent.typeConstructor, specialization.traitSymbol.primaryConstructor.asTerm)) // TODO: Check for other constructors .appliedToTypes(originalTraitSpecializedParent.argTypes) // .appliedToArgss(paramAccessors.map(_.map(ref))) - .appliedToArgss(newParamss) + .appliedToArgss(newParams1) // TODO: What about potential custom typeclass instances? How do we balance that with generating another version of the class every time? Probably just generate the basic version and then let them apply their own version want (based on some kind of hashing). Then we generate a whole new impl class / or anon class which is still specialised to their instances that they provided, at the time that we see it? // To be honest if our assumption is that we aren't very often going to do anything weird we can just always generate the class at the point of use, with the evidences specialized (but only if we don't ahve that one already - i.e. effectively consider the evidences as part of the name) @@ -181,124 +187,118 @@ class DesugarSpecializedTraits extends MacroTransform: // Put into body of class paramAccessorss.flatMap(syms => syms.map(sym => tpd.ValDef(sym.asTerm))) // .withFlags(Flags.LocalParamAccessor).withType(sym.info) ) - (classDef, classSymbol) + classDef } - override def transform(tree: Tree)(using Context): Tree = tree - match { - case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. - // HACK/TODO: Remove - // In the future we want to cache these on the class path - val existing = pkg.stats.flatMap({ - case t@TypeDef(name, rhs) if name.toString().contains("$sp$") => Some((name.toTypeName.toString, t.symbol.asClass)) - case _ => None - }).toMap - - val specializedSymbols = generateSpecializedTraitSymbols(pkg, existing) - - - // TODO: Make consistent in terms of when we generate the symbols vs the definitions - val generatedTraitStats = - if (specializedSymbols.getSpecializations.size != existing.size) // TODO: bin - specializedSymbols.getSpecializations.map(buildSpTraitTree) - else - List() - - val (generatedClassStats, classSymbols) = specializedSymbols.getSpecializationsForImplementation.map(buildImplClassTree).unzip - val implMap = specializedSymbols.getSpecializationsForImplementation.map(_._1).zip(classSymbols).toMap - - - // Use the TreeTypeMap to replace instances (can we do this without accidentally replacing the definitions? I think it should be ok) - val typeMap = new TypeMap: - def apply(t: Type) = t match { - case Specialization(spec) => - { - for (specializedSymbol <- specializedSymbols.get(spec)) - yield specializedSymbol.typeRef.appliedTo(spec.unspecializedTypeArgs.map(_.tpe)) - }.getOrElse(mapOver(t)) - case _ => mapOver(t) - } - - def treeMap(tree: Tree): Tree = tree match { - // Replace (anonymous class version of) new Foo[Int] {} with new Foo$impl$Int.asInstanceOf[Foo$sp$Int] - case Block(List(TypeDef(anon, Template(_, parentCalls: List[Tree], _, _))), - Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => - parentCalls(1) match { // only allowed to extend Object and our specialized trait - case Apply(Apply(tpe, ctorArgs), ev) => - val spec = Specialization.unapply(t.tpe).get - val specializedMap = implMap - Typed(Apply(Apply(Select(New(ref(implMap(spec))),ctor), ctorArgs), ev), t) - case _ => tree - } - - // Replace class Bar extends Foo[Int](params) with class Bar extends Foo$sp$Int(params) - // Note: We always drop the evidence params when creating these new specialized traits so we know that there are none, but we may need to revisit this if we decide we do want to copy the evidence parameters over - case Apply(TypeApply(fun@Select(New(tpt), init), args), ev) if fun.symbol.isConstructor => - val spec = Specialization(fun.symbol.owner, args) - val r = { - for (specializedSymbol <- specializedSymbols.get(spec)) - yield New(ref(specializedSymbol)).select(init).appliedToTypeTrees(spec.unspecializedTypeArgs) + // Returns (new stmts including original, new symbols including original) + private def transformStatements(stats: List[Tree], span: Span, specializations: SpecializedTraitCache): (List[Tree], SpecializedTraitCache) = { + val specializations1 = collectReferencedSpecializations(stats, specializations) + val generatedTraitStats = specializations1.getNewInterfaceSymbols.toList.map(buildInterfaceTraitTree) + val generatedClassStats = specializations1.getNewImplementationSymbols.toList.map(buildImplementationClassTree) + + val specializations2 = specializations1.installNewInterfaceSymbols.installNewImplementationSymbols + + // Use the TreeTypeMap to replace instances (can we do this without accidentally replacing the definitions? I think it should be ok) + val typeMap = new TypeMap: + def apply(t: Type) = t match { + case Specialization(spec) => + { + for (specializedSymbol <- specializations2.getInterfaceSymbol(spec)) + yield specializedSymbol.typeRef.appliedTo(spec.unspecializedTypeArgs.map(_.tpe)) + }.getOrElse(mapOver(t)) + case _ => mapOver(t) + } + + def treeMap(tree: Tree): Tree = tree match { + // Replace (anonymous class version of) new Foo[Int] {} with new Foo$impl$Int.asInstanceOf[Foo$sp$Int] + case Block(List(TypeDef(anon, Template(_, parentCalls: List[Tree], _, _))), + Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => + parentCalls(1) match { // only allowed to extend Object and our specialized trait + case Apply(Apply(tpe, ctorArgs), ev) => + val spec = Specialization.unapply(t.tpe).get + { // We don't replace non-specialized anonymous class instantiations e.g. new Foo[T] where T is defined in the enclosing scope. + for (specializedSymbol <- specializations2.getImplementationSymbol(spec)) + yield Typed(Apply(Apply(Select(New(ref(specializedSymbol)),ctor), ctorArgs), ev), t) }.getOrElse(tree) - r + case _ => tree + } + + // Replace class Bar extends Foo[Int](params) with class Bar extends Foo$sp$Int(params) + // Note: We always drop the evidence params when creating these new specialized traits so we know that there are none, but we may need to revisit this if we decide we do want to copy the evidence parameters over + case Apply(TypeApply(fun@Select(New(tpt), init), args), ev) if fun.symbol.isConstructor => + val spec = Specialization(fun.symbol.owner, args) + { + for (specializedSymbol <- specializations2.getInterfaceSymbol(spec)) + yield New(ref(specializedSymbol)).select(init).appliedToTypeTrees(spec.unspecializedTypeArgs) + }.getOrElse(tree) + + // Replace AppliedTypeTree instances in code + case Specialization(spec) => { + for (specializedSymbol <- specializations2.getInterfaceSymbol(spec)) + yield AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? + }.getOrElse(tree) + + case tree => tree + } + + val treeTypeMap = new TreeTypeMap(typeMap, treeMap) { + override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? + case dd@DefDef(name, paramss, tpt, preRhs) => + val transformedDef = super.transform(dd) + transformedDef.symbol.info = mapType(transformedDef.symbol.info) + transformedDef + + // TODO: Fix the Bar extends Foo case. Avoid updating the parents that we want to keep the same. + case impl@Template(constr, preParentsOrDerived, self, _) => + cpy.Template(impl)(body = impl.body.map(super.transform(_))) + case tree => super.transform(tree) + } + } - // Replace AppliedTypeTree instances in code - case Specialization(spec) => { - for (specializedSymbol <- specializedSymbols.get(spec)) - yield AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? - }.getOrElse(tree) - case tree => tree - } - - val treeTypeMap = new TreeTypeMap(typeMap, treeMap) { - override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? - case dd@DefDef(name, paramss, tpt, preRhs) => - val transformedDef = super.transform(dd) - transformedDef.symbol.info = mapType(transformedDef.symbol.info) - transformedDef - case impl@Template(constr, preParentsOrDerived, self, _) => // TODO/HACK: Remove with existing - cpy.Template(impl)(body = impl.body.map(super.transform(_))) - case tree => super.transform(tree) - } - } - cpy.PackageDef(pkg)(pid, generatedTraitStats ++ generatedClassStats ++ stats.map(treeTypeMap(_))) // TODO: Do we also want to apply the map over generatedTraitStats?? + // TODO: How do we calculate the spans correctly? + val generatedTraitStats1 = generatedTraitStats.map(trtDef => Inlines.inlineParentInlineTraits(Inlines.transformInlineTrait(trtDef.withSpan(span)))) + val generatedClassStats1 = generatedClassStats.map(clsDef => Inlines.inlineParentInlineTraits(clsDef.withSpan(span))) + + if (generatedTraitStats1.isEmpty && generatedClassStats1.isEmpty) + (stats.map(treeTypeMap(_)), specializations2) + else + val (generatedTraitStats2, specializations3) = transformStatements(generatedTraitStats1, span, specializations2) + val (generatedClassStats2, specializations4) = transformStatements(generatedClassStats1, span, specializations3) + (generatedTraitStats2 ++ generatedClassStats2 ++ stats.map(treeTypeMap(_)), specializations4) + } + + override def transform(tree: Tree)(using Context): Tree = tree + match { // TODO: Is Package level processing really what we want? Given we are not going to output the classes somewhere else do we not really want either to deepFold the whole tree directly or do a more direct transform? + case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. + val (stats1, _) = transformStatements(stats, tree.span, SpecializedTraitCache(genInterfaceSymbol = newInterfaceTrait, genImplementationSymbol = newImplementationClass)) // TODO: Fix span + cpy.PackageDef(pkg)(pid, stats1) } + // TODO: There is a case where recursive expansion causes something to need an implementation where it didn't before. + + // TODO: Try with just generating new Foo(100) with no function to pass it to and no other references to Foo. this may not work because we might not // correctly detect it. // TODO : Is it not better to just delete the Specialized? - private def generateSpecializedTraitSymbols(tree: Tree, existing: Map[String, ClassSymbol])(using Context): SpecializedTraitCache = // HACK/TODO: Remove existing - tree.deepFold(SpecializedTraitCache())((foundSpecs, tree) => tree match - // case Typed(Apply(Select(New(anon),ctor),List()), t: TypeTree) => - // val z = anon.symbol - // val f = anon.symbol.isAnonymousClass - // foundSpecs - case Typed(Apply(Select(New(anon),ctor),List()), t: TypeTree) if anon.symbol.isAnonymousClass => - val maybeSpec = Specialization.unapply(t.tpe) - - maybeSpec.foreach( spec => - if (spec.hasSpecializedParams && !foundSpecs.contains(spec)) { - foundSpecs.add(spec, newSpecializedTraitInterfaceTrait(spec)) + private def collectReferencedSpecializations(stats: List[Tree], specializations: SpecializedTraitCache)(using Context): SpecializedTraitCache = + stats.foldLeft(specializations)((specializations, tree) => { + tree.deepFold(specializations)((specializations, tree) => tree match + case Typed(Apply(Select(New(anon),ctor),List()), t: TypeTree) if anon.symbol.isAnonymousClass => + t.tpe match { + case Specialization(spec) if spec.isSpecialized => specializations.addInterfaceAndImplementation(spec) + case _ => specializations } - foundSpecs.flagForImplementation(spec) // TODO: Need to think carefully about the behaviour when we are integrating libraries - should the library generate the implementation classes or the user? - // In any case we need to read back in either the $sp$ classes or the $impl$ traits to be able to work with them. - ) - foundSpecs - - // Is this fold going to be a problem? Or juist a good thing? Because we hit the child first - // // I guess ideally do this after already processing it down to the ArrayIterator$sp$Int then we just replace that with ArraytIterator - - // TODO: In theory since we are going to apply the tree type map anyway we can surely just collect up the specialisations we need and then later generate the new symbols? - // I think that's slightly cleaner. - case Specialization(spec) if (spec.isSpecialized && !foundSpecs.contains(spec)) => - if (existing.contains(DesugarSpecializedTraits.newSpecializedTraitName(spec).toString)) - then foundSpecs.add(spec, existing(DesugarSpecializedTraits.newSpecializedTraitName(spec).toString)) // TODO: Bin this obviosuly - else foundSpecs.add(spec, newSpecializedTraitInterfaceTrait(spec)) - case _ => foundSpecs - ) + case Specialization(spec) if (spec.isSpecialized) => specializations.addInterface(spec) + case _ => specializations + ) + }) } +end DesugarSpecializedTraits + // TODO: Need to think carefully about the behaviour when we are integrating libraries - should the library generate the implementation classes or the user? + // In any case we need to read back in either the $sp$ classes or the $impl$ traits to be able to work with them. object DesugarSpecializedTraits: val name: String = "desugarSpecializedTraits" @@ -316,29 +316,69 @@ object DesugarSpecializedTraits: generateName(specialization, str.SPECIALIZED_TRAIT_IMPL_SUFFIX) -class SpecializedTraitCache: - private val specializationMap: mutable.Map[Specialization, ClassSymbol] = mutable.Map.empty - private val flaggedForImplementation: mutable.Set[Specialization] = mutable.Set.empty - - def contains(specialization: Specialization)(using Context) = - specializationMap.contains(specialization) - - def add(specialization: Specialization, specializedSymbol: ClassSymbol)(using Context): SpecializedTraitCache = { - specializationMap(specialization) = specializedSymbol - this - } - - def get(specialization: Specialization)(using Context) = specializationMap.get(specialization) - - def getSpecializations: List[(Specialization, ClassSymbol)] = specializationMap.toList - - def flagForImplementation(spec: Specialization) = flaggedForImplementation.add(spec) - - def getSpecializationsForImplementation = flaggedForImplementation.toList.map(spec => (spec, specializationMap(spec))) +/* + Stores the specializations we have found in the program and the symbols for the interface traits and implementation classes + that will replace them. We generate these symbols when we enter the specializations into the cache, via the functions + we store in genInterfaceSymbol and genImplementationSymbol. + + Model: Contains two levels: + - interface/implementation symbols we have found since the last installNewInterface/ImplementationSymbols call + (i.e. typically on this iteration) ("new") + - Those we found prior to that call + + Invariant: (newImplementationSymbols \cup implementationSymbols) \subseteq (interfaceSymbols \cup newInterfaceSymbols). + This is enforced by only providing addInterface and addInterfaceAndImplementation, and allows the unchecked get in + getNeWImplementationSymbols. + +*/ +class SpecializedTraitCache( + private val newInterfaceSymbols: Map[Specialization, ClassSymbol] = Map.empty, + private val newImplementationSymbols: Map[Specialization, ClassSymbol] = Map.empty, + private val interfaceSymbols: Map[Specialization, ClassSymbol] = Map.empty, + private val implementationSymbols: Map[Specialization, ClassSymbol] = Map.empty, + private val genInterfaceSymbol: Specialization => ClassSymbol, + private val genImplementationSymbol: (Specialization, ClassSymbol) => ClassSymbol +): + def copy( + newInterfaceSymbols: Map[Specialization, ClassSymbol] = this.newInterfaceSymbols, + newImplementationSymbols: Map[Specialization, ClassSymbol] = this.newImplementationSymbols, + interfaceSymbols: Map[Specialization, ClassSymbol] = this.interfaceSymbols, + implementationSymbols: Map[Specialization, ClassSymbol] = this.implementationSymbols, + genInterfaceSymbol: Specialization => ClassSymbol = this.genInterfaceSymbol, + genImplementationSymbol: (Specialization, ClassSymbol) => ClassSymbol = this.genImplementationSymbol) + = SpecializedTraitCache(newInterfaceSymbols, newImplementationSymbols, interfaceSymbols, implementationSymbols, genInterfaceSymbol, genImplementationSymbol) + + def getInterfaceSymbol(spec: Specialization): Option[ClassSymbol] = newInterfaceSymbols.orElse(interfaceSymbols).lift(spec) + def getImplementationSymbol(spec: Specialization): Option[ClassSymbol] = newImplementationSymbols.orElse(implementationSymbols).lift(spec) + + def getNewInterfaceSymbols = newInterfaceSymbols.values + def getNewImplementationSymbols: List[(Specialization, ClassSymbol, ClassSymbol)] = newImplementationSymbols.map((k, v) => (k, getInterfaceSymbol(k).get, v)).toList + + def addInterface(spec: Specialization): SpecializedTraitCache = + if (newInterfaceSymbols.contains(spec) || interfaceSymbols.contains(spec)) then + this + else + this.copy(newInterfaceSymbols = newInterfaceSymbols + (spec -> genInterfaceSymbol(spec))) + def addInterfaceAndImplementation(spec: Specialization): SpecializedTraitCache = + if (newImplementationSymbols.contains(spec) || implementationSymbols.contains(spec)) then + this + else + println(s"adding spec ${spec.specialization}") + val withInterface = addInterface(spec) + withInterface.copy(newImplementationSymbols = withInterface.newImplementationSymbols + (spec -> genImplementationSymbol(spec, withInterface.getInterfaceSymbol(spec).get))) + + def installNewInterfaceSymbols = + this.copy( + newInterfaceSymbols = Map.empty, + interfaceSymbols = interfaceSymbols ++ newInterfaceSymbols) + + def installNewImplementationSymbols = + this.copy( + newImplementationSymbols = Map.empty, + implementationSymbols = implementationSymbols ++ newImplementationSymbols) end SpecializedTraitCache - /* Represents an application traitSymbol[typeArguments] */ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(using Context): // TODO: Can we get away with List[Type] object SpecializedEvidence { @@ -369,7 +409,8 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(usi // If inline trait Foo[T] has a method taking another Foo[T] there's no point specializing the reference // since the resulting sp$T$ would be the same as the starting trait. - def isSpecialized: Boolean = hasSpecializedParams && !(typeArguments.zip(traitSymbol.typeParams).forall(_ .tpe =:= _.typeRef)) + def isSpecialized: Boolean = + hasSpecializedParams && typeArguments.exists(tpt => !tpt.symbol.isTypeParam) // .zip(traitSymbol.typeParams).forall((t, s) => t.tpe =:= s.typeRef)) // Note: We only care about the specialized arguments for equality; a specialization of Vec[A: Specialized, B] with B = Int and one // with B = String can be considered to be the same as they use the same specialized trait @@ -381,6 +422,7 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(usi override def hashCode(): Int = (traitSymbol, specializedTypeArgs.map(_.tpe.widen.dealias.show)).hashCode() // TODO: Consider not using show for this for performance reasons (correctness also?) +end Specialization object Specialization: def unapply(tpt: Tree)(using Context): Option[Specialization] = tpt match { @@ -393,7 +435,7 @@ object Specialization: case AppliedType(tycon: Type, args: List[Type]) => Some(Specialization(tycon.typeSymbol, args.map(TypeTree(_)))) case _ => None } - +end Specialization // Would be nice to define a Specialization class I think // -> Map the specialized type params to Int etc @@ -430,7 +472,7 @@ object Specialization: // Would it be better to just copy rather than creating everything from scratch? I think this is right -// 1. Figure out which specialisations we need to generate +// 1. Figure out which specializations we need to generate // 2. Generate ArrayIterator$sp$Int and ArrayIterator$impl$Int wherever they live // 3. Replace ArrayIterator[Int] with ArrayIterator$sp$Int // 4. Replace new ArrayIterator[Int](xs) {} with new ArrayIterator$impl$Int(xs) {} @@ -478,8 +520,8 @@ object Specialization: // it's a very unlikely usecase and would fix the mixin problem - although // we could just try and fix the mixin problem properly. -// TODO: Create a proper benchmark -// TOOD: probably need to do this from Timothée private members are not renamed, only private parameter accessors are; +// TOOD: probably need to do this from Timothée: + // - private members are not renamed, only private parameter accessors are; // this needs to be changed so that all overridable private members are renamed // Implement postphase checks for inline traits // TODO: Don't synthesize specialized instances for random generic types probably - as Hamza said we want to be able to control the specialization @@ -487,3 +529,12 @@ object Specialization: // TODO: Check that when we extend parents we actually do so in the specialized forms. // TODO: Fix specialized-trait-collections-example.scala // TODO: Test extensively with inline methods and inline traits. + +// TODO: + // Need to enforce these: +// - can extend only a single specialized trait, +// - cannot mix in further classes or traits, and +// - cannot contain member definitions. +// I'm not sure we strictly need them though. + +// TODO: Put classes onto the classpath as desired. diff --git a/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala b/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala index 0390ccc12882..d93bd16c8879 100644 --- a/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala +++ b/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala @@ -27,6 +27,7 @@ class ReplaceInlinedTraitSymbols extends MiniPhase: override def description: String = ReplaceInlinedTraitSymbols.description override def changesMembers: Boolean = true override def changesParents: Boolean = true + override def runsAfter: Set[String] = Set("desugarSpecializedTraits", "specializeInlineTraits") override def transformSelect(tree: Select)(using Context): Tree = val qualType = tree.qualifier.tpe.widenDealias diff --git a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala index 3092349e19b8..356a49a8b7d9 100644 --- a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala @@ -31,7 +31,6 @@ class SpecializeInlineTraits extends MacroTransform, SymTransformer { override def changesParents: Boolean = true - override def runsAfter: Set[String] = Set("desugarSpecializedTraits") override def run(using Context): Unit = try super.run @@ -40,12 +39,13 @@ class SpecializeInlineTraits extends MacroTransform, SymTransformer { override def newTransformer(using Context): Transformer = new Transformer { override def transform(tree: Tree)(using Context): Tree = tree match { case tree: TypeDef if tree.symbol.isInlineTrait => - val tree1 = transformInlineTrait(tree) + val tree1 = Inlines.transformInlineTrait(tree) if Inlines.needsInlining(tree1) then Inlines.inlineParentInlineTraits(tree1) else tree1 case tree: TypeDef if Inlines.needsInlining(tree) => val tree1 = super.transform(tree).asInstanceOf[TypeDef] if tree1.tpe.isError then tree1 - else if tree1.symbol.isInlineTrait then transformInlineTrait(tree1) + else if tree1.symbol.isInlineTrait then + Inlines.inlineParentInlineTraits(Inlines.transformInlineTrait(tree1)) else Inlines.inlineParentInlineTraits(tree1) case _ => super.transform(tree) } @@ -63,50 +63,33 @@ class SpecializeInlineTraits extends MacroTransform, SymTransformer { case _ => } - private def transformInlineTrait(inlineTrait: TypeDef)(using Context): TypeDef = - val tpd.TypeDef(_, tmpl: Template) = inlineTrait: @unchecked - val body1 = tmpl.body.flatMap { - case innerClass: TypeDef if innerClass.symbol.isClass => - val newTrait = makeTraitFromInnerClass(innerClass) - val newType = makeTypeFromInnerClass(inlineTrait.symbol, innerClass, newTrait.symbol) - List(newTrait, newType) - case member: MemberDef => - List(member) - case _ => - // Remove non-memberdefs, as they are normally placed into $init() - Nil - } - val tmpl1 = cpy.Template(tmpl)(body = body1) - cpy.TypeDef(inlineTrait)(rhs = tmpl1) - end transformInlineTrait - - private def makeTraitFromInnerClass(innerClass: TypeDef)(using Context): TypeDef = - val TypeDef(name, tmpl: Template) = innerClass: @unchecked - val newInnerParents = tmpl.parents.mapConserve(ConcreteParentStripper.apply) - val tmpl1 = cpy.Template(tmpl)(parents = newInnerParents) // TODO .withType(???) - val newTrait = cpy.TypeDef(innerClass)(name = SpecializeInlineTraits.newInnerClassName(name), rhs = tmpl1) - newTrait.symbol.setFlag(Synthetic) - newTrait - end makeTraitFromInnerClass - - private def makeTypeFromInnerClass(parentSym: Symbol, innerClass: TypeDef, newTraitSym: Symbol)(using Context): TypeDef = - val upperBound = innerClass.symbol.primaryConstructor.info match { - case _: MethodType => - newTraitSym.typeRef - case poly: PolyType => - HKTypeLambda(poly.paramNames)(tl => poly.paramInfos, tl => newTraitSym.typeRef.appliedTo(tl.paramRefs.head)) - } - val newTypeSym = newSymbol( - owner = parentSym, - name = newTraitSym.name.asTypeName, - flags = innerClass.symbol.flags & (Private | Protected) | Synthetic, - info = TypeBounds.upper(upperBound), - privateWithin = innerClass.symbol.privateWithin, - coord = innerClass.symbol.coord, - nestingLevel = innerClass.symbol.nestingLevel, - ).asType - TypeDef(newTypeSym) - end makeTypeFromInnerClass + // private def makeTraitFromInnerClass(innerClass: TypeDef)(using Context): TypeDef = + // val TypeDef(name, tmpl: Template) = innerClass: @unchecked + // val newInnerParents = tmpl.parents.mapConserve(ConcreteParentStripper.apply) + // val tmpl1 = cpy.Template(tmpl)(parents = newInnerParents) // TODO .withType(???) + // val newTrait = cpy.TypeDef(innerClass)(name = SpecializeInlineTraits.newInnerClassName(name), rhs = tmpl1) + // newTrait.symbol.setFlag(Synthetic) + // newTrait + // end makeTraitFromInnerClass + + // private def makeTypeFromInnerClass(parentSym: Symbol, innerClass: TypeDef, newTraitSym: Symbol)(using Context): TypeDef = + // val upperBound = innerClass.symbol.primaryConstructor.info match { + // case _: MethodType => + // newTraitSym.typeRef + // case poly: PolyType => + // HKTypeLambda(poly.paramNames)(tl => poly.paramInfos, tl => newTraitSym.typeRef.appliedTo(tl.paramRefs.head)) + // } + // val newTypeSym = newSymbol( + // owner = parentSym, + // name = newTraitSym.name.asTypeName, + // flags = innerClass.symbol.flags & (Private | Protected) | Synthetic, + // info = TypeBounds.upper(upperBound), + // privateWithin = innerClass.symbol.privateWithin, + // coord = innerClass.symbol.coord, + // nestingLevel = innerClass.symbol.nestingLevel, + // ).asType + // TypeDef(newTypeSym) + // end makeTypeFromInnerClass private object ConcreteParentStripper extends TreeAccumulator[Tree] { def apply(tree: Tree)(using Context): Tree = apply(tree, tree) diff --git a/tests/pos/inline-trait-anonymous-class.scala b/tests/pos/inline-trait-anonymous-class.scala new file mode 100644 index 000000000000..fd46396850cd --- /dev/null +++ b/tests/pos/inline-trait-anonymous-class.scala @@ -0,0 +1,10 @@ +// While we don't allow inner nested classes inside inline traits, we do allow creation of anonymous classes inside methods +// inside inline traits - after all these are just ordinary methods. + +inline trait C[S]: + def v(x: S): S = x + def w: Unit = + val x = new C[S] {} + println("w") + +class B extends C[Char] diff --git a/tests/pos/specialized-trait-inlining-causes-implementation-required.scala b/tests/pos/specialized-trait-inlining-causes-implementation-required.scala new file mode 100644 index 000000000000..56b0600648ce --- /dev/null +++ b/tests/pos/specialized-trait-inlining-causes-implementation-required.scala @@ -0,0 +1,14 @@ +// A[Char] => A$sp$Char => C$sp$Char, but only once we inline the body of C$sp$Char do we realise that we need +// C$impl$Char as well. + +inline trait C[S: Specialized]: + def v(x: S): S = x + def w: Unit = + val x = new C[S] {} + println("w") + +inline trait A[T: Specialized]: + def x(y: C[T]): Unit = println("x") + +def main = + val y = new A[Char] {} diff --git a/tests/pos/specialized-trait-inlining-causes-specialization.scala b/tests/pos/specialized-trait-inlining-causes-specialization.scala new file mode 100644 index 000000000000..f928978d35b8 --- /dev/null +++ b/tests/pos/specialized-trait-inlining-causes-specialization.scala @@ -0,0 +1,9 @@ +inline trait D[R: Specialized] + +inline trait C[S: Specialized]: + def w(y: D[S]): Unit = println("w") + +inline trait A[T: Specialized]: + def x(y: C[T]): Unit = println("x") + +class B extends A[Char] diff --git a/tests/pos/specialized-trait-simplest.scala b/tests/pos/specialized-trait-simplest.scala new file mode 100644 index 000000000000..839b6af44d5c --- /dev/null +++ b/tests/pos/specialized-trait-simplest.scala @@ -0,0 +1,4 @@ +inline trait Foo[T: Specialized] + +@main def main = + val x = new Foo[Int] {} From e12b6a7f6154528b9915c733d648b0517a476aba Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 25 Mar 2026 18:41:21 +0100 Subject: [PATCH 060/254] Update inline-traits.md --- docs/_docs/internals/inline-traits.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/_docs/internals/inline-traits.md b/docs/_docs/internals/inline-traits.md index 28df61a57fd6..f88a033c14b9 100644 --- a/docs/_docs/internals/inline-traits.md +++ b/docs/_docs/internals/inline-traits.md @@ -2,7 +2,8 @@ ## Motivation Inline traits is a new attempt to solve the specialization problem for the JVM in a more convenient way than the `@specialized` annotation -from Scala 2. It works alongside `Specialized` traits, the latter being detailed in an accompanying document. +from Scala 2 (the main problem of this being code bloat as we generated all possible specializations at declaration time of the specialized +class). Inline traits work alongside `Specialized` traits, the latter being detailed in an accompanying document. The problem is as follows: due to the JVM's (lack of) support for generics, generic type parameters are erased by the compiler: ```scala @@ -24,7 +25,7 @@ class C() extends Object() { } ``` Thus type `T` is converted to `Object`, and so every use of `A[Int]()` must first build an `Object` containing the `Int` we want to pass (*boxing*), -in order to be able to call `A(x: Object)`. There is no `A(x: Int)`. When referencing `x` on an `A` we get an `Object` back, which we have to *unbox* +in order to be able to call `A(x: Object)`. There is no `A(x: int)`. When referencing `x` on an `A` we get an `Object` back, which we have to *unbox* (extract from the wrapping `Object`). This object creation and deletion is very slow. We desire a way to avoid this by generating specialized instances of classes which @@ -98,7 +99,7 @@ inline everything into the first class/object in the hierarchy, it becomes advan Furthermore: -- References to members of inline traits accessed on inline receivers point to the inlined version, to ensure we avoid unnecessary boxing: +- References to members of inline traits accessed on inline receivers point to the inlined version, to ensure we avoid unnecessary boxing: [1] ```scala inline trait A[T](val x: T): def foo#1: T @@ -109,7 +110,7 @@ class B extends A[Int](1) def fun(x: B) = x.foo // points to foo#2 ``` -- Inline traits may define private members, and these are handled specially: +- Inline traits may define private members, and these are handled specially: [2] - Private fields in the inline trait are inlined as private fields with a mangled name in the inline receiver. This ensures they do not collide with privates inherited from other inline traits. - The private fields are then no longer accessible in the inline trait, as it is transformed into a pure interface. We can't however easily delete them; therefore they are name-mangled and converted to protected to allow them to exist without a definition. @@ -219,6 +220,18 @@ This problem is addressed via `Specialized` traits; see the accompanying documen | Opaque types | ❌ | | Self types | ❌ +## Processing of inline traits in the compiler +Inline traits in user code are inlined in the phase `specializeInlineTraits`. The phase `replaceInlinedTraitSymbols` +is responsible for updating references to members of inline receivers to point to the inlined members, instead of the +generic members in the parent inline trait (see [1] above). Finally the phase `pruneInlineTraits` is responsible for +converting inline traits into pure interfaces by removing their right hand sides. It also handles the mangling in [2]. + +Specialized traits rely on the semantics of inline traits, as they desugar to inline traits. However, the phase +`desugarSpecializedTraits` inlines these inline traits itself (sharing code with the `specializeInlineTraits` phase). +This is necessary because otherwise there would be a circular dependency between the two phases (see the Specialized traits +document for more information). This means that we need to run `specializeInlineTraits` *first* (because we don't want to inline +twice for inline traits resulting from specialization). + ## Internal Note regarding versions of inline traits This behaviour is the same as that in Timothée's thesis except for the following points: - We now allow inline traits to be inlined directly into other inline traits as well as objects and classes. From 60030152dba3b2537dbf11545a320b827007759d Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 25 Mar 2026 20:10:41 +0100 Subject: [PATCH 061/254] Update specialize-traits.md to include an explanation of what we do where --- docs/_docs/internals/specialized-traits.md | 244 ++++++++++++++++----- 1 file changed, 193 insertions(+), 51 deletions(-) diff --git a/docs/_docs/internals/specialized-traits.md b/docs/_docs/internals/specialized-traits.md index 07b859acb6b4..82a140296585 100644 --- a/docs/_docs/internals/specialized-traits.md +++ b/docs/_docs/internals/specialized-traits.md @@ -1,15 +1,46 @@ -# Specialized Traits and Classes +# Specialized Traits +Specialized traits accompany inline traits as a new attempt to solve to the specialization problem in Scala 3, replacing the `@specialized` annotation from Scala 2. -Specialization is one of the few remaining desirable features from Scala 2 that are still missing in Scala 3. We could try to port the Scala 2 scheme, which would be non-trivial since the implementation is quite complex. But that scheme is problematic enough to suggest that we also look for alternatives. A possible alternative is described here. It is meant to complement the [proposal on inline traits](https://github.com/lampepfl/dotty/issues/15532). That proposal also contains a more detailed critique of Scala 2 specialization. -The parts in that proposal that mention a proposed new specialization design should be ignored; they are superseded by the proposal here. +As mentioned in the accompanying document on inline traits, inline traits have one shortcoming, namely that interfaces are not specialized. For example: -The main problem of Scala-2 specialization is code bloat. We have to pro-actively generate up to 11 copies of functions and classes when they have a specialized type parameter, and this grows exponentially with the number of such type parameters. Miniboxing tries to reduce the number under the exponent from ~10 to 3 or 4, but it has problems dealing with arrays. +```scala +inline trait Foo[T](x: T): + def foo = x + +class Bar extends Foo[Int](42) + +def f(b: Foo[Int]) = 37 + b.foo + +@main def main = + val x = Bar() + f(x) +``` + +In this code the call to `b.foo` will refer to the version of `foo` typed `foo: T` which becomes `foo: Object` during erasure, because we accessed `foo` on +an object of declared type `Foo` (even though `b`'s actual runtime type is `Bar`). This will in turn call a bridge method +which means the `foo: Int` method will be called, but unnecessary boxing and unboxing will be added: + +```scala +inline trait Foo + def foo#1(): Object + +class Bar extends Foo[Int](42): + override def foo#2(): Int = 42 + override def foo#3(): Object = Int.box(this.foo()) + +def f(b: Foo): Int = 37 + Int.unbox(b.foo#1()) // virtual call to foo#1 resolves to bridge method foo#3, which in turn calls actual method foo#2, adding boxing. +``` -Languages like C++, Rust, Go, D, or Zig avoid the proactive generation of all possible specializations by monomorphizing the whole program. This means we only need to generate a specialized version of a function or class if it is actually used in the program. On the other hand, a global monomorphization can lead itself to code bloat and long compile times. It is also a problematic choice for binary APIs. +Specialized traits seek to resolve this problem. We also want to avoid pro-actively generating all specializations (as in Scala 2 `@specialized`; code bloat), or monomorphising the whole program +(as in C++ templates; this leads to code bloat and long compile times, and is a problematic choice for binary APIs.) -This note discusses a different scheme to get specialization for Scala 3, which is somewhat between Scala 2's selective specialization and full monomorphization. As in Scala 2, specialized type parameters are tagged explicitly (but not with an annotation). But as for monomorphization, specializations are only generated if a specialized type is referenced in the program. To make this work efficiently, we need a way to transport information about possible specialization types through generic code (full monomorphization does not need that since it eliminates all generic code). +Specialized traits are an approach sitting between these two options. As in Scala 2, specialized type parameters are tagged explicitly (but with a context bound rather than an annotation). +However, as for monomorphization, specializations are only generated if a specialized type is referenced in the program. -We do that using a type class `Specialized` that is typically used as a context bound on a type parameter of some class. It indicates that we want to create specialized versions of that class where the type parameter is instantiated to the type argument. The specialized versions offer optimization opportunities compared to the generic class. +To make this work efficiently, we introduce a type class `Specialized` that is used as a context bound on a type parameter of some class. +It indicates that we want to create specialized versions of that class where the type parameter is instantiated to the type argument. +The specialized versions offer optimization opportunities compared to the generic class. This allow us to transport information about +possible specialization types through generic code (full monomorphization does not need that since it eliminates all generic code). ## Example @@ -47,7 +78,9 @@ A _specialized trait_ is an inline trait that has at least one `Specialized` con A specialized context bound (or its expansion to a context parameter) is only allowed for type parameters of inline methods and inline traits. Regular methods or traits or classes -cannot take `Specialized[T]` parameters. Hence, the only way to create a specialized trait is using an anonymous class instance, like in the `Vec.apply` method above. What's more, +cannot take `Specialized[T]` parameters. + +Hence, the only way to create a specialized trait is using an anonymous class instance, like in the `Vec.apply` method above. What's more, we require that each such anonymous class instance - can extend only a single specialized trait, @@ -57,10 +90,19 @@ we require that each such anonymous class instance So each such class instance is of the form `new A[Ts](ps1)...(psN) {}` where `A` is a specialized trait and the type parameters `Ts` and term parameters `ps1, ,,, psN` can also be absent. -The restrictions ensure that each time we create an instance of a specialized trait we know statically the classes of all `Specialized` type arguments. This enables us to implement the following expansion scheme: +The restrictions ensure that each time we create an instance of a specialized trait we know statically the classes of all `Specialized` type arguments. + + + + + +This enables us to implement the following expansion scheme. ## Expansion of Specialized Traits + + + A type instance of a specialized trait such as `Vec[Tp]` has a special erasure, which depends on the specializing supertype of `Tp`. @@ -89,26 +131,23 @@ named `Vec$impl$TN`. Here, `Vec$sp$TN` is the erasure of `Vec[T]` and the class The specialized instance traits are created on demand the first time they are mentioned in a type. For example, here is the definition of the specialized instance `Vec$sp$Int` for `Vec[Int]`: ```scala -inline trait Vec$sp$Int extends Vec[Int]: - def length: Int - def apply(x: Int): Int - def scalarProduct(other: Vec[T]): Int +inline trait Vec$sp$Int extends Vec[Int] ``` In general a specialized instance trait that specializes an inline trait `A[T]` with a specialization type `S`: - - drops all specialized trait parameters of `A`, - - adds `A[S]` as first parent trait, + - drops all `Specialized` trait parameters of `A` + - adds `A[S]` as first parent trait - _also_ adds all parents of `A` *in their specialized forms*, - - contains all specialized declarations of `A`. - - Is inline - - Maintains type parameters for type params not marked with `Specialized` in the original trait, and also for specializations to `T: Specialized` e.g. in the case of partial specializations. + - contains declarations from the body of `A` specialized to the type(s) in question (after inlining) + - Is an `inline trait`. This is for consistency; see [1]. + - Maintains type parameters for type params not marked with `Specialized` in the original trait, and also for specializations where the type *argument* is`T: Specialized', in the case of partial specializations. A specialized instance class for an inline trait `A` at specialized argument `S` - repeats the value parameters of inline trait `A`, - extends `A[S]` with these parameters - - extends the corresponding $sp$ trait + - extends the corresponding `$sp$` trait For example, here is the specialized instance class for `Vec` at `Int`: @@ -131,15 +170,74 @@ class Vec$impl$Int(elems: Array[Int])(using Numeric[Int]) extends Vec[Int](elems result ``` -More examples of expansions are shown in the case study below. +After inlining and pruning of inlined definitions, the `$sp$` trait becomes the following: +```scala +inline trait Vec$sp$Int extends Vec[Int]: + def length: Int + def apply(x: Int): Int + def scalarProduct(other: Vec[T]): Int +``` + +## Specialized Traits in the Compiler +We introduce a new phase `desugarSpecializedTraits` responsible for detecting specializations, generating the necessary `$sp$` and `$impl$` +classes for these specializations, and replacing references to e.g. `Vec[Int]` with `Vec$sp$Int` and `new Vec[Int] {}` with `new Vec$impl$Int`. + +Specialized traits rely on the semantics and implementation of inline traits, so it may seem logical that this phase would merely generate +the prototypes for the classes, and allow `specializeInlineTraits` to inline the bodies. We *do not* do this. Rather, the phase `desugarSpecializedTraits` directly performs inlining of the parent traits (`Vec[Int]` in the above example) into the +generated `$sp$` traits and `$impl$` classes, sharing the relevant code with the `specializeInlineTraits` phase through +the `Inlines.scala` file. + +This is done because while we want to keep the two phases separate and avoid coupling where possible (thus allowing e.g. specialized traits to be disabled +while maintaining inline traits), implementing specialized traits requires being able to alternate + between the inlining and specializing steps in some cases. Consider the following +example: + +```scala +inline trait D[R: Specialized] + +inline trait C[S: Specialized]: + def w(y: D[S]): Unit = println("w") + +inline trait A[T: Specialized]: + def x(y: C[T]): Unit = println("x") + +class B extends A[Char] +``` +If we run just the specialization part of the specialized trait processing (without the inlining yet), we get: + +```scala +inline trait A$sp$Char extends A[Char] +``` + +Inlining the body of `A` results in: +```scala +inline trait A$sp$Char extends A[Char]: + def x(y: C[Char]): Unit = println("x") +``` +Notice that this generates a reference to `C[Char]` which ought to be specialized, given that `S` is marked as `Specialized` in +the definition of `C`. Therefore we need to run the specialization process again. This time we will generate: +```scala +inline trait C$sp$Char extends C[Char] +``` +Inlining the body of `C` into this trait will create a reference to `D[Char]`, which also ought to be specialized. Thus it is clear that +it is possible to create arbitrarily long chains requiring alternating between specialized trait generation and inline trait inlining. We note that +this problem arises not only with the `$sp$` traits, but also the `$impl$` classes (see `specialized-trait-inlining-causes-implementation-required.scala`). + +To resolve this problem without alternating and looping the `specializeInlineTraits` and `desugarSpecializedTraits` phases in an inconvenient way, we opt to make: +- `specializeInlineTraits` responsible for inlining inline traits written directly in user code. +- `desugarSpecializedTraits` responsible for finding specializations and generating the required `$sp$` traits and `$impl$` classes, inlining the parent specialized traits into these classes, and repeating until no more inlining can be performed and no more `$sp$` traits and `$impl$` classes are needed. This phase also performs replacement of e.g. `Vec[Int]` with `Vec$sp$Int` and `new Vec[Int]` with `new Vec$impl$Int`. +- `pruneInlineTraits` and `replaceInlinedTraitSymbols` responsible for converting inline traits to pure interfaces and for replacing members accessed on inline receivers with the corresponding inlined symbols, *whether the inline traits in question come from inline traits in source code or specialized trait expansion, in both cases.* (see the document on inline traits for a more detailled description of these phases). + +In particular this decision means that we run `specializeInlineTraits` before `desugarSpecializedTraits`, as otherwise we may duplicate the inlined bodies of the `$sp$` traits and `$impl$` classes, since we have already inlined them in `desugarSpecializedTraits`. ## Caching of Specialized Traits and Classes + To avoid redundant repeated code generation of the same traits and classes, specialized instance traits and classes are cached. The compiler will put their tasty and classfile artifacts in a special directory on the class path. Each artifact will contain in an annotation a hash of the contents of the trait from which the instance was derived. Before creating a new specialized instance, the compiler will consult this directory to see whether an instance with the given name exists and whether its hash matches. In that case, the artifacts can be re-used. ## The `Specialized` Type Class - + The `Specialized` Type Class is erased at runtime. Instances of `Specialized[T]` are created automatically for types that do not contain type variables. @@ -169,7 +267,7 @@ inline trait Seq[T: Specialized](elems: Array[T]) extends Iterable[T]: def iterator: Iterator[T] = new ArrayIterator[T](elems) {} ``` -This generates the following instance traits: +This generates the following instance traits (after inlining and conversion to pure interfaces): ```scala inline trait Iterator$sp$Int extends Iterator[Int]: @@ -186,14 +284,14 @@ inline trait Seq$sp$Int extends Seq[Int], Iterable$sp$Int: def length: Int def apply(i: Int): Int ``` -Note that these traits repeat the parent types of their corresponding inline traits. For instance, `ArrayIterator$sp$Int` extends `ArrayIterator[Int]` as well as its parent `Iterator[Int]`. After erasure, the definition of -`ArrayIterator$sp$Int` becomes -```scala -inline trait ArrayIterator$sp$Int extends ArrayIterator, Iterator$sp$Int -``` -Hence, the erased `trait ArrayIterator$sp$Int` extends the general `ArrayIterator` trait as well as the specialized `Iterator$sp$Int` parent trait, which is what we want. +Note that these traits repeat the parent types of their corresponding inline traits. For instance, `ArrayIterator$sp$Int` extends `ArrayIterator[Int]` as well as the specialized version of its parent `Iterator$sp$Int`, so the specialized trait may be used in contexts expecting: + +- The specialized trait `ArrayIterator$sp$Int` itself +- An unspecialized `ArrayIterator` for example `ArrayIterator[T: Numeric]`, or parents thereof, e.g. `Iterator[T: Numeric]` +- Specialized traits higher in the specialized hierarchy for example `Iterator$sp$Int`. + +The specialized implementation classes for `ArrayIterator` and `Seq` are as follows (after inlining; iff `new Seq[Int] {}` and `ArrayIterator[Int] {}` are to be found in the program): -The specialized implementation classes for `ArrayIterator` and `Seq` are as follows: ```scala class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator[Int](elems): private var current = 0 @@ -211,11 +309,70 @@ class Seq$impl$Int(elems: Array[Int]) extends Seq$sp$Int, Seq[Int]: override def length: Int = elems.length override def apply(i: Int): Int = elems(i) ``` -These implementation classes are type correct as long as we inject the knowledge that a specialization trait + +Given that `ArrayIterator$sp$Int` extends `ArrayIterator[Int]`, directly extending `ArrayIterator[Int]` in `ArrayIterator$impl$Int` +may seem redundant. However, it is necessary in order to pass `elems` to `ArrayIterator[Int]`. +Traits are not allowed to pass parameters to each other, meaning we could not indirectly pass `elems` via `ArrayIterator$sp$Int`, +and furthermore we cannot simply leave out the parameter passing (on the basis that we only use the inlined `elems`) and thus +hope to avoid extending `ArrayIterator[Int]` directly, because extending `ArrayIterator$sp$Int` implies indirectly extending +`ArrayIterator[Int]` which is not allowed unless `ArrayIterator[Int]` is also mixed in directly to pass parameters (due to the rules of +trait parameter passing in Scala 3). + + +## [1] Why are the generated traits inline? +Consider the following: +```scala +inline trait A[T]: + def foo = "Hello World" +inline trait B extends A[Int] +trait C extends B +// expands to: +trait C extends B: + def foo = "Hello World" + +// vs... + +inline trait A[T: Specialized]: + def foo = "Hello World" +inline trait B extends A[Int] +trait C extends B +// expands to: +trait A$sp$Int: + def foo = "Hello World" +inline trait B extends A$sp$Int +trait C extends B +``` +We consider the fact that simply adding Specialized changes the location of +the inlined `foo` method to be inconsistent / confusing. Furthermore it would violate the rule +that ordinary traits may not extend inline traits, and causes problems with partial specializations: +```scala +// (1) +inline trait A[T: Specialized, D: Specialized]: + def foo: T + def bar: D +inline trait B[S: Specialized] extends A[S, Int] +trait C extends B[Char] + +// expands to: +trait A$sp$S$Int[S: Specialized] extends A[S, Int]: // (Ignoring the fact that Specialized may not be used on ordinary traits). + def foo: S + def bar: Int +inline trait B[W: Specialized] extends A$sp$S$Int[W] +trait C extends B[Char] +``` +The definitions are stuck in `A$sp$S$Int$` because it is not inline. This means we can never usefullly specialize on `W` even though it is declared `Specialized`. + +Side note: Because we make the generated traits inline, we modify the behaviour of inline traits relative to the original semantics from Timothée's thesis, such that inline traits extended by other inline traits are still inlined (instead of inlining only at the first ordinary class extending the family of inline traits). This is necessary so that `A$sp$S$Int` can be made inline and still contain the specialized declarations which we need when we use it as an interface. + + + + + + + - - - -# the following things are banned... + + From 69abd1585c20d9604777667a1ed9ec8fdcd71ec6 Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 25 Mar 2026 20:42:25 +0100 Subject: [PATCH 062/254] Fix some issues --- docs/_docs/internals/specialized-traits.md | 54 ++++++++++++---------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/docs/_docs/internals/specialized-traits.md b/docs/_docs/internals/specialized-traits.md index 82a140296585..59d82bd4b58a 100644 --- a/docs/_docs/internals/specialized-traits.md +++ b/docs/_docs/internals/specialized-traits.md @@ -37,11 +37,12 @@ Specialized traits seek to resolve this problem. We also want to avoid pro-activ Specialized traits are an approach sitting between these two options. As in Scala 2, specialized type parameters are tagged explicitly (but with a context bound rather than an annotation). However, as for monomorphization, specializations are only generated if a specialized type is referenced in the program. -To make this work efficiently, we introduce a type class `Specialized` that is used as a context bound on a type parameter of some class. -It indicates that we want to create specialized versions of that class where the type parameter is instantiated to the type argument. -The specialized versions offer optimization opportunities compared to the generic class. This allow us to transport information about +To make this work efficiently, we introduce a type class `Specialized` that is used as a context bound on a type parameter of some class. This allows us to transport information about possible specialization types through generic code (full monomorphization does not need that since it eliminates all generic code). +The `Specialized` annotation indicates that we want to create specialized versions of that class where the type parameter is instantiated to the type argument. +The specialized versions offer optimization opportunities compared to the generic class. + ## Example As a first example, consider a `Vec` trait for vectors over a numeric type. @@ -49,6 +50,7 @@ As a first example, consider a `Vec` trait for vectors over a numeric type. import scala.math.Numeric inline trait Vec[T: {Specialized, Numeric}](elems: Array[T]): + private val num = summon[Numeric[T]] def length = elems.length @@ -61,7 +63,7 @@ inline trait Vec[T: {Specialized, Numeric}](elems: Array[T]): result = num.plus(result, num.times(this(i), other(i))) result -object Vec: +object Vec: inline def apply[T: Specialized](elems: Array[T]) = new Vec[T](elems) {} end Vec ``` @@ -90,7 +92,7 @@ we require that each such anonymous class instance So each such class instance is of the form `new A[Ts](ps1)...(psN) {}` where `A` is a specialized trait and the type parameters `Ts` and term parameters `ps1, ,,, psN` can also be absent. -The restrictions ensure that each time we create an instance of a specialized trait we know statically the classes of all `Specialized` type arguments. +The restrictions ensure that each time we create an instance of a specialized trait we know statically the classes of all `Specialized` type arguments. @@ -101,7 +103,8 @@ This enables us to implement the following expansion scheme. ## Expansion of Specialized Traits - + A type instance of a specialized trait such as `Vec[Tp]` has a special erasure, which depends on the specializing supertype of `Tp`. @@ -158,15 +161,16 @@ class Vec$impl$Int(elems: Array[T]) extends Vec[Int](elems), Vec$sp$Int After inlining `Vec[Int]` the expanded class looks like this: ```scala class Vec$impl$Int(elems: Array[Int])(using Numeric[Int]) extends Vec[Int](elems), Vec$sp$Int: + private val Vec$$num: Numeric.IntIsIntegral // Methods converted to primitive operations without boxing def length: Int = elems.length def apply(i: Int): Int = elems(i) def scalarProduct(other: Vec[Int]): Int = require(this.length == other.length) - var result = num.fromInt(0) + var result = Vec$$num.fromInt(0) for i <- 0 until length do - result = num.plus(result, num.times(this(i), other(i))) + result = Vec$$num.plus(result, Vec$$num.times(this(i), other(i))) result ``` @@ -182,7 +186,7 @@ inline trait Vec$sp$Int extends Vec[Int]: We introduce a new phase `desugarSpecializedTraits` responsible for detecting specializations, generating the necessary `$sp$` and `$impl$` classes for these specializations, and replacing references to e.g. `Vec[Int]` with `Vec$sp$Int` and `new Vec[Int] {}` with `new Vec$impl$Int`. -Specialized traits rely on the semantics and implementation of inline traits, so it may seem logical that this phase would merely generate +Specialized traits rely on the semantics and implementation of inline traits, so it may seem logical that `desugarSpecializedTraits` would merely generate the prototypes for the classes, and allow `specializeInlineTraits` to inline the bodies. We *do not* do this. Rather, the phase `desugarSpecializedTraits` directly performs inlining of the parent traits (`Vec[Int]` in the above example) into the generated `$sp$` traits and `$impl$` classes, sharing the relevant code with the `specializeInlineTraits` phase through the `Inlines.scala` file. @@ -223,10 +227,10 @@ Inlining the body of `C` into this trait will create a reference to `D[Char]`, w it is possible to create arbitrarily long chains requiring alternating between specialized trait generation and inline trait inlining. We note that this problem arises not only with the `$sp$` traits, but also the `$impl$` classes (see `specialized-trait-inlining-causes-implementation-required.scala`). -To resolve this problem without alternating and looping the `specializeInlineTraits` and `desugarSpecializedTraits` phases in an inconvenient way, we opt to make: +To resolve this problem without alternating between and looping the `specializeInlineTraits` and `desugarSpecializedTraits` phases in an inconvenient way, we opt to make: - `specializeInlineTraits` responsible for inlining inline traits written directly in user code. - `desugarSpecializedTraits` responsible for finding specializations and generating the required `$sp$` traits and `$impl$` classes, inlining the parent specialized traits into these classes, and repeating until no more inlining can be performed and no more `$sp$` traits and `$impl$` classes are needed. This phase also performs replacement of e.g. `Vec[Int]` with `Vec$sp$Int` and `new Vec[Int]` with `new Vec$impl$Int`. -- `pruneInlineTraits` and `replaceInlinedTraitSymbols` responsible for converting inline traits to pure interfaces and for replacing members accessed on inline receivers with the corresponding inlined symbols, *whether the inline traits in question come from inline traits in source code or specialized trait expansion, in both cases.* (see the document on inline traits for a more detailled description of these phases). +- `pruneInlineTraits` and `replaceInlinedTraitSymbols` responsible respectively for converting inline traits to pure interfaces, and for replacing members accessed on inline receivers with the corresponding inlined symbols. This is *whether the inline traits in question come from inline traits in source code or specialized trait expansion, in both cases.* (see the document on inline traits for a more detailed description of these phases). In particular this decision means that we run `specializeInlineTraits` before `desugarSpecializedTraits`, as otherwise we may duplicate the inlined bodies of the `$sp$` traits and `$impl$` classes, since we have already inlined them in `desugarSpecializedTraits`. @@ -284,7 +288,7 @@ inline trait Seq$sp$Int extends Seq[Int], Iterable$sp$Int: def length: Int def apply(i: Int): Int ``` -Note that these traits repeat the parent types of their corresponding inline traits. For instance, `ArrayIterator$sp$Int` extends `ArrayIterator[Int]` as well as the specialized version of its parent `Iterator$sp$Int`, so the specialized trait may be used in contexts expecting: +Note that these traits repeat the parent types of their corresponding inline traits (but with specialization added). For instance, `ArrayIterator$sp$Int` extends `ArrayIterator[Int]` (as we would expect) *as well as* the specialized version of its parent `Iterator$sp$Int`, so the specialized trait may be used in contexts expecting: - The specialized trait `ArrayIterator$sp$Int` itself - An unspecialized `ArrayIterator` for example `ArrayIterator[T: Numeric]`, or parents thereof, e.g. `Iterator[T: Numeric]` @@ -300,7 +304,7 @@ class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, Ar override def next(): Int = try elems(current) finally current += 1 -class Seq$impl$Int(elems: Array[Int]) extends Seq$sp$Int, Seq[Int]: +class Seq$impl$Int(elems: Array[Int]) extends Seq$sp$Int, Seq[Int](elems): override def iterator: Iterator$sp$Int = new ArrayIterator$impl$Int(elems) override def forall(f: Int => Unit): Unit = @@ -325,9 +329,9 @@ Consider the following: inline trait A[T]: def foo = "Hello World" inline trait B extends A[Int] -trait C extends B -// expands to: -trait C extends B: +class C extends B +// inlines to: +class C extends B: def foo = "Hello World" // vs... @@ -335,16 +339,16 @@ trait C extends B: inline trait A[T: Specialized]: def foo = "Hello World" inline trait B extends A[Int] -trait C extends B -// expands to: +class C extends B +// would expand to: trait A$sp$Int: def foo = "Hello World" inline trait B extends A$sp$Int -trait C extends B +class C extends B ``` -We consider the fact that simply adding Specialized changes the location of -the inlined `foo` method to be inconsistent / confusing. Furthermore it would violate the rule -that ordinary traits may not extend inline traits, and causes problems with partial specializations: +We consider the fact that the location of the inlined `foo` method changes with only a simple +addition of `Specialized` to be inconsistent / confusing. Furthermore it would violate the rule +that ordinary traits may not extend inline traits, and causes problems with partial specialization: ```scala // (1) inline trait A[T: Specialized, D: Specialized]: @@ -353,16 +357,16 @@ inline trait A[T: Specialized, D: Specialized]: inline trait B[S: Specialized] extends A[S, Int] trait C extends B[Char] -// expands to: +// would expand to: trait A$sp$S$Int[S: Specialized] extends A[S, Int]: // (Ignoring the fact that Specialized may not be used on ordinary traits). def foo: S def bar: Int inline trait B[W: Specialized] extends A$sp$S$Int[W] trait C extends B[Char] ``` -The definitions are stuck in `A$sp$S$Int$` because it is not inline. This means we can never usefullly specialize on `W` even though it is declared `Specialized`. +The definitions are stuck in `A$sp$S$Int$` because it is not inline. This means we can never usefully specialize on `W` even though it is declared `Specialized`. -Side note: Because we make the generated traits inline, we modify the behaviour of inline traits relative to the original semantics from Timothée's thesis, such that inline traits extended by other inline traits are still inlined (instead of inlining only at the first ordinary class extending the family of inline traits). This is necessary so that `A$sp$S$Int` can be made inline and still contain the specialized declarations which we need when we use it as an interface. +Side note: Because we make the generated traits inline, we modify the behaviour of inline traits relative to the original semantics from Timothée's thesis, such that inline traits extended by other inline traits are still inlined (instead of inlining only at the first ordinary class extending the family of inline traits). This is necessary so that `A$sp$S$Int` can be made inline and still contain the specialized declarations which we need when we use it as an interface. The original argument for only inlining at the bottom of the hierarchy was to reduce code generation, and that this was sufficient when we only have inline traits, however the additional code generation is only linear in the number of traits in the sequence as we do not inline multiple copies, and we consider this acceptable to implement `Specialized`. From 4f5198884b6a4f97ebe69df6026f07aa97ca6477 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 26 Mar 2026 10:59:10 +0100 Subject: [PATCH 063/254] Delete print --- .../dotty/tools/dotc/transform/DesugarSpecializedTraits.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index e6df1f045ed0..8dc0d822f68d 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -363,7 +363,6 @@ class SpecializedTraitCache( if (newImplementationSymbols.contains(spec) || implementationSymbols.contains(spec)) then this else - println(s"adding spec ${spec.specialization}") val withInterface = addInterface(spec) withInterface.copy(newImplementationSymbols = withInterface.newImplementationSymbols + (spec -> genImplementationSymbol(spec, withInterface.getInterfaceSymbol(spec).get))) From 1e60be5a895cd4db1b8216c8dcb13f9366637ce2 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 26 Mar 2026 11:39:15 +0100 Subject: [PATCH 064/254] Fix inline-trait-specialized-desugar.scala --- .../dotty/tools/dotc/inlines/Inlines.scala | 3 +- .../transform/DesugarSpecializedTraits.scala | 14 +++- .../dotty/tools/dotc/transform/Getters.scala | 2 + .../inline-trait-specialized-desugar.scala | 69 ++++--------------- 4 files changed, 31 insertions(+), 57 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 239c7651be53..0058bad97040 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -864,8 +864,9 @@ object Inlines: paramAccessorsMapper .getParamAccessorRhs(vdef.symbol.owner, vdef.symbol.name) .getOrElse(inlinedRhs(vdef, inlinedSym)) + // TODO: We might only need to do this to evidence params but tbh I can't see much harm in applying it when we want to? - if (rhs.tpe.exists) + if (rhs.tpe.exists && !vdef.symbol.isMutableVar) // we can't narrow vars because e.g. var current = 0 would be narrowed to type 0 but someone may letter set i inlinedSym.info = rhs.tpe tpd.ValDef(inlinedSym.asTerm, rhs).withSpan(parent.span) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 8dc0d822f68d..2412cde7b65c 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -213,8 +213,8 @@ class DesugarSpecializedTraits extends MacroTransform: // Replace (anonymous class version of) new Foo[Int] {} with new Foo$impl$Int.asInstanceOf[Foo$sp$Int] case Block(List(TypeDef(anon, Template(_, parentCalls: List[Tree], _, _))), Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => - parentCalls(1) match { // only allowed to extend Object and our specialized trait - case Apply(Apply(tpe, ctorArgs), ev) => + parentCalls match { + case _ :: Apply(Apply(tpe, ctorArgs), ev) :: Nil => // only allowed to extend Object and our specialized trait val spec = Specialization.unapply(t.tpe).get { // We don't replace non-specialized anonymous class instantiations e.g. new Foo[T] where T is defined in the enclosing scope. for (specializedSymbol <- specializations2.getImplementationSymbol(spec)) @@ -537,3 +537,13 @@ end Specialization // I'm not sure we strictly need them though. // TODO: Put classes onto the classpath as desired. + +// Concerns: +// - The superclass of `C` is a top class, or `C` itself is a top class. +// Drop all specialized trait parameters of A + +// If we can manage to get rid of the inheritance there that could be helpful in terms of avoiding multiple values +// BUT: generate a version which is with just inline traits that has this problem as well. +// Need to deal with the caching at some point +// These implementation classes are type correct as long as we inject the knowledge that a specialization trait +// like `Seq$sp$Int` is equal to its parameterized version `Seq[Int]` diff --git a/compiler/src/dotty/tools/dotc/transform/Getters.scala b/compiler/src/dotty/tools/dotc/transform/Getters.scala index 11adf4da83d5..c63d1384a9e7 100644 --- a/compiler/src/dotty/tools/dotc/transform/Getters.scala +++ b/compiler/src/dotty/tools/dotc/transform/Getters.scala @@ -62,6 +62,8 @@ class Getters extends MiniPhase with SymTransformer { thisPhase => override def description: String = Getters.description + override def changesMembers: Boolean = true // TODO: Is this ok? I mean it does call enteredAfter via ensureSetter and that does change members. + override def transformSym(d: SymDenotation)(using Context): SymDenotation = { def noGetterNeeded = d.isOneOf(NoGetterNeededFlags) || diff --git a/tests/pos/inline-trait-specialized-desugar.scala b/tests/pos/inline-trait-specialized-desugar.scala index c54840c5fef2..3d947562dae5 100644 --- a/tests/pos/inline-trait-specialized-desugar.scala +++ b/tests/pos/inline-trait-specialized-desugar.scala @@ -1,43 +1,29 @@ -// They do this: (with Specialized type class) +// User code does this: (with Specialized type class) inline trait Iterator[T]: def hasNext: Boolean def next(): T -// They do this: (with Specialized type class) +// User code does this: (with Specialized type class) inline trait ArrayIterator[T](elems: Array[T]) extends Iterator[T]: - private var current = 0 + private var current: Int = 0 def hasNext: Boolean = current < elems.length def next(): T = try elems(current) finally current += 1 -// We generate these: -trait Iteratorsp$Int extends Iterator[Int] -trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator$sp$Int +// Specialized traits generates these signatures: +inline trait Iterator$sp$Int extends Iterator[Int] +inline trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator$sp$Int class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator[Int](elems) -// We could keep the signatures in the sp trait and then put the implementations in the impl class -// This would still require the modification to inline traits to inline all the way down, but also pruning at every step -// +// User code does this: +def foo(x: ArrayIterator[Int]): Int = x.next() - -// Inline traits does the magic of actually inlining the code and specialising from T to Int in that step. - - -// They do this: -// def foo(x: ArrayIterator[Int]): Int = x.next() -// We convert this to: +// Specialized traits converts this to def foo(x: ArrayIterator$sp$Int): Int = x.next() -// As long as we generate this (i.e. "do the special erasure") before we run inline traits we should be fine because then the reference will be replaced. - -// trait Foo$sp$Int -// - -// what if they do already some kind of with clause - -// They do this: -// class MyClassA -// class MyClassB extends MyClassA, ArrayIterator[Int] +// User code does this: +/* class MyClassA + class MyClassB extends MyClassA, ArrayIterator[Int] */ // We convert this to: class MyClassA @@ -46,35 +32,10 @@ class MyClassB extends MyClassA, ArrayIterator$sp$Int @main def main = val xs: Array[Int] = Array(1, 2, 3) - // They do this: - // new ArrayIterator[Int](xs) {} - - // new ArrayIterator$sp$Int with ArrayIterator[Int] (xs) {} + // User code does this: + /* val ai = new ArrayIterator[Int](xs) {} */ // We convert this to: - val ai = new ArrayIterator$impl$Int(xs) {} - - - + val ai = ArrayIterator$impl$Int(xs) println(ai.next()) - - -// Concerns: - // Specializing the "arr" field - // - Avoid boxing for internal values like `result` will that actually get done? - maybe through the last point "calls will be inlined" -// - The superclass of `C` is a top class, or `C` itself is a top class. -// Drop all specialized trait parameters of A -// - adds `A[S]` as first parent trait, -// - _also_ adds all parents of `A` in their specialized forms, -// - contains all specialized declarations of `A`. - -// and yet "second parent is not needed" -// - repeats the value parameters of trait `A`, -// - extends `A[S]`. - -// If we can manage to get rid of the inheritance there that could be helpful in terms of avoiding multiple values -// BUT: generate a version which is with just inline traits that has this problem as well. -// Need to deal with the caching at some point -// These implementation classes are type correct as long as we inject the knowledge that a specialization trait -// like `Seq$sp$Int` is equal to its parameterized version `Seq[Int]` From bdb5ddd6567950001bbd65f08c3861348663fb01 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 26 Mar 2026 11:58:14 +0100 Subject: [PATCH 065/254] Get rid of specialized-traits-unspecialized.scala --- .../transform/DesugarSpecializedTraits.scala | 2 + docs/_docs/internals/inline-traits.md | 27 ++- .../specialized-traits-unspecialized.scala | 187 ------------------ 3 files changed, 25 insertions(+), 191 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 2412cde7b65c..5a4f82776bb9 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -547,3 +547,5 @@ end Specialization // Need to deal with the caching at some point // These implementation classes are type correct as long as we inject the knowledge that a specialization trait // like `Seq$sp$Int` is equal to its parameterized version `Seq[Int]` + +// Warning for dropping Specialized qualifier or it doesn't compile? diff --git a/docs/_docs/internals/inline-traits.md b/docs/_docs/internals/inline-traits.md index f88a033c14b9..26ee644b67fd 100644 --- a/docs/_docs/internals/inline-traits.md +++ b/docs/_docs/internals/inline-traits.md @@ -133,7 +133,7 @@ class B extends A(true): private val A$$x: Int = 1 override def foo(): Int = if this.A$$b then this.A$$x.+(1) else 0 ``` -- An inline receiver may mix in multiple inline traits with colliding member names. In this case the latest extended trait prevails. In the following example calling `foo` on an instance of `C` will return "Bonjour". +- An inline receiver may mix in multiple inline traits with colliding member names. In this case the latest extended trait prevails. In the following example calling `foo` on an instance of `C` will return "Bonjour". This is in contrast to ordinary traits which require the `override` modifier in this case. ```scala inline trait A: def foo = "Hello World" @@ -143,6 +143,17 @@ inline trait B: class C extends A, B ``` +However, an inline receiver may not define a member whose name collides with the name of an inlined public member from a parent inline trait, unless the override modifier is used. +```scala +inline trait A: + def foo = "Hello World" + +inline trait B: + def foo = "Bonjour" + +class C extends A, B: + def foo = "Bonjour2" // Must be override. +``` - Inlined members of inline traits are typed with the type of the right hand side resulting from inlining. This is particularly important for typeclass instances: ```scala inline trait A[T: Numeric]: @@ -156,10 +167,18 @@ inline trait A[T: Numeric]: private val v: Numeric[T] = summon[Numeric[T]] class B extends A[Int]: - private given val A$$evidence$1: scala.math.Numeric.IntIsIntegral.type = scala.math.Numeric.IntIsIntegral - private val A$$v: scala.math.Numeric.IntIsIntegral = this.A$$evidence$1 + private given val A$$evidence$1: Numeric.IntIsIntegral.type = Numeric.IntIsIntegral + private val A$$v: Numeric.IntIsIntegral = this.A$$evidence$1 ``` -This means that references to `v.fromInt()`, `v.add()` etc are optimised and avoid boxing. +This means that references to `v.fromInt()`, `v.add()` etc are optimised and avoid boxing. However, this type acquisition is only applied to non-var members, as it could +lead to unsoundness if applied to `var`s: + +```scala +inline trait Counter extends Iterator: + private var current: Int = 0 + def next(): Int = current += 1 +``` +Narrowing `current` to the type of the initializer here would give it type `0`. This makes the increment operation in `next()` illegal. ## Benefits of inline traits We can now do the following with no boxing and unboxing: diff --git a/tests/pos/specialized-traits-unspecialized.scala b/tests/pos/specialized-traits-unspecialized.scala index aed576cd0947..e69de29bb2d1 100644 --- a/tests/pos/specialized-traits-unspecialized.scala +++ b/tests/pos/specialized-traits-unspecialized.scala @@ -1,187 +0,0 @@ -// (1) -inline trait A[T: Specialized, D: Specialized]: - def foo: T - def bar: D -inline trait B[S] extends A[S, Int] -trait C extends B[Char] - -// expands to: -trait A$sp$S$Int$[S] extends A[S, Int]: - def foo: S - def bar: Int -inline trait B[S] extends A$sp$S$Int[S] -trait C extends B[Char] -// so the foo can never be specialized, even if we specialize on S: -trait A$sp$S$Int$[S] extends A[S, Int]: - def foo: S - def bar: Int -inline trait B[S: Specialized] extends A$sp$S$Int[S] -trait B$sp$Char extends B[Char] -trait C extends B[Char] - - -inline trait A$sp$S$Int$[S] extends A[S, Int]: -inline trait B[S: Specialized] extends A$sp$S$Int[S] -inline trait B$sp$Char extends B[Char] -trait C extends B[Char] - def foo: S - def bar: Int - - -def fun[S](x: A$sp$S$Int[S]) - x.bar - - -def fun(x: B[Char]) - x.foo - -// (2) If we change the rules so that we can generate inline traits for $sp$: -inline trait A$sp$S$Int$[S] extends A[S, Int]: - def foo: S - def bar: Int -inline trait B[S] extends A$sp$S$Int[S]: - def foo: S - def bar: Int -trait C extends B[Char] - def foo: Char - def bar: Int - -// (3) And furthermore if we have Specialized on the B[S]: -inline trait A$sp$S$Int$[S] extends A[S, Int]: - def foo: S - def bar: Int -inline trait B[S: Specialized] extends A$sp$S$Int[S]: - def foo: S - def bar: Int -inline trait B$sp$Char$ extends B[Char]: - def foo: Char - def bar: Int -trait C extends B$sp$Char$ - def foo: Char - def bar: Int - -// And then I would argue that there could be value in adding a warning for dropping the Specialized qualifier in case (2) - - -// Should we be worried about code bloat due to inlining every time? I don't think so. -// We just need a rule to decide which method is selected. -inline trait A: - def foo = "Hello, World" - -inline trait B extends A: - over - def bar = "Boo" - -inline trait C extends A: - def baz = "baz" - -inline trait D extends A, B, C - -// Result: -inline trait A: - def foo#1 = "Hello, World" - -inline trait B extends A: - def foo#2 = "Hello, World" - def bar#1 = "Boo" - -// inline trait C extends A: -// def foo#3 = "Hello, World" -// def baz#1 = "baz" - -inline trait D extends A, B, C: - def foo#1 = "Hello, World" - def bar#1 = "Boo" - def baz#1 = "baz" - - -// At the moment, the following is rejected -inline trait A: - def foo = "Hello World" - -inline trait B: - def foo = "Bonjour" - -class C extends A, B: - def foo = "Bonjour2" - -def main = - val x = C() - println(x.foo) - -// and this is also rejected without the override modifier, but allowed with: -trait A: - def foo = "Hello World" - -trait B extends A: - def foo = "Bonjour" - -class C extends A, B - -def main = - val x = C() - println(x.foo) - -// while the following is allowed (and we take the value from the second trait i.e. B) -inline trait A: - def foo = "Hello World" - -inline trait B: - def foo = "Bonjour" - -class C extends A, B - -def main = - val x = C() - println(x.foo) -// I think this is fine and necessary if we want to make the resulting traits inline, because we need -// to extend from multiple inline traits sharing members. To be honest the behaviour will be more like the -// override case because they come from the same inheritance hierarchy anyway. - -// Alternative approach: -// (1) Erase S, concerned that this won't type correctly / will get a missing cast -// Also this is just not giving us the maximum amount of efficiency gain that we could get. -inline trait A[T: Specialized, D: Specialized]: - def foo: T - def bar: D -inline trait B[S] extends A[S, Int] -trait C extends B - - -trait A$sp$Any$Int extends A[Any, Int]: - def foo: Any - def bar: Int -inline trait B[S] extends A$sp$Any$Int -trait C extends B[Char] - -// (2) -// Don't erase S but just don't care about the loss of specialization -trait A$sp$S$Int extends A[S, Int]: - def foo: S - def bar: Int -inline trait B[S] extends A$sp$S$Int[S] -trait C extends B[Char] - - - -// This one is also kind of a massive problem.... -inline trait Spec[S: Specialized] - -inline trait A[T] - def x(y: Spec[T]) - -class B extends A[Char] - -// inline trait Spec2[W: Specialized] -// inline trait Spec[S: Specialized] -// def z(y: Spec2[S]) -// inline trait A[T] -// def x(y: Spec[T]) -// class B extends A[Char] - -// 1. Specialization does nothing because no materially specialized instances -// 2. Inlining generates reference to Spec[Char] which is materially specialized -// 3. Specialization generates Spec$sp$Char class -// 4. Inlining fills this class up which generates reference to Spec2[Char] -// 5. Specialization generates Spec2$sp$Char class -// 6. Inlining again and done. \ No newline at end of file From ee4eabb9853d955a71ea6bee733c4ddbe033d44f Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 26 Mar 2026 11:58:26 +0100 Subject: [PATCH 066/254] Add test for inline trait name shadow behaviour --- ...ait-multiple-parents-same-method-name-shadow.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/run/inline-trait-multiple-parents-same-method-name-shadow.scala diff --git a/tests/run/inline-trait-multiple-parents-same-method-name-shadow.scala b/tests/run/inline-trait-multiple-parents-same-method-name-shadow.scala new file mode 100644 index 000000000000..553bf40812a6 --- /dev/null +++ b/tests/run/inline-trait-multiple-parents-same-method-name-shadow.scala @@ -0,0 +1,11 @@ +inline trait A: + def foo = "Hello World" + +inline trait B: + def foo = "Bonjour" + +class C extends A, B + +@main def Test: Unit = + val c = C() + assert(c.foo == "Bonjour") From 25f938465e34346b656d4cb200b5de7d6df94a74 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 26 Mar 2026 12:25:37 +0100 Subject: [PATCH 067/254] Insist on direct implementation for parameter passing, also for inline traits --- compiler/src/dotty/tools/dotc/transform/Mixin.scala | 10 ++++++---- tests/pos/specialized-traits-unspecialized.scala | 0 .../inline-trait-specialized-desugar.scala | 10 ++++++---- 3 files changed, 12 insertions(+), 8 deletions(-) delete mode 100644 tests/pos/specialized-traits-unspecialized.scala rename tests/{pos => run}/inline-trait-specialized-desugar.scala (80%) diff --git a/compiler/src/dotty/tools/dotc/transform/Mixin.scala b/compiler/src/dotty/tools/dotc/transform/Mixin.scala index 9707f1ce199d..2caa8892e99f 100644 --- a/compiler/src/dotty/tools/dotc/transform/Mixin.scala +++ b/compiler/src/dotty/tools/dotc/transform/Mixin.scala @@ -275,7 +275,6 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase => transformFollowingDeep(superRef(baseCls.primaryConstructor).appliedToNone) :: Nil def traitInits(mixin: ClassSymbol): List[Tree] = { - if mixin.isInlineTrait then return Nil val argsIt = superCallsAndArgs.get(mixin) match case Some((_, _, args)) => args.iterator case _ => Iterator.empty @@ -319,9 +318,9 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase => val rhs = if (wasOneOf(getter, ParamAccessor)) nextArgument() - else if (getter.is(Lazy, butNot = Module)) + else if (!mixin.isInlineTrait && getter.is(Lazy, butNot = Module)) transformFollowing(superRef(getter).appliedToNone) - else if (getter.is(Module)) + else if (!mixin.isInlineTrait && getter.is(Module)) if ctx.settings.scalajs.value && getter.moduleClass.isJSType then if getter.is(Scala2x) then report.error( @@ -334,7 +333,10 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase => else Underscore(getter.info.resultType) // transformFollowing call is needed to make memoize & lazy vals run - transformFollowing(DefDef(mkForwarderSym(getter.asTerm), rhs)) + if (!mixin.isInlineTrait) then + transformFollowing(DefDef(mkForwarderSym(getter.asTerm), rhs)) + else + EmptyTree } else if wasOneOf(getter, ParamAccessor) then // mixin parameter field is defined by an override; evaluate the argument and throw it away diff --git a/tests/pos/specialized-traits-unspecialized.scala b/tests/pos/specialized-traits-unspecialized.scala deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/tests/pos/inline-trait-specialized-desugar.scala b/tests/run/inline-trait-specialized-desugar.scala similarity index 80% rename from tests/pos/inline-trait-specialized-desugar.scala rename to tests/run/inline-trait-specialized-desugar.scala index 3d947562dae5..00ad49b109c3 100644 --- a/tests/pos/inline-trait-specialized-desugar.scala +++ b/tests/run/inline-trait-specialized-desugar.scala @@ -23,13 +23,13 @@ def foo(x: ArrayIterator$sp$Int): Int = x.next() // User code does this: /* class MyClassA - class MyClassB extends MyClassA, ArrayIterator[Int] */ + class MyClassB extends MyClassA, ArrayIterator[Int](Array.from(Seq(1, 5))) */ // We convert this to: class MyClassA -class MyClassB extends MyClassA, ArrayIterator$sp$Int +class MyClassB extends MyClassA, ArrayIterator$sp$Int, ArrayIterator[Int](Array.from(Seq(1, 5))) -@main def main = +@main def Test = val xs: Array[Int] = Array(1, 2, 3) // User code does this: @@ -38,4 +38,6 @@ class MyClassB extends MyClassA, ArrayIterator$sp$Int // We convert this to: val ai = ArrayIterator$impl$Int(xs) - println(ai.next()) + val mcb = MyClassB() + assert(mcb.hasNext) + assert(ai.next() == 1) From 3ddb23e7692fcc32adb5e4f89f9de526bd1a1e2f Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 29 Mar 2026 20:55:34 +0200 Subject: [PATCH 068/254] Fix comment in benchmark file --- .../dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala index bc8356e39cc9..3c5add52ac41 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala @@ -1,4 +1,4 @@ -// Run with: scala-cli --power --jmh dotty/tests/run/specialized-traits-benchmark.scala +// Run with: scala-cli --power --jmh bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala // May have to run it again / delete .scala-build and rerun if you get a class not found // error from scala-cli first time - the --jmh flag is still experimental. //> using scala 3.8.3-RC1-bin-SNAPSHOT-nonbootstrapped From 95b997a8ff7d0efa804b186778ed80b4f4b3f55b Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 10:33:58 +0200 Subject: [PATCH 069/254] Clean up benchmark --- .../dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala index 3c5add52ac41..13f2f7239be3 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala @@ -1,6 +1,7 @@ // Run with: scala-cli --power --jmh bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala // May have to run it again / delete .scala-build and rerun if you get a class not found // error from scala-cli first time - the --jmh flag is still experimental. + //> using scala 3.8.3-RC1-bin-SNAPSHOT-nonbootstrapped package dotty.tools.benchmarks @@ -10,7 +11,6 @@ import java.util.concurrent.TimeUnit class VecManual(elems: Array[Int]): private val num = summon[Numeric[Int]] - private val x = 1 def length = elems.length @@ -25,7 +25,6 @@ class VecManual(elems: Array[Int]): class VecGeneric[T: Numeric](elems: Array[T]): private val num = summon[Numeric[T]] - private val x = 1 def length = elems.length @@ -40,7 +39,6 @@ class VecGeneric[T: Numeric](elems: Array[T]): inline trait VecSpecialized[T: {Specialized, Numeric}](elems: Array[T]): private val num = summon[Numeric[T]] - private val x = 1 def length = elems.length From 24e34574779a157661ffcdeaca2dbe288df6dbd7 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 10:40:27 +0200 Subject: [PATCH 070/254] Make specialized traits experimental, using erasedDefs for Specialized --- .../tools/benchmarks/SpecializedTraitsBenchmark.scala | 1 + compiler/src/dotty/tools/dotc/config/Feature.scala | 2 ++ library/src/scala/Specialized.scala | 9 +++++++-- library/src/scala/language.scala | 7 +++++++ library/src/scala/runtime/stdLibPatches/language.scala | 3 +++ ...d-trait-inlining-causes-implementation-required.scala | 2 ++ ...pecialized-trait-inlining-causes-specialization.scala | 2 ++ tests/pos/specialized-trait-simplest.scala | 2 ++ tests/pos/specialized-traits-inheritance.scala | 1 + tests/pos/specialized-traits-with-param.scala | 2 ++ tests/run/specialized-trait-maths.scala | 2 ++ .../specialized-trait-pathological-context-bounds.scala | 2 ++ tests/run/specialized-trait-vector-dot-product.scala | 4 ++-- ...cialized-traits-check-specialized-method-called.scala | 2 ++ 14 files changed, 37 insertions(+), 4 deletions(-) diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala index 13f2f7239be3..39dafb331bf6 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala @@ -3,6 +3,7 @@ // error from scala-cli first time - the --jmh flag is still experimental. //> using scala 3.8.3-RC1-bin-SNAPSHOT-nonbootstrapped +//> using options -language:experimental.specializedTraits package dotty.tools.benchmarks diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 09410c78eba4..31130835ec57 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -28,6 +28,7 @@ object Feature: val dependent = experimental("dependent") val erasedDefinitions = experimental("erasedDefinitions") + val specializedTraits = experimental("specializedTraits") val strictEqualityPatternMatching = experimental("strictEqualityPatternMatching") val symbolLiterals = deprecated("symbolLiterals") val saferExceptions = experimental("saferExceptions") @@ -61,6 +62,7 @@ object Feature: (scala2macros, "Allow Scala 2 macros"), (dependent, "Allow dependent method types"), (erasedDefinitions, "Allow erased definitions"), + (specializedTraits, "Allow specialized traits"), (strictEqualityPatternMatching, "relaxed CanEqual checks for ADT pattern matching"), (symbolLiterals, "Allow symbol literals"), (saferExceptions, "Enable safer exceptions"), diff --git a/library/src/scala/Specialized.scala b/library/src/scala/Specialized.scala index 078a2a3a376e..791295b3dc8a 100644 --- a/library/src/scala/Specialized.scala +++ b/library/src/scala/Specialized.scala @@ -1,5 +1,10 @@ package scala +import language.experimental.erasedDefinitions +import scala.annotation.nowarn + +trait Specialized[T] extends compiletime.Erased -trait Specialized[T] object Specialized: - def apply[T] = new Specialized[T] {} + /* @nowarn: New anonymous class definition will be duplicated at each inline site, + however it's erased at runtime so we don't care. */ + @nowarn inline def apply[T] = new Specialized[T] {} diff --git a/library/src/scala/language.scala b/library/src/scala/language.scala index 9819e1e0a07b..6d048fb003ed 100644 --- a/library/src/scala/language.scala +++ b/library/src/scala/language.scala @@ -244,6 +244,13 @@ object language { @compileTimeOnly("`erasedDefinitions` can only be used at compile time in import statements") object erasedDefinitions + /** Experimental support for specialized traits + * + * @see [[docs/_docs/internals/specialized-traits.md]] + */ + @compileTimeOnly("`specializedTraits` can only be used at compile time in import statements") + object specializedTraits + /** Experimental support for relaxed CanEqual checks for ADT pattern matching * * @see [[https://github.com/scala/improvement-proposals/pull/97]] diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index e574655c8168..633c5bf709d9 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -52,6 +52,9 @@ private[scala] object language: @compileTimeOnly("`erasedDefinitions` can only be used at compile time in import statements") object erasedDefinitions + @compileTimeOnly("`specializedTraits` can only be used at compile time in import statements") + object specializedTraits + /** Experimental support for relaxed CanEqual checks for ADT pattern matching * * @see [[https://github.com/scala/improvement-proposals/pull/97]] diff --git a/tests/pos/specialized-trait-inlining-causes-implementation-required.scala b/tests/pos/specialized-trait-inlining-causes-implementation-required.scala index 56b0600648ce..b2833f1084c5 100644 --- a/tests/pos/specialized-trait-inlining-causes-implementation-required.scala +++ b/tests/pos/specialized-trait-inlining-causes-implementation-required.scala @@ -1,3 +1,5 @@ +//> using options -language:experimental.specializedTraits + // A[Char] => A$sp$Char => C$sp$Char, but only once we inline the body of C$sp$Char do we realise that we need // C$impl$Char as well. diff --git a/tests/pos/specialized-trait-inlining-causes-specialization.scala b/tests/pos/specialized-trait-inlining-causes-specialization.scala index f928978d35b8..0283e7b2873c 100644 --- a/tests/pos/specialized-trait-inlining-causes-specialization.scala +++ b/tests/pos/specialized-trait-inlining-causes-specialization.scala @@ -1,3 +1,5 @@ +//> using options -language:experimental.specializedTraits + inline trait D[R: Specialized] inline trait C[S: Specialized]: diff --git a/tests/pos/specialized-trait-simplest.scala b/tests/pos/specialized-trait-simplest.scala index 839b6af44d5c..7c230c58c2bd 100644 --- a/tests/pos/specialized-trait-simplest.scala +++ b/tests/pos/specialized-trait-simplest.scala @@ -1,3 +1,5 @@ +//> using options -language:experimental.specializedTraits + inline trait Foo[T: Specialized] @main def main = diff --git a/tests/pos/specialized-traits-inheritance.scala b/tests/pos/specialized-traits-inheritance.scala index 75de30f0dc31..e0572aac84e0 100644 --- a/tests/pos/specialized-traits-inheritance.scala +++ b/tests/pos/specialized-traits-inheritance.scala @@ -1,3 +1,4 @@ +//> using options -language:experimental.specializedTraits inline trait Foo[T: Specialized](x: T): def foo = x diff --git a/tests/pos/specialized-traits-with-param.scala b/tests/pos/specialized-traits-with-param.scala index bbb4a57d0167..aa9cba4bf7c3 100644 --- a/tests/pos/specialized-traits-with-param.scala +++ b/tests/pos/specialized-traits-with-param.scala @@ -1,3 +1,5 @@ +//> using options -language:experimental.specializedTraits + inline trait Foo[T: Specialized](x: T): def foo = x diff --git a/tests/run/specialized-trait-maths.scala b/tests/run/specialized-trait-maths.scala index f2b47201c741..008a2590c7ba 100644 --- a/tests/run/specialized-trait-maths.scala +++ b/tests/run/specialized-trait-maths.scala @@ -1,3 +1,5 @@ +//> using options -language:experimental.specializedTraits + inline trait Foo[T: Specialized](x: T): def foo = x diff --git a/tests/run/specialized-trait-pathological-context-bounds.scala b/tests/run/specialized-trait-pathological-context-bounds.scala index 477ba8324d14..d299704034eb 100644 --- a/tests/run/specialized-trait-pathological-context-bounds.scala +++ b/tests/run/specialized-trait-pathological-context-bounds.scala @@ -1,3 +1,5 @@ +//> using options -language:experimental.specializedTraits + trait A[T, R, Q] inline trait Trait[T: {Specialized, Numeric}, S <: Object, Q: Numeric, R: Specialized, D: {Numeric, Specialized}](a: Int) extends A[S, Char, T] { diff --git a/tests/run/specialized-trait-vector-dot-product.scala b/tests/run/specialized-trait-vector-dot-product.scala index 935f82210106..9228fa2030b8 100644 --- a/tests/run/specialized-trait-vector-dot-product.scala +++ b/tests/run/specialized-trait-vector-dot-product.scala @@ -1,6 +1,7 @@ +//> using options -language:experimental.specializedTraits + inline trait Vec[T: {Specialized, Numeric}](elems: Array[T]): private val num = summon[Numeric[T]] - private val x = 1 def length = elems.length @@ -19,4 +20,3 @@ object Test: val y = new Vec[Int](Array(3, 4, 5, 6, 7)) {} val z = x.scalarProduct(y) assert(z == 85) - \ No newline at end of file diff --git a/tests/run/specialized-traits-check-specialized-method-called.scala b/tests/run/specialized-traits-check-specialized-method-called.scala index 9b5c153427a4..796c0809a35e 100644 --- a/tests/run/specialized-traits-check-specialized-method-called.scala +++ b/tests/run/specialized-traits-check-specialized-method-called.scala @@ -1,3 +1,5 @@ +//> using options -language:experimental.specializedTraits + // Check we actually call the specialized method that we should be calling // We can't easily check the return type, but we can check that we are calling a method // on Foo$impl$Int$ directly, with no bridge methods in between (this means it's the correct From 511f730783bd9b9f9da330e655095725e51e5183 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 10:41:16 +0200 Subject: [PATCH 071/254] Build new collections example from strawman --- ...pecialized-trait-collections-example.scala | 40 ----- tests/pos/specialized-traits-strawman.scala | 141 ------------------ ...pecialized-trait-collections-example.scala | 28 ++++ 3 files changed, 28 insertions(+), 181 deletions(-) delete mode 100644 tests/pos/specialized-trait-collections-example.scala delete mode 100644 tests/pos/specialized-traits-strawman.scala create mode 100644 tests/run/specialized-trait-collections-example.scala diff --git a/tests/pos/specialized-trait-collections-example.scala b/tests/pos/specialized-trait-collections-example.scala deleted file mode 100644 index de41d305a88b..000000000000 --- a/tests/pos/specialized-trait-collections-example.scala +++ /dev/null @@ -1,40 +0,0 @@ -inline trait Iterator[T: Specialized]: - def hasNext: Boolean - def next(): T - -// They do this: (with Specialized type class) -inline trait ArrayIterator[T: Specialized](elems: Array[T]) extends Iterator[T]: - private var current = 0 - def hasNext: Boolean = current < elems.length - def next(): T = try elems(current) finally current += 1 - -// We should generate these: -// trait Iteratorsp$Int extends Iterator[Int] -// trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator[Int] -// class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator[Int](elems) - - -// They do this: -def foo(x: ArrayIterator[Int]): Int = x.next() -// We should convert this to: -// def foo(x: ArrayIterator$sp$Int): Int = x.next() -// Check that the call to next() should be a specialized call and not have boxing - can compare to without specialized to see the impact. - -// They do this: -// class MyClassA -// class MyClassB extends MyClassA, ArrayIterator[Int] - -// // We convert this to: -// class MyClassA -// class MyClassB extends MyClassA, ArrayIterator$sp$Int - -// @main def main = -// val xs: Array[Int] = Array(1, 2, 3) - -// // They do this: -// // new ArrayIterator[Int](xs) {} - -// // We convert this to: -// val ai = new ArrayIterator$impl$Int(xs) {} -// println(ai.next()) -100 \ No newline at end of file diff --git a/tests/pos/specialized-traits-strawman.scala b/tests/pos/specialized-traits-strawman.scala deleted file mode 100644 index 27ec7ef110ff..000000000000 --- a/tests/pos/specialized-traits-strawman.scala +++ /dev/null @@ -1,141 +0,0 @@ -import language.experimental.erasedDefinitions - -// Source code - -/*inline*/ trait Iterator[T/*: Specialized*/]: - def hasNext: Boolean - def next(): T - -/*inline*/ trait ArrayIterator[T/*: Specialized*/](elems: Array[T]) extends Iterator[T]: - private var current = 0 - def hasNext: Boolean = current < elems.length - def next(): T = try elems(current) finally current += 1 - -/*inline*/ trait Iterable[T/*: Specialized*/]: - def iterator: Iterator[T] - def forall(f: T => Unit): Unit = - val it = iterator - while it.hasNext do f(it.next()) - -/*inline*/ trait Seq[T/*: Specialized*/](elems: Array[T]) -extends Iterable[T]: - def length: Int = elems.length - def apply(i: Int): T = elems(i) - def iterator: Iterator[T] = new ArrayIterator[T](elems) {} - -// Specialized trait APIs generated from specialization: - -trait Iterator_Int extends Iterator[Int]: - def hasNext: Boolean - def next(): Int - -trait ArrayIterator_Int extends ArrayIterator[Int], Iterator[Int] - -trait Iterable_Int extends Iterable[Int]: - def iterator: Iterator_Int - def forall(f: Int => Unit): Unit - -trait Seq_Int extends Seq[Int], Iterable[Int]: - def length: Int - def apply(i: Int): Int - -class ArrayIterator_Int$impl(elems: Array[Int]) extends ArrayIterator_Int - , ArrayIterator[Int](elems): // snd parent not needed in actual translation - private var current = 0 - override def hasNext: Boolean = - current < elems.length - override def next(): Int = - try elems(current) finally current += 1 - -class Seq_Int$impl(elems: Array[Int]) extends Seq_Int - , Seq[Int](elems): // snd parent not needed in actual translation - override def iterator: Iterator_Int = - new ArrayIterator_Int$impl(elems).asInstanceOf - // cast needed since the compiler does not not know that Iterable[Int] = Iterable_Int - // after erasure. No cast would be needed in the actual translation. - - override def forall(f: Int => Unit): Unit = - val it = iterator - while it.hasNext do f(it.next()) - override def length: Int = elems.length - override def apply(i: Int): Int = elems(i) - -// Inline trait APIs generated from inline traits transform: -object InlineTraitAPIs: - - trait Iterator[T]: - def hasNext: Boolean - def next(): T - - trait ArrayIterator[T] extends Iterator[T] - - trait Iterable[T]: - def iterator: Iterator[T] - def forall(f: T => Unit): Unit - - trait Seq[T] extends Iterable[T]: - def length: Int - def apply(i: Int): T -end InlineTraitAPIs - -/* Code after erasure: */ -object AfterErasure: - - trait Function1: - def apply(x: Any): Any - def apply_Int_Unit(x: Int): Unit - - trait Iterator: - def hasNext: Boolean - def next(): Any - - trait ArrayIterator extends Iterator - - trait Iterable: - def iterator: Iterator - def forall(f: Function1): Unit - - trait Seq: - def length: Int - def apply(i: Int): Any - - trait Iterator_Int extends Iterator: - def hasNext: Boolean - def next(): Int - - trait ArrayIterator_Int extends ArrayIterator, Iterator_Int - - trait Iterable_Int extends Iterable: - def iterator: Iterator_Int - def forall(f: Function1): Unit - - trait Seq_Int extends Seq, Iterable_Int: - def length: Int - def apply(i: Int): Int - - class ArrayIterator_Int$impl(elems: Array[Int]) extends ArrayIterator_Int: - private var current = 0 - override def hasNext: Boolean = - current < elems.length - override def next(): Int = - try elems(current) finally current += 1 - - /* Bridges: - override def next(): Any = Int.box(next()) - */ - end ArrayIterator_Int$impl - - class Seq_Int$impl(elems: Array[Int]) extends Seq_Int: - override def iterator: Iterator_Int = - new ArrayIterator_Int$impl(elems) - override def forall(f: Function1): Unit = - val it = iterator - while it.hasNext do f.apply_Int_Unit(it.next()) - override def length: Int = elems.length - override def apply(i: Int): Int = elems(i) - - /* Bridges: - override def iterator: Iterator = iterator - override def apply(i: Int): Any = Int.box(apply(i)) - */ - end Seq_Int$impl diff --git a/tests/run/specialized-trait-collections-example.scala b/tests/run/specialized-trait-collections-example.scala new file mode 100644 index 000000000000..3f7e5d7bb099 --- /dev/null +++ b/tests/run/specialized-trait-collections-example.scala @@ -0,0 +1,28 @@ +//> using options -language:experimental.specializedTraits + +inline trait Iterator[T: Specialized]: + def hasNext: Boolean + def next(): T + +inline trait ArrayIterator[T: Specialized](elems: Array[T]) extends Iterator[T]: + private var current = 0 + def hasNext: Boolean = current < elems.length + def next(): T = try elems(current) finally current += 1 + +inline trait Iterable[T: Specialized]: + def iterator: Iterator[T] + def forall(f: T => Unit): Unit = + val it = iterator + while it.hasNext do f(it.next()) + +inline trait Seq[T: Specialized](elems: Array[T]) extends Iterable[T]: + def length: Int = elems.length + def apply(i: Int): T = elems(i) + def iterator: Iterator[T] = new ArrayIterator[T](elems) {} + +@main def Test = + val elems = Array.from(scala.collection.immutable.Seq(1, 2, 3, 4, 5)) + val seq = new Seq[Int](elems) {} + var x = 0 + seq.forall(v => x += v) + assert(x == 15) From f55b8d034524ef5cfa2dc1eef7675067be550384 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 10:43:37 +0200 Subject: [PATCH 072/254] Allow specializations to match parent signatures in overriding methods --- .../dotty/tools/dotc/core/Denotations.scala | 10 ++++++--- .../src/dotty/tools/dotc/core/Signature.scala | 21 +++++++++++++++---- .../dotty/tools/dotc/core/TypeComparer.scala | 6 +++++- .../dotty/tools/dotc/typer/RefChecks.scala | 2 +- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Denotations.scala b/compiler/src/dotty/tools/dotc/core/Denotations.scala index 03152ab7e291..f471da57c3e5 100644 --- a/compiler/src/dotty/tools/dotc/core/Denotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Denotations.scala @@ -1022,9 +1022,13 @@ object Denotations { final def first: SingleDenotation = this final def last: SingleDenotation = this + def matchesOrSpecialized(other: SingleDenotation)(using Context): Boolean = + symbol.hasTargetName(other.symbol.targetName) + && matchesLoosely(other, allowSpecializations = true) + def matches(other: SingleDenotation)(using Context): Boolean = symbol.hasTargetName(other.symbol.targetName) - && matchesLoosely(other) + && matchesLoosely(other, allowSpecializations = false) /** `matches` without a target name check. * @@ -1034,7 +1038,7 @@ object Denotations { * erasure (see i8615b, i9109b), Erasure takes care of adding any necessary * bridge to make this work at runtime. */ - def matchesLoosely(other: SingleDenotation, alwaysCompareTypes: Boolean = false)(using Context): Boolean = + def matchesLoosely(other: SingleDenotation, alwaysCompareTypes: Boolean = false, allowSpecializations: Boolean = false)(using Context): Boolean = if isType then true else val thisLanguage = SourceLanguage(symbol) @@ -1042,7 +1046,7 @@ object Denotations { val commonLanguage = SourceLanguage.commonLanguage(thisLanguage, otherLanguage) val sig = signature(commonLanguage) val otherSig = other.signature(commonLanguage) - sig.matchDegree(otherSig) match + sig.matchDegree(otherSig, allowSpecializations) match case FullMatch => !alwaysCompareTypes || info.matches(other.info) case MethodNotAMethodMatch => diff --git a/compiler/src/dotty/tools/dotc/core/Signature.scala b/compiler/src/dotty/tools/dotc/core/Signature.scala index f8475a4cf314..6528187df2e6 100644 --- a/compiler/src/dotty/tools/dotc/core/Signature.scala +++ b/compiler/src/dotty/tools/dotc/core/Signature.scala @@ -6,6 +6,7 @@ import scala.annotation.tailrec import Names.*, Types.*, Contexts.*, StdNames.*, Decorators.* import TypeErasure.sigName import Signature.* +import dotty.tools.dotc.core.StdNames.str.SPECIALIZED_TRAIT_SUFFIX /** The signature of a denotation. * @@ -52,14 +53,26 @@ case class Signature(paramsSig: List[ParamSig], resSig: TypeName) { private def consistent(name1: ParamSig, name2: ParamSig) = name1 == name2 || name1 == tpnme.Uninstantiated || name2 == tpnme.Uninstantiated + // TODO: Put this somewhere else? + private def isSpecializedName(name1: ParamSig, name2: ParamSig) = (name1, name2) match { + case (n1: TypeName, n2: TypeName) => (n1.toTermName.split, n2.toTermName.split) match { + case ((prefix1, lastPart1, _), (prefix2, lastPart2, _)) => + prefix1 == prefix2 && lastPart2.startsWith((lastPart1 ++ SPECIALIZED_TRAIT_SUFFIX).toString()) + || lastPart1.startsWith((lastPart2 ++ SPECIALIZED_TRAIT_SUFFIX).toString()) + } + case _ => false + } + /** Does this signature coincide with that signature on their parameter parts? * This is the case if all parameter signatures are _consistent_, i.e. they are either * equal or on of them is tpnme.Uninstantiated. */ - final def consistentParams(that: Signature)(using Context): Boolean = { + final def consistentParams(that: Signature, allowSpecializations: Boolean = false)(using Context): Boolean = { @tailrec def loop(names1: List[ParamSig], names2: List[ParamSig]): Boolean = if (names1.isEmpty) names2.isEmpty - else !names2.isEmpty && consistent(names1.head, names2.head) && loop(names1.tail, names2.tail) + else !names2.isEmpty && (consistent(names1.head, names2.head) || ( + allowSpecializations && isSpecializedName(names1.head, names2.head) + )) && loop(names1.tail, names2.tail) loop(this.paramsSig, that.paramsSig) } @@ -85,8 +98,8 @@ case class Signature(paramsSig: List[ParamSig], resSig: TypeName) { * or `ParamMatch`. * If the parameters are inconsistent, the result is always `NoMatch`. */ - final def matchDegree(that: Signature)(using Context): MatchDegree = - if consistentParams(that) then + final def matchDegree(that: Signature, allowSpecializations: Boolean = false)(using Context): MatchDegree = + if consistentParams(that, allowSpecializations) then if resSig == that.resSig || isWildcard(resSig) || isWildcard(that.resSig) then FullMatch else if (this == NotAMethod) != (that == NotAMethod) then diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 746be4ae5766..07ca81193223 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -28,6 +28,8 @@ import NameKinds.WildcardParamName import MatchTypes.isConcrete import reporting.Message.Note import scala.util.boundary, boundary.break +import transform.DesugarSpecializedTraits + /** Provides methods to compare types. */ @@ -2410,7 +2412,9 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case formal2 :: rest2 => val formal2a = if (tp2.isParamDependent) formal2.subst(tp2, tp1) else formal2 val paramsMatch = - if precise then + if DesugarSpecializedTraits.isSpecializationOf(formal1, formal2a) then + true + else if precise then isSameTypeWhenFrozen(formal1, formal2a) else if isCaptureCheckingOrSetup then // allow to constrain capture set variables diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 51b3b3271bd7..cb9d96340b04 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -988,7 +988,7 @@ object RefChecks { def isSignatureMatch(sym: Symbol) = sym.isType || { val self = clazz.thisType - sym.asSeenFrom(self).matches(member.asSeenFrom(self)) + sym.asSeenFrom(self).matchesOrSpecialized(member.asSeenFrom(self)) && !incompatibleRepeatedParam(sym, member) } From 57b5e386d9b136bf2dc28b1a2c5bfd6eda2c96b3 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 10:50:44 +0200 Subject: [PATCH 073/254] Also inline interfaces in inline traits --- compiler/src/dotty/tools/dotc/inlines/Inlines.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 0058bad97040..68873fb6672e 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -75,7 +75,7 @@ object Inlines: def isInlineableFromInlineTrait(inlinedTraitSym: ClassSymbol, member: tpd.Tree)(using Context): Boolean = !(member.isInstanceOf[tpd.TypeDef] && inlinedTraitSym.typeParams.contains(member.symbol)) && !member.symbol.isAllOf(Inline) - && !member.symbol.is(Deferred) + // && !member.symbol.is(Deferred) // Also inline interfaces (see specialized-trait-collections-example.scala) /** Should call be inlined in this context? */ def needsInlining(tree: Tree)(using Context): Boolean = From 01375111fb87404ba930cd2d3d507b9f85234731 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 10:53:29 +0200 Subject: [PATCH 074/254] Specialize parents of inline traits, break out symbol replacement map, generate specializations recursively --- .../transform/DesugarSpecializedTraits.scala | 110 +++++++++++------- 1 file changed, 71 insertions(+), 39 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 5a4f82776bb9..441e511f4645 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -49,15 +49,25 @@ class DesugarSpecializedTraits extends MacroTransform: override def newTransformer(using Context): Transformer = new Transformer { - private def newInterfaceTrait(specialization: Specialization) = + private def newInterfaceTrait(specialization: Specialization, specializations: SpecializedTraitCache): (ClassSymbol, SpecializedTraitCache) = { val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. def apply(t: Type) = specialization.specializedTypeParamsToTypeArgumentsMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) // TODO: IF we can do just types we can get rid fo this + val inheritedParents = specialization.traitSymbol.denot.info.parents.filterNot(_ eq defn.ObjectType).map(tm(_)) + // Parents may be specializable and so we need to specialize them as well + // See ArrayIterator extends Iterator in specialized-trait-collections-example.scala + val specializations1 = inheritedParents.foldLeft(specializations)((specializations, parent) => + parent match { + case Specialization(spec) if spec.isSpecialized => (specializations.addInterface(spec)) + case _ => specializations + } + ) + // Create new trait val parents = defn.ObjectType - :: AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization).tpe // original trait, specialized - :: specialization.traitSymbol.denot.info.parents.filterNot(_ eq defn.ObjectType).map(tm(_)) // parents of the original trait, specialized - + :: AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization).tpe // original trait, specialized to Foo[Int] + :: inheritedParents.map(replaceSpecializedSymbolsMap(specializations1).typeMap(_)) // parents of the original trait, specialized to Foo$sp$Int + val traitSymbol = newNormalizedClassSymbol( specialization.traitSymbol.owner, DesugarSpecializedTraits.newSpecializedTraitName(specialization), @@ -83,7 +93,8 @@ class DesugarSpecializedTraits extends MacroTransform: val freshTypeVarMap = new TypeMap: def apply(t: Type) = tpMap.applyOrElse(t, mapOver) traitSymbol.info = ClassInfo(traitSymbol.owner.thisType, traitSymbol, traitSymbol.info.parents.map(freshTypeVarMap(_)), traitSymbol.info.decls) // TODO: What happens if the creator of the specialized inline trait provides a self type? - traitSymbol.entered + (traitSymbol.entered, specializations1) + } private def buildInterfaceTraitTree(interfaceSymbol: ClassSymbol)(using Context) = { val init = newDefaultConstructor(interfaceSymbol) @@ -190,20 +201,13 @@ class DesugarSpecializedTraits extends MacroTransform: classDef } - // Returns (new stmts including original, new symbols including original) - private def transformStatements(stats: List[Tree], span: Span, specializations: SpecializedTraitCache): (List[Tree], SpecializedTraitCache) = { - val specializations1 = collectReferencedSpecializations(stats, specializations) - val generatedTraitStats = specializations1.getNewInterfaceSymbols.toList.map(buildInterfaceTraitTree) - val generatedClassStats = specializations1.getNewImplementationSymbols.toList.map(buildImplementationClassTree) - - val specializations2 = specializations1.installNewInterfaceSymbols.installNewImplementationSymbols - + private def replaceSpecializedSymbolsMap(specializations: SpecializedTraitCache) = // Use the TreeTypeMap to replace instances (can we do this without accidentally replacing the definitions? I think it should be ok) val typeMap = new TypeMap: def apply(t: Type) = t match { case Specialization(spec) => { - for (specializedSymbol <- specializations2.getInterfaceSymbol(spec)) + for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) yield specializedSymbol.typeRef.appliedTo(spec.unspecializedTypeArgs.map(_.tpe)) }.getOrElse(mapOver(t)) case _ => mapOver(t) @@ -214,10 +218,10 @@ class DesugarSpecializedTraits extends MacroTransform: case Block(List(TypeDef(anon, Template(_, parentCalls: List[Tree], _, _))), Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => parentCalls match { - case _ :: Apply(Apply(tpe, ctorArgs), ev) :: Nil => // only allowed to extend Object and our specialized trait + case _ :+ Apply(Apply(tpe, ctorArgs), ev) => // extends Object, parents of spec trait, spec trait val spec = Specialization.unapply(t.tpe).get { // We don't replace non-specialized anonymous class instantiations e.g. new Foo[T] where T is defined in the enclosing scope. - for (specializedSymbol <- specializations2.getImplementationSymbol(spec)) + for (specializedSymbol <- specializations.getImplementationSymbol(spec)) yield Typed(Apply(Apply(Select(New(ref(specializedSymbol)),ctor), ctorArgs), ev), t) }.getOrElse(tree) case _ => tree @@ -228,20 +232,20 @@ class DesugarSpecializedTraits extends MacroTransform: case Apply(TypeApply(fun@Select(New(tpt), init), args), ev) if fun.symbol.isConstructor => val spec = Specialization(fun.symbol.owner, args) { - for (specializedSymbol <- specializations2.getInterfaceSymbol(spec)) + for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) yield New(ref(specializedSymbol)).select(init).appliedToTypeTrees(spec.unspecializedTypeArgs) }.getOrElse(tree) // Replace AppliedTypeTree instances in code case Specialization(spec) => { - for (specializedSymbol <- specializations2.getInterfaceSymbol(spec)) + for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) yield AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? }.getOrElse(tree) case tree => tree } - val treeTypeMap = new TreeTypeMap(typeMap, treeMap) { + new TreeTypeMap(typeMap, treeMap) { override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? case dd@DefDef(name, paramss, tpt, preRhs) => val transformedDef = super.transform(dd) @@ -250,22 +254,30 @@ class DesugarSpecializedTraits extends MacroTransform: // TODO: Fix the Bar extends Foo case. Avoid updating the parents that we want to keep the same. case impl@Template(constr, preParentsOrDerived, self, _) => - cpy.Template(impl)(body = impl.body.map(super.transform(_))) + cpy.Template(impl)(body = impl.body.map(transform(_))) case tree => super.transform(tree) } } + end replaceSpecializedSymbolsMap + // Returns (new stmts including original, new symbols including original) + private def transformStatements(stats: List[Tree], span: Span, specializations: SpecializedTraitCache): (List[Tree], SpecializedTraitCache) = { + val specializations1 = collectReferencedSpecializations(stats, specializations) + val generatedTraitStats = specializations1.getNewInterfaceSymbols.toList.map(buildInterfaceTraitTree) + val generatedClassStats = specializations1.getNewImplementationSymbols.toList.map(buildImplementationClassTree) + + val specializations2 = specializations1.installNewInterfaceSymbols.installNewImplementationSymbols // TODO: How do we calculate the spans correctly? val generatedTraitStats1 = generatedTraitStats.map(trtDef => Inlines.inlineParentInlineTraits(Inlines.transformInlineTrait(trtDef.withSpan(span)))) val generatedClassStats1 = generatedClassStats.map(clsDef => Inlines.inlineParentInlineTraits(clsDef.withSpan(span))) - + if (generatedTraitStats1.isEmpty && generatedClassStats1.isEmpty) - (stats.map(treeTypeMap(_)), specializations2) + (stats.map(replaceSpecializedSymbolsMap(specializations2)(_)), specializations2) else val (generatedTraitStats2, specializations3) = transformStatements(generatedTraitStats1, span, specializations2) val (generatedClassStats2, specializations4) = transformStatements(generatedClassStats1, span, specializations3) - (generatedTraitStats2 ++ generatedClassStats2 ++ stats.map(treeTypeMap(_)), specializations4) + (generatedTraitStats2 ++ generatedClassStats2 ++ stats.map(replaceSpecializedSymbolsMap(specializations4)(_)), specializations4) } override def transform(tree: Tree)(using Context): Tree = tree @@ -291,7 +303,8 @@ class DesugarSpecializedTraits extends MacroTransform: case Specialization(spec) if spec.isSpecialized => specializations.addInterfaceAndImplementation(spec) case _ => specializations } - case Specialization(spec) if (spec.isSpecialized) => specializations.addInterface(spec) + case Specialization(spec) if (spec.isSpecialized) => + specializations.addInterface(spec) case _ => specializations ) }) @@ -315,7 +328,17 @@ object DesugarSpecializedTraits: private[transform] def newImplementationClassName(specialization: Specialization)(using Context): TypeName = generateName(specialization, str.SPECIALIZED_TRAIT_IMPL_SUFFIX) - + // TODO: Put this somewhere else; consider if we want to do it like this? + def isSpecializationOf(type1: Type, type2: Type)(using Context) = + type2 match { + case Specialization(spec) => type1 match { + case AppliedType(tp, args) => + tp.typeSymbol.name == newSpecializedTraitName(spec) + case _ => false + } + case _ => false + } +end DesugarSpecializedTraits /* Stores the specializations we have found in the program and the symbols for the interface traits and implementation classes that will replace them. We generate these symbols when we enter the specializations into the cache, via the functions @@ -331,22 +354,30 @@ object DesugarSpecializedTraits: getNeWImplementationSymbols. */ + +object SpecializedTraitCache: + type SymbolMap = Map[Specialization, ClassSymbol] + type GenInterfaceSymbol = (Specialization, SpecializedTraitCache) => (ClassSymbol, SpecializedTraitCache) + type GenImplementationSymbol = (Specialization, ClassSymbol) => ClassSymbol + + class SpecializedTraitCache( - private val newInterfaceSymbols: Map[Specialization, ClassSymbol] = Map.empty, - private val newImplementationSymbols: Map[Specialization, ClassSymbol] = Map.empty, - private val interfaceSymbols: Map[Specialization, ClassSymbol] = Map.empty, - private val implementationSymbols: Map[Specialization, ClassSymbol] = Map.empty, - private val genInterfaceSymbol: Specialization => ClassSymbol, - private val genImplementationSymbol: (Specialization, ClassSymbol) => ClassSymbol + private val newInterfaceSymbols: SpecializedTraitCache.SymbolMap = Map.empty, + private val newImplementationSymbols: SpecializedTraitCache.SymbolMap = Map.empty, + private val interfaceSymbols: SpecializedTraitCache.SymbolMap = Map.empty, + private val implementationSymbols: SpecializedTraitCache.SymbolMap = Map.empty, + private val genInterfaceSymbol: SpecializedTraitCache.GenInterfaceSymbol, + private val genImplementationSymbol: SpecializedTraitCache.GenImplementationSymbol ): + def copy( - newInterfaceSymbols: Map[Specialization, ClassSymbol] = this.newInterfaceSymbols, - newImplementationSymbols: Map[Specialization, ClassSymbol] = this.newImplementationSymbols, - interfaceSymbols: Map[Specialization, ClassSymbol] = this.interfaceSymbols, - implementationSymbols: Map[Specialization, ClassSymbol] = this.implementationSymbols, - genInterfaceSymbol: Specialization => ClassSymbol = this.genInterfaceSymbol, - genImplementationSymbol: (Specialization, ClassSymbol) => ClassSymbol = this.genImplementationSymbol) - = SpecializedTraitCache(newInterfaceSymbols, newImplementationSymbols, interfaceSymbols, implementationSymbols, genInterfaceSymbol, genImplementationSymbol) + newInterfaceSymbols: SpecializedTraitCache.SymbolMap = this.newInterfaceSymbols, + newImplementationSymbols: SpecializedTraitCache.SymbolMap = this.newImplementationSymbols, + interfaceSymbols: SpecializedTraitCache.SymbolMap = this.interfaceSymbols, + implementationSymbols: SpecializedTraitCache.SymbolMap = this.implementationSymbols, + genInterfaceSymbol: SpecializedTraitCache.GenInterfaceSymbol = this.genInterfaceSymbol, + genImplementationSymbol: SpecializedTraitCache.GenImplementationSymbol = this.genImplementationSymbol) + = SpecializedTraitCache(newInterfaceSymbols, newImplementationSymbols, interfaceSymbols, implementationSymbols, genInterfaceSymbol, genImplementationSymbol) def getInterfaceSymbol(spec: Specialization): Option[ClassSymbol] = newInterfaceSymbols.orElse(interfaceSymbols).lift(spec) def getImplementationSymbol(spec: Specialization): Option[ClassSymbol] = newImplementationSymbols.orElse(implementationSymbols).lift(spec) @@ -358,7 +389,8 @@ class SpecializedTraitCache( if (newInterfaceSymbols.contains(spec) || interfaceSymbols.contains(spec)) then this else - this.copy(newInterfaceSymbols = newInterfaceSymbols + (spec -> genInterfaceSymbol(spec))) + val (targetSymbol, resultingCache) = genInterfaceSymbol(spec, this) + resultingCache.copy(newInterfaceSymbols = resultingCache.newInterfaceSymbols + (spec -> targetSymbol)) def addInterfaceAndImplementation(spec: Specialization): SpecializedTraitCache = if (newImplementationSymbols.contains(spec) || implementationSymbols.contains(spec)) then this From 9fd8f5c0b5e184884b8daa3d5862bb7694cd7b88 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 13:21:55 +0200 Subject: [PATCH 075/254] Enforce rules on inheritance from specialized traits --- .../transform/DesugarSpecializedTraits.scala | 67 ++++++++++++++++--- .../specialized-traits-inheritance.scala | 2 +- ...trait-inlining-causes-specialization.scala | 3 +- 3 files changed, 59 insertions(+), 13 deletions(-) rename tests/{pos => neg}/specialized-traits-inheritance.scala (69%) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 441e511f4645..80a3a0f05d34 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -32,6 +32,10 @@ import dotty.tools.dotc.core.Flags.GivenOrImplicit import dotty.tools.dotc.core.NameKinds.ContextBoundParamName import dotty.tools.dotc.inlines.Inlines import dotty.tools.dotc.util.Spans.Span +import dotty.tools.dotc.transform.DesugarSpecializedTraits.isSpecializationOf +import dotty.tools.dotc.report +import dotty.tools.dotc.transform.DesugarSpecializedTraits.isImplementationOf +import dotty.tools.dotc.core.Flags.InlineTrait class DesugarSpecializedTraits extends MacroTransform: @@ -252,8 +256,20 @@ class DesugarSpecializedTraits extends MacroTransform: transformedDef.symbol.info = mapType(transformedDef.symbol.info) transformedDef - // TODO: Fix the Bar extends Foo case. Avoid updating the parents that we want to keep the same. case impl@Template(constr, preParentsOrDerived, self, _) => + impl.parents.foreach(p => + p.tpe match { + case Specialization(spec) if + spec.hasSpecializedParams + && !impl.symbol.owner.isAnonymousClass // impl.symbol = the dummy class; owner is the actual class. + && !isSpecializationOf(impl.symbol.typeRef, p.tpe, allowImplementationClass = true) + && !isImplementationOf(impl.symbol.owner.name, p.tpe.typeSymbol.name) + && !impl.symbol.owner.isOneOf(InlineTrait) => + report.error("Specialized traits may only be extended by anonymous class instances or inline traits.", impl.srcPos) + case _ => + } + ) + cpy.Template(impl)(body = impl.body.map(transform(_))) case tree => super.transform(tree) } @@ -329,15 +345,29 @@ object DesugarSpecializedTraits: generateName(specialization, str.SPECIALIZED_TRAIT_IMPL_SUFFIX) // TODO: Put this somewhere else; consider if we want to do it like this? - def isSpecializationOf(type1: Type, type2: Type)(using Context) = + def isSpecializationOf(type1: Type, type2: Type, allowImplementationClass: Boolean = false)(using Context) = type2 match { case Specialization(spec) => type1 match { - case AppliedType(tp, args) => + case AppliedType(tp, args) => tp.typeSymbol.name == newSpecializedTraitName(spec) + || (allowImplementationClass && tp.typeSymbol.name == newImplementationClassName(spec)) + case tp: TypeRef => + (tp.typeSymbol.name.toString.contains(newSpecializedTraitName(spec).toString) && + tp.symbol.owner.name == newSpecializedTraitName(spec)) + || + (allowImplementationClass && + tp.typeSymbol.name.toString.contains(newImplementationClassName(spec).toString) && + tp.symbol.owner.name == newImplementationClassName(spec) + ) case _ => false } case _ => false } + + // TODO: Maybe make consistent with the isSpecializationOf function + def isImplementationOf(name1: Name, name2: Name)(using Context) = + name1.toString().replace(str.SPECIALIZED_TRAIT_IMPL_SUFFIX, str.SPECIALIZED_TRAIT_SUFFIX) == name2.toString() + end DesugarSpecializedTraits /* Stores the specializations we have found in the program and the symbols for the interface traits and implementation classes @@ -412,14 +442,7 @@ end SpecializedTraitCache /* Represents an application traitSymbol[typeArguments] */ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(using Context): // TODO: Can we get away with List[Type] - object SpecializedEvidence { - def unapply(tpe: Type)(using Context): Option[Type] = tpe match { - case AppliedType(tycon, List(tpeArg)) if tycon =:= ctx.definitions.SpecializedClass.typeRef => Some(tpeArg) - case _ => None - } - } - - val specializedTypeParams: List[Type] = traitSymbol.unforcedDecls.implicitDecls.collect(_.info match { case SpecializedEvidence(typeVar) => typeVar }) + val specializedTypeParams: List[Type] = Specialization.classSpecializedTypeParams(traitSymbol) private val specializedTypeParamsSet = specializedTypeParams.toSet private val paramToArgList = traitSymbol.typeParams.map(_.typeRef.asInstanceOf[Type]).zip(typeArguments) @@ -456,6 +479,13 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(usi end Specialization object Specialization: + private object SpecializedEvidence { + def unapply(tpe: Type)(using Context): Option[Type] = tpe match { + case AppliedType(tycon, List(tpeArg)) if tycon =:= ctx.definitions.SpecializedClass.typeRef => Some(tpeArg) + case _ => None + } + } + def unapply(tpt: Tree)(using Context): Option[Specialization] = tpt match { case AppliedTypeTree(specializedTrait: Ident, concreteTypeTrees: List[Tree]) => Some(Specialization(specializedTrait.denot.symbol, concreteTypeTrees)) case t: TypeTree => Specialization.unapply(t.tpe) @@ -466,6 +496,8 @@ object Specialization: case AppliedType(tycon: Type, args: List[Type]) => Some(Specialization(tycon.typeSymbol, args.map(TypeTree(_)))) case _ => None } + + def classSpecializedTypeParams(classSym: Symbol)(using Context): List[Type] = classSym.unforcedDecls.implicitDecls.collect(_.info match { case SpecializedEvidence(typeVar) => typeVar }) end Specialization // Would be nice to define a Specialization class I think @@ -581,3 +613,16 @@ end Specialization // like `Seq$sp$Int` is equal to its parameterized version `Seq[Int]` // Warning for dropping Specialized qualifier or it doesn't compile? +// TODO: Make name consistent for tests. +// TODO: In order to fix Foo extends Bar (banned for now) +// // case impl@Template(constr, preParentsOrDerived, self, _) => +// cpy.Template(impl)(body = impl.body.map(transform(_)), +// parents = // CAN POTENTIALLY MOVE THE OWNER CALL UP HERE. +// impl.parents.map(p => if isSpecializationOf(impl.symbol.typeRef, p.tpe, allowImplementationClass = true) then {println(impl.symbol.typeRef); println(p.tpe); p} else transform(p))) +// case tree => super.transform(tree) +// } +// Plus need another case in the normal map where you add sp trait as a parent as well as the original trait, AND update symbols. (or maybe switch to impl calss also possibhle). + +// end DesugarSpecializedTraits +// Also delete the other members that already got inlined or maybe we don't care. +// extend both traits diff --git a/tests/pos/specialized-traits-inheritance.scala b/tests/neg/specialized-traits-inheritance.scala similarity index 69% rename from tests/pos/specialized-traits-inheritance.scala rename to tests/neg/specialized-traits-inheritance.scala index e0572aac84e0..9431253d64e8 100644 --- a/tests/pos/specialized-traits-inheritance.scala +++ b/tests/neg/specialized-traits-inheritance.scala @@ -2,7 +2,7 @@ inline trait Foo[T: Specialized](x: T): def foo = x -class Bar extends Foo(10) +class Bar extends Foo(10) // error: Specialized traits may only be extended by anonymous class instances. def f(b: Foo[Int]) = println(s"We found the following value of foo ${b.foo}") diff --git a/tests/pos/specialized-trait-inlining-causes-specialization.scala b/tests/pos/specialized-trait-inlining-causes-specialization.scala index 0283e7b2873c..d99267f5b9fa 100644 --- a/tests/pos/specialized-trait-inlining-causes-specialization.scala +++ b/tests/pos/specialized-trait-inlining-causes-specialization.scala @@ -8,4 +8,5 @@ inline trait C[S: Specialized]: inline trait A[T: Specialized]: def x(y: C[T]): Unit = println("x") -class B extends A[Char] +def main = + val b = new A[Char] {} From 86185a4b5c1d7a2ee3e6377cbb055ed7b795fc86 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 13:23:58 +0200 Subject: [PATCH 076/254] Homogenise test names --- ...traits-with-param.scala => specialized-trait-with-param.scala} | 0 ...la => specialized-trait-check-specialized-method-called.scala} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/pos/{specialized-traits-with-param.scala => specialized-trait-with-param.scala} (100%) rename tests/run/{specialized-traits-check-specialized-method-called.scala => specialized-trait-check-specialized-method-called.scala} (100%) diff --git a/tests/pos/specialized-traits-with-param.scala b/tests/pos/specialized-trait-with-param.scala similarity index 100% rename from tests/pos/specialized-traits-with-param.scala rename to tests/pos/specialized-trait-with-param.scala diff --git a/tests/run/specialized-traits-check-specialized-method-called.scala b/tests/run/specialized-trait-check-specialized-method-called.scala similarity index 100% rename from tests/run/specialized-traits-check-specialized-method-called.scala rename to tests/run/specialized-trait-check-specialized-method-called.scala From 836cf7d9f153651a46a461b708e9540942ecdc46 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 14:02:18 +0200 Subject: [PATCH 077/254] Ban body classes in inline traits --- .../inline-trait-body-class-extends-inline-trait.scala | 2 +- tests/{pos => neg}/inline-trait-body-class-return.scala | 2 +- tests/{pos => neg}/inline-trait-body-class-sealed.scala | 2 +- tests/neg/inline-trait-body-class-simple.scala | 6 ++++++ tests/{pos => neg}/inline-trait-body-trait-simple.scala | 2 +- tests/{pos => neg}/inline-trait-co-nested.scala | 4 ++-- .../inline-trait-cross-reference-defined-after.scala | 4 ++-- tests/{pos => neg}/inline-trait-double-nested-class.scala | 2 +- .../inline-trait-nested-class-outside-ref.scala | 2 +- .../inline-trait-nested-class-parameter-passing.scala | 2 +- tests/pos/inline-trait-body-class-simple.scala | 6 ------ 11 files changed, 17 insertions(+), 17 deletions(-) rename tests/{pos => neg}/inline-trait-body-class-extends-inline-trait.scala (61%) rename tests/{pos => neg}/inline-trait-body-class-return.scala (54%) rename tests/{pos => neg}/inline-trait-body-class-sealed.scala (53%) create mode 100644 tests/neg/inline-trait-body-class-simple.scala rename tests/{pos => neg}/inline-trait-body-trait-simple.scala (56%) rename tests/{pos => neg}/inline-trait-co-nested.scala (51%) rename tests/{pos => neg}/inline-trait-cross-reference-defined-after.scala (50%) rename tests/{pos => neg}/inline-trait-double-nested-class.scala (72%) rename tests/{pos => neg}/inline-trait-nested-class-outside-ref.scala (60%) rename tests/{pos => neg}/inline-trait-nested-class-parameter-passing.scala (76%) delete mode 100644 tests/pos/inline-trait-body-class-simple.scala diff --git a/tests/pos/inline-trait-body-class-extends-inline-trait.scala b/tests/neg/inline-trait-body-class-extends-inline-trait.scala similarity index 61% rename from tests/pos/inline-trait-body-class-extends-inline-trait.scala rename to tests/neg/inline-trait-body-class-extends-inline-trait.scala index 1f1a3836fa98..0e52135b3bd6 100644 --- a/tests/pos/inline-trait-body-class-extends-inline-trait.scala +++ b/tests/neg/inline-trait-body-class-extends-inline-trait.scala @@ -1,5 +1,5 @@ inline trait A: - class Inner extends Trait[Int]: + class Inner extends Trait[Int]: // error: Inline traits may not define inner classes or traits. val x = 1 inline trait Trait[T]: diff --git a/tests/pos/inline-trait-body-class-return.scala b/tests/neg/inline-trait-body-class-return.scala similarity index 54% rename from tests/pos/inline-trait-body-class-return.scala rename to tests/neg/inline-trait-body-class-return.scala index ac960c590b87..d77c92f1fc06 100644 --- a/tests/pos/inline-trait-body-class-return.scala +++ b/tests/neg/inline-trait-body-class-return.scala @@ -1,5 +1,5 @@ inline trait A: - sealed class InnerA: + sealed class InnerA: // error: Inline traits may not define inner classes or traits. val x = 1 def generate(x: Int) = InnerA() diff --git a/tests/pos/inline-trait-body-class-sealed.scala b/tests/neg/inline-trait-body-class-sealed.scala similarity index 53% rename from tests/pos/inline-trait-body-class-sealed.scala rename to tests/neg/inline-trait-body-class-sealed.scala index ff28d164c617..737e36fba960 100644 --- a/tests/pos/inline-trait-body-class-sealed.scala +++ b/tests/neg/inline-trait-body-class-sealed.scala @@ -1,5 +1,5 @@ inline trait A: - sealed class InnerA: + sealed class InnerA: // error: Inline traits may not define inner classes or traits. val x = 1 class B extends A: diff --git a/tests/neg/inline-trait-body-class-simple.scala b/tests/neg/inline-trait-body-class-simple.scala new file mode 100644 index 000000000000..8c3ee234310a --- /dev/null +++ b/tests/neg/inline-trait-body-class-simple.scala @@ -0,0 +1,6 @@ +inline trait A: + class Inner: // error: Inline traits may not define inner classes or traits. + val x = 1 + +class B extends A: + def f = Inner().x \ No newline at end of file diff --git a/tests/pos/inline-trait-body-trait-simple.scala b/tests/neg/inline-trait-body-trait-simple.scala similarity index 56% rename from tests/pos/inline-trait-body-trait-simple.scala rename to tests/neg/inline-trait-body-trait-simple.scala index 14d4bcf64563..91b5bc380543 100644 --- a/tests/pos/inline-trait-body-trait-simple.scala +++ b/tests/neg/inline-trait-body-trait-simple.scala @@ -1,5 +1,5 @@ inline trait A[T]: - trait InnerA: + trait InnerA: // error: Inline traits may not contain inner classes. def x: T = ??? class B extends A[Int]: diff --git a/tests/pos/inline-trait-co-nested.scala b/tests/neg/inline-trait-co-nested.scala similarity index 51% rename from tests/pos/inline-trait-co-nested.scala rename to tests/neg/inline-trait-co-nested.scala index ac1e2a627b9c..fb600bcdd3a0 100644 --- a/tests/pos/inline-trait-co-nested.scala +++ b/tests/neg/inline-trait-co-nested.scala @@ -1,8 +1,8 @@ inline trait A: // At the moment this works with an ordinary trait but throws a TypeError with inline traits - sealed class InnerA: + sealed class InnerA: // error: Inline traits may not define inner classes or traits. val x = new InnerB - sealed class InnerB: + sealed class InnerB: // error: Inline traits may not define inner classes or traits. val x = new InnerA class B extends A: diff --git a/tests/pos/inline-trait-cross-reference-defined-after.scala b/tests/neg/inline-trait-cross-reference-defined-after.scala similarity index 50% rename from tests/pos/inline-trait-cross-reference-defined-after.scala rename to tests/neg/inline-trait-cross-reference-defined-after.scala index 43f87465d4ec..eb3fb51f4450 100644 --- a/tests/pos/inline-trait-cross-reference-defined-after.scala +++ b/tests/neg/inline-trait-cross-reference-defined-after.scala @@ -1,8 +1,8 @@ inline trait A: // At the moment this works with an ordinary trait but throws a TypeError with inline traits - sealed class InnerA: + sealed class InnerA: // error: Inline traits may not define inner classes or traits. val x = new InnerB - sealed class InnerB: + sealed class InnerB: // error: Inline traits may not define inner classes or traits. val x = 10 class B extends A: diff --git a/tests/pos/inline-trait-double-nested-class.scala b/tests/neg/inline-trait-double-nested-class.scala similarity index 72% rename from tests/pos/inline-trait-double-nested-class.scala rename to tests/neg/inline-trait-double-nested-class.scala index 08c446eeeee0..c8352791bfb6 100644 --- a/tests/pos/inline-trait-double-nested-class.scala +++ b/tests/neg/inline-trait-double-nested-class.scala @@ -1,5 +1,5 @@ inline trait A: - sealed class InnerA: + sealed class InnerA: // error: Inline traits may not define inner classes or traits. sealed class InnerInnerA: val x = 1 diff --git a/tests/pos/inline-trait-nested-class-outside-ref.scala b/tests/neg/inline-trait-nested-class-outside-ref.scala similarity index 60% rename from tests/pos/inline-trait-nested-class-outside-ref.scala rename to tests/neg/inline-trait-nested-class-outside-ref.scala index 4d0c31e6f507..6a432cec3ecf 100644 --- a/tests/pos/inline-trait-nested-class-outside-ref.scala +++ b/tests/neg/inline-trait-nested-class-outside-ref.scala @@ -1,5 +1,5 @@ inline trait A: - class InnerA: + class InnerA: // error: Inline traits may not define inner classes or traits. val x = 10 class B extends A: diff --git a/tests/pos/inline-trait-nested-class-parameter-passing.scala b/tests/neg/inline-trait-nested-class-parameter-passing.scala similarity index 76% rename from tests/pos/inline-trait-nested-class-parameter-passing.scala rename to tests/neg/inline-trait-nested-class-parameter-passing.scala index 321d4c5ef692..651e306e1089 100644 --- a/tests/pos/inline-trait-nested-class-parameter-passing.scala +++ b/tests/neg/inline-trait-nested-class-parameter-passing.scala @@ -1,5 +1,5 @@ inline trait A: - sealed class InnerA: + sealed class InnerA: // error: Inline traits may not define inner classes or traits. val x = 1 class B extends A: diff --git a/tests/pos/inline-trait-body-class-simple.scala b/tests/pos/inline-trait-body-class-simple.scala deleted file mode 100644 index 56476b96b44b..000000000000 --- a/tests/pos/inline-trait-body-class-simple.scala +++ /dev/null @@ -1,6 +0,0 @@ -inline trait A: - class Inner: - val x = 1 - -class B extends A: - def f = Inner().x \ No newline at end of file From be7f701be14834a113fc37f50c3bb7db5db09077 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 14:02:25 +0200 Subject: [PATCH 078/254] Fix test warning --- tests/run/inline-trait-inheritance-inline-grandparent.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/run/inline-trait-inheritance-inline-grandparent.scala b/tests/run/inline-trait-inheritance-inline-grandparent.scala index adc00b7b7bfe..670cff30c517 100644 --- a/tests/run/inline-trait-inheritance-inline-grandparent.scala +++ b/tests/run/inline-trait-inheritance-inline-grandparent.scala @@ -25,7 +25,7 @@ package grandParentWithArgs: val simpleC = SimpleC() println(simpleC.foo()) println(simpleC.bar("Test SimpleC")) - println + println() val c = C() println(c.foo()) From b378cc3ec74110e16f873e98653674d3f8ffd044 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 16:27:21 +0200 Subject: [PATCH 079/254] Ban inner classes in inline traits --- .../src/dotty/tools/dotc/inlines/Inlines.scala | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 68873fb6672e..f2b818875cbb 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -261,11 +261,17 @@ object Inlines: def transformInlineTrait(inlineTrait: TypeDef)(using Context): TypeDef = val tpd.TypeDef(_, tmpl: Template) = inlineTrait: @unchecked + + tmpl.body.foreach { + case innerClass: TypeDef if innerClass.symbol.isClass => report.error("Inline traits may not define inner classes or traits.", innerClass.srcPos) + case _ => + } + val body1 = tmpl.body.flatMap { - // case innerClass: TypeDef if innerClass.symbol.isClass => - // val newTrait = makeTraitFromInnerClass(innerClass) - // val newType = makeTypeFromInnerClass(inlineTrait.symbol, innerClass, newTrait.symbol) - // List(newTrait, newType) + /* case innerClass: TypeDef if innerClass.symbol.isClass => + val newTrait = makeTraitFromInnerClass(innerClass) + val newType = makeTypeFromInnerClass(inlineTrait.symbol, innerClass, newTrait.symbol) + List(newTrait, newType) */ case member: MemberDef => List(member) case _ => From 0a812a9781edc2537b895b915dad0891396a3408 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 16:28:35 +0200 Subject: [PATCH 080/254] Allow inline traits to override val params because we prune the params from the parent traits later so they need to live in the children --- compiler/src/dotty/tools/dotc/typer/RefChecks.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index cb9d96340b04..348345c48760 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -622,6 +622,7 @@ object RefChecks { overrideError("cannot have a @targetName annotation since external names would be different") else if other.is(ParamAccessor) && !isInheritedAccessor(member, other) && !member.is(Tracked) // see remark on tracked members above + && !other.owner.isInlineTrait // Allow inline traits to override val params because we prune the params from the parent traits later so they need to live in the children. then // (1.12) report.errorOrMigrationWarning( em"cannot override val parameter ${other.showLocated}", From 340b6a311b6cbad62d8d0ad481dfd20d2b4d25fa Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 16:29:49 +0200 Subject: [PATCH 081/254] Don't return inline trait val param bodies multiple times --- compiler/src/dotty/tools/dotc/transform/Mixin.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/Mixin.scala b/compiler/src/dotty/tools/dotc/transform/Mixin.scala index 2caa8892e99f..0245439cf25a 100644 --- a/compiler/src/dotty/tools/dotc/transform/Mixin.scala +++ b/compiler/src/dotty/tools/dotc/transform/Mixin.scala @@ -339,8 +339,7 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase => EmptyTree } else if wasOneOf(getter, ParamAccessor) then - // mixin parameter field is defined by an override; evaluate the argument and throw it away - nextArgument() + if (mixin.isInlineTrait) then {nextArgument(); EmptyTree} else nextArgument() else EmptyTree } From 6144dbf266f24ab3c760b6174fb1d1e8b6600680 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 18:02:41 +0200 Subject: [PATCH 082/254] Add more name clash tests --- ...child-overrides-parent-missing-override.scala | 5 +++++ .../inline-trait-parent-trait-param-access.scala | 4 ++++ ...lashes-with-child-method-needs-override.scala | 4 ++++ .../inline-trait-clashing-parent-members.scala | 13 +++++++++++++ .../inline-trait-clashing-parent-params.scala | 14 ++++++++++++++ ...inline-trait-clashing-parent-val-params.scala | 11 +++++++++++ tests/run/inline-trait-maximum-name-clash.scala | 16 ++++++++++++++++ ...-parent-param-clashes-with-child-member.scala | 10 ++++++++++ ...rent-param-clashes-with-child-val-param.scala | 9 +++++++++ ...rent-param-clashes-with-method-override.scala | 9 +++++++++ ...-trait-parent-param-clashes-with-method.scala | 8 ++++++++ ...ne-trait-parent-params-clash-with-child.scala | 10 ++++++++++ ...rent-val-param-clashes-with-child-param.scala | 10 ++++++++++ ...ait-parent-val-param-clashes-with-child.scala | 9 +++++++++ 14 files changed, 132 insertions(+) create mode 100644 tests/neg/inline-trait-child-overrides-parent-missing-override.scala create mode 100644 tests/neg/inline-trait-parent-trait-param-access.scala create mode 100644 tests/neg/inline-trait-parent-val-param-clashes-with-child-method-needs-override.scala create mode 100644 tests/run/inline-trait-clashing-parent-members.scala create mode 100644 tests/run/inline-trait-clashing-parent-params.scala create mode 100644 tests/run/inline-trait-clashing-parent-val-params.scala create mode 100644 tests/run/inline-trait-maximum-name-clash.scala create mode 100644 tests/run/inline-trait-parent-param-clashes-with-child-member.scala create mode 100644 tests/run/inline-trait-parent-param-clashes-with-child-val-param.scala create mode 100644 tests/run/inline-trait-parent-param-clashes-with-method-override.scala create mode 100644 tests/run/inline-trait-parent-param-clashes-with-method.scala create mode 100644 tests/run/inline-trait-parent-params-clash-with-child.scala create mode 100644 tests/run/inline-trait-parent-val-param-clashes-with-child-param.scala create mode 100644 tests/run/inline-trait-parent-val-param-clashes-with-child.scala diff --git a/tests/neg/inline-trait-child-overrides-parent-missing-override.scala b/tests/neg/inline-trait-child-overrides-parent-missing-override.scala new file mode 100644 index 000000000000..18aabebbcde0 --- /dev/null +++ b/tests/neg/inline-trait-child-overrides-parent-missing-override.scala @@ -0,0 +1,5 @@ + +inline trait A(val x: Int) + +class C extends A(10): + val x = 1000 // error: Needs override modifier diff --git a/tests/neg/inline-trait-parent-trait-param-access.scala b/tests/neg/inline-trait-parent-trait-param-access.scala new file mode 100644 index 000000000000..e283a5f90bd1 --- /dev/null +++ b/tests/neg/inline-trait-parent-trait-param-access.scala @@ -0,0 +1,4 @@ +inline trait A(x: Int) + +class C extends A(10): + val y = x // error: Not Found Error diff --git a/tests/neg/inline-trait-parent-val-param-clashes-with-child-method-needs-override.scala b/tests/neg/inline-trait-parent-val-param-clashes-with-child-method-needs-override.scala new file mode 100644 index 000000000000..a7a87b4d8bf7 --- /dev/null +++ b/tests/neg/inline-trait-parent-val-param-clashes-with-child-method-needs-override.scala @@ -0,0 +1,4 @@ +inline trait A(val x: Int) + +class C extends A(10): + def x = 1000 // error: Needs override marker diff --git a/tests/run/inline-trait-clashing-parent-members.scala b/tests/run/inline-trait-clashing-parent-members.scala new file mode 100644 index 000000000000..df5767b6aa9e --- /dev/null +++ b/tests/run/inline-trait-clashing-parent-members.scala @@ -0,0 +1,13 @@ +// We allow multiple inline traits to be mixed in with the same member names; we prefer the latest mixed-in name. + +inline trait A: + val x = 10 + +inline trait B: + val x = 11 + +class C extends A, B + +@main def Test = + val v = C() + assert(v.x == 11) diff --git a/tests/run/inline-trait-clashing-parent-params.scala b/tests/run/inline-trait-clashing-parent-params.scala new file mode 100644 index 000000000000..5103ff6291dc --- /dev/null +++ b/tests/run/inline-trait-clashing-parent-params.scala @@ -0,0 +1,14 @@ +// These params should be renamed (as they are private) so no clash + +inline trait A(x: Int): + val y = x + +inline trait B(x: Int): + val z = x + +class C extends A(10), B(11) + +@main def Test = + val v = C() + assert(v.y == 10) + assert(v.z == 11) diff --git a/tests/run/inline-trait-clashing-parent-val-params.scala b/tests/run/inline-trait-clashing-parent-val-params.scala new file mode 100644 index 000000000000..3d3d669e926b --- /dev/null +++ b/tests/run/inline-trait-clashing-parent-val-params.scala @@ -0,0 +1,11 @@ +// We allow multiple inline traits to be mixed in with the same member names; we prefer the latest mixed-in name. + +inline trait A(val x: Int) + +inline trait B(val x: Int) + +class C extends A(10), B(11) + +@main def Test = + val v = C() + assert(v.x == 11) diff --git a/tests/run/inline-trait-maximum-name-clash.scala b/tests/run/inline-trait-maximum-name-clash.scala new file mode 100644 index 000000000000..f0956b6e20dd --- /dev/null +++ b/tests/run/inline-trait-maximum-name-clash.scala @@ -0,0 +1,16 @@ +inline trait A(x: Int): + val z = x +inline trait B(x: Int): + val y = x +inline trait C(val x: Int) + +class D(x: Int, z: Int) extends A(314), B(1200), C(12): + override val y = x + val w = y + +@main def Test = + val v = D(100, 3) + assert(v.y == 100) + assert(v.w == 100) + assert(v.z == 314) + assert(v.x == 12) diff --git a/tests/run/inline-trait-parent-param-clashes-with-child-member.scala b/tests/run/inline-trait-parent-param-clashes-with-child-member.scala new file mode 100644 index 000000000000..a1b533f133d9 --- /dev/null +++ b/tests/run/inline-trait-parent-param-clashes-with-child-member.scala @@ -0,0 +1,10 @@ +// We should rename A.x here to avoid a clash. + +inline trait A(x: Int) + +class C extends A(10): + val x = 5 + +@main def Test = + val v = C() + assert(v.x == 5) diff --git a/tests/run/inline-trait-parent-param-clashes-with-child-val-param.scala b/tests/run/inline-trait-parent-param-clashes-with-child-val-param.scala new file mode 100644 index 000000000000..76731e6193fd --- /dev/null +++ b/tests/run/inline-trait-parent-param-clashes-with-child-val-param.scala @@ -0,0 +1,9 @@ +// Here we need to rename A.x to avoid a clash + +inline trait A(x: Int) + +class C(val x: Int) extends A(10) + +@main def Test = + val v = C(5) + assert(v.x == 5) diff --git a/tests/run/inline-trait-parent-param-clashes-with-method-override.scala b/tests/run/inline-trait-parent-param-clashes-with-method-override.scala new file mode 100644 index 000000000000..eb8db996b563 --- /dev/null +++ b/tests/run/inline-trait-parent-param-clashes-with-method-override.scala @@ -0,0 +1,9 @@ + +inline trait A(val x: Int) + +class C extends A(10): + override val x = 1000 + +@main def Test = + val v = C() + assert(v.x == 1000) diff --git a/tests/run/inline-trait-parent-param-clashes-with-method.scala b/tests/run/inline-trait-parent-param-clashes-with-method.scala new file mode 100644 index 000000000000..23bb7565b3bd --- /dev/null +++ b/tests/run/inline-trait-parent-param-clashes-with-method.scala @@ -0,0 +1,8 @@ +inline trait A(x: Int) + +class C extends A(10): + def x = 1000 + +@main def Test = + val v = C() + assert(v.x == 1000) diff --git a/tests/run/inline-trait-parent-params-clash-with-child.scala b/tests/run/inline-trait-parent-params-clash-with-child.scala new file mode 100644 index 000000000000..23c53a76b54b --- /dev/null +++ b/tests/run/inline-trait-parent-params-clash-with-child.scala @@ -0,0 +1,10 @@ +// Param x in A will be renamed so it doesn't clash. + +inline trait A(x: Int): + val y = x + +class C(x: Int) extends A(10) + +@main def Test = + val v = C(5) + assert(v.y == 10) diff --git a/tests/run/inline-trait-parent-val-param-clashes-with-child-param.scala b/tests/run/inline-trait-parent-val-param-clashes-with-child-param.scala new file mode 100644 index 000000000000..09bbad6424f7 --- /dev/null +++ b/tests/run/inline-trait-parent-val-param-clashes-with-child-param.scala @@ -0,0 +1,10 @@ +// Need to rename C.x to avoid a clash + +inline trait A(val x: Int): + val y = x + +class C(x: Int) extends A(10) + +@main def Test = + val v = C(5) + assert(v.y == 10) diff --git a/tests/run/inline-trait-parent-val-param-clashes-with-child.scala b/tests/run/inline-trait-parent-val-param-clashes-with-child.scala new file mode 100644 index 000000000000..eb8db996b563 --- /dev/null +++ b/tests/run/inline-trait-parent-val-param-clashes-with-child.scala @@ -0,0 +1,9 @@ + +inline trait A(val x: Int) + +class C extends A(10): + override val x = 1000 + +@main def Test = + val v = C() + assert(v.x == 1000) From e03b995e7fef8d84c0bb140efadd90e4e9489826 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 18:20:20 +0200 Subject: [PATCH 083/254] Fix tests --- ...-param-clashes-with-child-param-super-call.scala | 13 +++++++++++++ ...-parent-val-param-clashes-with-child-param.scala | 2 ++ 2 files changed, 15 insertions(+) create mode 100644 tests/run/inline-trait-parent-val-param-clashes-with-child-param-super-call.scala diff --git a/tests/run/inline-trait-parent-val-param-clashes-with-child-param-super-call.scala b/tests/run/inline-trait-parent-val-param-clashes-with-child-param-super-call.scala new file mode 100644 index 000000000000..016d02d141a0 --- /dev/null +++ b/tests/run/inline-trait-parent-val-param-clashes-with-child-param-super-call.scala @@ -0,0 +1,13 @@ +// Need to rename C.x to avoid a clash + +inline trait A(val x: Int): + val y = x + +class C(x: Int) extends A(10) + val z = x + +@main def Test = + val v = C(5) + assert(v.y == 10) + assert(v.x == 10) + assert(v.z == 5) diff --git a/tests/run/inline-trait-parent-val-param-clashes-with-child-param.scala b/tests/run/inline-trait-parent-val-param-clashes-with-child-param.scala index 09bbad6424f7..26da4d76d7fd 100644 --- a/tests/run/inline-trait-parent-val-param-clashes-with-child-param.scala +++ b/tests/run/inline-trait-parent-val-param-clashes-with-child-param.scala @@ -4,7 +4,9 @@ inline trait A(val x: Int): val y = x class C(x: Int) extends A(10) + val z = x @main def Test = val v = C(5) assert(v.y == 10) + assert(v.z == 5) From 50c3237732eac4880d3f17bfd50a95c0ab51051f Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 18:20:33 +0200 Subject: [PATCH 084/254] Fix typo --- compiler/src/dotty/tools/dotc/transform/SuperAccessors.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/SuperAccessors.scala b/compiler/src/dotty/tools/dotc/transform/SuperAccessors.scala index 0077cb969e3a..8d569ec9ff35 100644 --- a/compiler/src/dotty/tools/dotc/transform/SuperAccessors.scala +++ b/compiler/src/dotty/tools/dotc/transform/SuperAccessors.scala @@ -109,7 +109,7 @@ class SuperAccessors(thisPhase: DenotTransformer) { if (sym.isTerm && !sym.is(Method, butNot = Accessor) && !ctx.owner.isAllOf(ParamForwarder)) // ParamForwaders as installed ParamForwarding.scala do use super calls to vals - report.error(em"super may be not be used on ${sym.underlyingSymbol}", sel.srcPos) + report.error(em"super may not be used on ${sym.underlyingSymbol}", sel.srcPos) else if (isDisallowed(sym)) report.error(em"super not allowed here: use this.${sel.name} instead", sel.srcPos) else if (sym.is(Deferred)) { From 31133914a57f524896be0abe5b46c080e7e72429 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 18:24:22 +0200 Subject: [PATCH 085/254] Add child and grandchild override case --- .../inline-trait-child-and-grandchild-override.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/run/inline-trait-child-and-grandchild-override.scala diff --git a/tests/run/inline-trait-child-and-grandchild-override.scala b/tests/run/inline-trait-child-and-grandchild-override.scala new file mode 100644 index 000000000000..f2b0b72e9aa3 --- /dev/null +++ b/tests/run/inline-trait-child-and-grandchild-override.scala @@ -0,0 +1,11 @@ +inline trait GreatGrandParent: + val x = 10 +inline trait GrandParent extends GreatGrandParent: + override val x = 11 +inline trait Parent extends GrandParent: + override val x = 12 +class C extends Parent + +@main def Test = + val x = C() + assert(x.x == 12) From 01c5c10a2847da5fa80730c0a4d1b7a996f15813 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 30 Mar 2026 18:25:50 +0200 Subject: [PATCH 086/254] Update rules --- docs/_docs/internals/inline-traits.md | 20 +++++++++++++++----- docs/_docs/internals/specialized-traits.md | 8 ++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/_docs/internals/inline-traits.md b/docs/_docs/internals/inline-traits.md index 26ee644b67fd..a82e818a5cb3 100644 --- a/docs/_docs/internals/inline-traits.md +++ b/docs/_docs/internals/inline-traits.md @@ -121,7 +121,7 @@ inline trait A(b: Boolean): class B extends A(true) ``` -Is conveted to: +Is converted to: ```scala inline trait A(b: Boolean): @@ -133,7 +133,7 @@ class B extends A(true): private val A$$x: Int = 1 override def foo(): Int = if this.A$$b then this.A$$x.+(1) else 0 ``` -- An inline receiver may mix in multiple inline traits with colliding member names. In this case the latest extended trait prevails. In the following example calling `foo` on an instance of `C` will return "Bonjour". This is in contrast to ordinary traits which require the `override` modifier in this case. +- An inline receiver may mix in multiple inline traits with colliding member names. In this case the latest extended trait prevails. In the following example calling `foo` on an instance of `C` will return "Bonjour". This is in contrast to ordinary traits which require the `override` modifier in this case. ```scala inline trait A: def foo = "Hello World" @@ -143,7 +143,7 @@ inline trait B: class C extends A, B ``` -However, an inline receiver may not define a member whose name collides with the name of an inlined public member from a parent inline trait, unless the override modifier is used. +However, an inline receiver may not define a member whose name collides with the name of an inlined public member from a parent inline trait, unless the override modifier is used. This reflects the behaviour of ordinary traits. ```scala inline trait A: def foo = "Hello World" @@ -154,6 +154,14 @@ inline trait B: class C extends A, B: def foo = "Bonjour2" // Must be override. ``` +- Inline receivers may not access the parameters of their parents (these are private): +```scala +inline trait A(x: Int) + +class C extends A(10): + val y = x // error: Not Found Error +``` + - Inlined members of inline traits are typed with the type of the right hand side resulting from inlining. This is particularly important for typeclass instances: ```scala inline trait A[T: Numeric]: @@ -234,10 +242,12 @@ This problem is addressed via `Specialized` traits; see the accompanying documen |--------------------------|----------------------------------------------| | Methods | ✅ | | `val` / `var` Properties | ✅ | +| Private properies | ❌ | | `type`s | ✅ | -| Inner classes | ❌ | +| Inner classes/traits | ❌ | | Opaque types | ❌ | -| Self types | ❌ +| Self types | ❌ | +| Inheritance (of inline traits) | Only allowed by classes and inline traits | ## Processing of inline traits in the compiler Inline traits in user code are inlined in the phase `specializeInlineTraits`. The phase `replaceInlinedTraitSymbols` diff --git a/docs/_docs/internals/specialized-traits.md b/docs/_docs/internals/specialized-traits.md index 59d82bd4b58a..3f7ec0321863 100644 --- a/docs/_docs/internals/specialized-traits.md +++ b/docs/_docs/internals/specialized-traits.md @@ -322,6 +322,14 @@ hope to avoid extending `ArrayIterator[Int]` directly, because extending `ArrayI `ArrayIterator[Int]` which is not allowed unless `ArrayIterator[Int]` is also mixed in directly to pass parameters (due to the rules of trait parameter passing in Scala 3). +## Summary of restrictions on specialized traits + +| Behaviour | Is currently supported in... | +|--------------------------|-----------------------------------------------| +| Inheriting from specialized traits | In inline traits or anonymous class instances (for instance creation) only | +| Taking `Specialized` parameters| Only by inline traits | + + ## [1] Why are the generated traits inline? Consider the following: From b0eb1bf7f8e033f640b067e58b5ed32c53999acd Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 31 Mar 2026 16:29:53 +0200 Subject: [PATCH 087/254] Fix test typo and delete duplicate test --- ...l-param-clashes-with-child-param-super-call.scala | 4 +++- ...t-parent-val-param-clashes-with-child-param.scala | 12 ------------ 2 files changed, 3 insertions(+), 13 deletions(-) delete mode 100644 tests/run/inline-trait-parent-val-param-clashes-with-child-param.scala diff --git a/tests/run/inline-trait-parent-val-param-clashes-with-child-param-super-call.scala b/tests/run/inline-trait-parent-val-param-clashes-with-child-param-super-call.scala index 016d02d141a0..4820630c351d 100644 --- a/tests/run/inline-trait-parent-val-param-clashes-with-child-param-super-call.scala +++ b/tests/run/inline-trait-parent-val-param-clashes-with-child-param-super-call.scala @@ -3,7 +3,7 @@ inline trait A(val x: Int): val y = x -class C(x: Int) extends A(10) +class C(x: Int) extends A(10): val z = x @main def Test = @@ -11,3 +11,5 @@ class C(x: Int) extends A(10) assert(v.y == 10) assert(v.x == 10) assert(v.z == 5) + println(v.y) + println(v.x) diff --git a/tests/run/inline-trait-parent-val-param-clashes-with-child-param.scala b/tests/run/inline-trait-parent-val-param-clashes-with-child-param.scala deleted file mode 100644 index 26da4d76d7fd..000000000000 --- a/tests/run/inline-trait-parent-val-param-clashes-with-child-param.scala +++ /dev/null @@ -1,12 +0,0 @@ -// Need to rename C.x to avoid a clash - -inline trait A(val x: Int): - val y = x - -class C(x: Int) extends A(10) - val z = x - -@main def Test = - val v = C(5) - assert(v.y == 10) - assert(v.z == 5) From 7282f6d423e9878ec818a7861291c3e8139de60b Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 13 Apr 2026 11:00:20 +0200 Subject: [PATCH 088/254] Properly delete privates when we prune inline traits --- compiler/src/dotty/tools/dotc/Compiler.scala | 6 ++-- .../src/dotty/tools/dotc/core/StdNames.scala | 1 - .../dotty/tools/dotc/transform/Mixin.scala | 5 ++- .../dotc/transform/PruneInlineTraits.scala | 33 ++++++++++--------- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 395c131c1455..cf3a81f08c55 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -75,7 +75,8 @@ class Compiler { new ExpandSAMs, // Expand single abstract method closures to anonymous classes new ElimRepeated, // Rewrite vararg parameters and arguments new RefChecks, // Various checks mostly related to abstract members and overriding - new DropForMap) :: // Drop unused trailing map calls in for comprehensions + new DropForMap, // Drop unused trailing map calls in for comprehensions + new PruneInlineTraits) :: // Remove right-hand side of definitions in inline traits List(new init.Checker) :: // Check initialization of objects List(new ProtectedAccessors, // Add accessors for protected members new ExtensionMethods, // Expand methods of value classes with extension methods @@ -99,8 +100,7 @@ class Compiler { new ExplicitSelf, // Make references to non-trivial self types explicit as casts new StringInterpolatorOpt, // Optimizes raw and s and f string interpolators by rewriting them to string concatenations or formats new DropBreaks) :: // Optimize local Break throws by rewriting them - List(new PruneInlineTraits, // Remove right-hand side of definitions in inline traits - new PruneErasedDefs, // Drop erased definitions from scopes and simplify erased expressions + List(new PruneErasedDefs, // Drop erased definitions from scopes and simplify erased expressions new UninitializedDefs, // Replaces `compiletime.uninitialized` by `_` new InlinePatterns, // Remove placeholders of inlined patterns new VCInlineMethods, // Inlines calls to value class methods diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 43e56b6fe387..97448b2d0c20 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -27,7 +27,6 @@ object StdNames { inline val ANON_CLASS = "$anon" inline val ANON_FUN = "$anonfun" inline val INLINE_TRAIT_INNER_CLASS_SUFFIX = "$trait" - inline val INLINE_TRAIT_ERASED_PRIVATE_SUFFIX = "$inline_trait_erased_private" inline val SPECIALIZED_TRAIT_SUFFIX = "$sp$" inline val SPECIALIZED_TRAIT_IMPL_SUFFIX = "$impl$" inline val SPECIALIZED_TRAIT_TYPE_SEP = "$" diff --git a/compiler/src/dotty/tools/dotc/transform/Mixin.scala b/compiler/src/dotty/tools/dotc/transform/Mixin.scala index 0245439cf25a..1a7eb6b4c117 100644 --- a/compiler/src/dotty/tools/dotc/transform/Mixin.scala +++ b/compiler/src/dotty/tools/dotc/transform/Mixin.scala @@ -156,7 +156,7 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase => // ) // mixin.parentSyms.exists( // parentSym => parentSym.isInlineTrait && parentSym.info.decls.exists(d => { - // d.name == getter.name || getter.name ++ str.INLINE_TRAIT_ERASED_PRIVATE_SUFFIX == d.name.expandedName(parentSym)}) + // d.name == getter.name) // ) @@ -303,8 +303,7 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase => parentSym => parentSym.info.decls//.exists(d => d.name == getter.name || getter.name == d.name.expandedName(parentSym)) ) mixin.parentSyms.exists( - parentSym => parentSym.isInlineTrait && parentSym.info.decls.exists(d => { - d.name == getter.name || getter.name ++ str.INLINE_TRAIT_ERASED_PRIVATE_SUFFIX == d.name.expandedName(parentSym)}) + parentSym => parentSym.isInlineTrait && parentSym.info.decls.exists(d => d.name == getter.name) ) for diff --git a/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala index 7d4b271ac05b..611c195f6088 100644 --- a/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala @@ -20,36 +20,37 @@ class PruneInlineTraits extends MiniPhase with SymTransformer { thisTransform => override def description: String = PruneInlineTraits.description override def transformSym(sym: SymDenotation)(using Context): SymDenotation = - if isDeletable(sym) then sym.copySymDenotation(initFlags = (sym.flags ^ Private) | Deferred | Protected, name = sym.name ++ str.INLINE_TRAIT_ERASED_PRIVATE_SUFFIX) - else if isEraseable(sym) then sym.copySymDenotation(initFlags = sym.flags | Deferred) - else if sym.isInlineTrait then sym.copySymDenotation(initFlags = sym.flags | PureInterface | NoInits) + if isEraseable(sym) then sym.copySymDenotation(initFlags = sym.flags | Deferred) + else if sym.isInlineTrait then + val clsInfo = sym.asClass.classInfo + val clsInfo2 = clsInfo.derivedClassInfo(decls = + clsInfo.decls.filteredScope(!isDeletable(_)) + ) + sym.copySymDenotation(initFlags = sym.flags | PureInterface | NoInits, info = clsInfo2) else sym - - override def transformValDef(tree: ValDef)(using Context): Tree = - if isDeletable(tree.symbol) || isEraseable(tree.symbol) then cpy.ValDef(tree)(rhs = EmptyTree) - else tree - override def transformDefDef(tree: DefDef)(using Context): Tree = - if isDeletable(tree.symbol) || isEraseable(tree.symbol) then cpy.DefDef(tree)(rhs = EmptyTree) - else tree + override def transformTemplate(tree: Template)(using Context): Tree = + cpy.Template(tree)(body = tree.body.flatMap({ + case stmt: ValDef if isEraseable(stmt.symbol) => Some(cpy.ValDef(stmt)(rhs = EmptyTree)) + case stmt: DefDef if isEraseable(stmt.symbol) => Some(cpy.DefDef(stmt)(rhs = EmptyTree)) + case stmt: (ValDef | DefDef) if isDeletable(stmt.symbol) => None + case stmt => Some(stmt) + })) private def isEraseable(sym: SymDenotation)(using Context): Boolean = !sym.isType && !sym.isConstructor && !sym.is(Param) && !sym.is(ParamAccessor) - && !sym.is(Private) + && !sym.is(Local) && !sym.isLocalDummy + && sym.exists && sym.owner.isInlineTrait - // We also must erase private symbols because they can contain problematic defintions such - // as inline functions which need to be inlined (see tests/pos/inline-trait-private-nested-inline-must-delete.scala) - // It's hard to delete the actual symbol and we can't leave it private and deferred/with no definition - // Thus we settle for making it protected, deferred (no definition) and giving it a mangled name/ private def isDeletable(sym: SymDenotation)(using Context): Boolean = !sym.isType - && sym.is(Private) && sym.owner.isInlineTrait + && (sym.is(Local) || sym.is(Inline)) && !sym.is(Param) && !sym.is(ParamAccessor) } From b32bf3fee978c2e1a5e4c577a7b740720f6dee94 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 13 Apr 2026 11:02:21 +0200 Subject: [PATCH 089/254] Implement banning body classes in inline traits --- compiler/src/dotty/tools/dotc/inlines/Inlines.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index f2b818875cbb..4a93c20c861c 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -263,8 +263,8 @@ object Inlines: val tpd.TypeDef(_, tmpl: Template) = inlineTrait: @unchecked tmpl.body.foreach { - case innerClass: TypeDef if innerClass.symbol.isClass => report.error("Inline traits may not define inner classes or traits.", innerClass.srcPos) - case _ => + case innerClass: TypeDef if innerClass.symbol.isClass => report.error("Inline traits may not define inner classes or traits.", innerClass.srcPos) + case _ => } val body1 = tmpl.body.flatMap { From 70b17fff2db9ca2de797d1774ba215b58c6932cc Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 13 Apr 2026 11:03:54 +0200 Subject: [PATCH 090/254] Ban inlining of inline traits into classes defined inside inline traits --- compiler/src/dotty/tools/dotc/inlines/Inlines.scala | 3 +++ tests/neg/inline-trait-anonymous-class.scala | 7 +++++++ tests/pos/inline-trait-anonymous-class.scala | 10 ---------- 3 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 tests/neg/inline-trait-anonymous-class.scala delete mode 100644 tests/pos/inline-trait-anonymous-class.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 4a93c20c861c..b0123c39e35b 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -284,6 +284,9 @@ object Inlines: def inlineParentInlineTraits(cls: Tree)(using Context): Tree = cls match { + case cls @ tpd.TypeDef(_, impl: Template) if cls.symbol.owner.ownersIterator.exists(_.isInlineTrait) => // TODO: We can relax this if we use a seen list to avoid cycles + report.error("May not inline an inline trait into a class defined inside another inline trait. If you really need to do this, make the inline trait Specialized or move the class definition outside the trait.", cls.srcPos) + cls case cls @ tpd.TypeDef(_, impl: Template) => val clsOverriddenSyms = cls.symbol.info.decls.toList.flatMap(_.allOverriddenSymbols).toSet val newDefs = inContext(ctx.withOwner(cls.symbol)) { diff --git a/tests/neg/inline-trait-anonymous-class.scala b/tests/neg/inline-trait-anonymous-class.scala new file mode 100644 index 000000000000..508c33ae9380 --- /dev/null +++ b/tests/neg/inline-trait-anonymous-class.scala @@ -0,0 +1,7 @@ +inline trait C[S]: + def v(x: S): S = x + def w: Unit = + val x = new C[S] {} // error: May not inline an inline trait into a class defined inside another inline trait. If you really need to do this, make the inline trait Specialized or move the class definition outside the trait. + println("w") + +class B extends C[Char] diff --git a/tests/pos/inline-trait-anonymous-class.scala b/tests/pos/inline-trait-anonymous-class.scala deleted file mode 100644 index fd46396850cd..000000000000 --- a/tests/pos/inline-trait-anonymous-class.scala +++ /dev/null @@ -1,10 +0,0 @@ -// While we don't allow inner nested classes inside inline traits, we do allow creation of anonymous classes inside methods -// inside inline traits - after all these are just ordinary methods. - -inline trait C[S]: - def v(x: S): S = x - def w: Unit = - val x = new C[S] {} - println("w") - -class B extends C[Char] From a0b8f7d432cc345bed346f60437a736da348f61b Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 13 Apr 2026 11:08:45 +0200 Subject: [PATCH 091/254] Check all name clash cases --- .../src/dotty/tools/dotc/inlines/Inlines.scala | 9 ++++++++- tests/neg/inline-trait-clash-method-param.scala | 11 +++++++++++ ... => inline-trait-clash-val-param-method.scala} | 3 +++ .../neg/inline-trait-clash-val-param-param.scala | 15 +++++++++++++++ ...ash-val-param-val-param-missing-override.scala | 4 ++++ tests/pos/inline-trait-clash-type-type.scala | 8 ++++++++ tests/run/inline-trait-clash-method-method.scala | 9 +++++++++ .../run/inline-trait-clash-method-val-param.scala | 9 +++++++++ ...cala => inline-trait-clash-param-method.scala} | 0 tests/run/inline-trait-clash-param-param.scala | 10 ++++++++++ .../run/inline-trait-clash-param-val-param.scala | 11 +++++++++++ .../inline-trait-clash-val-param-val-param.scala | 8 ++++++++ tests/run/inline-trait-maximum-name-clash.scala | 11 ++++++----- ...t-parent-param-clashes-with-child-member.scala | 10 ---------- ...arent-param-clashes-with-child-val-param.scala | 9 --------- ...arent-param-clashes-with-method-override.scala | 9 --------- ...ine-trait-parent-params-clash-with-child.scala | 10 ---------- ...aram-clashes-with-child-param-super-call.scala | 15 --------------- ...rait-parent-val-param-clashes-with-child.scala | 9 --------- 19 files changed, 102 insertions(+), 68 deletions(-) create mode 100644 tests/neg/inline-trait-clash-method-param.scala rename tests/neg/{inline-trait-parent-val-param-clashes-with-child-method-needs-override.scala => inline-trait-clash-val-param-method.scala} (50%) create mode 100644 tests/neg/inline-trait-clash-val-param-param.scala create mode 100644 tests/neg/inline-trait-clash-val-param-val-param-missing-override.scala create mode 100644 tests/pos/inline-trait-clash-type-type.scala create mode 100644 tests/run/inline-trait-clash-method-method.scala create mode 100644 tests/run/inline-trait-clash-method-val-param.scala rename tests/run/{inline-trait-parent-param-clashes-with-method.scala => inline-trait-clash-param-method.scala} (100%) create mode 100644 tests/run/inline-trait-clash-param-param.scala create mode 100644 tests/run/inline-trait-clash-param-val-param.scala create mode 100644 tests/run/inline-trait-clash-val-param-val-param.scala delete mode 100644 tests/run/inline-trait-parent-param-clashes-with-child-member.scala delete mode 100644 tests/run/inline-trait-parent-param-clashes-with-child-val-param.scala delete mode 100644 tests/run/inline-trait-parent-param-clashes-with-method-override.scala delete mode 100644 tests/run/inline-trait-parent-params-clash-with-child.scala delete mode 100644 tests/run/inline-trait-parent-val-param-clashes-with-child-param-super-call.scala delete mode 100644 tests/run/inline-trait-parent-val-param-clashes-with-child.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index b0123c39e35b..592c6a5e8356 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -300,7 +300,14 @@ object Inlines: (inlinedDefs1, childDefs) } } - val impl1 = cpy.Template(impl)(body = newDefs._1 ::: newDefs._2) + val newbody = newDefs._1 ::: newDefs._2 + val paramAccessors = newbody.filter(_.symbol.is(ParamAccessor)) + + for pacc <- paramAccessors + otherstat <- newbody if !otherstat.symbol.is(ParamAccessor) && otherstat.denot.matches(pacc.denot.asSingleDenotation) + do report.error(s"Inlining of inline trait created name conflict on ${pacc.denot.name}. Constructor parameters of inline receivers may not collide with members of inline traits.", pacc.srcPos) + + val impl1 = cpy.Template(impl)(body = newbody) cpy.TypeDef(cls)(rhs = impl1) case _ => cls diff --git a/tests/neg/inline-trait-clash-method-param.scala b/tests/neg/inline-trait-clash-method-param.scala new file mode 100644 index 000000000000..2142bf3f4a6b --- /dev/null +++ b/tests/neg/inline-trait-clash-method-param.scala @@ -0,0 +1,11 @@ +inline trait A: + def x(b: Int) = "Hello world" + def z = "Hello world" + +class C(x: String, z: String) extends A: // error: Inlining of inline trait created name conflict on z. + val y = x + val w = z + +@main def Test = + val v = C("Overridden", "Overridden2") + assert(v.y == "Overridden") diff --git a/tests/neg/inline-trait-parent-val-param-clashes-with-child-method-needs-override.scala b/tests/neg/inline-trait-clash-val-param-method.scala similarity index 50% rename from tests/neg/inline-trait-parent-val-param-clashes-with-child-method-needs-override.scala rename to tests/neg/inline-trait-clash-val-param-method.scala index a7a87b4d8bf7..a7cb79962f4a 100644 --- a/tests/neg/inline-trait-parent-val-param-clashes-with-child-method-needs-override.scala +++ b/tests/neg/inline-trait-clash-val-param-method.scala @@ -2,3 +2,6 @@ inline trait A(val x: Int) class C extends A(10): def x = 1000 // error: Needs override marker + +class D extends A(10): + override def x = 1000 // error: needs to be a stable, immutable value diff --git a/tests/neg/inline-trait-clash-val-param-param.scala b/tests/neg/inline-trait-clash-val-param-param.scala new file mode 100644 index 000000000000..b3225b0d93fd --- /dev/null +++ b/tests/neg/inline-trait-clash-val-param-param.scala @@ -0,0 +1,15 @@ +// Not allowed due to name clash + +inline trait A(val x: Int): + val y = x + +class C(x: Int) extends A(10): // error: Inlining of inline trait created name conflict on x. Constructor parameters of inline receivers may not collide with members of inline traits. + val z = x + +@main def Test = + val v = C(5) + assert(v.y == 10) + assert(v.x == 10) + assert(v.z == 5) + println(v.y) + println(v.x) diff --git a/tests/neg/inline-trait-clash-val-param-val-param-missing-override.scala b/tests/neg/inline-trait-clash-val-param-val-param-missing-override.scala new file mode 100644 index 000000000000..917e0dbb3d8d --- /dev/null +++ b/tests/neg/inline-trait-clash-val-param-val-param-missing-override.scala @@ -0,0 +1,4 @@ + +inline trait A(val x: Int, val y: Int) +class C(val y: Int) extends A(10, 54): // error: Needs override + val x = 1000 // error: Needs override diff --git a/tests/pos/inline-trait-clash-type-type.scala b/tests/pos/inline-trait-clash-type-type.scala new file mode 100644 index 000000000000..4e71330d4a69 --- /dev/null +++ b/tests/pos/inline-trait-clash-type-type.scala @@ -0,0 +1,8 @@ +trait One +trait Two extends One + +inline trait A: + type T <: One + +class C extends A: + type T = Two diff --git a/tests/run/inline-trait-clash-method-method.scala b/tests/run/inline-trait-clash-method-method.scala new file mode 100644 index 000000000000..5469c8edeffa --- /dev/null +++ b/tests/run/inline-trait-clash-method-method.scala @@ -0,0 +1,9 @@ +inline trait A: + def x(y: String) = "Hello world" + +class C extends A: + def x(y: String) = "Hello world2" + +@main def Test = + val v = C() + assert(v.x("Hello World") == "Hello world2") diff --git a/tests/run/inline-trait-clash-method-val-param.scala b/tests/run/inline-trait-clash-method-val-param.scala new file mode 100644 index 000000000000..e7a2811ddd79 --- /dev/null +++ b/tests/run/inline-trait-clash-method-val-param.scala @@ -0,0 +1,9 @@ +inline trait A: + def x = "Hello world" + +class C extends A: + val x = "Overridden" + +@main def Test = + val v = C() + assert(v.x == "Overridden") diff --git a/tests/run/inline-trait-parent-param-clashes-with-method.scala b/tests/run/inline-trait-clash-param-method.scala similarity index 100% rename from tests/run/inline-trait-parent-param-clashes-with-method.scala rename to tests/run/inline-trait-clash-param-method.scala diff --git a/tests/run/inline-trait-clash-param-param.scala b/tests/run/inline-trait-clash-param-param.scala new file mode 100644 index 000000000000..4f4c0776a83b --- /dev/null +++ b/tests/run/inline-trait-clash-param-param.scala @@ -0,0 +1,10 @@ +// Param x in A will be renamed so it doesn't clash. + +inline trait A(x: Int) + +class C(x: String) extends A(10): + val y = x + +@main def Test = + val v = C("Hello World") + assert(v.y == "Hello World") diff --git a/tests/run/inline-trait-clash-param-val-param.scala b/tests/run/inline-trait-clash-param-val-param.scala new file mode 100644 index 000000000000..30807db9553e --- /dev/null +++ b/tests/run/inline-trait-clash-param-val-param.scala @@ -0,0 +1,11 @@ +// Allowed. We rename A.x, A.y to avoid a clash. + +inline trait A(x: Int, y: Int) + +class C(val x: Int) extends A(10, 100): + val y: Int = 10 + +@main def Test = + val v = C(5) + assert(v.x == 5) + assert(v.y == 10) diff --git a/tests/run/inline-trait-clash-val-param-val-param.scala b/tests/run/inline-trait-clash-val-param-val-param.scala new file mode 100644 index 000000000000..e1f46052c9fb --- /dev/null +++ b/tests/run/inline-trait-clash-val-param-val-param.scala @@ -0,0 +1,8 @@ +inline trait A(val x: Int, val y: Int) +class C(override val y: Int) extends A(10, 54): + override val x = 1000 + +@main def Test = + val v = C(44) + assert(v.x == 1000) + assert(v.y == 44) diff --git a/tests/run/inline-trait-maximum-name-clash.scala b/tests/run/inline-trait-maximum-name-clash.scala index f0956b6e20dd..c37e995636ca 100644 --- a/tests/run/inline-trait-maximum-name-clash.scala +++ b/tests/run/inline-trait-maximum-name-clash.scala @@ -4,13 +4,14 @@ inline trait B(x: Int): val y = x inline trait C(val x: Int) -class D(x: Int, z: Int) extends A(314), B(1200), C(12): +class D extends A(314), B(1200), C(12): + override val x = 1 override val y = x val w = y @main def Test = - val v = D(100, 3) - assert(v.y == 100) - assert(v.w == 100) + val v = D() + assert(v.y == 1) + assert(v.w == 1) assert(v.z == 314) - assert(v.x == 12) + assert(v.x == 1) diff --git a/tests/run/inline-trait-parent-param-clashes-with-child-member.scala b/tests/run/inline-trait-parent-param-clashes-with-child-member.scala deleted file mode 100644 index a1b533f133d9..000000000000 --- a/tests/run/inline-trait-parent-param-clashes-with-child-member.scala +++ /dev/null @@ -1,10 +0,0 @@ -// We should rename A.x here to avoid a clash. - -inline trait A(x: Int) - -class C extends A(10): - val x = 5 - -@main def Test = - val v = C() - assert(v.x == 5) diff --git a/tests/run/inline-trait-parent-param-clashes-with-child-val-param.scala b/tests/run/inline-trait-parent-param-clashes-with-child-val-param.scala deleted file mode 100644 index 76731e6193fd..000000000000 --- a/tests/run/inline-trait-parent-param-clashes-with-child-val-param.scala +++ /dev/null @@ -1,9 +0,0 @@ -// Here we need to rename A.x to avoid a clash - -inline trait A(x: Int) - -class C(val x: Int) extends A(10) - -@main def Test = - val v = C(5) - assert(v.x == 5) diff --git a/tests/run/inline-trait-parent-param-clashes-with-method-override.scala b/tests/run/inline-trait-parent-param-clashes-with-method-override.scala deleted file mode 100644 index eb8db996b563..000000000000 --- a/tests/run/inline-trait-parent-param-clashes-with-method-override.scala +++ /dev/null @@ -1,9 +0,0 @@ - -inline trait A(val x: Int) - -class C extends A(10): - override val x = 1000 - -@main def Test = - val v = C() - assert(v.x == 1000) diff --git a/tests/run/inline-trait-parent-params-clash-with-child.scala b/tests/run/inline-trait-parent-params-clash-with-child.scala deleted file mode 100644 index 23c53a76b54b..000000000000 --- a/tests/run/inline-trait-parent-params-clash-with-child.scala +++ /dev/null @@ -1,10 +0,0 @@ -// Param x in A will be renamed so it doesn't clash. - -inline trait A(x: Int): - val y = x - -class C(x: Int) extends A(10) - -@main def Test = - val v = C(5) - assert(v.y == 10) diff --git a/tests/run/inline-trait-parent-val-param-clashes-with-child-param-super-call.scala b/tests/run/inline-trait-parent-val-param-clashes-with-child-param-super-call.scala deleted file mode 100644 index 4820630c351d..000000000000 --- a/tests/run/inline-trait-parent-val-param-clashes-with-child-param-super-call.scala +++ /dev/null @@ -1,15 +0,0 @@ -// Need to rename C.x to avoid a clash - -inline trait A(val x: Int): - val y = x - -class C(x: Int) extends A(10): - val z = x - -@main def Test = - val v = C(5) - assert(v.y == 10) - assert(v.x == 10) - assert(v.z == 5) - println(v.y) - println(v.x) diff --git a/tests/run/inline-trait-parent-val-param-clashes-with-child.scala b/tests/run/inline-trait-parent-val-param-clashes-with-child.scala deleted file mode 100644 index eb8db996b563..000000000000 --- a/tests/run/inline-trait-parent-val-param-clashes-with-child.scala +++ /dev/null @@ -1,9 +0,0 @@ - -inline trait A(val x: Int) - -class C extends A(10): - override val x = 1000 - -@main def Test = - val v = C() - assert(v.x == 1000) From 76f04b70fa66af0c7c1c0b2a739b14f2b8d87557 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 13 Apr 2026 11:09:15 +0200 Subject: [PATCH 092/254] Run transform recursively as we may need to inline inline traits into the bodies of methods defined inside inline traits --- .../dotty/tools/dotc/transform/SpecializeInlineTraits.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala index 356a49a8b7d9..e5f79aa54b83 100644 --- a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala @@ -40,7 +40,8 @@ class SpecializeInlineTraits extends MacroTransform, SymTransformer { override def transform(tree: Tree)(using Context): Tree = tree match { case tree: TypeDef if tree.symbol.isInlineTrait => val tree1 = Inlines.transformInlineTrait(tree) - if Inlines.needsInlining(tree1) then Inlines.inlineParentInlineTraits(tree1) else tree1 + val tree2 = if Inlines.needsInlining(tree1) then Inlines.inlineParentInlineTraits(tree1) else tree1 + super.transform(tree2) // We may need to inline inline traits into the bodies of methods defined inside inline traits. case tree: TypeDef if Inlines.needsInlining(tree) => val tree1 = super.transform(tree).asInstanceOf[TypeDef] if tree1.tpe.isError then tree1 From a443c27e6a10a64fc2fe3ad3d4d4d59efa1ef36b Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 13 Apr 2026 11:09:26 +0200 Subject: [PATCH 093/254] Tidy comments --- .../transform/SpecializeInlineTraits.scala | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala index e5f79aa54b83..7944a9d0845a 100644 --- a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala @@ -64,33 +64,34 @@ class SpecializeInlineTraits extends MacroTransform, SymTransformer { case _ => } - // private def makeTraitFromInnerClass(innerClass: TypeDef)(using Context): TypeDef = - // val TypeDef(name, tmpl: Template) = innerClass: @unchecked - // val newInnerParents = tmpl.parents.mapConserve(ConcreteParentStripper.apply) - // val tmpl1 = cpy.Template(tmpl)(parents = newInnerParents) // TODO .withType(???) - // val newTrait = cpy.TypeDef(innerClass)(name = SpecializeInlineTraits.newInnerClassName(name), rhs = tmpl1) - // newTrait.symbol.setFlag(Synthetic) - // newTrait - // end makeTraitFromInnerClass - - // private def makeTypeFromInnerClass(parentSym: Symbol, innerClass: TypeDef, newTraitSym: Symbol)(using Context): TypeDef = - // val upperBound = innerClass.symbol.primaryConstructor.info match { - // case _: MethodType => - // newTraitSym.typeRef - // case poly: PolyType => - // HKTypeLambda(poly.paramNames)(tl => poly.paramInfos, tl => newTraitSym.typeRef.appliedTo(tl.paramRefs.head)) - // } - // val newTypeSym = newSymbol( - // owner = parentSym, - // name = newTraitSym.name.asTypeName, - // flags = innerClass.symbol.flags & (Private | Protected) | Synthetic, - // info = TypeBounds.upper(upperBound), - // privateWithin = innerClass.symbol.privateWithin, - // coord = innerClass.symbol.coord, - // nestingLevel = innerClass.symbol.nestingLevel, - // ).asType - // TypeDef(newTypeSym) - // end makeTypeFromInnerClass + /* private def makeTraitFromInnerClass(innerClass: TypeDef)(using Context): TypeDef = + val TypeDef(name, tmpl: Template) = innerClass: @unchecked + val newInnerParents = tmpl.parents.mapConserve(ConcreteParentStripper.apply) + val tmpl1 = cpy.Template(tmpl)(parents = newInnerParents) // TODO .withType(???) + val newTrait = cpy.TypeDef(innerClass)(name = SpecializeInlineTraits.newInnerClassName(name), rhs = tmpl1) + newTrait.symbol.setFlag(Synthetic) + newTrait + end makeTraitFromInnerClass + + private def makeTypeFromInnerClass(parentSym: Symbol, innerClass: TypeDef, newTraitSym: Symbol)(using Context): TypeDef = + val upperBound = innerClass.symbol.primaryConstructor.info match { + case _: MethodType => + newTraitSym.typeRef + case poly: PolyType => + HKTypeLambda(poly.paramNames)(tl => poly.paramInfos, tl => newTraitSym.typeRef.appliedTo(tl.paramRefs.head)) + } + val newTypeSym = newSymbol( + owner = parentSym, + name = newTraitSym.name.asTypeName, + flags = innerClass.symbol.flags & (Private | Protected) | Synthetic, + info = TypeBounds.upper(upperBound), + privateWithin = innerClass.symbol.privateWithin, + coord = innerClass.symbol.coord, + nestingLevel = innerClass.symbol.nestingLevel, + ).asType + TypeDef(newTypeSym) + end makeTypeFromInnerClass + */ private object ConcreteParentStripper extends TreeAccumulator[Tree] { def apply(tree: Tree)(using Context): Tree = apply(tree, tree) From 850b9a1223890adcfc58b45a0386c3393efd3083 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 13 Apr 2026 11:10:18 +0200 Subject: [PATCH 094/254] Specify error --- tests/neg/inline-trait-body-private-name-collision.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/neg/inline-trait-body-private-name-collision.scala b/tests/neg/inline-trait-body-private-name-collision.scala index 1e3d14480d82..c0f6624fd707 100644 --- a/tests/neg/inline-trait-body-private-name-collision.scala +++ b/tests/neg/inline-trait-body-private-name-collision.scala @@ -1,3 +1,3 @@ inline trait A: - private val x: Int = 1 // error + private val x: Int = 1 // error: inline traits cannot have non-local private members def eq(o: A) = o.x == x From fcc0f1dd940d34be0d626e973505fccec84f228b Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 13 Apr 2026 11:10:44 +0200 Subject: [PATCH 095/254] Update old test so doesn't break name clash rules --- tests/run/inline-trait-inheritance-inline-grandparent.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/run/inline-trait-inheritance-inline-grandparent.scala b/tests/run/inline-trait-inheritance-inline-grandparent.scala index 670cff30c517..91d1486d31f8 100644 --- a/tests/run/inline-trait-inheritance-inline-grandparent.scala +++ b/tests/run/inline-trait-inheritance-inline-grandparent.scala @@ -13,8 +13,8 @@ package grandParentWithArgs: def foo(): T = x def foooo(): T = y - inline trait Parent[T, U](x: T, z: U) extends GrandParent[U]: - def bar(a: T) = (a, x, y) + inline trait Parent[T, U](w: T, z: U) extends GrandParent[U]: + def bar(a: T) = (a, w, y) class C extends Parent("Hello", 1234), GrandParent(5678, 9) From 2d13df461f8775d507feea65e9b1cafea45295e0 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 13 Apr 2026 11:15:34 +0200 Subject: [PATCH 096/254] Check for safe-init warning --- .../neg/inline-trait-uninitialized-value-should-warn.scala | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/neg/inline-trait-uninitialized-value-should-warn.scala diff --git a/tests/neg/inline-trait-uninitialized-value-should-warn.scala b/tests/neg/inline-trait-uninitialized-value-should-warn.scala new file mode 100644 index 000000000000..2a605f38a71f --- /dev/null +++ b/tests/neg/inline-trait-uninitialized-value-should-warn.scala @@ -0,0 +1,7 @@ +//> using options -Werror -Wsafe-init + +inline trait A(val x: Int): + val f = z + val z: Int // nopos-error: This should warn with -Wsafe-init +class C extends A(10): + val z = 10 From add1abbab2352ba4b10bbe40bc4726cc7fbc781f Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 13 Apr 2026 11:19:26 +0200 Subject: [PATCH 097/254] Update specs --- docs/_docs/internals/inline-traits.md | 67 +++++++++++++++++----- docs/_docs/internals/specialized-traits.md | 3 +- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/docs/_docs/internals/inline-traits.md b/docs/_docs/internals/inline-traits.md index a82e818a5cb3..4ae54afe1ee4 100644 --- a/docs/_docs/internals/inline-traits.md +++ b/docs/_docs/internals/inline-traits.md @@ -112,7 +112,7 @@ def fun(x: B) = ``` - Inline traits may define private members, and these are handled specially: [2] - Private fields in the inline trait are inlined as private fields with a mangled name in the inline receiver. This ensures they do not collide with privates inherited from other inline traits. - - The private fields are then no longer accessible in the inline trait, as it is transformed into a pure interface. We can't however easily delete them; therefore they are name-mangled and converted to protected to allow them to exist without a definition. + - The private fields are then no longer accessible in the inline trait, as it is transformed into a pure interface, so we delete them. ```scala inline trait A(b: Boolean): @@ -125,7 +125,6 @@ Is converted to: ```scala inline trait A(b: Boolean): - protected val x$inline_trait_erased_private#4481: Int def foo(): Int class B extends A(true): @@ -143,7 +142,7 @@ inline trait B: class C extends A, B ``` -However, an inline receiver may not define a member whose name collides with the name of an inlined public member from a parent inline trait, unless the override modifier is used. This reflects the behaviour of ordinary traits. + + +- inline traits may define inline members (e.g. `inline def`, `inline val`). References to these are inlined as the body of the trait is inlined into the inline receiver, but the members themselves are not inlined and are deleted from the parent trait. E.g.: +```scala +inline trait A: + inline val x = 1 + +class B extends A: + def f = x +``` +becomes: +```scala +inline trait A +class B extends A: + def f = 1 ``` + + +- There is the potential for name clashes between members / parameters of inline traits and parameters / members of inline receivers. These are handled in the following way: + +| Inline Trait Member Type | Inline Receiver Member Type | Behaviour | Justification | Same as `trait` | +|---------------------------------------|----------------------------------------------|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------| +| `val` / `var` incl. `val`/`var` param | `val` / `var` incl. `val` / `var` param | Needs `override`| As above | ❌ (ordinary trait will warn on this; we allow it with no warning) | +| `val` / `var` incl. `val`/`var` param | Primary constructor (local) param | Not allowed | We cannot rename the constructor param because users may specify it by name when constructing the class, and we can't have a conflict with the generated parameter accessor, yet the parameter accessor and parameter must have the same name. | ❌ | +| `val` / `var` incl. `val`/`var` param | Method | Not allowed | As normal traits, without `override` will be told "needs override"; with `override` will be told "not a stable immutable value" | ✅ | +| Primary ctor (local) param | `val` / `var` incl. `val` / `var` param | Allowed | We need to inline the generated parameter accessors into the receiver, but these are renamed (prefixed with the inline trait name) when doing so, therefore fine. This does not affect the name in the original trait. | ✅ | +| Primary ctor (local) param | Primary constructor (local) param | Allowed | We need to inline the generated parameter accessors into the receiver, but these are renamed (prefixed with the inline trait name) when doing so, therefore fine. This does not affect the name in the original trait. | ✅ | +| Primary ctor (local) param | Method | Allowed | We need to inline the generated parameter accessors into the receiver, but these are renamed (prefixed with the inline trait name) when doing so, therefore fine. This does not affect the name in the original trait. | ✅ | +| Method | `val` / `var` incl. `val` / `var` param | Allowed | | ❌ `trait` requires `override` TODO: CHANGE SO WE MATCH? | +| Method | Primary constructor (local) param | Allowed unless sig match | We have the same issue as with `val`/`var` params if the signature matches, so we need to ban it. Otherwise we allow it as in `trait`. | ❌ | +| Method | Method | Allowed | | ❌ `trait` requires `override` TODO: CHANGE SO WE MATCH? | +| Type | Type | Allowed | Usual rules apply | ✅ | + - Inline receivers may not access the parameters of their parents (these are private): ```scala inline trait A(x: Int) @@ -188,6 +218,8 @@ inline trait Counter extends Iterator: ``` Narrowing `current` to the type of the initializer here would give it type `0`. This makes the increment operation in `next()` illegal. +- Inline methods defined inside an inline trait are inlined directly when the body is inlined. This means that they do not exist in the inline receivers. They are then deleted from the inline trait. + ## Benefits of inline traits We can now do the following with no boxing and unboxing: ```scala @@ -238,16 +270,22 @@ This problem is addressed via `Specialized` traits; see the accompanying documen ## Interaction with other language features -| Language feature | Is currently supported inside inline traits? | -|--------------------------|----------------------------------------------| -| Methods | ✅ | -| `val` / `var` Properties | ✅ | -| Private properies | ❌ | -| `type`s | ✅ | -| Inner classes/traits | ❌ | -| Opaque types | ❌ | -| Self types | ❌ | -| Inheritance (of inline traits) | Only allowed by classes and inline traits | +| Language feature | Is currently supported inside inline traits? | +|-------------------------------------|----------------------------------------------| +| Methods | ✅ | +| `val` / `var` Properties | ✅ | +| Non-local private members[*] | ❌ | +| `type`s | ✅ | +| Inner classes/traits | ❌ | +| Opaque types | ❌ | +| Self types | ❌ | +| Inheritance (of inline traits) | Only allowed by classes and inline traits | +| Instantiation of inline traits [**] | ❌ | + +[*] That is, members which are labelled private and accessed from within the class on other instances of the class. +Local private members (members with the same access patterns as the former `private[this]`) are allowed. + +[**] While inline traits may not define inner classes as direct members, they may have methods which themsleves define classes. This is permitted only if the classes do not extend from an inline trait. In particular this means that methods of inline traits may not create anonymous instances of inline traits e.g. `new A() {}`. The only exception to this is if the trait being instantiated (`A` here) is `Specialized`, because the instantiation will not produce an anonymous class inside the trait (see the document on Specialized traits). ## Processing of inline traits in the compiler Inline traits in user code are inlined in the phase `specializeInlineTraits`. The phase `replaceInlinedTraitSymbols` @@ -268,3 +306,4 @@ This behaviour is the same as that in Timothée's thesis except for the followin - He allows inline traits to contain inner classes in principle, however in practice they don't work which is why we ban them. - We specialize types of member accesses on e.g. Numeric - He in principle allows traits to extend inline traits although it doesn't work that well; we think we probably want to forbid this. + - We also fix a number of bugs in the implementation, some of which have a minor effect on the processing and interaction with the rest of the compiler phases, e.g. we apply pruneInlineTraits slightly earlier than in the original implementation to avoid spurious warnings with -Wsafe-init. diff --git a/docs/_docs/internals/specialized-traits.md b/docs/_docs/internals/specialized-traits.md index 3f7ec0321863..3054ce38ccab 100644 --- a/docs/_docs/internals/specialized-traits.md +++ b/docs/_docs/internals/specialized-traits.md @@ -573,4 +573,5 @@ This proposal - + + From 24944f80c8cdc5d62dd170f31e3f10dbb946e53f Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 13 Apr 2026 11:20:16 +0200 Subject: [PATCH 098/254] Make sure no spurious warn --- tests/pos/inline-trait-y-equals-x-inlined-nowarn.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/pos/inline-trait-y-equals-x-inlined-nowarn.scala diff --git a/tests/pos/inline-trait-y-equals-x-inlined-nowarn.scala b/tests/pos/inline-trait-y-equals-x-inlined-nowarn.scala new file mode 100644 index 000000000000..4eb0d1d684ab --- /dev/null +++ b/tests/pos/inline-trait-y-equals-x-inlined-nowarn.scala @@ -0,0 +1,10 @@ +//> using options -Werror -Wsafe-init + +inline trait A(val x: Int): + val y = x // We need to be careful not to warn on this when we inline it but leave the body in the parent. Pruning early enough avoids this. +class C extends A(10) + +@main def Test = + val v = C() + println(v.y) + println(v.x) From 8d62780e5cfeb92d937fa3d907916365131c24d1 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 13 Apr 2026 12:08:16 +0200 Subject: [PATCH 099/254] Fix multiple files for inline traits by reading stats start earlier --- compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index faca5037cef9..9e62837d1cc8 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -1168,12 +1168,12 @@ class TreeUnpickler(reader: TastyReader, .map(_.changeOwner(localDummy, constr.symbol))) else parents + val statsStart = currentAddr val lazyStats = readLater(end, rdr => { val stats = rdr.readIndexedStats(localDummy, end) tparams ++ vparams ++ stats }) if cls.isInlineTrait then - val statsStart = currentAddr cls.addAnnotation(LazyBodyAnnotation { (ctx0: Context) ?=> val ctx1 = localContext(cls)(using ctx0).addMode(Mode.ReadPositions) inContext(sourceChangeContext(Addr(0))(using ctx1)) { From e1fc5fd55b594109cb3d7caad24d8a248fa71a98 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 13 Apr 2026 13:44:24 +0200 Subject: [PATCH 100/254] Deal with trait extends inline trait and vice versa --- .../transform/SpecializeInlineTraits.scala | 8 ++++++++ docs/_docs/internals/inline-traits.md | 20 ++++++++++++++++++- ...rait-extends-inline-trait-indirectly.scala | 8 ++++++++ ...line-trait-with-params-uninitialized.scala | 4 ++++ .../inline-trait-extends-abstract-class.scala | 8 ++++++++ tests/pos/inline-trait-extends-class.scala | 8 ++++++++ tests/pos/inline-trait-extends-trait.scala | 8 ++++++++ .../pos/inline-trait-trait-inheritance.scala | 6 ------ tests/pos/trait-extends-inline-trait.scala | 3 +++ .../inline-trait-indirect-with-params.scala | 11 ++++++++++ tests/run/inline-trait-param-no-shadow.scala | 10 ---------- ...-trait-param-shadows-parent-indirect.scala | 13 ++++++------ .../inline-trait-param-shadows-parent.scala | 9 --------- 13 files changed, 83 insertions(+), 33 deletions(-) create mode 100644 tests/neg/trait-extends-inline-trait-indirectly.scala create mode 100644 tests/neg/trait-extends-inline-trait-with-params-uninitialized.scala create mode 100644 tests/pos/inline-trait-extends-abstract-class.scala create mode 100644 tests/pos/inline-trait-extends-class.scala create mode 100644 tests/pos/inline-trait-extends-trait.scala delete mode 100644 tests/pos/inline-trait-trait-inheritance.scala create mode 100644 tests/pos/trait-extends-inline-trait.scala create mode 100644 tests/run/inline-trait-indirect-with-params.scala delete mode 100644 tests/run/inline-trait-param-no-shadow.scala delete mode 100644 tests/run/inline-trait-param-shadows-parent.scala diff --git a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala index 7944a9d0845a..40669d8d6f92 100644 --- a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala @@ -43,6 +43,14 @@ class SpecializeInlineTraits extends MacroTransform, SymTransformer { val tree2 = if Inlines.needsInlining(tree1) then Inlines.inlineParentInlineTraits(tree1) else tree1 super.transform(tree2) // We may need to inline inline traits into the bodies of methods defined inside inline traits. case tree: TypeDef if Inlines.needsInlining(tree) => + if tree.symbol.isAllOf(Trait, butNot = Inline) then + val problemParents = tree.symbol.info.parents.filter( + p => p.classSymbol.isInlineTrait + && p.classSymbol.primaryConstructor.paramSymss.exists(paramList => paramList.nonEmpty && paramList.head.isTerm) + ) + problemParents.foreach( p => + report.error(s"Only parameterless inline traits may be extended by ordinary traits. Make ${tree.symbol} inline or remove inline ${p.typeSymbol}'s parameter list.", tree.srcPos) + ) val tree1 = super.transform(tree).asInstanceOf[TypeDef] if tree1.tpe.isError then tree1 else if tree1.symbol.isInlineTrait then diff --git a/docs/_docs/internals/inline-traits.md b/docs/_docs/internals/inline-traits.md index 4ae54afe1ee4..6c4391763d51 100644 --- a/docs/_docs/internals/inline-traits.md +++ b/docs/_docs/internals/inline-traits.md @@ -34,7 +34,7 @@ use primitive types instead of `Object`. These can be used in situations where t ## Solution An inline trait is defined just like a normal trait, but with an `inline` modifier. -Inline traits may be extended by objects, classes or other inline traits, *but not by ordinary traits*. +Inline traits may be freely extended by objects, classes or other inline traits. An inline trait may also be extended by an ordinary trait, but only if the inline trait does not take parameters [***]. The following is an example of the use of an `inline trait`. @@ -220,6 +220,24 @@ Narrowing `current` to the type of the initializer here would give it type `0`. - Inline methods defined inside an inline trait are inlined directly when the body is inlined. This means that they do not exist in the inline receivers. They are then deleted from the inline trait. +[***] Why? +Consider: +```scala +inline trait A[T](x: T): + val y = x +trait B extends A[Int] +class C extends A[Int](10), B +``` +After inlining, `B.y` is defined in terms of `B.A$$x` (the inlined copy of the parameter accessor of `x`), but this is undefined as we don't have the parameter value in `B`. +In `C` this is not a problem as we have the parameter value `10`. Even if we try to provide the value by leaving `A$$x` abstract and overriding in `C` +this will not work as `B.A$$x` will still be undefined. + +Therefore there is no case in which this could be useful, and it is likely to cause confusion, so we ban it. +However, there is a reasonable case for `trait extends inline trait` in general, to control the inlining and reduce code duplication so we allow +this pattern if the inline trait has no parameters. + +Note that this restriction does not block `inline trait extends trait` or `inline trait extends class` which are allowed without restriction. + ## Benefits of inline traits We can now do the following with no boxing and unboxing: ```scala diff --git a/tests/neg/trait-extends-inline-trait-indirectly.scala b/tests/neg/trait-extends-inline-trait-indirectly.scala new file mode 100644 index 000000000000..70977dd3105b --- /dev/null +++ b/tests/neg/trait-extends-inline-trait-indirectly.scala @@ -0,0 +1,8 @@ +inline trait A[T](x: T): + val y = x +inline trait B extends A[Int] +class C extends B // error: parameterized trait A is indirectly implemented, needs to be implemented directly so that arguments can be passed + +object Test: + def main(args: Array[String]): Unit = + val z = new C diff --git a/tests/neg/trait-extends-inline-trait-with-params-uninitialized.scala b/tests/neg/trait-extends-inline-trait-with-params-uninitialized.scala new file mode 100644 index 000000000000..37646bc9b7da --- /dev/null +++ b/tests/neg/trait-extends-inline-trait-with-params-uninitialized.scala @@ -0,0 +1,4 @@ +inline trait A[T](x: T): + val y = x +trait B extends A[Int] // error: only parameterless inline traits may be extended by ordinary traits. +class C extends B, A[Int](4) diff --git a/tests/pos/inline-trait-extends-abstract-class.scala b/tests/pos/inline-trait-extends-abstract-class.scala new file mode 100644 index 000000000000..ce1b2c1a10af --- /dev/null +++ b/tests/pos/inline-trait-extends-abstract-class.scala @@ -0,0 +1,8 @@ +abstract class A: + def foo: Int + +inline trait B(x: Int) extends A: + val y = x + override def foo = 10 + +class C extends B(15) diff --git a/tests/pos/inline-trait-extends-class.scala b/tests/pos/inline-trait-extends-class.scala new file mode 100644 index 000000000000..46cfce7da628 --- /dev/null +++ b/tests/pos/inline-trait-extends-class.scala @@ -0,0 +1,8 @@ +class A: + def foo: Int = 12 + +inline trait B(x: Int) extends A: + val y = x + override def foo = 10 + +class C extends B(15) diff --git a/tests/pos/inline-trait-extends-trait.scala b/tests/pos/inline-trait-extends-trait.scala new file mode 100644 index 000000000000..0977d7faa67a --- /dev/null +++ b/tests/pos/inline-trait-extends-trait.scala @@ -0,0 +1,8 @@ +trait A: + def foo: Int + +inline trait B(x: Int) extends A: + val y = x + override def foo = 10 + +class C extends B(15) diff --git a/tests/pos/inline-trait-trait-inheritance.scala b/tests/pos/inline-trait-trait-inheritance.scala deleted file mode 100644 index a381078c6cb7..000000000000 --- a/tests/pos/inline-trait-trait-inheritance.scala +++ /dev/null @@ -1,6 +0,0 @@ -inline trait A(x: Int): - val y = x - -trait C extends A - -class D extends C, A(15) diff --git a/tests/pos/trait-extends-inline-trait.scala b/tests/pos/trait-extends-inline-trait.scala new file mode 100644 index 000000000000..8aa4040aadc4 --- /dev/null +++ b/tests/pos/trait-extends-inline-trait.scala @@ -0,0 +1,3 @@ +inline trait A: + def x = 1 +trait B extends A // This is fine as long as A has no parameters. diff --git a/tests/run/inline-trait-indirect-with-params.scala b/tests/run/inline-trait-indirect-with-params.scala new file mode 100644 index 000000000000..fccd89cf9db4 --- /dev/null +++ b/tests/run/inline-trait-indirect-with-params.scala @@ -0,0 +1,11 @@ +inline trait A[T](x: T): + val y = x + def foo() = x +inline trait B extends A[Int] +class C extends A[Int](10), B + +object Test: + def main(args: Array[String]): Unit = + val z: B = new C + assert(z.foo() == 10) + assert(z.y == 10) \ No newline at end of file diff --git a/tests/run/inline-trait-param-no-shadow.scala b/tests/run/inline-trait-param-no-shadow.scala deleted file mode 100644 index 7f469b831e15..000000000000 --- a/tests/run/inline-trait-param-no-shadow.scala +++ /dev/null @@ -1,10 +0,0 @@ -// TODO: Decide if we want to allow this or not (might ban trait extends inline trait pattern) - -inline trait A[T](x: T): - val y = x -trait B extends A[Int] -class C extends B - -object Test: - def main(args: Array[String]): Unit = - val z = new C diff --git a/tests/run/inline-trait-param-shadows-parent-indirect.scala b/tests/run/inline-trait-param-shadows-parent-indirect.scala index f5106f0ee039..964b95f5acfb 100644 --- a/tests/run/inline-trait-param-shadows-parent-indirect.scala +++ b/tests/run/inline-trait-param-shadows-parent-indirect.scala @@ -1,16 +1,15 @@ inline trait A[T](x: T): def y = x -trait B extends A[Int] -trait D extends A[Int] +inline trait B extends A[Int] +inline trait D extends A[Int] -// These two are not really necessary for the case but add to the "indirectness" -trait E extends B -trait F extends D +inline trait E extends B +inline trait F extends D -class C extends E, F +class C extends E, F, A[Int](100) object Test: def main(args: Array[String]): Unit = val z = new C - println("Testing") + assert(z.y == 100) diff --git a/tests/run/inline-trait-param-shadows-parent.scala b/tests/run/inline-trait-param-shadows-parent.scala deleted file mode 100644 index c7aaaeeb8a4c..000000000000 --- a/tests/run/inline-trait-param-shadows-parent.scala +++ /dev/null @@ -1,9 +0,0 @@ -inline trait A[T](x: T): - def y = x -trait B extends A[Int] -class C extends B, A[Int](4) - -object Test: - def main(args: Array[String]): Unit = - val z = new C - println("Testing") From 16f24b5c86ca881406ab980ef3b6959c8002bfd7 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 14 Apr 2026 10:47:59 +0200 Subject: [PATCH 101/254] Don't hash referenced inline traits in SBT ExtractAPI --- compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index 5061ae5773b8..bbaa72cb8813 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -816,10 +816,14 @@ private class ExtractAPICollector(nonLocalClassSymbols: mutable.HashSet[Symbol]) p match case ref: RefTree @unchecked => val sym = ref.symbol - if sym.is(Inline, butNot = Param) && !seenInlineCache.contains(sym) then + if sym.is(Inline, butNot = Param | Trait) && !seenInlineCache.contains(sym) then // An inline method that calls another inline method will eventually inline the call // at a non-inline callsite, in this case if the implementation of the nested call // changes, then the callsite will have a different API, we should hash the definition + // In contrast, if an inline trait A extends an inline trait B, B's body is inlined into + // A, so a change to B will cause a change to A directly, so we can skip this, and if + // an inline method accesses a parameter of an inline trait it doesn't care about the definition + // of the trait. h = MurmurHash3.mix(h, apiDefinition(sym, inlineOrigin).hashCode) case _ => From e3e4c749913c84fb9b860e416a08bd6a2dcf535b Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 14 Apr 2026 10:49:05 +0200 Subject: [PATCH 102/254] Add inline val to multiple stages generic defs --- tests/pos/inline-trait-multiple-stages-generic-defs/A_1.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/pos/inline-trait-multiple-stages-generic-defs/A_1.scala b/tests/pos/inline-trait-multiple-stages-generic-defs/A_1.scala index 83e2f62dcd45..c033e5471d4a 100644 --- a/tests/pos/inline-trait-multiple-stages-generic-defs/A_1.scala +++ b/tests/pos/inline-trait-multiple-stages-generic-defs/A_1.scala @@ -6,4 +6,5 @@ inline trait A[T](x: T): val j: T var k: T = x - inline def b(a: T): T = x + inline val property = x + inline def method(a: T): T = x From d0ea6e37be653abb1e52d8be3aeb70f1e5ae3714 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 14 Apr 2026 10:52:42 +0200 Subject: [PATCH 103/254] Add inline-trait-multiple-stages-inline-traits --- .../inline-trait-multiple-stages-inline-traits/A_1.scala | 3 +++ .../inline-trait-multiple-stages-inline-traits/B_2.scala | 2 ++ .../inline-trait-multiple-stages-inline-traits/C_3.scala | 6 ++++++ 3 files changed, 11 insertions(+) create mode 100644 tests/pos/inline-trait-multiple-stages-inline-traits/A_1.scala create mode 100644 tests/pos/inline-trait-multiple-stages-inline-traits/B_2.scala create mode 100644 tests/pos/inline-trait-multiple-stages-inline-traits/C_3.scala diff --git a/tests/pos/inline-trait-multiple-stages-inline-traits/A_1.scala b/tests/pos/inline-trait-multiple-stages-inline-traits/A_1.scala new file mode 100644 index 000000000000..2c130a7d8613 --- /dev/null +++ b/tests/pos/inline-trait-multiple-stages-inline-traits/A_1.scala @@ -0,0 +1,3 @@ +inline trait A: + def foo = "hello world" + \ No newline at end of file diff --git a/tests/pos/inline-trait-multiple-stages-inline-traits/B_2.scala b/tests/pos/inline-trait-multiple-stages-inline-traits/B_2.scala new file mode 100644 index 000000000000..68b16caeff9b --- /dev/null +++ b/tests/pos/inline-trait-multiple-stages-inline-traits/B_2.scala @@ -0,0 +1,2 @@ +inline trait B extends A: + def bar = "Good morning world" diff --git a/tests/pos/inline-trait-multiple-stages-inline-traits/C_3.scala b/tests/pos/inline-trait-multiple-stages-inline-traits/C_3.scala new file mode 100644 index 000000000000..da7b94cef862 --- /dev/null +++ b/tests/pos/inline-trait-multiple-stages-inline-traits/C_3.scala @@ -0,0 +1,6 @@ +class C extends B + +@main def main = + val x = C() + println(x.foo) + println(x.bar) From 7060dbe23d5472e0d9dd6b0490045044be0c3c8d Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 14 Apr 2026 11:07:09 +0200 Subject: [PATCH 104/254] Homogenise test names --- ...aits-inheritance.scala => specialized-trait-inheritance.scala} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/neg/{specialized-traits-inheritance.scala => specialized-trait-inheritance.scala} (100%) diff --git a/tests/neg/specialized-traits-inheritance.scala b/tests/neg/specialized-trait-inheritance.scala similarity index 100% rename from tests/neg/specialized-traits-inheritance.scala rename to tests/neg/specialized-trait-inheritance.scala From 963e1ed9fac26ee3427ea9845c932b264b4715ef Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 14 Apr 2026 11:37:30 +0200 Subject: [PATCH 105/254] Remove unnecessary comment --- docs/_docs/internals/specialized-traits.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/internals/specialized-traits.md b/docs/_docs/internals/specialized-traits.md index 3054ce38ccab..c31ef8e00bb3 100644 --- a/docs/_docs/internals/specialized-traits.md +++ b/docs/_docs/internals/specialized-traits.md @@ -161,7 +161,7 @@ class Vec$impl$Int(elems: Array[T]) extends Vec[Int](elems), Vec$sp$Int After inlining `Vec[Int]` the expanded class looks like this: ```scala class Vec$impl$Int(elems: Array[Int])(using Numeric[Int]) extends Vec[Int](elems), Vec$sp$Int: - private val Vec$$num: Numeric.IntIsIntegral // Methods converted to primitive operations without boxing + private val Vec$$num: Numeric.IntIsIntegral def length: Int = elems.length def apply(i: Int): Int = elems(i) From 4af61f3fae2d420cf08893eef252b1716dac048f Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 14 Apr 2026 11:44:07 +0200 Subject: [PATCH 106/254] Add coord and compilation unit info --- .../tools/dotc/transform/DesugarSpecializedTraits.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 80a3a0f05d34..e0b4ba477aeb 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -79,7 +79,8 @@ class DesugarSpecializedTraits extends MacroTransform: parents, NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? specialization.traitSymbol.privateWithin, - // TODO: Do we need a compUnit info? + specialization.traitSymbol.coord, + specialization.traitSymbol.compilationUnitInfo ) // Create type parameters for new trait @@ -139,7 +140,8 @@ class DesugarSpecializedTraits extends MacroTransform: parents, NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? specialization.traitSymbol.privateWithin, - // TODO: Do we need a compUnit info? + specialization.traitSymbol.coord, + specialization.traitSymbol.compilationUnitInfo ).entered // TODO: Do we want to share some code with the newSpecializedInterfaceTrait and buildInterfaceTraitTree? From 8fc1498f55c32ce892160e7dc738251cb051e82c Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 14 Apr 2026 11:44:59 +0200 Subject: [PATCH 107/254] Remove comment --- .../dotty/tools/dotc/transform/DesugarSpecializedTraits.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index e0b4ba477aeb..a20fc9b0f6ef 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -208,7 +208,6 @@ class DesugarSpecializedTraits extends MacroTransform: } private def replaceSpecializedSymbolsMap(specializations: SpecializedTraitCache) = - // Use the TreeTypeMap to replace instances (can we do this without accidentally replacing the definitions? I think it should be ok) val typeMap = new TypeMap: def apply(t: Type) = t match { case Specialization(spec) => From 08da50d07e1b8b69951a91a3480295c40c9bfe30 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 14 Apr 2026 13:38:03 +0200 Subject: [PATCH 108/254] Build bridges for specialized traits --- .../transform/DesugarSpecializedTraits.scala | 34 ++++++++++++++++++- .../ReplaceInlinedTraitSymbols.scala | 6 +++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index a20fc9b0f6ef..0e27077b0d4c 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -233,6 +233,7 @@ class DesugarSpecializedTraits extends MacroTransform: } // Replace class Bar extends Foo[Int](params) with class Bar extends Foo$sp$Int(params) + // TODO: Why do we still have this case if we don't allow this pattern? // Note: We always drop the evidence params when creating these new specialized traits so we know that there are none, but we may need to revisit this if we decide we do want to copy the evidence parameters over case Apply(TypeApply(fun@Select(New(tpt), init), args), ev) if fun.symbol.isConstructor => val spec = Specialization(fun.symbol.owner, args) @@ -271,7 +272,38 @@ class DesugarSpecializedTraits extends MacroTransform: } ) - cpy.Template(impl)(body = impl.body.map(transform(_))) + // If a class has a specialized member which was overriding a parent member, this override is lost because we specialize the types. + // E.g. def foo(Vec$sp$Int) cannot override def foo(Vec[Int]) because signatures must match exactly for overriding. + // However, specialized trait is based on the invariant that ∀T. T <: Foo[Int] => T <: Foo$sp$Int (and note that the reverse <= holds trivially by inheritance). + // This means it is safe to build bridge methods which simply apply the relevant casts so that we satisfy the interface, although we don't expect to call these. + def isMapped(t: Type) = mapType(t) != t + + val bridgeMethods = impl.body.collect { + case ddef@DefDef(name, paramss, _, _) if ddef.symbol.allOverriddenSymbols.nonEmpty && (ddef.termParamss.exists(params => params.exists(p => isMapped(p.symbol.info)) || isMapped(ddef.symbol.localReturnType))) => + val bridgeSym = ddef.symbol.copy().entered + val rhsFun: List[List[Tree]] => Tree = + newParamss => + This(impl.symbol.owner.asClass).select(ddef.symbol) + .appliedToArgss( + newParamss.map( + params => params.map(p => p.cast(mapType(p.symbol.info))) + ) + ).cast(ddef.symbol.localReturnType) + ddef.symbol.flags = ddef.symbol.flags &~ Flags.Override + // Any callers of the original method will have been redirected to the bridge method because it has a signature match with the method they were calling + // We want to force them to call the original method, since we know they can. + ctx.inlineTraitState.registerInlinedSymbol(bridgeSym, ddef.symbol, impl.symbol.owner.thisType.widenDealias) + DefDef(bridgeSym.asTerm, rhsFun) + + case vdef: ValDef if isMapped(vdef.symbol.info) => + vdef.symbol.flags = vdef.symbol.flags &~ Flags.Override + val bridgeSym = vdef.symbol.copy().entered + ctx.inlineTraitState.registerInlinedSymbol(bridgeSym, vdef.symbol, impl.symbol.owner.thisType.widenDealias) + ValDef(bridgeSym.asTerm, This(impl.symbol.owner.asClass).select(vdef.symbol).cast(vdef.symbol.info)) + } + val mappedbody = impl.body.map(transform(_)) + + cpy.Template(impl)(body = mappedbody ::: bridgeMethods) case tree => super.transform(tree) } } diff --git a/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala b/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala index d93bd16c8879..cf9779723abd 100644 --- a/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala +++ b/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala @@ -41,4 +41,8 @@ class ReplaceInlinedTraitSymbols extends MiniPhase: override def runsAfterGroupsOf: Set[String] = Set("specializeInlineTraits") object ReplaceInlinedTraitSymbols: val name: String = "replaceInlinedTraitSymbols" - val description: String = "Replace symbols referring to inline trait members with resulting inlined member symbols" + val description: String = "Replace symbols referring to inline trait members with resulting inlined member symbols. Also replace bridge method calls with specialized method calls for specialized traits." + /* We need to replace symbols referring to inlined methods / members because otherwise we will still point + to the parent symbol (this was resolved before we generated the new symbols) and so we won't get the efficiency gain. + See tests/pos/inline-trait-return-ref.scala. We also need to do this outside the inline traits themselves (i.e. in + the whole program - see tests/pos/inline-trait-parent-ref.scala) */ From d49054db3d0bcc15819c3a90d86d77913e7fc779 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 14 Apr 2026 13:38:12 +0200 Subject: [PATCH 109/254] Fix docs --- docs/_docs/internals/specialized-traits.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/internals/specialized-traits.md b/docs/_docs/internals/specialized-traits.md index c31ef8e00bb3..fbdb3e349701 100644 --- a/docs/_docs/internals/specialized-traits.md +++ b/docs/_docs/internals/specialized-traits.md @@ -166,7 +166,7 @@ class Vec$impl$Int(elems: Array[Int])(using Numeric[Int]) extends Vec[Int](elems def length: Int = elems.length def apply(i: Int): Int = elems(i) - def scalarProduct(other: Vec[Int]): Int = + def scalarProduct(other: Vec$sp$Int): Int = require(this.length == other.length) var result = Vec$$num.fromInt(0) for i <- 0 until length do From f71f34294acac6b2062c8c641a93defbae3ff671 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 14 Apr 2026 15:59:13 +0200 Subject: [PATCH 110/254] Revert "Allow specializations to match parent signatures in overriding methods" This reverts commit f55b8d034524ef5cfa2dc1eef7675067be550384. --- .../dotty/tools/dotc/core/Denotations.scala | 10 +++------ .../src/dotty/tools/dotc/core/Signature.scala | 21 ++++--------------- .../dotty/tools/dotc/core/TypeComparer.scala | 6 +----- .../dotty/tools/dotc/typer/RefChecks.scala | 2 +- 4 files changed, 9 insertions(+), 30 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Denotations.scala b/compiler/src/dotty/tools/dotc/core/Denotations.scala index f471da57c3e5..03152ab7e291 100644 --- a/compiler/src/dotty/tools/dotc/core/Denotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Denotations.scala @@ -1022,13 +1022,9 @@ object Denotations { final def first: SingleDenotation = this final def last: SingleDenotation = this - def matchesOrSpecialized(other: SingleDenotation)(using Context): Boolean = - symbol.hasTargetName(other.symbol.targetName) - && matchesLoosely(other, allowSpecializations = true) - def matches(other: SingleDenotation)(using Context): Boolean = symbol.hasTargetName(other.symbol.targetName) - && matchesLoosely(other, allowSpecializations = false) + && matchesLoosely(other) /** `matches` without a target name check. * @@ -1038,7 +1034,7 @@ object Denotations { * erasure (see i8615b, i9109b), Erasure takes care of adding any necessary * bridge to make this work at runtime. */ - def matchesLoosely(other: SingleDenotation, alwaysCompareTypes: Boolean = false, allowSpecializations: Boolean = false)(using Context): Boolean = + def matchesLoosely(other: SingleDenotation, alwaysCompareTypes: Boolean = false)(using Context): Boolean = if isType then true else val thisLanguage = SourceLanguage(symbol) @@ -1046,7 +1042,7 @@ object Denotations { val commonLanguage = SourceLanguage.commonLanguage(thisLanguage, otherLanguage) val sig = signature(commonLanguage) val otherSig = other.signature(commonLanguage) - sig.matchDegree(otherSig, allowSpecializations) match + sig.matchDegree(otherSig) match case FullMatch => !alwaysCompareTypes || info.matches(other.info) case MethodNotAMethodMatch => diff --git a/compiler/src/dotty/tools/dotc/core/Signature.scala b/compiler/src/dotty/tools/dotc/core/Signature.scala index 6528187df2e6..f8475a4cf314 100644 --- a/compiler/src/dotty/tools/dotc/core/Signature.scala +++ b/compiler/src/dotty/tools/dotc/core/Signature.scala @@ -6,7 +6,6 @@ import scala.annotation.tailrec import Names.*, Types.*, Contexts.*, StdNames.*, Decorators.* import TypeErasure.sigName import Signature.* -import dotty.tools.dotc.core.StdNames.str.SPECIALIZED_TRAIT_SUFFIX /** The signature of a denotation. * @@ -53,26 +52,14 @@ case class Signature(paramsSig: List[ParamSig], resSig: TypeName) { private def consistent(name1: ParamSig, name2: ParamSig) = name1 == name2 || name1 == tpnme.Uninstantiated || name2 == tpnme.Uninstantiated - // TODO: Put this somewhere else? - private def isSpecializedName(name1: ParamSig, name2: ParamSig) = (name1, name2) match { - case (n1: TypeName, n2: TypeName) => (n1.toTermName.split, n2.toTermName.split) match { - case ((prefix1, lastPart1, _), (prefix2, lastPart2, _)) => - prefix1 == prefix2 && lastPart2.startsWith((lastPart1 ++ SPECIALIZED_TRAIT_SUFFIX).toString()) - || lastPart1.startsWith((lastPart2 ++ SPECIALIZED_TRAIT_SUFFIX).toString()) - } - case _ => false - } - /** Does this signature coincide with that signature on their parameter parts? * This is the case if all parameter signatures are _consistent_, i.e. they are either * equal or on of them is tpnme.Uninstantiated. */ - final def consistentParams(that: Signature, allowSpecializations: Boolean = false)(using Context): Boolean = { + final def consistentParams(that: Signature)(using Context): Boolean = { @tailrec def loop(names1: List[ParamSig], names2: List[ParamSig]): Boolean = if (names1.isEmpty) names2.isEmpty - else !names2.isEmpty && (consistent(names1.head, names2.head) || ( - allowSpecializations && isSpecializedName(names1.head, names2.head) - )) && loop(names1.tail, names2.tail) + else !names2.isEmpty && consistent(names1.head, names2.head) && loop(names1.tail, names2.tail) loop(this.paramsSig, that.paramsSig) } @@ -98,8 +85,8 @@ case class Signature(paramsSig: List[ParamSig], resSig: TypeName) { * or `ParamMatch`. * If the parameters are inconsistent, the result is always `NoMatch`. */ - final def matchDegree(that: Signature, allowSpecializations: Boolean = false)(using Context): MatchDegree = - if consistentParams(that, allowSpecializations) then + final def matchDegree(that: Signature)(using Context): MatchDegree = + if consistentParams(that) then if resSig == that.resSig || isWildcard(resSig) || isWildcard(that.resSig) then FullMatch else if (this == NotAMethod) != (that == NotAMethod) then diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index eec49febeed9..c292e64e75e0 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -28,8 +28,6 @@ import NameKinds.WildcardParamName import MatchTypes.isConcrete import reporting.Message.Note import scala.util.boundary, boundary.break -import transform.DesugarSpecializedTraits - /** Provides methods to compare types. */ @@ -2415,9 +2413,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case formal2 :: rest2 => val formal2a = if (tp2.isParamDependent) formal2.subst(tp2, tp1) else formal2 val paramsMatch = - if DesugarSpecializedTraits.isSpecializationOf(formal1, formal2a) then - true - else if precise then + if precise then isSameTypeWhenFrozen(formal1, formal2a) else if isCaptureCheckingOrSetup then // allow to constrain capture set variables diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index f6eed06deb2f..0cd14bab8294 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -995,7 +995,7 @@ object RefChecks { def isSignatureMatch(sym: Symbol) = sym.isType || { val self = clazz.thisType - sym.asSeenFrom(self).matchesOrSpecialized(member.asSeenFrom(self)) + sym.asSeenFrom(self).matches(member.asSeenFrom(self)) && !incompatibleRepeatedParam(sym, member) } From 4b4bc5f0c969b2cf2bffb55bbcb30d3296c7710f Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 15 Apr 2026 12:10:38 +0200 Subject: [PATCH 111/254] Test super call in inline trait transform --- tests/pos/inline-trait-nested-expansion.scala | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/pos/inline-trait-nested-expansion.scala diff --git a/tests/pos/inline-trait-nested-expansion.scala b/tests/pos/inline-trait-nested-expansion.scala new file mode 100644 index 000000000000..4035f91b9867 --- /dev/null +++ b/tests/pos/inline-trait-nested-expansion.scala @@ -0,0 +1,14 @@ +inline trait I1: + def foo = "foo" + +inline trait I2: + def bar = "bar" + +trait T1 extends I1: + trait T2 extends I2 + +def main = + val a = new T1() {} + val b = new a.T2() {} + a.foo + b.bar From 7e4f439b0ffc07aaca2fd756f7fced02ddef96f0 Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 17 Apr 2026 17:10:32 +0200 Subject: [PATCH 112/254] Update version and comment for Benchmark --- .../dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala index 39dafb331bf6..749ca0fec274 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala @@ -1,8 +1,9 @@ // Run with: scala-cli --power --jmh bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala // May have to run it again / delete .scala-build and rerun if you get a class not found // error from scala-cli first time - the --jmh flag is still experimental. +// Don't forget to publish the compiler first and check that the version below corresponds to the generated version. -//> using scala 3.8.3-RC1-bin-SNAPSHOT-nonbootstrapped +//> using scala 3.8.4-RC1-bin-SNAPSHOT-nonbootstrapped //> using options -language:experimental.specializedTraits package dotty.tools.benchmarks From adb7444e5d2721b5e03decefe5a4f526438eaeed Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 17 Apr 2026 17:17:59 +0200 Subject: [PATCH 113/254] Create spec methods --- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../transform/DesugarSpecializedTraits.scala | 39 ++++++++++++++----- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 1466d6ab611d..001d111d03bb 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -30,6 +30,7 @@ object StdNames { inline val SPECIALIZED_TRAIT_SUFFIX = "$sp$" inline val SPECIALIZED_TRAIT_IMPL_SUFFIX = "$impl$" inline val SPECIALIZED_TRAIT_TYPE_SEP = "$" + inline val SPECIALIZED_METHOD_TARGET_NAME_SUFFIX = "$spec" inline val REPL_SESSION_LINE = "rs$line$" inline val REPL_ASSIGN_SUFFIX = "$assign" diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 0e27077b0d4c..ba844fed373e 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -36,6 +36,8 @@ import dotty.tools.dotc.transform.DesugarSpecializedTraits.isSpecializationOf import dotty.tools.dotc.report import dotty.tools.dotc.transform.DesugarSpecializedTraits.isImplementationOf import dotty.tools.dotc.core.Flags.InlineTrait +import dotty.tools.dotc.core.Annotations.Annotation +import dotty.tools.dotc.core.Constants.Constant class DesugarSpecializedTraits extends MacroTransform: @@ -248,15 +250,36 @@ class DesugarSpecializedTraits extends MacroTransform: yield AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? }.getOrElse(tree) + // Select a method which is specialized; we need to make sure we call the specialized version + case sel@Select(qualifier, name) if typeMap(sel.symbol.info) != sel.symbol.info => + Select(qualifier, name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX) case tree => tree } new TreeTypeMap(typeMap, treeMap) { override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? case dd@DefDef(name, paramss, tpt, preRhs) => - val transformedDef = super.transform(dd) - transformedDef.symbol.info = mapType(transformedDef.symbol.info) - transformedDef + val transformedDef = super.transform(dd).asInstanceOf[DefDef] + + if transformedDef.symbol.info != mapType(transformedDef.symbol.info) then + val specializedSymbol = newSymbol( + transformedDef.symbol.owner, + transformedDef.symbol.name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX, + transformedDef.symbol.flags &~ Flags.Override, + info = mapType(transformedDef.symbol.info), + transformedDef.symbol.privateWithin, + transformedDef.symbol.coord, + transformedDef.symbol.nestingLevel + ).entered + + val rhsFun: List[List[Tree]] => Tree = paramss => + val oldParamSyms = transformedDef.paramss.flatten.map(_.symbol) + val newParamSyms = paramss.flatten.map(_.symbol) + transformedDef.rhs.subst(oldParamSyms, newParamSyms).changeOwner(transformedDef.symbol, specializedSymbol) + + DefDef(specializedSymbol.asTerm, rhsFun) + else + transformedDef case impl@Template(constr, preParentsOrDerived, self, _) => impl.parents.foreach(p => @@ -279,8 +302,8 @@ class DesugarSpecializedTraits extends MacroTransform: def isMapped(t: Type) = mapType(t) != t val bridgeMethods = impl.body.collect { - case ddef@DefDef(name, paramss, _, _) if ddef.symbol.allOverriddenSymbols.nonEmpty && (ddef.termParamss.exists(params => params.exists(p => isMapped(p.symbol.info)) || isMapped(ddef.symbol.localReturnType))) => - val bridgeSym = ddef.symbol.copy().entered + // TODO: Probably can just do this isMapped on the whole type. + case ddef@DefDef(name, paramss, _, _) if ddef.symbol.allOverriddenSymbols.nonEmpty && (ddef.termParamss.exists(params => params.exists(p => isMapped(p.symbol.info))) || isMapped(ddef.symbol.localReturnType)) => val rhsFun: List[List[Tree]] => Tree = newParamss => This(impl.symbol.owner.asClass).select(ddef.symbol) @@ -289,14 +312,12 @@ class DesugarSpecializedTraits extends MacroTransform: params => params.map(p => p.cast(mapType(p.symbol.info))) ) ).cast(ddef.symbol.localReturnType) - ddef.symbol.flags = ddef.symbol.flags &~ Flags.Override // Any callers of the original method will have been redirected to the bridge method because it has a signature match with the method they were calling - // We want to force them to call the original method, since we know they can. - ctx.inlineTraitState.registerInlinedSymbol(bridgeSym, ddef.symbol, impl.symbol.owner.thisType.widenDealias) - DefDef(bridgeSym.asTerm, rhsFun) + DefDef(ddef.symbol.asTerm, rhsFun) case vdef: ValDef if isMapped(vdef.symbol.info) => vdef.symbol.flags = vdef.symbol.flags &~ Flags.Override + vdef.symbol.setTargetName(vdef.name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX) val bridgeSym = vdef.symbol.copy().entered ctx.inlineTraitState.registerInlinedSymbol(bridgeSym, vdef.symbol, impl.symbol.owner.thisType.widenDealias) ValDef(bridgeSym.asTerm, This(impl.symbol.owner.asClass).select(vdef.symbol).cast(vdef.symbol.info)) From 2a96a114abedb1e1041f42b91d0e6673d6228e4d Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 17 Apr 2026 17:19:09 +0200 Subject: [PATCH 114/254] Inline nested inline traits calls correctly --- .../dotty/tools/dotc/inlines/Inlines.scala | 50 +++++++++++++++++-- .../transform/SpecializeInlineTraits.scala | 11 ++-- tests/run/inline-trait-nested.scala | 13 +++++ 3 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 tests/run/inline-trait-nested.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 5f44d8ed329f..ab6700198d84 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -27,6 +27,10 @@ import util.Spans.{Span, spanCoord} import dotty.tools.dotc.core.Periods.PhaseId import dotty.tools.dotc.util.chaining.* import NameOps.expandedName +import dotty.tools.dotc.core.Annotations.ConcreteBodyAnnotation +import dotty.tools.dotc.core.Annotations.LazyBodyAnnotation +import dotty.tools.dotc.core.Scopes.EmptyScope +import dotty.tools.dotc.core.Scopes.MutableScope /** Support for querying inlineable methods and for inlining calls to such methods */ object Inlines: @@ -284,9 +288,9 @@ object Inlines: def inlineParentInlineTraits(cls: Tree)(using Context): Tree = cls match { - case cls @ tpd.TypeDef(_, impl: Template) if cls.symbol.owner.ownersIterator.exists(_.isInlineTrait) => // TODO: We can relax this if we use a seen list to avoid cycles - report.error("May not inline an inline trait into a class defined inside another inline trait. If you really need to do this, make the inline trait Specialized or move the class definition outside the trait.", cls.srcPos) - cls + // case cls @ tpd.TypeDef(_, impl: Template) if cls.symbol.owner.ownersIterator.exists(_.isInlineTrait) => // TODO: We can relax this if we use a seen list to avoid cycles + // report.error("May not inline an inline trait into a class defined inside another inline trait. If you really need to do this, make the inline trait Specialized or move the class definition outside the trait.", cls.srcPos) + // cls case cls @ tpd.TypeDef(_, impl: Template) => val clsOverriddenSyms = cls.symbol.info.decls.toList.flatMap(_.allOverriddenSymbols).toSet val newDefs = inContext(ctx.withOwner(cls.symbol)) { @@ -941,8 +945,46 @@ object Inlines: if rhs.isEmpty then rhs else + + val symbolMap = mutable.Map[Symbol, Symbol]() // TODO make version of inlined that does not return bindings? - Inlined(tpd.ref(parentSym), Nil, inlined(rhs)._2).withSpan(parent.span) + val rhs1 = Inlined(tpd.ref(parentSym), Nil, inlined(rhs)._2).withSpan(parent.span) + val ttmap = TreeTypeMap(treeMap = { + case tree@TypeDef(name, tmpl: Template) if Inlines.needsInlining(tree) => + val newSym = tree.symbol.copy() + newSym.info = ClassInfo(tree.symbol.owner.thisType, newSym.asClass, tree.symbol.asClass.parentTypes, Scopes.newScope) + + val newConstructorSymbol = tree.symbol.primaryConstructor.copy(owner = newSym) + val rt = newSym.typeRef.appliedTo(newSym.typeParams.map(_.typeRef)) + def resultType(tpe: Type): Type = tpe match { + case mt @ MethodType(paramNames) => mt.derivedLambdaType(paramNames, mt.paramInfos, rt) + case pt : PolyType => pt.derivedLambdaType(pt.paramNames, pt.paramInfos, resultType(pt.resType)) + } + newConstructorSymbol.info = resultType(newConstructorSymbol.info) + newConstructorSymbol.info = PolyType.fromParams(newConstructorSymbol.owner.typeParams, newConstructorSymbol.info) + + val childSyms = tree.symbol.info.decls + .filter(sym => tmpl.body.exists(vddef => vddef.symbol == sym)) + .map(_.copy(owner = newSym)) + childSyms.foreach(p => p.entered) + newConstructorSymbol.entered + + val tmpl1 = tmpl.changeOwner(tree.symbol, newSym) + + val rhsFun: List[List[Tree]] => Tree = + paramss => + val oldParamSyms = tmpl1.constr.paramss.flatten.map(_.symbol) + val newParamSyms = paramss.flatten.map(_.symbol) + tmpl1.constr.rhs.subst(oldParamSyms, newParamSyms) + + val ctor = tpd.DefDef(newConstructorSymbol.asTerm, rhsFun) + symbolMap(tree.symbol) = newSym + tpd.ClassDefWithParents(newSym.asClass, ctor, tmpl1.parents, tmpl1.body) + case tree => tree + }) + + val rhs2 = ttmap(rhs1) + TreeTypeMap(substFrom = symbolMap.keys.toList, substTo = symbolMap.values.toList)(rhs2) private class ParamAccessorsMapper: private val paramAccessorsTrees: mutable.Map[Symbol, Map[Name, Tree]] = mutable.Map.empty diff --git a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala index 40669d8d6f92..691dea2864d1 100644 --- a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala @@ -51,11 +51,12 @@ class SpecializeInlineTraits extends MacroTransform, SymTransformer { problemParents.foreach( p => report.error(s"Only parameterless inline traits may be extended by ordinary traits. Make ${tree.symbol} inline or remove inline ${p.typeSymbol}'s parameter list.", tree.srcPos) ) - val tree1 = super.transform(tree).asInstanceOf[TypeDef] - if tree1.tpe.isError then tree1 - else if tree1.symbol.isInlineTrait then - Inlines.inlineParentInlineTraits(Inlines.transformInlineTrait(tree1)) - else Inlines.inlineParentInlineTraits(tree1) + val tree1 = + if tree.symbol.isInlineTrait then + Inlines.inlineParentInlineTraits(Inlines.transformInlineTrait(tree)) + else Inlines.inlineParentInlineTraits(tree) + super.transform(tree1) + case _ => super.transform(tree) } } diff --git a/tests/run/inline-trait-nested.scala b/tests/run/inline-trait-nested.scala new file mode 100644 index 000000000000..ca223265385a --- /dev/null +++ b/tests/run/inline-trait-nested.scala @@ -0,0 +1,13 @@ +inline trait Trait[T]: + def do_something() = println("Good morning") + +inline trait A[T]: + def foo: Trait[T] + +inline trait B extends A[Int]: + override def foo = new Trait[Int] {} + +@main def Test = + val b = new B() {} + val f = b.foo + f.do_something() From a86be36427dc322165904e52c613bd35532c1e7f Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 17 Apr 2026 19:48:49 +0200 Subject: [PATCH 115/254] Block cyclic inline trait inlining --- .../src/dotty/tools/dotc/inlines/Inlines.scala | 18 +++++++++++------- tests/neg/inline-trait-anonymous-class.scala | 7 ------- ...ine-trait-self-inline-anonymous-class.scala | 5 +++++ .../inline-trait-self-inline-three-cycle.scala | 17 +++++++++++++++++ .../inline-trait-self-inline-two-cycle.scala | 11 +++++++++++ 5 files changed, 44 insertions(+), 14 deletions(-) delete mode 100644 tests/neg/inline-trait-anonymous-class.scala create mode 100644 tests/neg/inline-trait-self-inline-anonymous-class.scala create mode 100644 tests/neg/inline-trait-self-inline-three-cycle.scala create mode 100644 tests/neg/inline-trait-self-inline-two-cycle.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index ab6700198d84..e3b4030c9af0 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -296,12 +296,16 @@ object Inlines: val newDefs = inContext(ctx.withOwner(cls.symbol)) { inlineTraitAncestors(cls).foldLeft((List.empty[Tree], impl.body)){ case ((inlineDefs, childDefs), parent) => - val parentTraitInliner = InlineParentTrait(parent) - val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) - val inlinedDefs1 = inlineDefs ::: parentTraitInliner.expandDefs(overriddenSymbols) - cls.symbol.flags = updateFlagsFromInlinedParent(cls.symbol.flags, parent.symbol.flags) - - (inlinedDefs1, childDefs) + if cls.symbol.ownersIterator.contains(parent.symbol) then + // TODO: This appears at the inline trait D line rather than the line corresponding to the inlining - should we be worried ? + report.error("Inlining of inline traits looped, which will create an infinitely long program. This is not allowed.", cls.sourcePos) + (inlineDefs, childDefs) + else + val parentTraitInliner = InlineParentTrait(parent) + val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) + val inlinedDefs1 = inlineDefs ::: parentTraitInliner.expandDefs(overriddenSymbols) + cls.symbol.flags = updateFlagsFromInlinedParent(cls.symbol.flags, parent.symbol.flags) + (inlinedDefs1, childDefs) } } val newbody = newDefs._1 ::: newDefs._2 @@ -948,7 +952,7 @@ object Inlines: val symbolMap = mutable.Map[Symbol, Symbol]() // TODO make version of inlined that does not return bindings? - val rhs1 = Inlined(tpd.ref(parentSym), Nil, inlined(rhs)._2).withSpan(parent.span) + val rhs1 = Inlined(tpd.ref(parentSym).withSpan(parentSym.span), Nil, inlined(rhs)._2).withSpan(parent.span) val ttmap = TreeTypeMap(treeMap = { case tree@TypeDef(name, tmpl: Template) if Inlines.needsInlining(tree) => val newSym = tree.symbol.copy() diff --git a/tests/neg/inline-trait-anonymous-class.scala b/tests/neg/inline-trait-anonymous-class.scala deleted file mode 100644 index 508c33ae9380..000000000000 --- a/tests/neg/inline-trait-anonymous-class.scala +++ /dev/null @@ -1,7 +0,0 @@ -inline trait C[S]: - def v(x: S): S = x - def w: Unit = - val x = new C[S] {} // error: May not inline an inline trait into a class defined inside another inline trait. If you really need to do this, make the inline trait Specialized or move the class definition outside the trait. - println("w") - -class B extends C[Char] diff --git a/tests/neg/inline-trait-self-inline-anonymous-class.scala b/tests/neg/inline-trait-self-inline-anonymous-class.scala new file mode 100644 index 000000000000..784ceaab9dec --- /dev/null +++ b/tests/neg/inline-trait-self-inline-anonymous-class.scala @@ -0,0 +1,5 @@ +inline trait C[S]: + def v(x: S): S = x + def w: Unit = + val x = new C[S] {} // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. + println("w") diff --git a/tests/neg/inline-trait-self-inline-three-cycle.scala b/tests/neg/inline-trait-self-inline-three-cycle.scala new file mode 100644 index 000000000000..551333cd3abf --- /dev/null +++ b/tests/neg/inline-trait-self-inline-three-cycle.scala @@ -0,0 +1,17 @@ +inline trait C[S]: // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. + def v(x: S): S = x + def w: Unit = + val x = new D[S] {} + println("w") + +inline trait D[S]: // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. + def v(x: S): S = x + def w: Unit = + val x = new E[S] {} + println("w") + +inline trait E[S]: // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. + def v(x: S): S = x + def w: Unit = + val x = new C[S] {} + println("w") diff --git a/tests/neg/inline-trait-self-inline-two-cycle.scala b/tests/neg/inline-trait-self-inline-two-cycle.scala new file mode 100644 index 000000000000..a638da31bc01 --- /dev/null +++ b/tests/neg/inline-trait-self-inline-two-cycle.scala @@ -0,0 +1,11 @@ +inline trait C[S]: // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. + def v(x: S): S = x + def w: Unit = + val x = new D[S] {} + println("w") + +inline trait D[S]: // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. + def v(x: S): S = x + def w: Unit = + val x = new C[S] {} + println("w") From 49b972e90ab152c805564e331f115affef9476f9 Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 17 Apr 2026 19:50:26 +0200 Subject: [PATCH 116/254] Update spec --- docs/_docs/internals/inline-traits.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/_docs/internals/inline-traits.md b/docs/_docs/internals/inline-traits.md index 6c4391763d51..d73b7da1b9ed 100644 --- a/docs/_docs/internals/inline-traits.md +++ b/docs/_docs/internals/inline-traits.md @@ -94,9 +94,25 @@ class C extends B, A[Int](1): override def x = 1 override def foo = x ``` -While it is not immediately obvious why permitting inline traits to be inlined into other inline traits is useful (we could simply -inline everything into the first class/object in the hierarchy, it becomes advantageous when we bring in the `Specialized` annotation; see the accompanying document). +[!] This may be surprising compared to inline methods, as calls to inline methods from other inline methods are only inlined +when the outer inline method is inlined. While it is not immediately obvious why permitting inline traits to be inlined into other inline traits is useful (we could simply +inline everything into the first receiver which is not inline in the hierarchy), it becomes advantageous when we bring in the `Specialized` annotation; see the accompanying document. +Of course, patterns creating cycles of inlining are banned: +```scala +inline trait C[S]: + def v(x: S): S = x + def w: Unit = + val x = new D[S] {} + println("w") + +inline trait D[S]: + def v(x: S): S = x + def w: Unit = + val x = new C[S] {} + println("w") + +``` Furthermore: - References to members of inline traits accessed on inline receivers point to the inlined version, to ensure we avoid unnecessary boxing: [1] From 4e6f6805a08d5575f0278ddfc29b56367ad0ec7a Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 17 Apr 2026 20:25:52 +0200 Subject: [PATCH 117/254] Ban specialized trait not inline --- .../transform/DesugarSpecializedTraits.scala | 16 +++++++++++++++- tests/neg/specialized-trait-not-inline.scala | 3 +++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 tests/neg/specialized-trait-not-inline.scala diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index ba844fed373e..f4994b39cdea 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -38,6 +38,7 @@ import dotty.tools.dotc.transform.DesugarSpecializedTraits.isImplementationOf import dotty.tools.dotc.core.Flags.InlineTrait import dotty.tools.dotc.core.Annotations.Annotation import dotty.tools.dotc.core.Constants.Constant +import dotty.tools.dotc.util.SrcPos class DesugarSpecializedTraits extends MacroTransform: @@ -353,6 +354,19 @@ class DesugarSpecializedTraits extends MacroTransform: override def transform(tree: Tree)(using Context): Tree = tree match { // TODO: Is Package level processing really what we want? Given we are not going to output the classes somewhere else do we not really want either to deepFold the whole tree directly or do a more direct transform? case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. + + def checkType(t: Type, pos: SrcPos) = t.widen.dealias match { + case Specialization.SpecializedEvidence(_) => + report.error(s"Only inline traits and inline functions may take Specialized type parameters", pos) + case _ => + } + + tree.foreachSubTree { // TODO: This is not particularly efficient + case d@DefDef(name, paramss, tpt, preRhs) if d.symbol.isConstructor && !d.symbol.owner.is(Flags.Inline) => d.paramss.flatten.foreach(p => checkType(p.tpe, d.srcPos)) + case d@DefDef(name, paramss, tpt, preRhs) if !d.symbol.isConstructor && !d.symbol.is(Flags.Inline) => d.paramss.flatten.foreach(p => checkType(p.tpe, d.srcPos)) + case _ => + } + val (stats1, _) = transformStatements(stats, tree.span, SpecializedTraitCache(genInterfaceSymbol = newInterfaceTrait, genImplementationSymbol = newImplementationClass)) // TODO: Fix span cpy.PackageDef(pkg)(pid, stats1) } @@ -533,7 +547,7 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(usi end Specialization object Specialization: - private object SpecializedEvidence { + object SpecializedEvidence { def unapply(tpe: Type)(using Context): Option[Type] = tpe match { case AppliedType(tycon, List(tpeArg)) if tycon =:= ctx.definitions.SpecializedClass.typeRef => Some(tpeArg) case _ => None diff --git a/tests/neg/specialized-trait-not-inline.scala b/tests/neg/specialized-trait-not-inline.scala new file mode 100644 index 000000000000..a9c710115752 --- /dev/null +++ b/tests/neg/specialized-trait-not-inline.scala @@ -0,0 +1,3 @@ +//> using options -language:experimental.specializedTraits +trait Trait[T: Specialized] // error: Only inline traits and inline functions may take Specialized type parameters +def t[T: Specialized](a: T) = "Output" // error: Only inline traits and inline functions may take Specialized type parameters From 167ee4aaa6d151c8460c0dc89c3f6bbe691686bd Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 17 Apr 2026 20:58:52 +0200 Subject: [PATCH 118/254] Fix indentation --- tests/run/specialized-trait-maths.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/run/specialized-trait-maths.scala b/tests/run/specialized-trait-maths.scala index 008a2590c7ba..25ca7da8ceec 100644 --- a/tests/run/specialized-trait-maths.scala +++ b/tests/run/specialized-trait-maths.scala @@ -5,7 +5,7 @@ inline trait Foo[T: Specialized](x: T): def f(b: Foo[Int]) = 37 + b.foo - object Test: +object Test: def main(args: Array[String]): Unit = { val x = new Foo[Int](42) {} val y = f(x) From c29d4f05ad821a19c7b7c0a278985be93149cbe9 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 19 Apr 2026 13:26:14 +0200 Subject: [PATCH 119/254] Fix bugs with bridge method mapping --- .../transform/DesugarSpecializedTraits.scala | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index f4994b39cdea..3e297d79bc6b 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -39,6 +39,7 @@ import dotty.tools.dotc.core.Flags.InlineTrait import dotty.tools.dotc.core.Annotations.Annotation import dotty.tools.dotc.core.Constants.Constant import dotty.tools.dotc.util.SrcPos +import dotty.tools.dotc.core.Decorators.nestedMap class DesugarSpecializedTraits extends MacroTransform: @@ -262,7 +263,7 @@ class DesugarSpecializedTraits extends MacroTransform: case dd@DefDef(name, paramss, tpt, preRhs) => val transformedDef = super.transform(dd).asInstanceOf[DefDef] - if transformedDef.symbol.info != mapType(transformedDef.symbol.info) then + if transformedDef.symbol.info != mapType(transformedDef.symbol.info) && transformedDef.symbol.allOverriddenSymbols.nonEmpty then val specializedSymbol = newSymbol( transformedDef.symbol.owner, transformedDef.symbol.name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX, @@ -301,29 +302,33 @@ class DesugarSpecializedTraits extends MacroTransform: // However, specialized trait is based on the invariant that ∀T. T <: Foo[Int] => T <: Foo$sp$Int (and note that the reverse <= holds trivially by inheritance). // This means it is safe to build bridge methods which simply apply the relevant casts so that we satisfy the interface, although we don't expect to call these. def isMapped(t: Type) = mapType(t) != t - + + val mappedbody = impl.body.map(transform(_)) val bridgeMethods = impl.body.collect { - // TODO: Probably can just do this isMapped on the whole type. - case ddef@DefDef(name, paramss, _, _) if ddef.symbol.allOverriddenSymbols.nonEmpty && (ddef.termParamss.exists(params => params.exists(p => isMapped(p.symbol.info))) || isMapped(ddef.symbol.localReturnType)) => - val rhsFun: List[List[Tree]] => Tree = - newParamss => - This(impl.symbol.owner.asClass).select(ddef.symbol) + case ddef@DefDef(name, paramss, _, _) if ddef.symbol.allOverriddenSymbols.nonEmpty && isMapped(ddef.symbol.info) => + // Any callers of the original method will have been redirected to the bridge method because it has a signature match with the method they were calling + val x = cpy.DefDef(ddef)( + rhs= + This(impl.symbol.owner.asClass).select(ddef.symbol.name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX) .appliedToArgss( - newParamss.map( - params => params.map(p => p.cast(mapType(p.symbol.info))) + ddef.termParamss.map( + params => params.map(p => + ref(p.symbol).cast(mapType(p.symbol.info))) ) - ).cast(ddef.symbol.localReturnType) - // Any callers of the original method will have been redirected to the bridge method because it has a signature match with the method they were calling - DefDef(ddef.symbol.asTerm, rhsFun) - - case vdef: ValDef if isMapped(vdef.symbol.info) => + ).cast(mapType(ddef.symbol.localReturnType)) + ) + x.symbol.rawParamss = x.paramss.nestedMap(_.symbol) + x + + case vdef: ValDef if vdef.symbol.allOverriddenSymbols.nonEmpty && isMapped(vdef.symbol.info) => + println("VDEF") + println(vdef.symbol) vdef.symbol.flags = vdef.symbol.flags &~ Flags.Override vdef.symbol.setTargetName(vdef.name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX) val bridgeSym = vdef.symbol.copy().entered ctx.inlineTraitState.registerInlinedSymbol(bridgeSym, vdef.symbol, impl.symbol.owner.thisType.widenDealias) ValDef(bridgeSym.asTerm, This(impl.symbol.owner.asClass).select(vdef.symbol).cast(vdef.symbol.info)) } - val mappedbody = impl.body.map(transform(_)) cpy.Template(impl)(body = mappedbody ::: bridgeMethods) case tree => super.transform(tree) @@ -377,8 +382,6 @@ class DesugarSpecializedTraits extends MacroTransform: // TODO: Try with just generating new Foo(100) with no function to pass it to and no other references to Foo. this may not work because we might not // correctly detect it. - // TODO : Is it not better to just delete the Specialized? - private def collectReferencedSpecializations(stats: List[Tree], specializations: SpecializedTraitCache)(using Context): SpecializedTraitCache = stats.foldLeft(specializations)((specializations, tree) => { tree.deepFold(specializations)((specializations, tree) => tree match From 7956e29489a63a4b6572103149d95f936517b176 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 19 Apr 2026 14:07:20 +0200 Subject: [PATCH 120/254] Fix bug with self-inlining of inline traits --- .../dotty/tools/dotc/inlines/Inlines.scala | 2 +- ...ses-implementation-required-loop-bad.scala | 16 +++++++ ...ining-causes-implementation-required.scala | 43 +++++++++++++++++-- 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 tests/neg/specialized-trait-inlining-causes-implementation-required-loop-bad.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index e3b4030c9af0..ca763fb2636a 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -296,7 +296,7 @@ object Inlines: val newDefs = inContext(ctx.withOwner(cls.symbol)) { inlineTraitAncestors(cls).foldLeft((List.empty[Tree], impl.body)){ case ((inlineDefs, childDefs), parent) => - if cls.symbol.ownersIterator.contains(parent.symbol) then + if cls.symbol.ownersIterator.contains(symbolFromParent(parent)) then // TODO: This appears at the inline trait D line rather than the line corresponding to the inlining - should we be worried ? report.error("Inlining of inline traits looped, which will create an infinitely long program. This is not allowed.", cls.sourcePos) (inlineDefs, childDefs) diff --git a/tests/neg/specialized-trait-inlining-causes-implementation-required-loop-bad.scala b/tests/neg/specialized-trait-inlining-causes-implementation-required-loop-bad.scala new file mode 100644 index 000000000000..17f20331557c --- /dev/null +++ b/tests/neg/specialized-trait-inlining-causes-implementation-required-loop-bad.scala @@ -0,0 +1,16 @@ +//> using options -language:experimental.specializedTraits + +// A[Char] => A$sp$Char => C$sp$Char, but only once we inline the body of C$sp$Char do we realise that we need +// C$impl$Char as well. + +inline trait C[S: Specialized]: + def v(x: S): S = x + def w: Unit = + val x = new C[S] {} // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. + println("w") + +inline trait A[T: Specialized]: + def x(y: C[T]): Unit = println("x") + +def main = + val y = new A[Char] {} diff --git a/tests/pos/specialized-trait-inlining-causes-implementation-required.scala b/tests/pos/specialized-trait-inlining-causes-implementation-required.scala index b2833f1084c5..01859563a2a3 100644 --- a/tests/pos/specialized-trait-inlining-causes-implementation-required.scala +++ b/tests/pos/specialized-trait-inlining-causes-implementation-required.scala @@ -1,16 +1,53 @@ //> using options -language:experimental.specializedTraits -// A[Char] => A$sp$Char => C$sp$Char, but only once we inline the body of C$sp$Char do we realise that we need -// C$impl$Char as well. +// A[Char] => A$sp$Char, A$impl$Char => C$sp$Char, but only once we inline the body of C$sp$Char do we realise that we need +// B$impl$Char as well. + +// TODO: However, arguably we don't actually need B$impl$Char + +inline trait B[E: Specialized] inline trait C[S: Specialized]: def v(x: S): S = x def w: Unit = - val x = new C[S] {} + val x = new B[S] {} println("w") inline trait A[T: Specialized]: def x(y: C[T]): Unit = println("x") +def foo(x: B[Char]) = "A" + def main = val y = new A[Char] {} + +/* +inline trait B[E: Specialized] + +inline trait C[S: Specialized]: + def v(x: S): S + def w: Unit + +inline trait A[T: Specialized]: + def x(y: C[T]): Unit + +inline trait A$sp$Char: + def x(y: C$sp$Char): Unit + +class A$impl$Char: + def x(y: C$sp$Char): Unit = println("x") + +inline trait B$sp$Char + +class B$impl$Char + +inline trait C$sp$Char: + def v(x: Char): Char + def w: Unit + +def foo(x: B$sp$Char) = "A" + +def main = + val y = new A$impl$Char() {} + +*/ From 93188f31df1cdac8e2a4fbe30b810bff2693c320 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 19 Apr 2026 17:24:05 +0200 Subject: [PATCH 121/254] Add specialized trait as val --- tests/run/specialized-trait-as-val.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/run/specialized-trait-as-val.scala diff --git a/tests/run/specialized-trait-as-val.scala b/tests/run/specialized-trait-as-val.scala new file mode 100644 index 000000000000..dcbc06aa2585 --- /dev/null +++ b/tests/run/specialized-trait-as-val.scala @@ -0,0 +1,10 @@ +//> using options -language:experimental.specializedTraits +inline trait Trait[T: Specialized]: + def do_something() = println("Good morning") + +inline trait A[T: Specialized]: + val t = new Trait[T] {} + +@main def Test = + val b = new A[Int]() {} + b.t.do_something() From 02735d7dcb96d81b035cc5aa4b725851ac6dea5b Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 19 Apr 2026 17:25:15 +0200 Subject: [PATCH 122/254] Fix symbol mapping owner chain --- .../src/dotty/tools/dotc/inlines/Inlines.scala | 9 +++++++-- ...al-def-inner-class-inner-class-owners.scala | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests/pos/inline-trait-val-def-inner-class-inner-class-owners.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index ca763fb2636a..aae616af7371 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -969,7 +969,9 @@ object Inlines: val childSyms = tree.symbol.info.decls .filter(sym => tmpl.body.exists(vddef => vddef.symbol == sym)) - .map(_.copy(owner = newSym)) + .tapEach(sym => symbolMap(sym) = sym.copy(owner = newSym)) + .map(symbolMap) + childSyms.foreach(p => p.entered) newConstructorSymbol.entered @@ -988,7 +990,10 @@ object Inlines: }) val rhs2 = ttmap(rhs1) - TreeTypeMap(substFrom = symbolMap.keys.toList, substTo = symbolMap.values.toList)(rhs2) + TreeTypeMap(substFrom = symbolMap.keys.toList, + substTo = symbolMap.values.toList, + oldOwners = symbolMap.keys.toList, + newOwners = symbolMap.values.toList)(rhs2) private class ParamAccessorsMapper: private val paramAccessorsTrees: mutable.Map[Symbol, Map[Name, Tree]] = mutable.Map.empty diff --git a/tests/pos/inline-trait-val-def-inner-class-inner-class-owners.scala b/tests/pos/inline-trait-val-def-inner-class-inner-class-owners.scala new file mode 100644 index 000000000000..91362304819f --- /dev/null +++ b/tests/pos/inline-trait-val-def-inner-class-inner-class-owners.scala @@ -0,0 +1,18 @@ +inline trait Trait[T]: + def do_something() = println("Good morning") + +trait Trait2: + def bar() = println("bar") + +inline trait A[T]: + def foo: Trait[T] + +inline trait B extends A[Int]: + override def foo = new Trait[Int] { + val x = new Trait2() {} + } + +@main def Test = + val b = new B() {} + val f = b.foo + f.do_something() From a816ba16493cb09f2fc8468341527a3e7845ab28 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 19 Apr 2026 17:26:44 +0200 Subject: [PATCH 123/254] Fix var name --- .../tools/dotc/transform/DesugarSpecializedTraits.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 3e297d79bc6b..78b758b32b0b 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -307,7 +307,7 @@ class DesugarSpecializedTraits extends MacroTransform: val bridgeMethods = impl.body.collect { case ddef@DefDef(name, paramss, _, _) if ddef.symbol.allOverriddenSymbols.nonEmpty && isMapped(ddef.symbol.info) => // Any callers of the original method will have been redirected to the bridge method because it has a signature match with the method they were calling - val x = cpy.DefDef(ddef)( + val ddef2 = cpy.DefDef(ddef)( rhs= This(impl.symbol.owner.asClass).select(ddef.symbol.name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX) .appliedToArgss( @@ -315,10 +315,10 @@ class DesugarSpecializedTraits extends MacroTransform: params => params.map(p => ref(p.symbol).cast(mapType(p.symbol.info))) ) - ).cast(mapType(ddef.symbol.localReturnType)) + ).cast(ddef.symbol.localReturnType) ) - x.symbol.rawParamss = x.paramss.nestedMap(_.symbol) - x + ddef2.symbol.rawParamss = ddef2.paramss.nestedMap(_.symbol) + ddef2 case vdef: ValDef if vdef.symbol.allOverriddenSymbols.nonEmpty && isMapped(vdef.symbol.info) => println("VDEF") From 79f02c590aa75eee58428f93c04e84e472fd9efe Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 19 Apr 2026 17:28:07 +0200 Subject: [PATCH 124/254] Fix specialized trait as member val --- .../transform/DesugarSpecializedTraits.scala | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 78b758b32b0b..0f0c645eefe8 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -258,8 +258,25 @@ class DesugarSpecializedTraits extends MacroTransform: case tree => tree } + // TODO: Do we acvtually need to worry about these cases if we have enough limitations? new TreeTypeMap(typeMap, treeMap) { override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? + case vd@ValDef(name, tpt, preRhs) => + val transformedDef = super.transform(vd).asInstanceOf[ValDef] + if transformedDef.symbol.info != mapType(transformedDef.symbol.info) && transformedDef.symbol.allOverriddenSymbols.nonEmpty then + val specializedSymbol = newSymbol( + transformedDef.symbol.owner, + transformedDef.symbol.name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX, + transformedDef.symbol.flags &~ Flags.Override, + info = mapType(transformedDef.symbol.info), + transformedDef.symbol.privateWithin, + transformedDef.symbol.coord, + transformedDef.symbol.nestingLevel + ).entered + ValDef(specializedSymbol.asTerm, transformedDef.rhs.changeOwner(transformedDef.symbol, specializedSymbol)) + else + transformedDef + case dd@DefDef(name, paramss, tpt, preRhs) => val transformedDef = super.transform(dd).asInstanceOf[DefDef] @@ -321,13 +338,9 @@ class DesugarSpecializedTraits extends MacroTransform: ddef2 case vdef: ValDef if vdef.symbol.allOverriddenSymbols.nonEmpty && isMapped(vdef.symbol.info) => - println("VDEF") - println(vdef.symbol) - vdef.symbol.flags = vdef.symbol.flags &~ Flags.Override - vdef.symbol.setTargetName(vdef.name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX) - val bridgeSym = vdef.symbol.copy().entered - ctx.inlineTraitState.registerInlinedSymbol(bridgeSym, vdef.symbol, impl.symbol.owner.thisType.widenDealias) - ValDef(bridgeSym.asTerm, This(impl.symbol.owner.asClass).select(vdef.symbol).cast(vdef.symbol.info)) + cpy.ValDef(vdef)( + rhs = This(impl.symbol.owner.asClass).select(vdef.symbol.name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX).cast(vdef.symbol.info) + ) } cpy.Template(impl)(body = mappedbody ::: bridgeMethods) From 29061f5905f8e1568a1f983cdd65dfd92d69baa3 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 19 Apr 2026 18:23:16 +0200 Subject: [PATCH 125/254] Add and fix inline trait val def inner class inner class owners init --- .../src/dotty/tools/dotc/inlines/Inlines.scala | 11 ++++++++++- ...def-inner-class-inner-class-owners-init.scala | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 tests/pos/inline-trait-val-def-inner-class-inner-class-owners-init.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index aae616af7371..e516d0d21c89 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -903,7 +903,10 @@ object Inlines: // TODO: We might only need to do this to evidence params but tbh I can't see much harm in applying it when we want to? if (rhs.tpe.exists && !vdef.symbol.isMutableVar) // we can't narrow vars because e.g. var current = 0 would be narrowed to type 0 but someone may letter set i inlinedSym.info = rhs.tpe - tpd.ValDef(inlinedSym.asTerm, rhs).withSpan(parent.span) + + val rhs1 = rhs.changeNonLocalOwners(inlinedSym) // if rhs.symbol.exists then rhs.changeOwner(rhs.symbol.owner, inlinedSym) else rhs + + tpd.ValDef(inlinedSym.asTerm, rhs1).withSpan(parent.span) private def inlinedDefDef(ddef: DefDef, inlinedSym: Symbol)(using Context): DefDef = val rhsFun: List[List[Tree]] => Tree = @@ -953,6 +956,10 @@ object Inlines: val symbolMap = mutable.Map[Symbol, Symbol]() // TODO make version of inlined that does not return bindings? val rhs1 = Inlined(tpd.ref(parentSym).withSpan(parentSym.span), Nil, inlined(rhs)._2).withSpan(parent.span) + + // In case of nested inline trait inlines, because BodyAnnotation is out of date, + // body inlined misses nested expansion, but we have the symbols for the items that should be there + // Remove them so that they can be inlined prperly later. val ttmap = TreeTypeMap(treeMap = { case tree@TypeDef(name, tmpl: Template) if Inlines.needsInlining(tree) => val newSym = tree.symbol.copy() @@ -967,6 +974,8 @@ object Inlines: newConstructorSymbol.info = resultType(newConstructorSymbol.info) newConstructorSymbol.info = PolyType.fromParams(newConstructorSymbol.owner.typeParams, newConstructorSymbol.info) + symbolMap(tree.symbol.primaryConstructor) = newConstructorSymbol + val childSyms = tree.symbol.info.decls .filter(sym => tmpl.body.exists(vddef => vddef.symbol == sym)) .tapEach(sym => symbolMap(sym) = sym.copy(owner = newSym)) diff --git a/tests/pos/inline-trait-val-def-inner-class-inner-class-owners-init.scala b/tests/pos/inline-trait-val-def-inner-class-inner-class-owners-init.scala new file mode 100644 index 000000000000..3296df058706 --- /dev/null +++ b/tests/pos/inline-trait-val-def-inner-class-inner-class-owners-init.scala @@ -0,0 +1,16 @@ +inline trait Trait[T](x: Any): + def do_something() = println("Good morning") + +trait Trait2: + def bar() = println("bar") + +inline trait A[T]: + def foo: Trait[T] + +inline trait B extends A[Int]: + override def foo = new Trait[Int](new Trait2() {}) {} + +@main def Test = + val b = new B() {} + val f = b.foo + f.do_something() From f29456913ba51ce15c58de352dbe8ffb694bc258 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 19 Apr 2026 18:37:32 +0200 Subject: [PATCH 126/254] Add and fix specialized-trait-anonymous-class-as-param --- .../tools/dotc/transform/DesugarSpecializedTraits.scala | 4 ++-- .../pos/specialized-trait-anonymous-class-as-param.scala | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 tests/pos/specialized-trait-anonymous-class-as-param.scala diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 0f0c645eefe8..3c93c2ce9012 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -224,14 +224,14 @@ class DesugarSpecializedTraits extends MacroTransform: def treeMap(tree: Tree): Tree = tree match { // Replace (anonymous class version of) new Foo[Int] {} with new Foo$impl$Int.asInstanceOf[Foo$sp$Int] - case Block(List(TypeDef(anon, Template(_, parentCalls: List[Tree], _, _))), + case Block(List(an@TypeDef(anon, Template(_, parentCalls: List[Tree], _, _))), Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => parentCalls match { case _ :+ Apply(Apply(tpe, ctorArgs), ev) => // extends Object, parents of spec trait, spec trait val spec = Specialization.unapply(t.tpe).get { // We don't replace non-specialized anonymous class instantiations e.g. new Foo[T] where T is defined in the enclosing scope. for (specializedSymbol <- specializations.getImplementationSymbol(spec)) - yield Typed(Apply(Apply(Select(New(ref(specializedSymbol)),ctor), ctorArgs), ev), t) + yield Typed(Apply(Apply(Select(New(ref(specializedSymbol)),ctor), ctorArgs.map(_.changeNonLocalOwners(an.symbol.owner))), ev), t) }.getOrElse(tree) case _ => tree } diff --git a/tests/pos/specialized-trait-anonymous-class-as-param.scala b/tests/pos/specialized-trait-anonymous-class-as-param.scala new file mode 100644 index 000000000000..c0f7ce1ea8bc --- /dev/null +++ b/tests/pos/specialized-trait-anonymous-class-as-param.scala @@ -0,0 +1,8 @@ +//> using options -language:experimental.specializedTraits +inline trait Trait[T: Specialized](x: Any): + def do_something() = println("Good morning") + +trait Trait2 + +@main def Test = + val b = new Trait(new Trait2() {}) {} From 5f885279da133a6ef7438ad37bb203e53ded508a Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 20 Apr 2026 16:24:41 +0200 Subject: [PATCH 127/254] Add comment --- tests/run/inline-trait-nested.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/run/inline-trait-nested.scala b/tests/run/inline-trait-nested.scala index ca223265385a..a3727e9aeb2a 100644 --- a/tests/run/inline-trait-nested.scala +++ b/tests/run/inline-trait-nested.scala @@ -1,3 +1,6 @@ +// While we don't allow inner nested classes inside inline traits, we do allow creation of anonymous classes inside methods +// inside inline traits - after all these are just ordinary methods. + inline trait Trait[T]: def do_something() = println("Good morning") From 71791b7e707d312ed553e97738901ea0330a8b9b Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 20 Apr 2026 17:18:45 +0200 Subject: [PATCH 128/254] Add old inline trait test --- tests/pos/inline-trait-parent-method-call.scala | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tests/pos/inline-trait-parent-method-call.scala diff --git a/tests/pos/inline-trait-parent-method-call.scala b/tests/pos/inline-trait-parent-method-call.scala new file mode 100644 index 000000000000..ad519a55ebfe --- /dev/null +++ b/tests/pos/inline-trait-parent-method-call.scala @@ -0,0 +1,5 @@ +inline trait A: + def generate(x: Int) = x + 1 + +class B extends A: + val y = generate(7) From e115e18c35131e6cfa773d4315eb2475217d866a Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 20 Apr 2026 17:19:37 +0200 Subject: [PATCH 129/254] Add more inline-trait edge case tests --- ...ine-trait-extends-trait-with-parameters.scala | 8 ++++++++ ...ine-trait-inter-trait-inlining-no-cycle.scala | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/pos/inline-trait-extends-trait-with-parameters.scala create mode 100644 tests/pos/inline-trait-inter-trait-inlining-no-cycle.scala diff --git a/tests/pos/inline-trait-extends-trait-with-parameters.scala b/tests/pos/inline-trait-extends-trait-with-parameters.scala new file mode 100644 index 000000000000..63ea6b9f0b3b --- /dev/null +++ b/tests/pos/inline-trait-extends-trait-with-parameters.scala @@ -0,0 +1,8 @@ +trait A(val z: Int): + def foo: Int + +inline trait B(x: Int) extends A: + val y = x + z + override def foo = 10 + +class C extends B(15), A(10) diff --git a/tests/pos/inline-trait-inter-trait-inlining-no-cycle.scala b/tests/pos/inline-trait-inter-trait-inlining-no-cycle.scala new file mode 100644 index 000000000000..31da912767e7 --- /dev/null +++ b/tests/pos/inline-trait-inter-trait-inlining-no-cycle.scala @@ -0,0 +1,16 @@ +inline trait F: + def i = println("HELLO WORLD") + +inline trait E: + def h = + new F() {} + +inline trait D: + def g = + new E() {} + +inline trait C: + def f = + new D() {} + +class MyClass extends C, D, E, F From df27cddea369356d4bf855d9fc5fe5187b0c1005 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 20 Apr 2026 17:27:09 +0200 Subject: [PATCH 130/254] Add var param clash cases --- .../neg/inline-trait-clash-var-param-method.scala | 7 +++++++ .../neg/inline-trait-clash-var-param-param.scala | 15 +++++++++++++++ .../inline-trait-clash-var-param-var-param.scala | 8 ++++++++ .../run/inline-trait-clash-method-var-param.scala | 9 +++++++++ .../run/inline-trait-clash-param-var-param.scala | 11 +++++++++++ 5 files changed, 50 insertions(+) create mode 100644 tests/neg/inline-trait-clash-var-param-method.scala create mode 100644 tests/neg/inline-trait-clash-var-param-param.scala create mode 100644 tests/neg/inline-trait-clash-var-param-var-param.scala create mode 100644 tests/run/inline-trait-clash-method-var-param.scala create mode 100644 tests/run/inline-trait-clash-param-var-param.scala diff --git a/tests/neg/inline-trait-clash-var-param-method.scala b/tests/neg/inline-trait-clash-var-param-method.scala new file mode 100644 index 000000000000..25b4d73d697c --- /dev/null +++ b/tests/neg/inline-trait-clash-var-param-method.scala @@ -0,0 +1,7 @@ +inline trait A(var x: Int) + +class C extends A(10): + def x = 1000 // error: Needs override marker + +class D extends A(10): + override def x = 1000 // error: cannot override a mutable value diff --git a/tests/neg/inline-trait-clash-var-param-param.scala b/tests/neg/inline-trait-clash-var-param-param.scala new file mode 100644 index 000000000000..3147ebfeb368 --- /dev/null +++ b/tests/neg/inline-trait-clash-var-param-param.scala @@ -0,0 +1,15 @@ +// Not allowed due to name clash + +inline trait A(var x: Int): + val y = x + +class C(x: Int) extends A(10): // error: Inlining of inline trait created name conflict on x. Constructor parameters of inline receivers may not collide with members of inline traits. + val z = x + +@main def Test = + val v = C(5) + assert(v.y == 10) + assert(v.x == 10) + assert(v.z == 5) + println(v.y) + println(v.x) diff --git a/tests/neg/inline-trait-clash-var-param-var-param.scala b/tests/neg/inline-trait-clash-var-param-var-param.scala new file mode 100644 index 000000000000..64173b7321a6 --- /dev/null +++ b/tests/neg/inline-trait-clash-var-param-var-param.scala @@ -0,0 +1,8 @@ +inline trait A(val x: Int, var y: Int) +class C(override var y: Int) extends A(10, 54): // error: Cannot override a mutable variable + override val x = 1000 + +@main def Test = + val v = C(44) + assert(v.x == 1000) + assert(v.y == 44) diff --git a/tests/run/inline-trait-clash-method-var-param.scala b/tests/run/inline-trait-clash-method-var-param.scala new file mode 100644 index 000000000000..c1788f2a40c4 --- /dev/null +++ b/tests/run/inline-trait-clash-method-var-param.scala @@ -0,0 +1,9 @@ +inline trait A: + def x = "Hello world" + +class C extends A: + var x = "Overridden" + +@main def Test = + val v = C() + assert(v.x == "Overridden") diff --git a/tests/run/inline-trait-clash-param-var-param.scala b/tests/run/inline-trait-clash-param-var-param.scala new file mode 100644 index 000000000000..c2514fb686d0 --- /dev/null +++ b/tests/run/inline-trait-clash-param-var-param.scala @@ -0,0 +1,11 @@ +// Allowed. We rename A.x, A.y to avoid a clash. + +inline trait A(x: Int, y: Int) + +class C(var x: Int) extends A(10, 100): + val y: Int = 10 + +@main def Test = + val v = C(5) + assert(v.x == 5) + assert(v.y == 10) From 117e94d0a00b255b76d191e171505da6c25307d3 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 20 Apr 2026 17:48:46 +0200 Subject: [PATCH 131/254] Add specialized-trait-requires-inline-trait-inlining.scala --- ...cialized-trait-requires-inline-trait-inlining.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/run/specialized-trait-requires-inline-trait-inlining.scala diff --git a/tests/run/specialized-trait-requires-inline-trait-inlining.scala b/tests/run/specialized-trait-requires-inline-trait-inlining.scala new file mode 100644 index 000000000000..e7ca14b09744 --- /dev/null +++ b/tests/run/specialized-trait-requires-inline-trait-inlining.scala @@ -0,0 +1,10 @@ +inline trait T1[T]: + def boo(x: T): T = x + +inline trait T[T: Specialized]: + def ff = new T1[T]() { + def id(x: T): T = x + } + +def main = + val a = new T[Int]() {} From 58c76d48bc4d062f7727a50251f79c04abe523b0 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 21 Apr 2026 11:34:24 +0200 Subject: [PATCH 132/254] Add inline trait opaque type test case --- tests/pos/inline-trait-opaque-type.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/pos/inline-trait-opaque-type.scala diff --git a/tests/pos/inline-trait-opaque-type.scala b/tests/pos/inline-trait-opaque-type.scala new file mode 100644 index 000000000000..f6a5ed11f410 --- /dev/null +++ b/tests/pos/inline-trait-opaque-type.scala @@ -0,0 +1,11 @@ +inline trait A: + opaque type Special = Int + inline val b = 10 + def x: Special = b + +class B extends A + +def foo = + val b = B() + println(b.x) + \ No newline at end of file From ce320bf7c0f17e18e7936535c26c86ff69aefc39 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 21 Apr 2026 11:35:15 +0200 Subject: [PATCH 133/254] Allow inlining during desugarSpecializedTraits --- compiler/src/dotty/tools/dotc/core/Phases.scala | 1 + compiler/src/dotty/tools/dotc/inlines/Inlines.scala | 1 + 2 files changed, 2 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index 0d4628276c98..2aca93bd1cdf 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -577,6 +577,7 @@ object Phases { def sbtExtractAPIPhase(using Context): Phase = ctx.base.sbtExtractAPIPhase def picklerPhase(using Context): Phase = ctx.base.picklerPhase def specializeInlineTraitsPhase(using Context): Phase = ctx.base.specializeInlineTraitsPhase + def desugarSpecializedTraitsPhase(using Context): Phase = ctx.base.desugarSpecializedTraitsPhase def inliningPhase(using Context): Phase = ctx.base.inliningPhase def stagingPhase(using Context): Phase = ctx.base.stagingPhase def splicingPhase(using Context): Phase = ctx.base.splicingPhase diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index e516d0d21c89..038f99bd23a6 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -89,6 +89,7 @@ object Inlines: ctx.phase == Phases.inliningPhase || (ctx.phase == Phases.typerPhase && needsTransparentInlining(tree)) || (ctx.phase == Phases.specializeInlineTraitsPhase && !tree.symbol.is(Macro)) + || (ctx.phase == Phases.desugarSpecializedTraitsPhase && !tree.symbol.is(Macro)) ) && !ctx.typer.hasInliningErrors && !ctx.base.stopInlining From 6589807b6d17c150e5d83263564b3d35099c1640 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 21 Apr 2026 11:37:58 +0200 Subject: [PATCH 134/254] Add drops Specialized qualifier case --- tests/neg/specialized-trait-drops-specialized-qualifier.scala | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 tests/neg/specialized-trait-drops-specialized-qualifier.scala diff --git a/tests/neg/specialized-trait-drops-specialized-qualifier.scala b/tests/neg/specialized-trait-drops-specialized-qualifier.scala new file mode 100644 index 000000000000..3d88201b474f --- /dev/null +++ b/tests/neg/specialized-trait-drops-specialized-qualifier.scala @@ -0,0 +1,3 @@ +//> using options -language:experimental.specializedTraits +inline trait T1[T: Specialized] +inline trait T2[S] extends T1[S] // error: S must be Specialized as it substitutes for T: Specialized in inline trait T1 From ca1fc6d1c4f0490d20b322566c75c973fe8a731c Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 21 Apr 2026 17:26:37 +0200 Subject: [PATCH 135/254] Add specialized-trait-requires-inline-trait-inlining.scala --- .../specialized-trait-requires-inline-trait-inlining.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/run/specialized-trait-requires-inline-trait-inlining.scala b/tests/run/specialized-trait-requires-inline-trait-inlining.scala index e7ca14b09744..2f5532f93e7c 100644 --- a/tests/run/specialized-trait-requires-inline-trait-inlining.scala +++ b/tests/run/specialized-trait-requires-inline-trait-inlining.scala @@ -1,3 +1,5 @@ +//> using options -language:experimental.specializedTraits + inline trait T1[T]: def boo(x: T): T = x @@ -6,5 +8,5 @@ inline trait T[T: Specialized]: def id(x: T): T = x } -def main = +@main def Test = val a = new T[Int]() {} From d9275d23193432ca1fa79d93f0ed6b99439fca91 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 21 Apr 2026 17:35:08 +0200 Subject: [PATCH 136/254] Add more tests --- ...ed-trait-function-takes-specialized-trait.scala | 14 ++++++++++++++ .../specialized-trait-inline-def-specialized.scala | 10 ++++++++++ ...-complete-specialization-with-return-type.scala | 14 ++++++++++++++ .../specialized-trait-partial-specialization.scala | 7 +++++++ tests/pos/specialized-trait-val-parameter.scala | 6 ++++++ tests/run/specialized-trait-as-parameter.scala | 10 ++++++++++ tests/run/specialized-trait-as-return-type.scala | 14 ++++++++++++++ 7 files changed, 75 insertions(+) create mode 100644 tests/pos/specialized-trait-function-takes-specialized-trait.scala create mode 100644 tests/pos/specialized-trait-inline-def-specialized.scala create mode 100644 tests/pos/specialized-trait-partial-complete-specialization-with-return-type.scala create mode 100644 tests/pos/specialized-trait-partial-specialization.scala create mode 100644 tests/pos/specialized-trait-val-parameter.scala create mode 100644 tests/run/specialized-trait-as-parameter.scala create mode 100644 tests/run/specialized-trait-as-return-type.scala diff --git a/tests/pos/specialized-trait-function-takes-specialized-trait.scala b/tests/pos/specialized-trait-function-takes-specialized-trait.scala new file mode 100644 index 000000000000..6cf930cf0134 --- /dev/null +++ b/tests/pos/specialized-trait-function-takes-specialized-trait.scala @@ -0,0 +1,14 @@ +//> using options -language:experimental.specializedTraits + +inline trait Container[T: Specialized](val elem: T) + +def apply(f: Container[Int] => Unit, v: Container[Int]) = + f(v) + +def f(e: Container[Int]) = + println(e.elem) + +@main def Test = + val v = new Container(10) {} + apply(f, v) + apply((e: Container[Int]) => println(e), v) diff --git a/tests/pos/specialized-trait-inline-def-specialized.scala b/tests/pos/specialized-trait-inline-def-specialized.scala new file mode 100644 index 000000000000..a14b230f98bb --- /dev/null +++ b/tests/pos/specialized-trait-inline-def-specialized.scala @@ -0,0 +1,10 @@ +//> using options -language:experimental.specializedTraits + +inline trait Vec[T: Specialized](val x: T) + +// inline def foo[T: Specialized](v: Vec[T]) = +// v.x + +@main def Test = + val v = new Vec[Int](10) {} + // println(foo(v)) diff --git a/tests/pos/specialized-trait-partial-complete-specialization-with-return-type.scala b/tests/pos/specialized-trait-partial-complete-specialization-with-return-type.scala new file mode 100644 index 000000000000..f2d1d71c051b --- /dev/null +++ b/tests/pos/specialized-trait-partial-complete-specialization-with-return-type.scala @@ -0,0 +1,14 @@ +//> using options -language:experimental.specializedTraits +inline trait Trait[T: Specialized]: + def do_something() = println("Good morning") + +inline trait A[T: Specialized]: + def foo: Trait[T] + +inline trait B extends A[Int]: + override def foo = new Trait[Int] {} + +@main def Test = + val b = new B() {} + val f = b.foo + f.do_something() diff --git a/tests/pos/specialized-trait-partial-specialization.scala b/tests/pos/specialized-trait-partial-specialization.scala new file mode 100644 index 000000000000..7faa5eaa345a --- /dev/null +++ b/tests/pos/specialized-trait-partial-specialization.scala @@ -0,0 +1,7 @@ +//> using options -language:experimental.specializedTraits + +inline trait Map[K: Specialized, V: Specialized] +inline trait MapFromInt[V: Specialized] extends Map[Int, V] +inline trait MapToInt[K: Specialized] extends Map[K, Int] +inline trait MapIntInt1 extends MapFromInt[Int] +inline trait MapIntInt2 extends MapToInt[Int] diff --git a/tests/pos/specialized-trait-val-parameter.scala b/tests/pos/specialized-trait-val-parameter.scala new file mode 100644 index 000000000000..403561ec0794 --- /dev/null +++ b/tests/pos/specialized-trait-val-parameter.scala @@ -0,0 +1,6 @@ +//> using options -language:experimental.specializedTraits + +inline trait Vec[T: Specialized](val x: T) + +def main = + val v = new Vec[Int](10) {} diff --git a/tests/run/specialized-trait-as-parameter.scala b/tests/run/specialized-trait-as-parameter.scala new file mode 100644 index 000000000000..ac5eb130a89b --- /dev/null +++ b/tests/run/specialized-trait-as-parameter.scala @@ -0,0 +1,10 @@ +//> using options -language:experimental.specializedTraits +inline trait Trait[T: Specialized]: + def do_something() = println("Good morning") + +inline trait A[T: Specialized]: + def foo(x: Trait[T]) = x + +@main def Test = + val b = new A[Long]() {} + b.foo(new Trait[Long] {}).do_something() diff --git a/tests/run/specialized-trait-as-return-type.scala b/tests/run/specialized-trait-as-return-type.scala new file mode 100644 index 000000000000..2307b766eb0b --- /dev/null +++ b/tests/run/specialized-trait-as-return-type.scala @@ -0,0 +1,14 @@ +//> using options -language:experimental.specializedTraits +inline trait Trait[T: Specialized]: + def do_something() = println("Good morning") + +inline trait A[T: Specialized]: + def foo: Trait[T] + +inline trait B[S: Specialized] extends A[S]: + override def foo = new Trait[S] {} + +@main def Test = + val b = new B[Int]() {} + val f = b.foo + f.do_something() From 8c36495473464d27dd43cbb58fce8fef9bfbd137 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 21 Apr 2026 17:35:54 +0200 Subject: [PATCH 137/254] Fix specialized trait with val param --- .../tools/dotc/transform/DesugarSpecializedTraits.scala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 3c93c2ce9012..0d061bd0dd52 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -40,6 +40,8 @@ import dotty.tools.dotc.core.Annotations.Annotation import dotty.tools.dotc.core.Constants.Constant import dotty.tools.dotc.util.SrcPos import dotty.tools.dotc.core.Decorators.nestedMap +import dotty.tools.dotc.core.NameOps.expandedName + class DesugarSpecializedTraits extends MacroTransform: @@ -165,8 +167,10 @@ class DesugarSpecializedTraits extends MacroTransform: } val nonTypeParams = specialization.traitSymbol.primaryConstructor.rawParamss.tail - val valueParams = nonTypeParams.map(_.map(param => param.copy(owner = init, info = tm(param.info)))) // .map(_.filterNot(isSyntheticEvidence) - + + // We need to map the parameter names to avoid a name clash with val params from parents (see tests/pos/specialized-trait-val-parameter.scala) + val valueParams = nonTypeParams.map(_.map(param => param.copy(owner = init, info = tm(param.info), name=param.name.expandedName(classSymbol)))) // .map(_.filterNot(isSyntheticEvidence) + init.setParamss(valueParams) val paramAccessorss = valueParams.map(params => params.map(_.copy(owner = classSymbol, flags= Flags.LocalParamAccessor))) From 69b1822ed6648b3d63067c6ec0e49dc544feb2aa Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 21 Apr 2026 17:36:36 +0200 Subject: [PATCH 138/254] Add specialized-trait-var-parameters --- tests/pos/specialized-trait-var-parameter.scala | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tests/pos/specialized-trait-var-parameter.scala diff --git a/tests/pos/specialized-trait-var-parameter.scala b/tests/pos/specialized-trait-var-parameter.scala new file mode 100644 index 000000000000..31207dd8d58e --- /dev/null +++ b/tests/pos/specialized-trait-var-parameter.scala @@ -0,0 +1,6 @@ +//> using options -language:experimental.specializedTraits + +inline trait Vec[T: Specialized](var x: T) + +def main = + val v = new Vec[Int](10) {} From 1c2538265d9cf72361d839e1ade916ccb31825aa Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 21 Apr 2026 17:37:31 +0200 Subject: [PATCH 139/254] Fix symbol -> denot --- .../dotty/tools/dotc/transform/DesugarSpecializedTraits.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 0d061bd0dd52..9a00669d47cd 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -256,8 +256,7 @@ class DesugarSpecializedTraits extends MacroTransform: yield AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? }.getOrElse(tree) - // Select a method which is specialized; we need to make sure we call the specialized version - case sel@Select(qualifier, name) if typeMap(sel.symbol.info) != sel.symbol.info => + case sel@Select(qualifier, name) if typeMap(sel.denot.info) != sel.denot.info => Select(qualifier, name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX) case tree => tree } From 32588577996eda1f30a8050c1c107da4429b6b64 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 21 Apr 2026 20:45:24 +0200 Subject: [PATCH 140/254] Switch to deleting Vec parent --- .../src/dotty/tools/dotc/core/StdNames.scala | 2 +- .../transform/DesugarSpecializedTraits.scala | 80 ++++++++++++++++--- 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 001d111d03bb..8f0679a79f97 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -30,7 +30,7 @@ object StdNames { inline val SPECIALIZED_TRAIT_SUFFIX = "$sp$" inline val SPECIALIZED_TRAIT_IMPL_SUFFIX = "$impl$" inline val SPECIALIZED_TRAIT_TYPE_SEP = "$" - inline val SPECIALIZED_METHOD_TARGET_NAME_SUFFIX = "$spec" + /* inline val SPECIALIZED_METHOD_TARGET_NAME_SUFFIX = "$spec" */ inline val REPL_SESSION_LINE = "rs$line$" inline val REPL_ASSIGN_SUFFIX = "$assign" diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 9a00669d47cd..ac661aa94afa 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -68,7 +68,7 @@ class DesugarSpecializedTraits extends MacroTransform: // See ArrayIterator extends Iterator in specialized-trait-collections-example.scala val specializations1 = inheritedParents.foldLeft(specializations)((specializations, parent) => parent match { - case Specialization(spec) if spec.isSpecialized => (specializations.addInterface(spec)) + case Specialization(spec) if spec.isSpecialized => specializations.addInterface(spec) case _ => specializations } ) @@ -256,15 +256,28 @@ class DesugarSpecializedTraits extends MacroTransform: yield AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? }.getOrElse(tree) - case sel@Select(qualifier, name) if typeMap(sel.denot.info) != sel.denot.info => - Select(qualifier, name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX) + /* case sel@Select(qualifier, name) if typeMap(sel.denot.info) != sel.denot.info => + Select(qualifier, name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX) */ case tree => tree } // TODO: Do we acvtually need to worry about these cases if we have enough limitations? new TreeTypeMap(typeMap, treeMap) { override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? - case vd@ValDef(name, tpt, preRhs) => + case dd@DefDef(name, paramss, tpt, preRhs) => + val transformedDef = super.transform(dd) + transformedDef.symbol.info = mapType(transformedDef.symbol.info) + if transformedDef.symbol.allOverriddenSymbols.isEmpty then + transformedDef.symbol.flags = transformedDef.symbol.flags &~ Flags.Override + transformedDef + + case vd@ValDef(name, tpt, preRhs) => + val transformedDef = super.transform(vd) + transformedDef.symbol.info = mapType(transformedDef.symbol.info) + if transformedDef.symbol.allOverriddenSymbols.isEmpty then + transformedDef.symbol.flags = transformedDef.symbol.flags &~ Flags.Override + transformedDef + /*case vd@ValDef(name, tpt, preRhs) => val transformedDef = super.transform(vd).asInstanceOf[ValDef] if transformedDef.symbol.info != mapType(transformedDef.symbol.info) && transformedDef.symbol.allOverriddenSymbols.nonEmpty then val specializedSymbol = newSymbol( @@ -301,7 +314,7 @@ class DesugarSpecializedTraits extends MacroTransform: DefDef(specializedSymbol.asTerm, rhsFun) else - transformedDef + transformedDef*/ case impl@Template(constr, preParentsOrDerived, self, _) => impl.parents.foreach(p => @@ -317,13 +330,16 @@ class DesugarSpecializedTraits extends MacroTransform: } ) + /* // If a class has a specialized member which was overriding a parent member, this override is lost because we specialize the types. // E.g. def foo(Vec$sp$Int) cannot override def foo(Vec[Int]) because signatures must match exactly for overriding. // However, specialized trait is based on the invariant that ∀T. T <: Foo[Int] => T <: Foo$sp$Int (and note that the reverse <= holds trivially by inheritance). // This means it is safe to build bridge methods which simply apply the relevant casts so that we satisfy the interface, although we don't expect to call these. def isMapped(t: Type) = mapType(t) != t - + */ val mappedbody = impl.body.map(transform(_)) + + /* val bridgeMethods = impl.body.collect { case ddef@DefDef(name, paramss, _, _) if ddef.symbol.allOverriddenSymbols.nonEmpty && isMapped(ddef.symbol.info) => // Any callers of the original method will have been redirected to the bridge method because it has a signature match with the method they were calling @@ -344,9 +360,15 @@ class DesugarSpecializedTraits extends MacroTransform: cpy.ValDef(vdef)( rhs = This(impl.symbol.owner.asClass).select(vdef.symbol.name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX).cast(vdef.symbol.info) ) - } - - cpy.Template(impl)(body = mappedbody ::: bridgeMethods) + } */ + + /* We need to map parents of non-specialized inline traits (see tests/pos/specialized-trait-partial-complete-specialization-with-return-type.scala, we need + to map the A[Int] reference to A$sp$Int in B's parents) */ + val mappedparents = impl.parents.map(transform(_)) + val oldInfo = impl.symbol.owner.info.asInstanceOf[ClassInfo] + impl.symbol.owner.info = oldInfo.derivedClassInfo(declaredParents = oldInfo.declaredParents.map(mapType(_))) + + cpy.Template(impl)(body = mappedbody, parents = mappedparents) case tree => super.transform(tree) } } @@ -360,10 +382,44 @@ class DesugarSpecializedTraits extends MacroTransform: val specializations2 = specializations1.installNewInterfaceSymbols.installNewImplementationSymbols + // We have Vec$sp$Int extends Vec[Int] in order to do the inlining, but then remove this parent + // afterwards to avoid interface implementation problems (see tests/run/specialized-trait-as-parameter.scala, + // tests/run/specialized-trait-as-return-type.scala) + extension (classTree: Tree) + def updateParents(parentUpdater: List[Type] => List[Type]) = (classTree: @unchecked) match { + case td@TypeDef(name, t@Template(constr, preParentsOrDerived, self, preBody)) => + td.symbol.info = td.symbol.info match { + case ci: ClassInfo => ci.derivedClassInfo(declaredParents=parentUpdater(ci.declaredParents)) + } + ClassDef(td.symbol.asClass, constr, t.body) + } + // TODO: How do we calculate the spans correctly? - val generatedTraitStats1 = generatedTraitStats.map(trtDef => Inlines.inlineParentInlineTraits(Inlines.transformInlineTrait(trtDef.withSpan(span)))) - val generatedClassStats1 = generatedClassStats.map(clsDef => Inlines.inlineParentInlineTraits(clsDef.withSpan(span))) + val ttmap = new TreeTypeMap(treeMap = { + case tree: TypeDef if tree.symbol.isInlineTrait => + val tree1 = Inlines.transformInlineTrait(tree) + val tree2 = if Inlines.needsInlining(tree1) then Inlines.inlineParentInlineTraits(tree1) else tree1 + tree2 + case tree: TypeDef if Inlines.needsInlining(tree) => + Inlines.inlineParentInlineTraits(tree) + case t => t + }) + // Why does it cause no denotation to happen? + val generatedTraitStats1 = generatedTraitStats.map(trtDef => /*Inlines.inlineParentInlineTraits(Inlines.transformInlineTrait(*/ttmap(trtDef.withSpan(span))/*))*/).map: + _.updateParents { parents => (parents: @unchecked) match + case obj :: original :: parents => obj :: parents + } + + val generatedClassStats1 = generatedClassStats.map(clsDef => /*Inlines.inlineParentInlineTraits(*/ttmap(clsDef.withSpan(span))/*)*/).map: + _.updateParents { parents => (parents: @unchecked) match + case obj :: traitSp :: originalSpec :: Nil => obj :: traitSp :: Nil + } + + /* We need to inline recursively throughout generated specialized traits - see tests/run/specialized-trait-requires-inline-trait-inlining.scala */ + // val generatedTraitStats1 = generatedTraitStats1a.map(ttmap(_)) + // val generatedClassStats1 = generatedClassStats1a.map(ttmap(_)) + if (generatedTraitStats1.isEmpty && generatedClassStats1.isEmpty) (stats.map(replaceSpecializedSymbolsMap(specializations2)(_)), specializations2) else @@ -568,7 +624,7 @@ end Specialization object Specialization: object SpecializedEvidence { def unapply(tpe: Type)(using Context): Option[Type] = tpe match { - case AppliedType(tycon, List(tpeArg)) if tycon =:= ctx.definitions.SpecializedClass.typeRef => Some(tpeArg) + case AppliedType(tycon, List(tpeArg)) if (tycon =:= ctx.definitions.SpecializedClass.typeRef && tpeArg.typeSymbol.isTypeParam) => Some(tpeArg) case _ => None } } From 6c369ce2f26b0554c55b32bfe29d2f415955e021 Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 22 Apr 2026 10:52:24 +0200 Subject: [PATCH 141/254] Ban Vec[?] when Specialized --- .../transform/DesugarSpecializedTraits.scala | 7 +++++++ ...zed-trait-question-mark-in-inheritance.scala | 6 ++++++ ...cialized-trait-question-mark-interface.scala | 17 +++++++++++++++++ tests/neg/specialized-trait-question-mark.scala | 5 +++++ ...-question-mark-non-specialized-allowed.scala | 8 ++++++++ ...lized-trait-subtyping-once-specialized.scala | 9 +++++++++ 6 files changed, 52 insertions(+) create mode 100644 tests/neg/specialized-trait-question-mark-in-inheritance.scala create mode 100644 tests/neg/specialized-trait-question-mark-interface.scala create mode 100644 tests/neg/specialized-trait-question-mark.scala create mode 100644 tests/pos/specialized-trait-question-mark-non-specialized-allowed.scala create mode 100644 tests/run/specialized-trait-subtyping-once-specialized.scala diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index ac661aa94afa..139ee30a9b66 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -463,6 +463,13 @@ class DesugarSpecializedTraits extends MacroTransform: case _ => specializations } case Specialization(spec) if (spec.isSpecialized) => + val problematicArguments = spec.specializedTypeArgs.filter { + case t: TypeBoundsTree => true + case _ => false + } + if problematicArguments.nonEmpty then + problematicArguments.foreach: tr => + report.error("Wildcard types may not be substituted for Specialized type parameters.", tr.srcPos) specializations.addInterface(spec) case _ => specializations ) diff --git a/tests/neg/specialized-trait-question-mark-in-inheritance.scala b/tests/neg/specialized-trait-question-mark-in-inheritance.scala new file mode 100644 index 000000000000..a72f21e5c7c9 --- /dev/null +++ b/tests/neg/specialized-trait-question-mark-in-inheritance.scala @@ -0,0 +1,6 @@ +//> using options -language:experimental.specializedTraits +inline trait T1[T: Specialized] +inline trait T2[T: Specialized] extends T1[?] // error: Wildcard types may not be substituted for Specialized type parameters. + +inline trait T3[T: Specialized, E, F: Numeric] +inline trait T4 extends T3[?, ?, ?] // error: Wildcard types may not be substituted for Specialized type parameters. diff --git a/tests/neg/specialized-trait-question-mark-interface.scala b/tests/neg/specialized-trait-question-mark-interface.scala new file mode 100644 index 000000000000..4df161f111e7 --- /dev/null +++ b/tests/neg/specialized-trait-question-mark-interface.scala @@ -0,0 +1,17 @@ +//> using options -language:experimental.specializedTraits +inline trait Trait[S: Specialized] + +def foo(x: Trait[?], y: Trait[? <: Long]) = // error: Wildcard types may not be substituted for Specialized type parameters. // error: Wildcard types may not be substituted for Specialized type parameters. + println("HELLO WORLD") + +def bar(x: Trait[_]) = // error: Wildcard types may not be substituted for Specialized type parameters. + println("HELLO WORLD") + +def baz(x: Trait[? >: Long]) = // error: Wildcard types may not be substituted for Specialized type parameters. + println("HELLO WORLD") + +def foo2(x: Trait[? >: Long <: Long]) = // error: Wildcard types may not be substituted for Specialized type parameters. + println("HELLO WORLD") + +def main = + foo(new Trait[Int]() {}, new Trait[Long]() {}) diff --git a/tests/neg/specialized-trait-question-mark.scala b/tests/neg/specialized-trait-question-mark.scala new file mode 100644 index 000000000000..67d2594249b9 --- /dev/null +++ b/tests/neg/specialized-trait-question-mark.scala @@ -0,0 +1,5 @@ +//> using options -language:experimental.specializedTraits +inline trait Trait[S: Specialized] + +def main = + val x = new Trait[?] {} // error: Type argument must be fully defined diff --git a/tests/pos/specialized-trait-question-mark-non-specialized-allowed.scala b/tests/pos/specialized-trait-question-mark-non-specialized-allowed.scala new file mode 100644 index 000000000000..74aadecea4e4 --- /dev/null +++ b/tests/pos/specialized-trait-question-mark-non-specialized-allowed.scala @@ -0,0 +1,8 @@ +//> using options -language:experimental.specializedTraits +inline trait T3[T: Specialized, E, F: Numeric] +def foo(v: T3[Int, ?, ?]) = + println("HELLO") + +def main = + val x = new T3[Int, String, Float] {} + foo(x) diff --git a/tests/run/specialized-trait-subtyping-once-specialized.scala b/tests/run/specialized-trait-subtyping-once-specialized.scala new file mode 100644 index 000000000000..9ae5fbd742fa --- /dev/null +++ b/tests/run/specialized-trait-subtyping-once-specialized.scala @@ -0,0 +1,9 @@ +//> using options -language:experimental.specializedTraits +inline trait T1[T: Specialized] +inline trait T2[T: Specialized] extends T1[T] + +def foo(x: T1[Int]) = println("foo") + +@main def Test = + val x = new T2[Int]() {} + foo(x) From b6be5277edb2350a0b09fa5492a3880e91423124 Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 22 Apr 2026 11:06:00 +0200 Subject: [PATCH 142/254] Move comments --- .../tools/dotc/transform/DesugarSpecializedTraits.scala | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 139ee30a9b66..d3e0086eb79f 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -392,8 +392,9 @@ class DesugarSpecializedTraits extends MacroTransform: case ci: ClassInfo => ci.derivedClassInfo(declaredParents=parentUpdater(ci.declaredParents)) } ClassDef(td.symbol.asClass, constr, t.body) - } + } + /* We need to inline recursively throughout generated specialized traits - see tests/run/specialized-trait-requires-inline-trait-inlining.scala */ // TODO: How do we calculate the spans correctly? val ttmap = new TreeTypeMap(treeMap = { case tree: TypeDef if tree.symbol.isInlineTrait => @@ -416,10 +417,6 @@ class DesugarSpecializedTraits extends MacroTransform: case obj :: traitSp :: originalSpec :: Nil => obj :: traitSp :: Nil } - /* We need to inline recursively throughout generated specialized traits - see tests/run/specialized-trait-requires-inline-trait-inlining.scala */ - // val generatedTraitStats1 = generatedTraitStats1a.map(ttmap(_)) - // val generatedClassStats1 = generatedClassStats1a.map(ttmap(_)) - if (generatedTraitStats1.isEmpty && generatedClassStats1.isEmpty) (stats.map(replaceSpecializedSymbolsMap(specializations2)(_)), specializations2) else From 1793a95efbf2212953e9880a7aaa3e8cada8d436 Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 22 Apr 2026 13:31:13 +0200 Subject: [PATCH 143/254] Explain removal of parent types --- docs/_docs/internals/specialized-traits.md | 57 ++++++++++++++-------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/docs/_docs/internals/specialized-traits.md b/docs/_docs/internals/specialized-traits.md index fbdb3e349701..befb727a1f47 100644 --- a/docs/_docs/internals/specialized-traits.md +++ b/docs/_docs/internals/specialized-traits.md @@ -179,7 +179,34 @@ After inlining and pruning of inlined definitions, the `$sp$` trait becomes the inline trait Vec$sp$Int extends Vec[Int]: def length: Int def apply(x: Int): Int - def scalarProduct(other: Vec[T]): Int + def scalarProduct(other: Vec$sp$Int): Int +``` + +The unspecialized parent types (`Vec[Int]`) are then removed after inlining. This is necessary to avoid interface conflicts with the specialized +members. In particular, in the above example `def scalarProduct(other: Vec$sp$Int)` does not implement `def scalarProduct(other: Vec[T])` as defined +in `Vec[T]`. This is not a problem because all instances of `Vec[Int]` are replaced by `Vec$sp$Int` in the whole program, and `Vec[?]` is banned. Therefore, +there are no situations where a `Vec$sp$Int` may be passed as an argument to a parameter of type `Vec[Int]`. This gives the final result: + +```scala +inline trait Vec$sp$Int extends Vec[Int]: + def length: Int + def apply(x: Int): Int + def scalarProduct(other: Vec$sp$Int): Int +``` +The same transformation is applied to the `$impl$` classes for the same reason: +```scala +class Vec$impl$Int(elems: Array[Int])(using Numeric[Int]) extends Vec$sp$Int: + private val Vec$$num: Numeric.IntIsIntegral + + def length: Int = elems.length + def apply(i: Int): Int = elems(i) + + def scalarProduct(other: Vec$sp$Int): Int = + require(this.length == other.length) + var result = Vec$$num.fromInt(0) + for i <- 0 until length do + result = Vec$$num.plus(result, Vec$$num.times(this(i), other(i))) + result ``` ## Specialized Traits in the Compiler @@ -271,40 +298,39 @@ inline trait Seq[T: Specialized](elems: Array[T]) extends Iterable[T]: def iterator: Iterator[T] = new ArrayIterator[T](elems) {} ``` -This generates the following instance traits (after inlining and conversion to pure interfaces): +This generates the following instance traits (after inlining, conversion to pure interfaces and parent removal): ```scala -inline trait Iterator$sp$Int extends Iterator[Int]: +inline trait Iterator$sp$Int: def hasNext: Boolean def next(): Int -inline trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator$sp$Int +inline trait ArrayIterator$sp$Int extends Iterator$sp$Int -inline trait Iterable$sp$Int extends Iterable[Int]: +inline trait Iterable$sp$Int: def iterator: Iterator$sp$Int def forall(f: Int => Unit): Unit -inline trait Seq$sp$Int extends Seq[Int], Iterable$sp$Int: +inline trait Seq$sp$Int extends Iterable$sp$Int: def length: Int def apply(i: Int): Int ``` -Note that these traits repeat the parent types of their corresponding inline traits (but with specialization added). For instance, `ArrayIterator$sp$Int` extends `ArrayIterator[Int]` (as we would expect) *as well as* the specialized version of its parent `Iterator$sp$Int`, so the specialized trait may be used in contexts expecting: +Note that these traits repeat the parent types of their corresponding inline traits (but with specialization added). For instance, `ArrayIterator$sp$Int` extends the specialized version of its parent `Iterator$sp$Int`, so the specialized trait may be used in contexts expecting: -- The specialized trait `ArrayIterator$sp$Int` itself -- An unspecialized `ArrayIterator` for example `ArrayIterator[T: Numeric]`, or parents thereof, e.g. `Iterator[T: Numeric]` +- The specialized trait `ArrayIterator$sp$Int` itself (i.e. `ArrayIterator[Int]` in source code) - Specialized traits higher in the specialized hierarchy for example `Iterator$sp$Int`. The specialized implementation classes for `ArrayIterator` and `Seq` are as follows (after inlining; iff `new Seq[Int] {}` and `ArrayIterator[Int] {}` are to be found in the program): ```scala -class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator[Int](elems): +class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int: private var current = 0 override def hasNext: Boolean = current < elems.length override def next(): Int = try elems(current) finally current += 1 -class Seq$impl$Int(elems: Array[Int]) extends Seq$sp$Int, Seq[Int](elems): +class Seq$impl$Int(elems: Array[Int]) extends Seq$sp$Int: override def iterator: Iterator$sp$Int = new ArrayIterator$impl$Int(elems) override def forall(f: Int => Unit): Unit = @@ -314,20 +340,13 @@ class Seq$impl$Int(elems: Array[Int]) extends Seq$sp$Int, Seq[Int](elems): override def apply(i: Int): Int = elems(i) ``` -Given that `ArrayIterator$sp$Int` extends `ArrayIterator[Int]`, directly extending `ArrayIterator[Int]` in `ArrayIterator$impl$Int` -may seem redundant. However, it is necessary in order to pass `elems` to `ArrayIterator[Int]`. -Traits are not allowed to pass parameters to each other, meaning we could not indirectly pass `elems` via `ArrayIterator$sp$Int`, -and furthermore we cannot simply leave out the parameter passing (on the basis that we only use the inlined `elems`) and thus -hope to avoid extending `ArrayIterator[Int]` directly, because extending `ArrayIterator$sp$Int` implies indirectly extending -`ArrayIterator[Int]` which is not allowed unless `ArrayIterator[Int]` is also mixed in directly to pass parameters (due to the rules of -trait parameter passing in Scala 3). - ## Summary of restrictions on specialized traits | Behaviour | Is currently supported in... | |--------------------------|-----------------------------------------------| | Inheriting from specialized traits | In inline traits or anonymous class instances (for instance creation) only | | Taking `Specialized` parameters| Only by inline traits | +| Use of `?` bounds | May not be used for Specialized parameters; however may be used for non-Specialized parameters in specialized traits. | From 7395c721b51edadf36a18b2d4892695d06895c7b Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 22 Apr 2026 13:53:25 +0200 Subject: [PATCH 144/254] Check foo[S: Specialized] <= Vec[S: Specialized] --- .../transform/DesugarSpecializedTraits.scala | 45 +++++++++++++------ docs/_docs/internals/specialized-traits.md | 43 ++++++++++++++++++ ...ed-trait-drops-specialized-qualifier.scala | 6 ++- ...specialized-trait-nested-specialized.scala | 20 +++++++++ 4 files changed, 100 insertions(+), 14 deletions(-) create mode 100644 tests/neg/specialized-trait-nested-specialized.scala diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index d3e0086eb79f..1a5431d185ae 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -459,15 +459,34 @@ class DesugarSpecializedTraits extends MacroTransform: case Specialization(spec) if spec.isSpecialized => specializations.addInterfaceAndImplementation(spec) case _ => specializations } - case Specialization(spec) if (spec.isSpecialized) => - val problematicArguments = spec.specializedTypeArgs.filter { - case t: TypeBoundsTree => true - case _ => false + case Specialization(spec) => + if (spec.isSpecialized) { + // Block Vec[?] and similar + spec.specializedTypeArgs.filter { + case t: TypeBoundsTree => true + case _ => false + }.foreach: tr => + report.error("Wildcard types may not be substituted for Specialized type parameters.", tr.srcPos) + + specializations.addInterface(spec) + } else { + // Check foo[S: Specialized] <= Vec[S: Specialized] + spec.specializedTypeArgs.flatMap(arg => { // For each type we are using in a Specialized position + arg.tpe.widen.dealias.namedPartsWith(part => // Find all type params within that type that are not marked as Specialized so we can error + part.typeSymbol.isTypeParam && + (!(if part.typeSymbol.owner.isClass then part.typeSymbol.owner.primaryConstructor else part.typeSymbol.owner).paramSymss.flatten.exists( + d => d.info match { + case Specialization.SpecializedEvidence(tpeArg) => + tpeArg.typeSymbol.isTypeParam && tpeArg.typeSymbol.name == part.name + case _ => false + } + )) + ) + }).foreach: tr => + if tr.denot.symbol.srcPos.span.exists then + report.error(s"${tr.typeSymbol} used in a Specialized position, so it must be marked as Specialized at its definition.", tr.denot.symbol.srcPos) + specializations } - if problematicArguments.nonEmpty then - problematicArguments.foreach: tr => - report.error("Wildcard types may not be substituted for Specialized type parameters.", tr.srcPos) - specializations.addInterface(spec) case _ => specializations ) }) @@ -589,14 +608,14 @@ end SpecializedTraitCache /* Represents an application traitSymbol[typeArguments] */ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(using Context): // TODO: Can we get away with List[Type] - val specializedTypeParams: List[Type] = Specialization.classSpecializedTypeParams(traitSymbol) + val specializedTypeParams: List[Type] = Specialization.classSpecializedTypeParams(traitSymbol) // Type parameters marked with Specialized private val specializedTypeParamsSet = specializedTypeParams.toSet private val paramToArgList = traitSymbol.typeParams.map(_.typeRef.asInstanceOf[Type]).zip(typeArguments) - val unspecializedTypeParams: List[Type] = paramToArgList.filterNot((tParam, tArg) => specializedTypeParamsSet(tParam)).map(_._1) - val specializedTypeArgs: List[Tree] = paramToArgList.filter((tParam, tArg) => specializedTypeParamsSet(tParam)).map(_._2) - val unspecializedTypeArgs: List[Tree] = paramToArgList.filterNot((tParam, tArg) => specializedTypeParamsSet(tParam)).map(_._2) + val unspecializedTypeParams: List[Type] = paramToArgList.filterNot((tParam, tArg) => specializedTypeParamsSet(tParam)).map(_._1) // Type parameters not marked with Specialized + val specializedTypeArgs: List[Tree] = paramToArgList.filter((tParam, tArg) => specializedTypeParamsSet(tParam)).map(_._2) // Type arguments provided to parameters that are marked with Specialized at their definition + val unspecializedTypeArgs: List[Tree] = paramToArgList.filterNot((tParam, tArg) => specializedTypeParamsSet(tParam)).map(_._2) // Type arguments provided to parameters that are not marked with Specialized at their definition val specializedTypeParamsToTypeArgumentsMap: Map[Type, Tree] = paramToArgList.toMap.filter((k, v) => specializedTypeParamsSet(k)) val specialization: List[Tree] = traitSymbol.typeParams.map(_.typeRef).map(specializedTypeParamsToTypeArgumentsMap.applyOrElse(_, TypeTree(_))) // TODO: Don't really like this name @@ -611,7 +630,7 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(usi // If inline trait Foo[T] has a method taking another Foo[T] there's no point specializing the reference // since the resulting sp$T$ would be the same as the starting trait. def isSpecialized: Boolean = - hasSpecializedParams && typeArguments.exists(tpt => !tpt.symbol.isTypeParam) // .zip(traitSymbol.typeParams).forall((t, s) => t.tpe =:= s.typeRef)) + hasSpecializedParams && typeArguments.exists(!_.tpe.existsPart(_.typeSymbol.isTypeParam)) //) !tpt.symbol.isTypeParam) // .zip(traitSymbol.typeParams).forall((t, s) => t.tpe =:= s.typeRef)) // Note: We only care about the specialized arguments for equality; a specialization of Vec[A: Specialized, B] with B = Int and one // with B = String can be considered to be the same as they use the same specialized trait diff --git a/docs/_docs/internals/specialized-traits.md b/docs/_docs/internals/specialized-traits.md index befb727a1f47..daf945c315d2 100644 --- a/docs/_docs/internals/specialized-traits.md +++ b/docs/_docs/internals/specialized-traits.md @@ -349,6 +349,49 @@ class Seq$impl$Int(elems: Array[Int]) extends Seq$sp$Int: | Use of `?` bounds | May not be used for Specialized parameters; however may be used for non-Specialized parameters in specialized traits. | +## Transportation of Specialized through generic code +It may surprise you to note that the following is valid scala. The `Numeric` constraint on `T` is only checked when +a concrete type is provided for `S` (and by extension `T`) when instantiating `T2`. +```scala +inline trait T1[T: Numeric] +inline trait T2[S] extends T1[S] +``` +In constrast, we do not allow this type of behaviour for `Specialized`. This is largely to avoid confusion. In particular: +```scala +inline trait T1[T: Specialized] +inline trait T2[S] extends T1[S] +val x = new T2[Int]() {} +``` +Should `x` be a Specialized `$impl$` instance or a normal anonymous class? If we naively look at just `S`'s definition we would say no, +but this is complicated by the fact that the generated anonymous class will also mixin `T1` directly as well as `T2`. Could we specialize +just for `T1`? This is hard to imagine. Furthermore if we don't specialize, this is also counter intuitive because T1 is then never specialized +to Int, because we have no specialized instance of trait `T2` to specialize into. Therefore we require users to explicitly transport `Specialized` +through their code, in the following way: + +```scala +inline trait T1[T: Specialized] + +inline trait T2 extends T1[List[Int]] // ok +inline trait T3[S] extends T1[List[S]] // error: S should be specialized +inline trait T4[S] extends T1[List[List[S]]] // error: S should be specialized +inline trait T5[S: Specialized] extends T1[List[S]] // ok; should only specialize later +inline trait T6[T[_], S] extends T1[T[S]] // error: T should be specialized // error: S should be specialized +inline trait T7[T[_]] extends T1[T[Int]] // error: T should be specialized + +inline def foo1[S](x: T1[List[S]]): Int = 10 // error: S should be specialized +inline def foo2(x: T1[List[Int]]): Int = 10 // ok +inline def foo3[S](x: T1[List[List[S]]]): Int = 10 // error: S should be specialized +inline def foo4[S: Specialized](x: T1[List[List[S]]]): Int = 10 // ok + +inline def bar1[S] = new T1[List[S]]() {} // error: S should be specialized +inline def bar2 = new T1[List[Int]]() {} // ok +inline def bar3[S] = new T1[List[List[S]]]() {} // error: S should be specialized +inline def bar4[S: Specialized] = new T1[List[List[S]]]() {} // ok +``` +Note that in the cases with `List` this restriction is not imposed because of the definition of `List`, but rather simply because we have a +type variable `S` which is not marked as Specialized which appears `somewhere inside' a type in a Specialized position. Again this is done to +avoid confusion. A user extending `T1[List[S]]` would likely expect some degree of specialization to given the definition of `T1`, but this +is not possible if `S` is not marked as `Specialized`. ## [1] Why are the generated traits inline? Consider the following: diff --git a/tests/neg/specialized-trait-drops-specialized-qualifier.scala b/tests/neg/specialized-trait-drops-specialized-qualifier.scala index 3d88201b474f..e3ae8727cc90 100644 --- a/tests/neg/specialized-trait-drops-specialized-qualifier.scala +++ b/tests/neg/specialized-trait-drops-specialized-qualifier.scala @@ -1,3 +1,7 @@ //> using options -language:experimental.specializedTraits inline trait T1[T: Specialized] -inline trait T2[S] extends T1[S] // error: S must be Specialized as it substitutes for T: Specialized in inline trait T1 +inline trait T2[S] extends T1[S] // error: type S used in a Specialized position, so it must be marked as Specialized at its definition. + +inline def foo[E](x: T1[E]) = 100 // error: type E used in a Specialized position, so it must be marked as Specialized at its definition. + +inline def foo[F] = new T1[F]() {} // error: type F used in a Specialized position, so it must be marked as Specialized at its definition. diff --git a/tests/neg/specialized-trait-nested-specialized.scala b/tests/neg/specialized-trait-nested-specialized.scala new file mode 100644 index 000000000000..b764759ccaa4 --- /dev/null +++ b/tests/neg/specialized-trait-nested-specialized.scala @@ -0,0 +1,20 @@ +//> using options -language:experimental.specializedTraits +inline trait T1[T: Specialized] + +inline trait T2 extends T1[List[Int]] // ok +inline trait T3[S] extends T1[List[S]] // error: S should be specialized +inline trait T4[S] extends T1[List[List[S]]] // error: S should be specialized +inline trait T5[S: Specialized] extends T1[List[S]] // ok; should only specialize later +inline trait T6[T[_], S] extends T1[T[S]] // error: T should be specialized // error: S should be specialized +inline trait T7[T[_]] extends T1[T[Int]] // error: T should be specialized + +inline def foo1[S](x: T1[List[S]]): Int = 10 // error: S should be specialized +inline def foo2(x: T1[List[Int]]): Int = 10 // ok +inline def foo3[S](x: T1[List[List[S]]]): Int = 10 // error: S should be specialized +inline def foo4[S: Specialized](x: T1[List[List[S]]]): Int = 10 // ok + +inline def bar1[S] = new T1[List[S]]() {} // error: S should be specialized +inline def bar2 = new T1[List[Int]]() {} // ok +inline def bar3[S] = new T1[List[List[S]]]() {} // error: S should be specialized +inline def bar4[S: Specialized] = new T1[List[List[S]]]() {} // ok + From 82c10549a3869cf5ab3ad9ce27fa7e022064e8de Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 22 Apr 2026 15:02:25 +0200 Subject: [PATCH 145/254] Don't warn for anonymous class inlining when it's a Specialized trait instance --- .../dotc/transform/DesugarSpecializedTraits.scala | 11 +++++++++++ .../dotty/tools/dotc/transform/FirstTransform.scala | 5 ++++- ...zed-trait-inline-anonymous-class-defn-nowarn.scala | 5 +++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tests/pos/specialized-trait-inline-anonymous-class-defn-nowarn.scala diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 1a5431d185ae..ffc9cc06e81e 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -664,6 +664,17 @@ object Specialization: } def classSpecializedTypeParams(classSym: Symbol)(using Context): List[Type] = classSym.unforcedDecls.implicitDecls.collect(_.info match { case SpecializedEvidence(typeVar) => typeVar }) + + def anonymousClassIsSpecialized(tree: Tree)(using Context) = tree match { + case TypeDef(anon, Template(_, parentCalls: List[Tree], _, _)) => + parentCalls match { + case _ :+ Apply(Apply(t@tpe, ctorArgs), ev) => // extends Object, parents of spec trait, spec trait + val spec = Specialization.unapply(t.tpe.resultType.resultType) + spec.get.hasSpecializedParams + case _ => true + } + case _ => true + } end Specialization // Would be nice to define a Specialization class I think diff --git a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala index 81cc73c26ed6..0e0d211faf35 100644 --- a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala +++ b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala @@ -22,6 +22,7 @@ import inlines.Inlines.inInlineMethod import util.Property import inlines.Inlines import reporting.InlinedAnonClassWarning +import dotty.tools.dotc.transform.Specialization.anonymousClassIsSpecialized object FirstTransform { val name: String = "firstTransform" @@ -210,7 +211,9 @@ class FirstTransform extends MiniPhase with SymTransformer { thisPhase => } override def transformTypeDef(tree: TypeDef)(using Context): Tree = - if tree.symbol.isAnonymousClass && Inlines.inInlineMethod then + if tree.symbol.isAnonymousClass && Inlines.inInlineMethod && + !anonymousClassIsSpecialized(tree) + then report.warning(InlinedAnonClassWarning(), tree.symbol.sourcePos) tree diff --git a/tests/pos/specialized-trait-inline-anonymous-class-defn-nowarn.scala b/tests/pos/specialized-trait-inline-anonymous-class-defn-nowarn.scala new file mode 100644 index 000000000000..752941780694 --- /dev/null +++ b/tests/pos/specialized-trait-inline-anonymous-class-defn-nowarn.scala @@ -0,0 +1,5 @@ +//> using options -language:experimental.specializedTraits -Werror +inline trait T1[T: Specialized] + +inline def foo[F: Specialized] = new T1[F]() {} // By default this warns Anonymous class will be defined at each use site, which may lead to a larger number of classfiles. This is not true for Specialized traits as we share the $impl$ class instances. +inline def foo[F: Specialized, E] = new T1[F, E]() {} // This should also not warn even though E is not specialized From e52001e48cc20249b917a26ab9ca1d697176e4f9 Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 22 Apr 2026 17:19:16 +0200 Subject: [PATCH 146/254] Fix syntax error in test --- ...specialized-trait-inline-anonymous-class-defn-nowarn.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/pos/specialized-trait-inline-anonymous-class-defn-nowarn.scala b/tests/pos/specialized-trait-inline-anonymous-class-defn-nowarn.scala index 752941780694..01ecce142333 100644 --- a/tests/pos/specialized-trait-inline-anonymous-class-defn-nowarn.scala +++ b/tests/pos/specialized-trait-inline-anonymous-class-defn-nowarn.scala @@ -1,5 +1,7 @@ //> using options -language:experimental.specializedTraits -Werror inline trait T1[T: Specialized] +inline trait T2[T: Specialized, E] inline def foo[F: Specialized] = new T1[F]() {} // By default this warns Anonymous class will be defined at each use site, which may lead to a larger number of classfiles. This is not true for Specialized traits as we share the $impl$ class instances. -inline def foo[F: Specialized, E] = new T1[F, E]() {} // This should also not warn even though E is not specialized + +inline def foo[F: Specialized, E] = new T2[F, E]() {} // This should also not warn even though E is not specialized From 85db37e5f1d28f8554fe4126283d4e70f009728e Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 22 Apr 2026 17:21:34 +0200 Subject: [PATCH 147/254] Make impl classes also pass Spec type params to parent sp classes; evidences unchanged --- .../transform/DesugarSpecializedTraits.scala | 79 +++++++++++-------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index ffc9cc06e81e..868a1f069bb0 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -89,49 +89,55 @@ class DesugarSpecializedTraits extends MacroTransform: specialization.traitSymbol.compilationUnitInfo ) - // Create type parameters for new trait - val tps = newTypeParams(traitSymbol, - specialization.unspecializedTypeParams.map(_.typeSymbol.name.asTypeName), - EmptyFlags, - targets => targets.map(t => specialization.traitSymbol.typeParams.find(_.name == t.name).get.info.bounds) - ) - tps.foreach(traitSymbol.enter(_, EmptyScope)) - - - // Replace old type parameters that were copied from original trait with new ones - // inside the parents of the new trait - val tpMap: Map[Type, Type] = specialization.unspecializedTypeParams.zip(tps.map(_.typeRef)).toMap - val freshTypeVarMap = new TypeMap: - def apply(t: Type) = tpMap.applyOrElse(t, mapOver) - traitSymbol.info = ClassInfo(traitSymbol.owner.thisType, traitSymbol, traitSymbol.info.parents.map(freshTypeVarMap(_)), traitSymbol.info.decls) // TODO: What happens if the creator of the specialized inline trait provides a self type? + buildTypeParameters(traitSymbol, specialization) (traitSymbol.entered, specializations1) } private def buildInterfaceTraitTree(interfaceSymbol: ClassSymbol)(using Context) = { val init = newDefaultConstructor(interfaceSymbol) - // Fix constructor so that it: - // 1) Has correct generic type parameters - // 2) Returns the correct type corresponding to those type parameters applied to this trait - val rt = interfaceSymbol.typeRef.appliedTo(interfaceSymbol.typeParams.map(_.typeRef)) - def resultType(tpe: Type): Type = tpe match { - case mt @ MethodType(paramNames) => mt.derivedLambdaType(paramNames, mt.paramInfos, rt) - case pt : PolyType => pt.derivedLambdaType(pt.paramNames, pt.paramInfos, resultType(pt.resType)) - } - init.info = resultType(init.info) - init.info = PolyType.fromParams(init.owner.typeParams, init.info) - // TODO: Confirm that we don't need to worry about copying the evidence parameters over from the old constructor // These should be dealt with when we instantiate the original trait as a parent of this one. Otherwise we should be // able to copy them over, apply the specialization (keeping e.g. Numeric[Int] that arises from this) and // pruning any that belong to Specialized. - + fixConstructor(init, interfaceSymbol) ClassDef(interfaceSymbol, DefDef(init.entered), Nil) } + /* Fix constructor so that it: + 1) Has correct generic type parameters + 2) Returns the correct type corresponding to those type parameters applied */ + private def fixConstructor(init: Symbol, traitOrClassSymbol: ClassSymbol) = + val rt = traitOrClassSymbol.typeRef.appliedTo(traitOrClassSymbol.typeParams.map(_.typeRef)) + println(rt) + def resultType(tpe: Type): Option[Type] = tpe match { + case mt @ MethodType(paramNames) => Some(mt.derivedLambdaType(paramNames, mt.paramInfos, resultType(mt.resultType).getOrElse(rt))) + case pt : PolyType => Some(pt.derivedLambdaType(pt.paramNames, pt.paramInfos, resultType(pt.resType).get)) + case _ => None + } + init.info = resultType(init.info).get + init.info = PolyType.fromParams(init.owner.typeParams, init.info) + + private def buildTypeParameters(traitOrClassSymbol: ClassSymbol, specialization: Specialization) = + val tps = newTypeParams(traitOrClassSymbol, + specialization.unspecializedTypeParams.map(_.typeSymbol.name.asTypeName), + EmptyFlags, + targets => targets.map(t => specialization.traitSymbol.typeParams.find(_.name == t.name).get.info.bounds) + ) + tps.foreach(traitOrClassSymbol.enter(_, EmptyScope)) + + // Replace old type parameters that were copied from original trait with new ones + // inside the parents of the new trait + val tpMap: Map[Type, Type] = specialization.unspecializedTypeParams.zip(tps.map(_.typeRef)).toMap + val freshTypeVarMap = new TypeMap: + def apply(t: Type) = tpMap.applyOrElse(t, mapOver) + + // TODO: What happens if the creator of the specialized inline trait provides a self type? + traitOrClassSymbol.info = ClassInfo(traitOrClassSymbol.owner.thisType, traitOrClassSymbol, traitOrClassSymbol.info.parents.map(freshTypeVarMap(_)), traitOrClassSymbol.info.decls) + private def generateImplementationClassParents(specialization: Specialization, interfaceSymbol: ClassSymbol) = val objectParent = defn.ObjectType - val traitSpParent = interfaceSymbol.typeRef.appliedTo(specialization.unspecializedTypeArgs.map(_.tpe)) + val traitSpParent = interfaceSymbol.typeRef.appliedTo(specialization.unspecializedTypeParams) // Set using old unspecializedTypeParams and replace after. val originalTraitSpecializedParent = AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.typeArguments).tpe (objectParent, traitSpParent, originalTraitSpecializedParent) @@ -139,7 +145,7 @@ class DesugarSpecializedTraits extends MacroTransform: val (objectParent, traitSpParent, originalTraitSpecializedParent) = generateImplementationClassParents(specialization, interfaceSymbol) val parents = List(objectParent, traitSpParent, originalTraitSpecializedParent) - newNormalizedClassSymbol( + val newImplementationClassSymbol = newNormalizedClassSymbol( specialization.traitSymbol.owner, DesugarSpecializedTraits.newImplementationClassName(specialization), Flags.Synthetic, @@ -148,10 +154,15 @@ class DesugarSpecializedTraits extends MacroTransform: specialization.traitSymbol.privateWithin, specialization.traitSymbol.coord, specialization.traitSymbol.compilationUnitInfo - ).entered + ) + + buildTypeParameters(newImplementationClassSymbol, specialization) + + newImplementationClassSymbol.entered // TODO: Do we want to share some code with the newSpecializedInterfaceTrait and buildInterfaceTraitTree? // TODO: Standardise a bit so that we either generate the symbols and later the classes or not. + // TODO: Tidy this up a bit with functions private def buildImplementationClassTree(specialization: Specialization, interfaceSymbol: ClassSymbol, classSymbol: ClassSymbol)(using Context) = { val (objectParent, traitSpParent, originalTraitSpecializedParent) = generateImplementationClassParents(specialization, interfaceSymbol) val init = newDefaultConstructor(classSymbol) @@ -170,14 +181,16 @@ class DesugarSpecializedTraits extends MacroTransform: // We need to map the parameter names to avoid a name clash with val params from parents (see tests/pos/specialized-trait-val-parameter.scala) val valueParams = nonTypeParams.map(_.map(param => param.copy(owner = init, info = tm(param.info), name=param.name.expandedName(classSymbol)))) // .map(_.filterNot(isSyntheticEvidence) + val typeParams = classSymbol.typeParams.map(_.copy()) - init.setParamss(valueParams) + init.setParamss(typeParams :: valueParams) val paramAccessorss = valueParams.map(params => params.map(_.copy(owner = classSymbol, flags= Flags.LocalParamAccessor))) paramAccessorss.foreach(_.foreach(classSymbol.enter(_))) init.info = tm2(specialization.traitSymbol.primaryConstructor.info.appliedTo(specialization.typeArguments.map(_.tpe))) - + + fixConstructor(init, classSymbol) val typer = Typer(ctx.nestingLevel + 1) // TODO: actually get these from the user. val newParamss = @@ -235,7 +248,7 @@ class DesugarSpecializedTraits extends MacroTransform: val spec = Specialization.unapply(t.tpe).get { // We don't replace non-specialized anonymous class instantiations e.g. new Foo[T] where T is defined in the enclosing scope. for (specializedSymbol <- specializations.getImplementationSymbol(spec)) - yield Typed(Apply(Apply(Select(New(ref(specializedSymbol)),ctor), ctorArgs.map(_.changeNonLocalOwners(an.symbol.owner))), ev), t) + yield Typed(Apply(Apply(Select(New(ref(specializedSymbol)),ctor).appliedToTypeTrees(spec.unspecializedTypeArgs), ctorArgs.map(_.changeNonLocalOwners(an.symbol.owner))), ev), t) }.getOrElse(tree) case _ => tree } From 4dcfd6ba06fd4436bc209510876061c73868f456 Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 22 Apr 2026 17:25:08 +0200 Subject: [PATCH 148/254] Fix print and uncomment test --- .../tools/dotc/transform/DesugarSpecializedTraits.scala | 1 - tests/pos/specialized-trait-inline-def-specialized.scala | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 868a1f069bb0..b2d85352a0e9 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -109,7 +109,6 @@ class DesugarSpecializedTraits extends MacroTransform: 2) Returns the correct type corresponding to those type parameters applied */ private def fixConstructor(init: Symbol, traitOrClassSymbol: ClassSymbol) = val rt = traitOrClassSymbol.typeRef.appliedTo(traitOrClassSymbol.typeParams.map(_.typeRef)) - println(rt) def resultType(tpe: Type): Option[Type] = tpe match { case mt @ MethodType(paramNames) => Some(mt.derivedLambdaType(paramNames, mt.paramInfos, resultType(mt.resultType).getOrElse(rt))) case pt : PolyType => Some(pt.derivedLambdaType(pt.paramNames, pt.paramInfos, resultType(pt.resType).get)) diff --git a/tests/pos/specialized-trait-inline-def-specialized.scala b/tests/pos/specialized-trait-inline-def-specialized.scala index a14b230f98bb..98332c74994e 100644 --- a/tests/pos/specialized-trait-inline-def-specialized.scala +++ b/tests/pos/specialized-trait-inline-def-specialized.scala @@ -2,9 +2,8 @@ inline trait Vec[T: Specialized](val x: T) -// inline def foo[T: Specialized](v: Vec[T]) = -// v.x +inline def foo[T: Specialized](v: Vec[T]) = v.x @main def Test = val v = new Vec[Int](10) {} - // println(foo(v)) + println(foo(v)) From f6dceb49c82c8c3d02af07780af26b8884d7f5d8 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 23 Apr 2026 18:01:14 +0200 Subject: [PATCH 149/254] Fix self types and opaque types --- .../src/dotty/tools/dotc/core/Types.scala | 7 ++ .../dotty/tools/dotc/inlines/Inlines.scala | 27 +++++++- docs/_docs/internals/inline-traits.md | 66 +++++++++++++++++-- tests/neg/inline-trait-opaque-type-fail.scala | 14 ++++ ...nline-trait-self-type-missing-parent.scala | 6 ++ .../neg/inline-trait-self-type-problems.scala | 23 +++++++ tests/pos/inline-trait-opaque-type.scala | 14 ++-- tests/pos/inline-trait-self-type.scala | 26 ++++++++ 8 files changed, 167 insertions(+), 16 deletions(-) create mode 100644 tests/neg/inline-trait-opaque-type-fail.scala create mode 100644 tests/neg/inline-trait-self-type-missing-parent.scala create mode 100644 tests/neg/inline-trait-self-type-problems.scala create mode 100644 tests/pos/inline-trait-self-type.scala diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 1b83bd0bb20a..76492ea0f962 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1357,6 +1357,13 @@ object Types extends TypeUtils { case _ => this } + /* Extract annotations and opaque type laiases; removing the internal type. */ + def extractAnnotationsAndOpaqueTypeAliases(using Context): Type = this match { + case AnnotatedType(tp1, annot) => AnnotatedType(tp1.extractAnnotationsAndOpaqueTypeAliases, annot) + case RefinedType(parent, rname, TypeAlias(alias)) => RefinedType(parent.extractAnnotationsAndOpaqueTypeAliases, rname, TypeAlias(alias)) + case _ => NoType + } + /** Strip PolyType prefixes */ def stripPoly(using Context): Type = this match { case tp: PolyType => tp.resType.stripPoly diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 038f99bd23a6..070c64d9cc71 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -303,6 +303,12 @@ object Inlines: (inlineDefs, childDefs) else val parentTraitInliner = InlineParentTrait(parent) + + // Update self type + val newSelfType = cls.symbol.asClass.classDenot.givenSelfType & parentTraitInliner.inlinedSelfType.extractAnnotationsAndOpaqueTypeAliases + cls.symbol.info = cls.symbol.asClass.classInfo.derivedClassInfo(selfInfo=newSelfType) + + // Inline body val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) val inlinedDefs1 = inlineDefs ::: parentTraitInliner.expandDefs(overriddenSymbols) cls.symbol.flags = updateFlagsFromInlinedParent(cls.symbol.flags, parent.symbol.flags) @@ -316,7 +322,16 @@ object Inlines: otherstat <- newbody if !otherstat.symbol.is(ParamAccessor) && otherstat.denot.matches(pacc.denot.asSingleDenotation) do report.error(s"Inlining of inline trait created name conflict on ${pacc.denot.name}. Constructor parameters of inline receivers may not collide with members of inline traits.", pacc.srcPos) - val impl1 = cpy.Template(impl)(body = newbody) + val impl1 = cpy.Template(impl)(body = newbody, + self= + if cls.symbol.asClass.classDenot.givenSelfType.exists then + cpy.ValDef(impl.self)(tpt= + TypeTree(cls.symbol.asClass.classDenot.givenSelfType) + .withSpan(impl.self.tpt.span.orElse(cls.symbol.span))) + .withSpan(impl.self.span.orElse(cls.symbol.span)) + .cloneIn(cls.symbol.source) + else impl.self + ) cpy.TypeDef(cls)(rhs = impl1) case _ => cls @@ -775,6 +790,9 @@ object Inlines: private val childThisType = ctx.owner.thisType private val childThisTree = This(ctx.owner.asClass).withSpan(parent.span) + def inlinedSelfType = + inlinerTypeMap(parentSym.asClass.classDenot.givenSelfType) + def expandDefs(overriddenDecls: Set[Symbol]): List[Tree] = paramAccessorsMapper.registerParamValuesOf(parent) val stats = Inlines.defsToInline(parentSym).filterNot(stat => overriddenDecls.contains(stat.symbol)) @@ -943,7 +961,12 @@ object Inlines: inlined(clsDef1)._2.withSpan(clsDef.span) private def inlinedTypeDef(tdef: TypeDef, inlinedSym: Symbol)(using Context): TypeDef = - tpd.TypeDef(inlinedSym.asType).withSpan(parent.span) + val tdef2 = tpd.TypeDef(inlinedSym.asType).withSpan(parent.span) + if inlinedSym.isOpaqueAlias then + cpy.TypeDef(tdef2)(rhs=TypeTree(inlinedSym.opaqueAlias)) + else + tdef2 + private def inlinedRhs(vddef: ValOrDefDef, inlinedSym: Symbol)(using Context): Tree = val rhs = vddef.rhs.changeOwner(vddef.symbol, inlinedSym) diff --git a/docs/_docs/internals/inline-traits.md b/docs/_docs/internals/inline-traits.md index d73b7da1b9ed..bb8177fbf88f 100644 --- a/docs/_docs/internals/inline-traits.md +++ b/docs/_docs/internals/inline-traits.md @@ -308,18 +308,70 @@ This problem is addressed via `Specialized` traits; see the accompanying documen |-------------------------------------|----------------------------------------------| | Methods | ✅ | | `val` / `var` Properties | ✅ | -| Non-local private members[*] | ❌ | +| Non-local private members [3] | ❌ | | `type`s | ✅ | -| Inner classes/traits | ❌ | -| Opaque types | ❌ | -| Self types | ❌ | +| Inner classes/traits | ❌ [7] | +| Self types | ✅ [6] | | Inheritance (of inline traits) | Only allowed by classes and inline traits | -| Instantiation of inline traits [**] | ❌ | +| Instantiation of inline traits [4] | ✅ | +| Opaque types | ✅ [5] | -[*] That is, members which are labelled private and accessed from within the class on other instances of the class. +[3] That is, members which are labelled private and accessed from within the class on other instances of the class. Local private members (members with the same access patterns as the former `private[this]`) are allowed. -[**] While inline traits may not define inner classes as direct members, they may have methods which themsleves define classes. This is permitted only if the classes do not extend from an inline trait. In particular this means that methods of inline traits may not create anonymous instances of inline traits e.g. `new A() {}`. The only exception to this is if the trait being instantiated (`A` here) is `Specialized`, because the instantiation will not produce an anonymous class inside the trait (see the document on Specialized traits). +[4] As long as this doesn't create a cycle e.g.: +```scala +inline trait C[S]: // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. + def v(x: S): S = x + def w: Unit = + val x = new D[S] {} + println("w") + +inline trait D[S]: // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. + def v(x: S): S = x + def w: Unit = + val x = new C[S] {} + println("w") +``` + +[5] Supported with same behaviour as in normal traits. In particular, the following is completely fine, and will be inlined into B. +```scala +inline trait A[T](val x: T): + opaque type Special = T + + def getSpecial: Special = x + def eatSpecial(y: Special) = "Mmm, that was tasty!" + +class B extends A[Int](100) +``` + +In contrast, it is not possible to use inlining to "cheat" opaque types, even though it is tempting to try to argue that +the opaque type will be inlined into B and therefore its alias should be visible. This is not allowed because type checking +is performed before inline traits, and this follows the logic that inline traits are an optimisation on top of normal traits, +rather than a semantic change to them. +```scala +inline trait A[T](val x: T): + opaque type Special = T + +class B extends A[Int](100): + def foo: Special = 10 // error: 10 does not conform to Special +``` + +[6] +Self types are supported but are not inlined. We argue this is desirable as it ensures that the behaviour of inline traits +with self types mirrors that of ordinary traits with self types. Inlining of self types would effectively remove any restictions that +these self types seek to impose because the subclass would automatically have a matching self type to that of the parent class. This +prevents an error from being thrown, irrespective of whether the subclass implements the desired traits. +Therefore, when using self types on inline traits, the behaviour observed is the following (as in ordinary traits) e.g.: +```scala +trait A[T]: + this: T1 => + +trait D extends A[Int] // error: self type of D does not conform to that of A +``` + +[7] +While inline traits may not directly define inner classes, they may contain methods which define classes within their bodies. ## Processing of inline traits in the compiler Inline traits in user code are inlined in the phase `specializeInlineTraits`. The phase `replaceInlinedTraitSymbols` diff --git a/tests/neg/inline-trait-opaque-type-fail.scala b/tests/neg/inline-trait-opaque-type-fail.scala new file mode 100644 index 000000000000..126404829191 --- /dev/null +++ b/tests/neg/inline-trait-opaque-type-fail.scala @@ -0,0 +1,14 @@ +inline trait A[T](val x: T): + opaque type Special = T + type Ordinary = T + opaque type Special2 = Int + + def foo1: Special = 10 // error: 10 does not conform to T + def bar1: Ordinary = 10 // error: 10 does not conform to T + def baz1: Special2 = 10 // This one is fine + +class B extends A[Int](100): + def foo2: Special = 10 // error: 10 does not conform to Special (same behaviour as ordinary traits) + def bar2: Ordinary = 10 // This one is fine + def baz2: Special2 = 10 // error: 10 does not conform to Special (same behaviour as ordinary traits) + \ No newline at end of file diff --git a/tests/neg/inline-trait-self-type-missing-parent.scala b/tests/neg/inline-trait-self-type-missing-parent.scala new file mode 100644 index 000000000000..33b781ac026b --- /dev/null +++ b/tests/neg/inline-trait-self-type-missing-parent.scala @@ -0,0 +1,6 @@ +trait T1 +inline trait A[T]: + this: T1 => + +class B extends A[Int] // error: illegal inheritance: class B does not conform to self type +class C extends A, T1 diff --git a/tests/neg/inline-trait-self-type-problems.scala b/tests/neg/inline-trait-self-type-problems.scala new file mode 100644 index 000000000000..1e4af2a8d0ef --- /dev/null +++ b/tests/neg/inline-trait-self-type-problems.scala @@ -0,0 +1,23 @@ +trait T1 +trait T2 +trait T3 +class Test + +trait A[T]: + this: T1 => + +trait D extends A[Int] // error: self type of D does not conform to that of A +trait E extends D + +trait B[T]: + this: T2 & T1 => + +trait F extends A[Int], B[Int] // error: self type of F does not conform to that of A + +trait C[T]: + this: T => + +trait H extends C[Test]: // error self type of H does not conform to that of C + this: T3 => + +class Cl2 extends Test with H with T3 diff --git a/tests/pos/inline-trait-opaque-type.scala b/tests/pos/inline-trait-opaque-type.scala index f6a5ed11f410..a922fb2033e9 100644 --- a/tests/pos/inline-trait-opaque-type.scala +++ b/tests/pos/inline-trait-opaque-type.scala @@ -1,11 +1,11 @@ -inline trait A: - opaque type Special = Int - inline val b = 10 - def x: Special = b +inline trait A[T](val x: T): + opaque type Special = T -class B extends A + def getSpecial: Special = x + def eatSpecial(y: Special) = "Mmm, that was tasty!" + +class B extends A[Int](100) def foo = val b = B() - println(b.x) - \ No newline at end of file + println(b.eatSpecial(b.getSpecial)) diff --git a/tests/pos/inline-trait-self-type.scala b/tests/pos/inline-trait-self-type.scala new file mode 100644 index 000000000000..bf57e7b3d3e1 --- /dev/null +++ b/tests/pos/inline-trait-self-type.scala @@ -0,0 +1,26 @@ +trait T1 +trait T2 +trait T3 +class Test + +inline trait A[T]: + this: T1 => + +inline trait D extends A[Int]: + this: T1 => +inline trait E extends D: + this: T1 => + +inline trait B[T]: + this: T2 & T1 => + +inline trait F extends A[Int], B[Int]: + this: T2 & T1 => + +inline trait C[T]: + this: T => + +inline trait H extends C[Test]: + this: T3 & Test => + +class Cl2 extends Test with H with T3 From 89f9e48885a6371b243b4b7b4c1b6b11dd680c09 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 23 Apr 2026 18:09:35 +0200 Subject: [PATCH 150/254] Add new line at end of file --- tests/pos/inline-trait-body-val.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pos/inline-trait-body-val.scala b/tests/pos/inline-trait-body-val.scala index be20419ffb9f..33552386cf79 100644 --- a/tests/pos/inline-trait-body-val.scala +++ b/tests/pos/inline-trait-body-val.scala @@ -2,4 +2,4 @@ inline trait A: val x = 1 class B extends A: - def f = x \ No newline at end of file + def f = x From ca33ba68e21e12fdc62a545ffa0b13f8d665f1d2 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 23 Apr 2026 18:10:25 +0200 Subject: [PATCH 151/254] Add another new line at end of file --- tests/pos/inline-trait-body-val-inline.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pos/inline-trait-body-val-inline.scala b/tests/pos/inline-trait-body-val-inline.scala index b60a39cfb8fe..37f76fbd915c 100644 --- a/tests/pos/inline-trait-body-val-inline.scala +++ b/tests/pos/inline-trait-body-val-inline.scala @@ -2,4 +2,4 @@ inline trait A: inline val x = 1 class B extends A: - def f = x \ No newline at end of file + def f = x From ab26f5656e45d817757f0d7e434b94d444d2ad14 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 23 Apr 2026 19:11:58 +0200 Subject: [PATCH 152/254] Fix inconsistency with override requriement between inline trait and ordinary trait parent/child --- .../src/dotty/tools/dotc/inlines/Inlines.scala | 17 +++++++++++++++++ docs/_docs/internals/inline-traits.md | 8 ++++---- ...ait-clash-method-method-needs-override.scala | 9 +++++++++ ...-clash-method-val-param-needs-override.scala | 9 +++++++++ ...lash-method-var-param-missing-override.scala | 9 +++++++++ .../inline-trait-clash-val-param-method-2.scala | 4 ++++ .../inline-trait-clash-val-param-method.scala | 3 --- .../inline-trait-clash-var-param-method-2.scala | 4 ++++ .../inline-trait-clash-var-param-method.scala | 3 --- .../run/inline-trait-clash-method-method.scala | 2 +- .../inline-trait-clash-method-val-param.scala | 4 ++-- .../inline-trait-clash-method-var-param.scala | 4 ++-- 12 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 tests/neg/inline-trait-clash-method-method-needs-override.scala create mode 100644 tests/neg/inline-trait-clash-method-val-param-needs-override.scala create mode 100644 tests/neg/inline-trait-clash-method-var-param-missing-override.scala create mode 100644 tests/neg/inline-trait-clash-val-param-method-2.scala create mode 100644 tests/neg/inline-trait-clash-var-param-method-2.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 070c64d9cc71..977ef6c6d89f 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -31,6 +31,7 @@ import dotty.tools.dotc.core.Annotations.ConcreteBodyAnnotation import dotty.tools.dotc.core.Annotations.LazyBodyAnnotation import dotty.tools.dotc.core.Scopes.EmptyScope import dotty.tools.dotc.core.Scopes.MutableScope +import dotty.tools.dotc.reporting.OverrideError /** Support for querying inlineable methods and for inlining calls to such methods */ object Inlines: @@ -293,6 +294,22 @@ object Inlines: // report.error("May not inline an inline trait into a class defined inside another inline trait. If you really need to do this, make the inline trait Specialized or move the class definition outside the trait.", cls.srcPos) // cls case cls @ tpd.TypeDef(_, impl: Template) => + + + // We need to enforce the constraint that vals and defs that override have the `override` modifier + // here to ensure that the behaviour is the same as ordinary traits. The usual check only applies + // in refChecks which is after pruneInlineTraits so it won't fire for inline traits even though it should. + cls.symbol.info.decls.toList.foreach: decl => + if !decl.is(Override) && decl.allOverriddenSymbols.filterNot(sym => sym.is(Deferred)).nonEmpty then + report.error( + OverrideError("needs `override` modifier", + decl.allOverriddenSymbols.toList.head.owner.info, + decl, + decl.allOverriddenSymbols.toList.head, + NoType, + NoType), + decl.srcPos + ) val clsOverriddenSyms = cls.symbol.info.decls.toList.flatMap(_.allOverriddenSymbols).toSet val newDefs = inContext(ctx.withOwner(cls.symbol)) { inlineTraitAncestors(cls).foldLeft((List.empty[Tree], impl.body)){ diff --git a/docs/_docs/internals/inline-traits.md b/docs/_docs/internals/inline-traits.md index bb8177fbf88f..4fdf1cc58649 100644 --- a/docs/_docs/internals/inline-traits.md +++ b/docs/_docs/internals/inline-traits.md @@ -189,15 +189,15 @@ class B extends A: | Inline Trait Member Type | Inline Receiver Member Type | Behaviour | Justification | Same as `trait` | |---------------------------------------|----------------------------------------------|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------| -| `val` / `var` incl. `val`/`var` param | `val` / `var` incl. `val` / `var` param | Needs `override`| As above | ❌ (ordinary trait will warn on this; we allow it with no warning) | -| `val` / `var` incl. `val`/`var` param | Primary constructor (local) param | Not allowed | We cannot rename the constructor param because users may specify it by name when constructing the class, and we can't have a conflict with the generated parameter accessor, yet the parameter accessor and parameter must have the same name. | ❌ | +| `val` / `var` incl. `val`/`var` param | `val` / `var` incl. `val` / `var` param | Needs `override`| Ensures behaviour matches that of normal traits. However note that normal traits will warn if we try to override a val parameter. This warning is turned off for inline traits, otherwise every inline trait with a val parameter would warn after inlining. | ✅ | +| `val` / `var` incl. `val`/`var` param | Primary constructor (local) param | Not allowed | We cannot rename the constructor param because users may specify it by name when constructing the class, and we can't have a conflict with the generated parameter accessor, yet the parameter accessor and parameter must have the same name, so we end up with a conflict between the inlined val/var param and the constructor param. Therefore we have to ban this. | ❌ | | `val` / `var` incl. `val`/`var` param | Method | Not allowed | As normal traits, without `override` will be told "needs override"; with `override` will be told "not a stable immutable value" | ✅ | | Primary ctor (local) param | `val` / `var` incl. `val` / `var` param | Allowed | We need to inline the generated parameter accessors into the receiver, but these are renamed (prefixed with the inline trait name) when doing so, therefore fine. This does not affect the name in the original trait. | ✅ | | Primary ctor (local) param | Primary constructor (local) param | Allowed | We need to inline the generated parameter accessors into the receiver, but these are renamed (prefixed with the inline trait name) when doing so, therefore fine. This does not affect the name in the original trait. | ✅ | | Primary ctor (local) param | Method | Allowed | We need to inline the generated parameter accessors into the receiver, but these are renamed (prefixed with the inline trait name) when doing so, therefore fine. This does not affect the name in the original trait. | ✅ | -| Method | `val` / `var` incl. `val` / `var` param | Allowed | | ❌ `trait` requires `override` TODO: CHANGE SO WE MATCH? | +| Method | `val` / `var` incl. `val` / `var` param | Requires `override` | | ✅ | | Method | Primary constructor (local) param | Allowed unless sig match | We have the same issue as with `val`/`var` params if the signature matches, so we need to ban it. Otherwise we allow it as in `trait`. | ❌ | -| Method | Method | Allowed | | ❌ `trait` requires `override` TODO: CHANGE SO WE MATCH? | +| Method | Method | Requires `override` | | ✅ | | Type | Type | Allowed | Usual rules apply | ✅ | - Inline receivers may not access the parameters of their parents (these are private): diff --git a/tests/neg/inline-trait-clash-method-method-needs-override.scala b/tests/neg/inline-trait-clash-method-method-needs-override.scala new file mode 100644 index 000000000000..61e94c7f5020 --- /dev/null +++ b/tests/neg/inline-trait-clash-method-method-needs-override.scala @@ -0,0 +1,9 @@ +inline trait A: + def x(y: String) = "Hello world" + +class C extends A: + def x(y: String) = "Hello world2" // error: Needs override + +@main def Test = + val v = C() + assert(v.x("Hello World") == "Hello world2") diff --git a/tests/neg/inline-trait-clash-method-val-param-needs-override.scala b/tests/neg/inline-trait-clash-method-val-param-needs-override.scala new file mode 100644 index 000000000000..82d7d0093972 --- /dev/null +++ b/tests/neg/inline-trait-clash-method-val-param-needs-override.scala @@ -0,0 +1,9 @@ +inline trait A: + def x = "Hello world" + +class C extends A: + val x = "Overridden" // error: Needs override + +@main def Test = + val v = C() + assert(v.x == "Overridden") diff --git a/tests/neg/inline-trait-clash-method-var-param-missing-override.scala b/tests/neg/inline-trait-clash-method-var-param-missing-override.scala new file mode 100644 index 000000000000..70077210d794 --- /dev/null +++ b/tests/neg/inline-trait-clash-method-var-param-missing-override.scala @@ -0,0 +1,9 @@ +inline trait A: + def x = "Hello world" + +class C extends A: + var x = "Overridden" // error: Needs override + +@main def Test = + val v = C() + assert(v.x == "Overridden") diff --git a/tests/neg/inline-trait-clash-val-param-method-2.scala b/tests/neg/inline-trait-clash-val-param-method-2.scala new file mode 100644 index 000000000000..a7a87b4d8bf7 --- /dev/null +++ b/tests/neg/inline-trait-clash-val-param-method-2.scala @@ -0,0 +1,4 @@ +inline trait A(val x: Int) + +class C extends A(10): + def x = 1000 // error: Needs override marker diff --git a/tests/neg/inline-trait-clash-val-param-method.scala b/tests/neg/inline-trait-clash-val-param-method.scala index a7cb79962f4a..697b82f28721 100644 --- a/tests/neg/inline-trait-clash-val-param-method.scala +++ b/tests/neg/inline-trait-clash-val-param-method.scala @@ -1,7 +1,4 @@ inline trait A(val x: Int) -class C extends A(10): - def x = 1000 // error: Needs override marker - class D extends A(10): override def x = 1000 // error: needs to be a stable, immutable value diff --git a/tests/neg/inline-trait-clash-var-param-method-2.scala b/tests/neg/inline-trait-clash-var-param-method-2.scala new file mode 100644 index 000000000000..494e30d9dd52 --- /dev/null +++ b/tests/neg/inline-trait-clash-var-param-method-2.scala @@ -0,0 +1,4 @@ +inline trait A(var x: Int) + +class D extends A(10): + override def x = 1000 // error: cannot override a mutable value diff --git a/tests/neg/inline-trait-clash-var-param-method.scala b/tests/neg/inline-trait-clash-var-param-method.scala index 25b4d73d697c..8b90379516b4 100644 --- a/tests/neg/inline-trait-clash-var-param-method.scala +++ b/tests/neg/inline-trait-clash-var-param-method.scala @@ -2,6 +2,3 @@ inline trait A(var x: Int) class C extends A(10): def x = 1000 // error: Needs override marker - -class D extends A(10): - override def x = 1000 // error: cannot override a mutable value diff --git a/tests/run/inline-trait-clash-method-method.scala b/tests/run/inline-trait-clash-method-method.scala index 5469c8edeffa..8871681d977e 100644 --- a/tests/run/inline-trait-clash-method-method.scala +++ b/tests/run/inline-trait-clash-method-method.scala @@ -2,7 +2,7 @@ inline trait A: def x(y: String) = "Hello world" class C extends A: - def x(y: String) = "Hello world2" + override def x(y: String) = "Hello world2" @main def Test = val v = C() diff --git a/tests/run/inline-trait-clash-method-val-param.scala b/tests/run/inline-trait-clash-method-val-param.scala index e7a2811ddd79..7e7ebab600cb 100644 --- a/tests/run/inline-trait-clash-method-val-param.scala +++ b/tests/run/inline-trait-clash-method-val-param.scala @@ -1,8 +1,8 @@ inline trait A: - def x = "Hello world" + def x = "Hello world" class C extends A: - val x = "Overridden" + override val x = "Overridden" @main def Test = val v = C() diff --git a/tests/run/inline-trait-clash-method-var-param.scala b/tests/run/inline-trait-clash-method-var-param.scala index c1788f2a40c4..45cc7fdd64ab 100644 --- a/tests/run/inline-trait-clash-method-var-param.scala +++ b/tests/run/inline-trait-clash-method-var-param.scala @@ -1,8 +1,8 @@ inline trait A: - def x = "Hello world" + def x = "Hello world" class C extends A: - var x = "Overridden" + override var x = "Overridden" @main def Test = val v = C() From f54e694c5e511817ee7a1e88e3a56761573d34ac Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 23 Apr 2026 19:22:46 +0200 Subject: [PATCH 153/254] Remove redundant doc --- docs/_docs/internals/inline-traits.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/docs/_docs/internals/inline-traits.md b/docs/_docs/internals/inline-traits.md index 4fdf1cc58649..5fdc930414d3 100644 --- a/docs/_docs/internals/inline-traits.md +++ b/docs/_docs/internals/inline-traits.md @@ -157,17 +157,6 @@ inline trait B: def foo = "Bonjour" class C extends A, B -``` - - inline traits may define inline members (e.g. `inline def`, `inline val`). References to these are inlined as the body of the trait is inlined into the inline receiver, but the members themselves are not inlined and are deleted from the parent trait. E.g.: ```scala From 41b0673ce1f6d94831fe0e6fdd50c55cdef65379 Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 24 Apr 2026 16:26:23 +0200 Subject: [PATCH 154/254] Fix super calls and deal with override conflict between parents; comment out more inner class stuff --- .../dotty/tools/dotc/inlines/Inlines.scala | 112 ++++++++++++------ .../transform/DesugarSpecializedTraits.scala | 46 ++++--- .../transform/SpecializeInlineTraits.scala | 9 +- docs/_docs/internals/inline-traits.md | 26 +++- .../neg/inline-trait-body-class-return.scala | 2 +- ...inline-trait-clashing-parent-members.scala | 11 ++ ...nline-trait-clashing-parent-methods.scala} | 6 +- ...ine-trait-clashing-parent-val-params.scala | 9 ++ tests/neg/trait-inline-trait-clashing.scala | 8 ++ ...inline-trait-clashing-parent-members.scala | 13 -- ...ine-trait-clashing-parent-val-params.scala | 11 -- .../inline-trait-enclosing-super-call.scala | 14 +++ ...iple-parents-same-method-name-shadow.scala | 11 -- ...ne-trait-override-in-parent-triangle.scala | 14 +++ tests/run/inline-trait-super-call.scala | 24 ++++ 15 files changed, 216 insertions(+), 100 deletions(-) create mode 100644 tests/neg/inline-trait-clashing-parent-members.scala rename tests/{pos/inline-trait-inheritance-same-name.scala => neg/inline-trait-clashing-parent-methods.scala} (55%) create mode 100644 tests/neg/inline-trait-clashing-parent-val-params.scala create mode 100644 tests/neg/trait-inline-trait-clashing.scala delete mode 100644 tests/run/inline-trait-clashing-parent-members.scala delete mode 100644 tests/run/inline-trait-clashing-parent-val-params.scala create mode 100644 tests/run/inline-trait-enclosing-super-call.scala delete mode 100644 tests/run/inline-trait-multiple-parents-same-method-name-shadow.scala create mode 100644 tests/run/inline-trait-override-in-parent-triangle.scala create mode 100644 tests/run/inline-trait-super-call.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 977ef6c6d89f..8e61a4ff1429 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -32,6 +32,9 @@ import dotty.tools.dotc.core.Annotations.LazyBodyAnnotation import dotty.tools.dotc.core.Scopes.EmptyScope import dotty.tools.dotc.core.Scopes.MutableScope import dotty.tools.dotc.reporting.OverrideError +import dotty.tools.dotc.typer.RefChecks.OverridingPairsChecker +import dotty.tools.dotc.typer.ErrorReporting.err +import dotty.tools.dotc.core.NameKinds.DefaultGetterName /** Support for querying inlineable methods and for inlining calls to such methods */ object Inlines: @@ -265,14 +268,15 @@ object Inlines: updatedFlags - def transformInlineTrait(inlineTrait: TypeDef)(using Context): TypeDef = - val tpd.TypeDef(_, tmpl: Template) = inlineTrait: @unchecked - - tmpl.body.foreach { + private def checkInnerClasses(tmpl: Template)(using Context) = + tmpl.body.foreach { case innerClass: TypeDef if innerClass.symbol.isClass => report.error("Inline traits may not define inner classes or traits.", innerClass.srcPos) case _ => } - + + def checkAndTransformInlineTrait(inlineTrait: TypeDef)(using Context): TypeDef = + val tpd.TypeDef(_, tmpl: Template) = inlineTrait: @unchecked + checkInnerClasses(tmpl) val body1 = tmpl.body.flatMap { /* case innerClass: TypeDef if innerClass.symbol.isClass => val newTrait = makeTraitFromInnerClass(innerClass) @@ -286,7 +290,48 @@ object Inlines: } val tmpl1 = cpy.Template(tmpl)(body = body1) cpy.TypeDef(inlineTrait)(rhs = tmpl1) - end transformInlineTrait + end checkAndTransformInlineTrait + + + private def checkInlineTraitOverrides(clsSym: ClassSymbol)(using Context) = + /* We need to enforce `override` modifier constraints + here to ensure that the behaviour is the same as ordinary traits. The usual checks only apply + in refChecks which is too late for us. */ + + // TODO: This does cause some code duplication + def checkInlineTraitOverride(member: Symbol, other: Symbol) = + if !member.is(Override) && !other.is(Deferred) && member.owner == clsSym then + report.error( + OverrideError("needs `override` modifier", + other.info, + member, + other, + NoType, + NoType), + member.srcPos + ) + else if member.owner != clsSym && other.owner != clsSym + && !other.owner.derivesFrom(member.owner) + && !(member.isAnyOverride || member.hasAnnotation(defn.UncheckedOverrideAnnot)) + && (!other.is(Deferred) || other.isAllOf(Given | HasDefault)) + && !member.is(Deferred) + && !other.name.is(DefaultGetterName) then + + report.error( + OverrideError( + s"${clsSym} inherits conflicting members:\n " + + err.infoString(other, clsSym.asClass.thisType, showLocation = true) + " and\n " + + err.infoString(member, clsSym.thisType, showLocation = true) + + "\n(Note: this can be resolved by declaring an override in " + clsSym + ".)", + other.info, + member, + other, + NoType, + NoType) + , + clsSym.srcPos + ) + OverridingPairsChecker(clsSym, clsSym.thisType).checkAll(checkInlineTraitOverride) def inlineParentInlineTraits(cls: Tree)(using Context): Tree = cls match { @@ -294,22 +339,7 @@ object Inlines: // report.error("May not inline an inline trait into a class defined inside another inline trait. If you really need to do this, make the inline trait Specialized or move the class definition outside the trait.", cls.srcPos) // cls case cls @ tpd.TypeDef(_, impl: Template) => - - - // We need to enforce the constraint that vals and defs that override have the `override` modifier - // here to ensure that the behaviour is the same as ordinary traits. The usual check only applies - // in refChecks which is after pruneInlineTraits so it won't fire for inline traits even though it should. - cls.symbol.info.decls.toList.foreach: decl => - if !decl.is(Override) && decl.allOverriddenSymbols.filterNot(sym => sym.is(Deferred)).nonEmpty then - report.error( - OverrideError("needs `override` modifier", - decl.allOverriddenSymbols.toList.head.owner.info, - decl, - decl.allOverriddenSymbols.toList.head, - NoType, - NoType), - decl.srcPos - ) + checkInlineTraitOverrides(cls.symbol.asClass) val clsOverriddenSyms = cls.symbol.info.decls.toList.flatMap(_.allOverriddenSymbols).toSet val newDefs = inContext(ctx.withOwner(cls.symbol)) { inlineTraitAncestors(cls).foldLeft((List.empty[Tree], impl.body)){ @@ -329,7 +359,9 @@ object Inlines: val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) val inlinedDefs1 = inlineDefs ::: parentTraitInliner.expandDefs(overriddenSymbols) cls.symbol.flags = updateFlagsFromInlinedParent(cls.symbol.flags, parent.symbol.flags) - (inlinedDefs1, childDefs) + + val childDefs1 = parentTraitInliner.adaptSuperCalls(childDefs) + (inlinedDefs1, childDefs1) } } val newbody = newDefs._1 ::: newDefs._2 @@ -812,9 +844,10 @@ object Inlines: def expandDefs(overriddenDecls: Set[Symbol]): List[Tree] = paramAccessorsMapper.registerParamValuesOf(parent) - val stats = Inlines.defsToInline(parentSym).filterNot(stat => overriddenDecls.contains(stat.symbol)) - stats.map{ - case member: MemberDef => Left((member, inlinedSym(member.symbol))) // Private symbols must be entered before the RHSs are inlined + val stats = Inlines.defsToInline(parentSym).filterNot(stat => overriddenDecls.contains(stat.symbol) && stat.symbol.is(Deferred)) + + stats.map{ // Private symbols must be entered before the RHSs are inlined + case member: MemberDef => Left((member, inlinedSym(member.symbol, overriddenDecls))) case stat => Right(stat) }.map{ case Left((tree, inlinedSym)) => expandStat(tree, inlinedSym) @@ -822,6 +855,15 @@ object Inlines: } end expandDefs + def adaptSuperCalls(defs: List[Tree]) = + val ttmap = TreeTypeMap(treeMap = { + case sel@Select(Super(qual, mix), name) if sel.symbol.owner == parentSym => + // Either method overridden so needs mangling, or not, in which case call directly by original name. + Select(This(ctx.owner.asClass), paramAccessorsMapper.getParamAccessorName(sel.symbol.owner, name).getOrElse(name)) + case tree => tree + }) + defs.map(ttmap(_)) + protected class InlineTraitTypeMap extends InlinerTypeMap { override def apply(t: Type) = super.apply(t) match { case t: ThisType if t.cls == parentSym => childThisType @@ -877,13 +919,13 @@ object Inlines: inlinedValDef(stat, inlinedSym) case stat: DefDef => inlinedDefDef(stat, inlinedSym) - case stat @ TypeDef(_, _: Template) => - inlinedClassDef(stat, inlinedSym.asClass) + case stat @ TypeDef(_, _: Template) => EmptyTree + /* inlinedClassDef(stat, inlinedSym.asClass) */ // Inner classes are not allowed for now. case stat: TypeDef => inlinedTypeDef(stat, inlinedSym) - private def inlinedSym(sym: Symbol, withoutFlags: FlagSet = EmptyFlags)(using Context): Symbol = - val newSym = if sym.isClass then inlinedClassSym(sym.asClass, withoutFlags) else inlinedMemberSym(sym, withoutFlags) + private def inlinedSym(sym: Symbol, overriddenDecls: Set[Symbol], withoutFlags: FlagSet = EmptyFlags)(using Context): Symbol = + val newSym = if sym.isClass then inlinedClassSym(sym.asClass, withoutFlags) else inlinedMemberSym(sym, overriddenDecls, withoutFlags) ctx.inlineTraitState.registerInlinedSymbol(sym, newSym, ctx.owner.thisType.widenDealias) newSym @@ -915,12 +957,13 @@ object Inlines: sym } - private def inlinedMemberSym(sym: Symbol, withoutFlags: FlagSet = EmptyFlags)(using Context): Symbol = + private def inlinedMemberSym(sym: Symbol, overriddenDecls: Set[Symbol], withoutFlags: FlagSet = EmptyFlags)(using Context): Symbol = var name = sym.name var flags = sym.flags | Synthetic if sym.isTermParamAccessor then flags &~= ParamAccessor - if sym.is(Local) then + if sym.is(Local) || (overriddenDecls.contains(sym)) then name = paramAccessorsMapper.registerNewName(sym) + flags |= (Private | Local) else flags |= Override sym.copy( @@ -956,12 +999,14 @@ object Inlines: inlinedRhs(ddef1, inlinedSym) tpd.DefDef(inlinedSym.asTerm, rhsFun).withSpan(parent.span) + /* private def inlinedPrimaryConstructorDefDef(ddef: DefDef)(using Context): DefDef = // TODO check if symbol must be copied val inlinedSym = inlinedMemberSym(ddef.symbol, withoutFlags = Override) val constr = inlinedDefDef(ddef, inlinedSym) cpy.DefDef(constr)(tpt = TypeTree(defn.UnitType), rhs = EmptyTree) - + */ + /* private def inlinedClassDef(clsDef: TypeDef, inlinedCls: ClassSymbol)(using Context): Tree = val TypeDef(_, tmpl: Template) = clsDef: @unchecked val (constr, body) = inContext(ctx.withOwner(inlinedCls)) { @@ -976,6 +1021,7 @@ object Inlines: } val clsDef1 = tpd.ClassDefWithParents(inlinedCls, constr, tmpl.parents, body) // TODO add correct parent tree inlined(clsDef1)._2.withSpan(clsDef.span) + */ private def inlinedTypeDef(tdef: TypeDef, inlinedSym: Symbol)(using Context): TypeDef = val tdef2 = tpd.TypeDef(inlinedSym.asType).withSpan(parent.span) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index b2d85352a0e9..fa2bbde3fcad 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -400,17 +400,21 @@ class DesugarSpecializedTraits extends MacroTransform: extension (classTree: Tree) def updateParents(parentUpdater: List[Type] => List[Type]) = (classTree: @unchecked) match { case td@TypeDef(name, t@Template(constr, preParentsOrDerived, self, preBody)) => - td.symbol.info = td.symbol.info match { - case ci: ClassInfo => ci.derivedClassInfo(declaredParents=parentUpdater(ci.declaredParents)) - } - ClassDef(td.symbol.asClass, constr, t.body) + td.symbol.info = td.symbol.info match { + case ci: ClassInfo => ci.derivedClassInfo(declaredParents=parentUpdater(ci.declaredParents)) + } + } + + def refreshClassDef = (classTree: @unchecked) match { + case td@TypeDef(name, t@Template(constr, preParentsOrDerived, self, preBody)) => + ClassDef(td.symbol.asClass, constr, t.body) } /* We need to inline recursively throughout generated specialized traits - see tests/run/specialized-trait-requires-inline-trait-inlining.scala */ // TODO: How do we calculate the spans correctly? val ttmap = new TreeTypeMap(treeMap = { case tree: TypeDef if tree.symbol.isInlineTrait => - val tree1 = Inlines.transformInlineTrait(tree) + val tree1 = Inlines.checkAndTransformInlineTrait(tree) val tree2 = if Inlines.needsInlining(tree1) then Inlines.inlineParentInlineTraits(tree1) else tree1 tree2 case tree: TypeDef if Inlines.needsInlining(tree) => @@ -418,21 +422,27 @@ class DesugarSpecializedTraits extends MacroTransform: case t => t }) - // Why does it cause no denotation to happen? - val generatedTraitStats1 = generatedTraitStats.map(trtDef => /*Inlines.inlineParentInlineTraits(Inlines.transformInlineTrait(*/ttmap(trtDef.withSpan(span))/*))*/).map: - _.updateParents { parents => (parents: @unchecked) match - case obj :: original :: parents => obj :: parents - } - - val generatedClassStats1 = generatedClassStats.map(clsDef => /*Inlines.inlineParentInlineTraits(*/ttmap(clsDef.withSpan(span))/*)*/).map: - _.updateParents { parents => (parents: @unchecked) match - case obj :: traitSp :: originalSpec :: Nil => obj :: traitSp :: Nil - } - - if (generatedTraitStats1.isEmpty && generatedClassStats1.isEmpty) + val generatedTraitStats1 = generatedTraitStats.map(trtDef => ttmap(trtDef.withSpan(span))) + val generatedClassStats1 = generatedClassStats.map(clsDef => ttmap(clsDef.withSpan(span))) + .tapEach: + _.updateParents { parents => (parents: @unchecked) match + case obj :: traitSp :: originalSpec :: Nil => obj :: traitSp :: Nil + } + .map(refreshClassDef) + + // We need to do this after inlining into the $impl$ classes otherwise we break + // overriding/interface implementation rules during the inlining. + val generatedTraitStats1a = generatedTraitStats1 + .tapEach: + _.updateParents { parents => (parents: @unchecked) match + case obj :: original :: parents => obj :: parents + } + .map(refreshClassDef) + + if (generatedTraitStats1a.isEmpty && generatedClassStats1.isEmpty) (stats.map(replaceSpecializedSymbolsMap(specializations2)(_)), specializations2) else - val (generatedTraitStats2, specializations3) = transformStatements(generatedTraitStats1, span, specializations2) + val (generatedTraitStats2, specializations3) = transformStatements(generatedTraitStats1a, span, specializations2) val (generatedClassStats2, specializations4) = transformStatements(generatedClassStats1, span, specializations3) (generatedTraitStats2 ++ generatedClassStats2 ++ stats.map(replaceSpecializedSymbolsMap(specializations4)(_)), specializations4) } diff --git a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala index 691dea2864d1..9556ba60855d 100644 --- a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala @@ -39,7 +39,7 @@ class SpecializeInlineTraits extends MacroTransform, SymTransformer { override def newTransformer(using Context): Transformer = new Transformer { override def transform(tree: Tree)(using Context): Tree = tree match { case tree: TypeDef if tree.symbol.isInlineTrait => - val tree1 = Inlines.transformInlineTrait(tree) + val tree1 = Inlines.checkAndTransformInlineTrait(tree) val tree2 = if Inlines.needsInlining(tree1) then Inlines.inlineParentInlineTraits(tree1) else tree1 super.transform(tree2) // We may need to inline inline traits into the bodies of methods defined inside inline traits. case tree: TypeDef if Inlines.needsInlining(tree) => @@ -53,7 +53,7 @@ class SpecializeInlineTraits extends MacroTransform, SymTransformer { ) val tree1 = if tree.symbol.isInlineTrait then - Inlines.inlineParentInlineTraits(Inlines.transformInlineTrait(tree)) + Inlines.inlineParentInlineTraits(Inlines.checkAndTransformInlineTrait(tree)) else Inlines.inlineParentInlineTraits(tree) super.transform(tree1) @@ -61,11 +61,12 @@ class SpecializeInlineTraits extends MacroTransform, SymTransformer { } } - override def transformSym(symd: SymDenotation)(using Context): SymDenotation = - if symd.isClass && symd.owner.isInlineTrait && !symd.is(Module) then + override def transformSym(symd: SymDenotation)(using Context): SymDenotation = symd + /* if symd.isClass && symd.owner.isInlineTrait && !symd.is(Module) then symd.copySymDenotation(name = SpecializeInlineTraits.newInnerClassName(symd.name), initFlags = (symd.flags &~ Final) | Trait) else symd +*/ override def checkPostCondition(tree: Tree)(using Context): Unit = tree match { diff --git a/docs/_docs/internals/inline-traits.md b/docs/_docs/internals/inline-traits.md index 5fdc930414d3..34bcc5c3cc9a 100644 --- a/docs/_docs/internals/inline-traits.md +++ b/docs/_docs/internals/inline-traits.md @@ -148,7 +148,7 @@ class B extends A(true): private val A$$x: Int = 1 override def foo(): Int = if this.A$$b then this.A$$x.+(1) else 0 ``` -- An inline receiver may mix in multiple inline traits with colliding member names. In this case the latest extended trait prevails. In the following example calling `foo` on an instance of `C` will return "Bonjour". This is in contrast to ordinary traits which require the `override` modifier in this case. +- An inline receiver may mix in multiple inline traits with colliding member names. This follows the same rules as normal traits. In particular, this must usually be disambiguated with an override. ```scala inline trait A: def foo = "Hello World" @@ -156,7 +156,17 @@ inline trait A: inline trait B: def foo = "Bonjour" -class C extends A, B +class C extends A, B // error: C inherits conflicting members A.foo and B.foo +``` +A typical way to disambiguate would be using `super`. For example: +```scala +class C extends A, B: + override def foo = super[A].foo +``` +This syntax is supported for inline traits. Note however that as inline traits are converted to pure interfaces it is not possible to make a direct call to the +method on A or B. Furthermore if we allowed this, specialization would be lost. Therefore, overridden methods are inlined into the inline receiver with a mangled name, +e.g. `A$$foo$`, `B$$foo` and the `override def foo` in `C` will delegate to one of these methods. Super calls to non-overridden methods are also supported. +These are transformed to point directly to the corresponding inlined methods with no need for name mangling. - inline traits may define inline members (e.g. `inline def`, `inline val`). References to these are inlined as the body of the trait is inlined into the inline receiver, but the members themselves are not inlined and are deleted from the parent trait. E.g.: ```scala @@ -380,5 +390,13 @@ This behaviour is the same as that in Timothée's thesis except for the followin - We now do replacement of member accesses to point to the inlined versions throughout the whole code, not just in the bodies of inner classes - He allows inline traits to contain inner classes in principle, however in practice they don't work which is why we ban them. - We specialize types of member accesses on e.g. Numeric - - He in principle allows traits to extend inline traits although it doesn't work that well; we think we probably want to forbid this. - - We also fix a number of bugs in the implementation, some of which have a minor effect on the processing and interaction with the rest of the compiler phases, e.g. we apply pruneInlineTraits slightly earlier than in the original implementation to avoid spurious warnings with -Wsafe-init. + - He in principle allows traits to extend inline traits although it doesn't work that well; we impose concrete rules on this: + - Trait extends inline trait is only allowed if the inline trait is parameterless + - Inline trait extends trait is always allowed + - We modify some of the rules around overrides and conflicting members in order to make the behaviour more consistent with ordinary traits. + In particular we require `override` in a number of locations where previously conflicts were resolved on the basis of "last extending trait wins". + - We also fix a number of bugs in the implementation, some of which have a minor effect on the processing and interaction with the rest of the compiler phases, e.g. we apply pruneInlineTraits slightly earlier than in the original implementation to avoid spurious warnings with -Wsafe-init, and we fix flags, and add support for nested inlines. + - We also implement some extra inlining such as opaque types, super references, and self types. + - We enforce a number of rules that were previously implicit, with proper errors. + - We do the RHS type narrowing for vals (not vars) described above as an optimisation + - We change the handling of private members in pruning of inline traits (we now delete them completely) diff --git a/tests/neg/inline-trait-body-class-return.scala b/tests/neg/inline-trait-body-class-return.scala index d77c92f1fc06..8e3bbd32e3d7 100644 --- a/tests/neg/inline-trait-body-class-return.scala +++ b/tests/neg/inline-trait-body-class-return.scala @@ -1,7 +1,7 @@ inline trait A: sealed class InnerA: // error: Inline traits may not define inner classes or traits. val x = 1 - def generate(x: Int) = InnerA() + def generate(x: Int) = new InnerA() {} class B extends A: val y = generate(7) diff --git a/tests/neg/inline-trait-clashing-parent-members.scala b/tests/neg/inline-trait-clashing-parent-members.scala new file mode 100644 index 000000000000..e217baf4c70a --- /dev/null +++ b/tests/neg/inline-trait-clashing-parent-members.scala @@ -0,0 +1,11 @@ +inline trait A: + val x = 10 + +inline trait B: + val x = 11 + +class C extends A, B // error: C inherits conflicting members x from A and B + +@main def Test = + val v = C() + assert(v.x == 11) diff --git a/tests/pos/inline-trait-inheritance-same-name.scala b/tests/neg/inline-trait-clashing-parent-methods.scala similarity index 55% rename from tests/pos/inline-trait-inheritance-same-name.scala rename to tests/neg/inline-trait-clashing-parent-methods.scala index b3aa1b2bb7d2..002014e88417 100644 --- a/tests/pos/inline-trait-inheritance-same-name.scala +++ b/tests/neg/inline-trait-clashing-parent-methods.scala @@ -4,8 +4,4 @@ inline trait A: inline trait B: def foo = "Bonjour" -class C extends A, B - -def main = - val x = C() - println(x.foo) +class C extends A, B // error: C inherits conflicting members diff --git a/tests/neg/inline-trait-clashing-parent-val-params.scala b/tests/neg/inline-trait-clashing-parent-val-params.scala new file mode 100644 index 000000000000..e803c42d9312 --- /dev/null +++ b/tests/neg/inline-trait-clashing-parent-val-params.scala @@ -0,0 +1,9 @@ +inline trait A(val x: Int) + +inline trait B(val x: Int) + +class C extends A(10), B(11) // error: C inherits conflicting members + +@main def Test = + val v = C() + assert(v.x == 11) diff --git a/tests/neg/trait-inline-trait-clashing.scala b/tests/neg/trait-inline-trait-clashing.scala new file mode 100644 index 000000000000..3d862d975805 --- /dev/null +++ b/tests/neg/trait-inline-trait-clashing.scala @@ -0,0 +1,8 @@ +trait Foo: + def foo = 10 + +inline trait Bar: + def foo = 10 + +class C extends Bar, Foo // error: C inherits conflicting members +class D extends Foo, Bar // error: D inherits conflicting members diff --git a/tests/run/inline-trait-clashing-parent-members.scala b/tests/run/inline-trait-clashing-parent-members.scala deleted file mode 100644 index df5767b6aa9e..000000000000 --- a/tests/run/inline-trait-clashing-parent-members.scala +++ /dev/null @@ -1,13 +0,0 @@ -// We allow multiple inline traits to be mixed in with the same member names; we prefer the latest mixed-in name. - -inline trait A: - val x = 10 - -inline trait B: - val x = 11 - -class C extends A, B - -@main def Test = - val v = C() - assert(v.x == 11) diff --git a/tests/run/inline-trait-clashing-parent-val-params.scala b/tests/run/inline-trait-clashing-parent-val-params.scala deleted file mode 100644 index 3d3d669e926b..000000000000 --- a/tests/run/inline-trait-clashing-parent-val-params.scala +++ /dev/null @@ -1,11 +0,0 @@ -// We allow multiple inline traits to be mixed in with the same member names; we prefer the latest mixed-in name. - -inline trait A(val x: Int) - -inline trait B(val x: Int) - -class C extends A(10), B(11) - -@main def Test = - val v = C() - assert(v.x == 11) diff --git a/tests/run/inline-trait-enclosing-super-call.scala b/tests/run/inline-trait-enclosing-super-call.scala new file mode 100644 index 000000000000..2016de44b827 --- /dev/null +++ b/tests/run/inline-trait-enclosing-super-call.scala @@ -0,0 +1,14 @@ +inline trait A: + def bar = "A" + +class E: + def bar = "E" + +class D extends E: + class C extends A: + def foo = D.super.bar // We shouldn't touch this super call when inlining A + +@main def Test = + val d = D() + val c = d.C() + assert(c.foo == "E") diff --git a/tests/run/inline-trait-multiple-parents-same-method-name-shadow.scala b/tests/run/inline-trait-multiple-parents-same-method-name-shadow.scala deleted file mode 100644 index 553bf40812a6..000000000000 --- a/tests/run/inline-trait-multiple-parents-same-method-name-shadow.scala +++ /dev/null @@ -1,11 +0,0 @@ -inline trait A: - def foo = "Hello World" - -inline trait B: - def foo = "Bonjour" - -class C extends A, B - -@main def Test: Unit = - val c = C() - assert(c.foo == "Bonjour") diff --git a/tests/run/inline-trait-override-in-parent-triangle.scala b/tests/run/inline-trait-override-in-parent-triangle.scala new file mode 100644 index 000000000000..bbf06a8244a5 --- /dev/null +++ b/tests/run/inline-trait-override-in-parent-triangle.scala @@ -0,0 +1,14 @@ +inline trait Foo: + def foo = "Foo" + +inline trait Bar extends Foo: + override def foo = "Bar" + +class C extends Bar, Foo +class D extends Foo, Bar + +@main def Test = + val c = C() + val d = D() + assert(c.foo == "Bar") + assert(d.foo == "Bar") diff --git a/tests/run/inline-trait-super-call.scala b/tests/run/inline-trait-super-call.scala new file mode 100644 index 000000000000..e2e335799599 --- /dev/null +++ b/tests/run/inline-trait-super-call.scala @@ -0,0 +1,24 @@ +inline trait A: + def foo = "A" + def bar = "bar" + +inline trait B: + def foo = "B" + +class C extends A, B: + override def foo = super.foo + +class D extends A, B: + override def foo = super[A].foo + +class E extends A, B: + override def foo = super[B].foo + def baz = super[A].bar // No override; this also needs to work + +@main def Test: Unit = + val c = C() + assert(c.foo == "B") + val d = D() + assert(d.foo == "A") + val e = E() + assert(e.foo == "B") From 3878e9e902ded3df3fe49eb14e80be5c3c21ce03 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 27 Apr 2026 13:46:52 +0200 Subject: [PATCH 155/254] Fix overrides nothing regresion --- compiler/src/dotty/tools/dotc/core/NameOps.scala | 2 ++ .../src/dotty/tools/dotc/core/SymDenotations.scala | 8 ++++++++ .../dotc/transform/DesugarSpecializedTraits.scala | 4 ++-- ...alized-trait-no-regression-overrides-nothing.scala | 11 +++++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 tests/neg/specialized-trait-no-regression-overrides-nothing.scala diff --git a/compiler/src/dotty/tools/dotc/core/NameOps.scala b/compiler/src/dotty/tools/dotc/core/NameOps.scala index f157da843f41..9f5b1af3a6c5 100644 --- a/compiler/src/dotty/tools/dotc/core/NameOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NameOps.scala @@ -77,6 +77,8 @@ object NameOps { def isAnonymousFunctionName: Boolean = name.startsWith(str.ANON_FUN) def isUnapplyName: Boolean = name == nme.unapply || name == nme.unapplySeq def isRightAssocOperatorName: Boolean = name.lastPart.last == ':' + def isSpecializedTraitInterfaceName: Boolean = name.toString.contains(str.SPECIALIZED_TRAIT_SUFFIX) + def isSpecializedTraitImplementationName: Boolean = name.toString.contains(str.SPECIALIZED_TRAIT_IMPL_SUFFIX) /** Does this name match `[{letter | digit} '_'] op`? * diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index be40bbb91712..5dd7da032552 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -647,6 +647,14 @@ object SymDenotations { final def isAnonymousClass(using Context): Boolean = isClass && initial.name.isAnonymousClassName + /** Is this symbol an specialized trait interface? */ + final def isSpecializedTraitInterface(using Context): Boolean = + isClass && name.isSpecializedTraitInterfaceName + + /** Is this symbol an specialized trait implementation class? */ + final def isSpecializedTraitImplementationClass(using Context): Boolean = + isClass && name.isSpecializedTraitImplementationName + final def isAnonymousFunction(using Context): Boolean = this.symbol.is(Method) && initial.name.isAnonymousFunctionName diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index fa2bbde3fcad..8d25ad475e0e 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -279,14 +279,14 @@ class DesugarSpecializedTraits extends MacroTransform: case dd@DefDef(name, paramss, tpt, preRhs) => val transformedDef = super.transform(dd) transformedDef.symbol.info = mapType(transformedDef.symbol.info) - if transformedDef.symbol.allOverriddenSymbols.isEmpty then + if transformedDef.symbol.allOverriddenSymbols.isEmpty && (transformedDef.symbol.owner.isSpecializedTraitInterface || transformedDef.symbol.owner.isSpecializedTraitImplementationClass) then transformedDef.symbol.flags = transformedDef.symbol.flags &~ Flags.Override transformedDef case vd@ValDef(name, tpt, preRhs) => val transformedDef = super.transform(vd) transformedDef.symbol.info = mapType(transformedDef.symbol.info) - if transformedDef.symbol.allOverriddenSymbols.isEmpty then + if transformedDef.symbol.allOverriddenSymbols.isEmpty && (transformedDef.symbol.owner.isSpecializedTraitInterface || transformedDef.symbol.owner.isSpecializedTraitImplementationClass) then transformedDef.symbol.flags = transformedDef.symbol.flags &~ Flags.Override transformedDef /*case vd@ValDef(name, tpt, preRhs) => diff --git a/tests/neg/specialized-trait-no-regression-overrides-nothing.scala b/tests/neg/specialized-trait-no-regression-overrides-nothing.scala new file mode 100644 index 000000000000..3028d1fee92e --- /dev/null +++ b/tests/neg/specialized-trait-no-regression-overrides-nothing.scala @@ -0,0 +1,11 @@ +trait A1: + def foo = "A" + +trait B1: + override def foo = "B" // error: foo overrides nothing + +inline trait A2: + def foo = "A" + +inline trait B2: + override def foo = "B" // error: foo overrides nothing From e04a884bd3715ccbcf81b5a03f9cd76ccfe1324e Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 27 Apr 2026 15:40:57 +0200 Subject: [PATCH 156/254] Fix inline trait super calls --- .../dotty/tools/dotc/inlines/Inlines.scala | 18 +++++++++++---- .../tools/dotc/transform/SuperAccessors.scala | 1 + docs/_docs/internals/inline-traits.md | 5 +++- .../inline-trait-clash-method-var-param.scala | 2 +- ...rait-override-private-member-deleted.scala | 5 ++++ .../inline-trait-supercall-into-trait.scala | 7 ++++++ ...line-trait-super-call-non-overridden.scala | 5 ++++ ...-enclosing-supercall-in-inline-trait.scala | 13 +++++++++++ tests/run/inline-trait-super-chain.scala | 23 +++++++++++++++++++ 9 files changed, 73 insertions(+), 6 deletions(-) rename tests/{run => neg}/inline-trait-clash-method-var-param.scala (63%) create mode 100644 tests/neg/inline-trait-override-private-member-deleted.scala create mode 100644 tests/neg/inline-trait-supercall-into-trait.scala create mode 100644 tests/pos/inline-trait-super-call-non-overridden.scala create mode 100644 tests/run/inline-trait-enclosing-supercall-in-inline-trait.scala create mode 100644 tests/run/inline-trait-super-chain.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 8e61a4ff1429..f2e91c454bd7 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -361,7 +361,7 @@ object Inlines: cls.symbol.flags = updateFlagsFromInlinedParent(cls.symbol.flags, parent.symbol.flags) val childDefs1 = parentTraitInliner.adaptSuperCalls(childDefs) - (inlinedDefs1, childDefs1) + (parentTraitInliner.adaptSuperCalls(inlinedDefs1), childDefs1) } } val newbody = newDefs._1 ::: newDefs._2 @@ -857,9 +857,12 @@ object Inlines: def adaptSuperCalls(defs: List[Tree]) = val ttmap = TreeTypeMap(treeMap = { + // We go through all ancestor inline traits so eventually we will find the one with matching parentSym case sel@Select(Super(qual, mix), name) if sel.symbol.owner == parentSym => - // Either method overridden so needs mangling, or not, in which case call directly by original name. - Select(This(ctx.owner.asClass), paramAccessorsMapper.getParamAccessorName(sel.symbol.owner, name).getOrElse(name)) + // At that point either the method is overridden so needs mangling (and we just copied and mangled it in this inlining phase), + // or not, in which case call directly by original name. In both cases we are calling the method resulting from inlining, on the + // inline receiver class. + Select(This(ctx.owner.asClass), paramAccessorsMapper.getParamAccessorName(sel.symbol.owner, name).getOrElse(name)) case tree => tree }) defs.map(ttmap(_)) @@ -882,7 +885,13 @@ object Inlines: case _ => tree } - case Select(qual, name) => + case sel@Select(Super(qual, mix), name) => + if sel.symbol.owner.isInlineTrait then // We need to leave this intact so that adaptSuperCalls can redirect it later + sel + else + report.error("Inline traits may not contain superclass references to classes or non-inline traits.", sel.srcPos) + sel + case sel@Select(qual, name) => inContext(ctx.withSource(tree.source)) { // Need to ensure we preserve the fact that this Select was inlined // potentially from a different file. Recreating it discards that info. // See: inline-trait-multiple-stages-generic-defs. @@ -964,6 +973,7 @@ object Inlines: if sym.is(Local) || (overriddenDecls.contains(sym)) then name = paramAccessorsMapper.registerNewName(sym) flags |= (Private | Local) + flags &~= Override // private override is illegal; if the inlined method was already override then we might make it private override by accident. else flags |= Override sym.copy( diff --git a/compiler/src/dotty/tools/dotc/transform/SuperAccessors.scala b/compiler/src/dotty/tools/dotc/transform/SuperAccessors.scala index 8d569ec9ff35..545315afc8f2 100644 --- a/compiler/src/dotty/tools/dotc/transform/SuperAccessors.scala +++ b/compiler/src/dotty/tools/dotc/transform/SuperAccessors.scala @@ -156,6 +156,7 @@ class SuperAccessors(thisPhase: DenotTransformer) { val needAccessor = name.isTermName // Types don't need super accessors && !sym.isInlineMethod // Inline methods are not called at runtime so they don't need superaccessors. + && !clazz.isInlineTrait // Inline traits deal with super calls in their own way && (clazz != currentClass || !validCurrentClass || mix.name.isEmpty && clazz.is(Trait)) if (needAccessor) atPhase(thisPhase.next)(superAccessorCall(sel, mix.name)) diff --git a/docs/_docs/internals/inline-traits.md b/docs/_docs/internals/inline-traits.md index 34bcc5c3cc9a..af108975fce9 100644 --- a/docs/_docs/internals/inline-traits.md +++ b/docs/_docs/internals/inline-traits.md @@ -251,7 +251,10 @@ Therefore there is no case in which this could be useful, and it is likely to ca However, there is a reasonable case for `trait extends inline trait` in general, to control the inlining and reduce code duplication so we allow this pattern if the inline trait has no parameters. -Note that this restriction does not block `inline trait extends trait` or `inline trait extends class` which are allowed without restriction. +Note that this restriction does not block `inline trait extends trait` or `inline trait extends class` which are allowed with one (other) restriction: + - Inline traits may not contain `super` references to classes or non-inline traits. This is because `super` references in scala may only reference + direct parents, and, after inlining, those references that were to direct parents in the original inline traits would now have to point to ancestor + classes which are further than 1 hop away. ## Benefits of inline traits We can now do the following with no boxing and unboxing: diff --git a/tests/run/inline-trait-clash-method-var-param.scala b/tests/neg/inline-trait-clash-method-var-param.scala similarity index 63% rename from tests/run/inline-trait-clash-method-var-param.scala rename to tests/neg/inline-trait-clash-method-var-param.scala index 45cc7fdd64ab..18caa31af679 100644 --- a/tests/run/inline-trait-clash-method-var-param.scala +++ b/tests/neg/inline-trait-clash-method-var-param.scala @@ -2,7 +2,7 @@ inline trait A: def x = "Hello world" class C extends A: - override var x = "Overridden" + override var x = "Overridden" // error: Setter x_= overrides nothing @main def Test = val v = C() diff --git a/tests/neg/inline-trait-override-private-member-deleted.scala b/tests/neg/inline-trait-override-private-member-deleted.scala new file mode 100644 index 000000000000..6ef7c6c0ee8d --- /dev/null +++ b/tests/neg/inline-trait-override-private-member-deleted.scala @@ -0,0 +1,5 @@ +inline trait Foo: + private def foo = 10 + +class A extends Foo: + override def foo = 11 // error: method foo overrides nothing diff --git a/tests/neg/inline-trait-supercall-into-trait.scala b/tests/neg/inline-trait-supercall-into-trait.scala new file mode 100644 index 000000000000..faf19faf43ba --- /dev/null +++ b/tests/neg/inline-trait-supercall-into-trait.scala @@ -0,0 +1,7 @@ +trait A: + def foo = "A" + +inline trait B extends A: + override def foo = super.foo // error: Inline trait smay not contain superclass references to classes or non-inline traits + +class C1 extends B diff --git a/tests/pos/inline-trait-super-call-non-overridden.scala b/tests/pos/inline-trait-super-call-non-overridden.scala new file mode 100644 index 000000000000..ffd37cca62f0 --- /dev/null +++ b/tests/pos/inline-trait-super-call-non-overridden.scala @@ -0,0 +1,5 @@ +inline trait A: + def foo = 10 + +inline trait B extends A: + def bar = super.foo diff --git a/tests/run/inline-trait-enclosing-supercall-in-inline-trait.scala b/tests/run/inline-trait-enclosing-supercall-in-inline-trait.scala new file mode 100644 index 000000000000..ba2627b8dab0 --- /dev/null +++ b/tests/run/inline-trait-enclosing-supercall-in-inline-trait.scala @@ -0,0 +1,13 @@ +trait A: + def foo = 10 + +class B extends A: + inline trait T: + def foo = B.super.foo + + class C2 extends T + +@main def Test = + val x = new B() + val y = new x.C2() + assert(y.foo == 10) diff --git a/tests/run/inline-trait-super-chain.scala b/tests/run/inline-trait-super-chain.scala new file mode 100644 index 000000000000..53ba24d8aa72 --- /dev/null +++ b/tests/run/inline-trait-super-chain.scala @@ -0,0 +1,23 @@ +inline trait A: + def foo = "A" + +inline trait B extends A: + override def foo = super.foo + +inline trait C extends B: + override def foo = "B" + +inline trait D extends C: + override def foo = super.foo + +inline trait E extends D: + override def foo = super.foo + +inline trait F extends E: + override def foo = super.foo + +class C1 extends F + +@main def Test = + val cl = C1() + assert(cl.foo == "B") From 4b1491f712c0ec5045793392532d160ac54831a7 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 27 Apr 2026 17:14:38 +0200 Subject: [PATCH 157/254] add another specialized trait case --- .../specialized-trait-simplest-implicit-type-name.scala | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tests/pos/specialized-trait-simplest-implicit-type-name.scala diff --git a/tests/pos/specialized-trait-simplest-implicit-type-name.scala b/tests/pos/specialized-trait-simplest-implicit-type-name.scala new file mode 100644 index 000000000000..39ca98b6456f --- /dev/null +++ b/tests/pos/specialized-trait-simplest-implicit-type-name.scala @@ -0,0 +1,8 @@ +//> using options -language:experimental.specializedTraits + +inline trait Foo[T: Specialized](val x: T) +inline trait Bar[T: Specialized] + +@main def main = + val x = new Foo(10) {} // Type name not provided explicitly; this should still be specialized to Int. + val y = new Bar() {} // This will be specialized to Nothing. From 94d12b1b408eb417e86bc07b3d40c0d1ecd01d38 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 27 Apr 2026 17:25:38 +0200 Subject: [PATCH 158/254] Make Specialized trait sealed --- library/src/scala/Specialized.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/scala/Specialized.scala b/library/src/scala/Specialized.scala index 791295b3dc8a..27c67aa37321 100644 --- a/library/src/scala/Specialized.scala +++ b/library/src/scala/Specialized.scala @@ -2,7 +2,7 @@ package scala import language.experimental.erasedDefinitions import scala.annotation.nowarn -trait Specialized[T] extends compiletime.Erased +sealed trait Specialized[T] extends compiletime.Erased object Specialized: /* @nowarn: New anonymous class definition will be duplicated at each inline site, From cb779dbb99ad3166ed4191ee0ba1751a39e2c5b5 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 27 Apr 2026 17:26:03 +0200 Subject: [PATCH 159/254] Add more test cases --- tests/neg/specialized-trait-nested-specialized.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/neg/specialized-trait-nested-specialized.scala b/tests/neg/specialized-trait-nested-specialized.scala index b764759ccaa4..54d2753a9bcb 100644 --- a/tests/neg/specialized-trait-nested-specialized.scala +++ b/tests/neg/specialized-trait-nested-specialized.scala @@ -18,3 +18,7 @@ inline def bar2 = new T1[List[Int]]() {} // ok inline def bar3[S] = new T1[List[List[S]]]() {} // error: S should be specialized inline def bar4[S: Specialized] = new T1[List[List[S]]]() {} // ok +inline def baz1[T] = new T1[T1[T]]() {} // error: T should be Specialized +inline def baz2[T](x: T1[T1[T]]) = 1 // error: T should be Specialized +inline def baz3[T: Specialized] = new T1[T1[T]]() {} // ok +inline def baz4[T: Specialized](x: T1[T1[T]]) = 1 // ok From fa18e6f7e70139f84d8736f26b8b59711f9823ab Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 28 Apr 2026 14:11:22 +0200 Subject: [PATCH 160/254] Tidy up a bit --- .../transform/DesugarSpecializedTraits.scala | 192 ++---------------- 1 file changed, 12 insertions(+), 180 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 8d25ad475e0e..5a174dabed8c 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -42,7 +42,6 @@ import dotty.tools.dotc.util.SrcPos import dotty.tools.dotc.core.Decorators.nestedMap import dotty.tools.dotc.core.NameOps.expandedName - class DesugarSpecializedTraits extends MacroTransform: override def phaseName: String = DesugarSpecializedTraits.name @@ -56,7 +55,6 @@ class DesugarSpecializedTraits extends MacroTransform: try super.run catch case _: CompilationUnit.SuspendException => () - override def newTransformer(using Context): Transformer = new Transformer { private def newInterfaceTrait(specialization: Specialization, specializations: SpecializedTraitCache): (ClassSymbol, SpecializedTraitCache) = { @@ -76,7 +74,7 @@ class DesugarSpecializedTraits extends MacroTransform: // Create new trait val parents = defn.ObjectType :: AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization).tpe // original trait, specialized to Foo[Int] - :: inheritedParents.map(replaceSpecializedSymbolsMap(specializations1).typeMap(_)) // parents of the original trait, specialized to Foo$sp$Int + :: inheritedParents.map(replaceSpecializedSymbolsMap(specializations1).typeMap(_)) // parents of the original trait, specialized to Foo$sp$Int val traitSymbol = newNormalizedClassSymbol( specialization.traitSymbol.owner, @@ -268,8 +266,6 @@ class DesugarSpecializedTraits extends MacroTransform: yield AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? }.getOrElse(tree) - /* case sel@Select(qualifier, name) if typeMap(sel.denot.info) != sel.denot.info => - Select(qualifier, name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX) */ case tree => tree } @@ -289,51 +285,13 @@ class DesugarSpecializedTraits extends MacroTransform: if transformedDef.symbol.allOverriddenSymbols.isEmpty && (transformedDef.symbol.owner.isSpecializedTraitInterface || transformedDef.symbol.owner.isSpecializedTraitImplementationClass) then transformedDef.symbol.flags = transformedDef.symbol.flags &~ Flags.Override transformedDef - /*case vd@ValDef(name, tpt, preRhs) => - val transformedDef = super.transform(vd).asInstanceOf[ValDef] - if transformedDef.symbol.info != mapType(transformedDef.symbol.info) && transformedDef.symbol.allOverriddenSymbols.nonEmpty then - val specializedSymbol = newSymbol( - transformedDef.symbol.owner, - transformedDef.symbol.name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX, - transformedDef.symbol.flags &~ Flags.Override, - info = mapType(transformedDef.symbol.info), - transformedDef.symbol.privateWithin, - transformedDef.symbol.coord, - transformedDef.symbol.nestingLevel - ).entered - ValDef(specializedSymbol.asTerm, transformedDef.rhs.changeOwner(transformedDef.symbol, specializedSymbol)) - else - transformedDef - - case dd@DefDef(name, paramss, tpt, preRhs) => - val transformedDef = super.transform(dd).asInstanceOf[DefDef] - - if transformedDef.symbol.info != mapType(transformedDef.symbol.info) && transformedDef.symbol.allOverriddenSymbols.nonEmpty then - val specializedSymbol = newSymbol( - transformedDef.symbol.owner, - transformedDef.symbol.name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX, - transformedDef.symbol.flags &~ Flags.Override, - info = mapType(transformedDef.symbol.info), - transformedDef.symbol.privateWithin, - transformedDef.symbol.coord, - transformedDef.symbol.nestingLevel - ).entered - - val rhsFun: List[List[Tree]] => Tree = paramss => - val oldParamSyms = transformedDef.paramss.flatten.map(_.symbol) - val newParamSyms = paramss.flatten.map(_.symbol) - transformedDef.rhs.subst(oldParamSyms, newParamSyms).changeOwner(transformedDef.symbol, specializedSymbol) - - DefDef(specializedSymbol.asTerm, rhsFun) - else - transformedDef*/ case impl@Template(constr, preParentsOrDerived, self, _) => impl.parents.foreach(p => p.tpe match { case Specialization(spec) if spec.hasSpecializedParams - && !impl.symbol.owner.isAnonymousClass // impl.symbol = the dummy class; owner is the actual class. + && !impl.symbol.owner.isAnonymousClass /* impl.symbol is the dummy local class; owner is the actual class. */ && !isSpecializationOf(impl.symbol.typeRef, p.tpe, allowImplementationClass = true) && !isImplementationOf(impl.symbol.owner.name, p.tpe.typeSymbol.name) && !impl.symbol.owner.isOneOf(InlineTrait) => @@ -342,37 +300,7 @@ class DesugarSpecializedTraits extends MacroTransform: } ) - /* - // If a class has a specialized member which was overriding a parent member, this override is lost because we specialize the types. - // E.g. def foo(Vec$sp$Int) cannot override def foo(Vec[Int]) because signatures must match exactly for overriding. - // However, specialized trait is based on the invariant that ∀T. T <: Foo[Int] => T <: Foo$sp$Int (and note that the reverse <= holds trivially by inheritance). - // This means it is safe to build bridge methods which simply apply the relevant casts so that we satisfy the interface, although we don't expect to call these. - def isMapped(t: Type) = mapType(t) != t - */ val mappedbody = impl.body.map(transform(_)) - - /* - val bridgeMethods = impl.body.collect { - case ddef@DefDef(name, paramss, _, _) if ddef.symbol.allOverriddenSymbols.nonEmpty && isMapped(ddef.symbol.info) => - // Any callers of the original method will have been redirected to the bridge method because it has a signature match with the method they were calling - val ddef2 = cpy.DefDef(ddef)( - rhs= - This(impl.symbol.owner.asClass).select(ddef.symbol.name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX) - .appliedToArgss( - ddef.termParamss.map( - params => params.map(p => - ref(p.symbol).cast(mapType(p.symbol.info))) - ) - ).cast(ddef.symbol.localReturnType) - ) - ddef2.symbol.rawParamss = ddef2.paramss.nestedMap(_.symbol) - ddef2 - - case vdef: ValDef if vdef.symbol.allOverriddenSymbols.nonEmpty && isMapped(vdef.symbol.info) => - cpy.ValDef(vdef)( - rhs = This(impl.symbol.owner.asClass).select(vdef.symbol.name ++ str.SPECIALIZED_METHOD_TARGET_NAME_SUFFIX).cast(vdef.symbol.info) - ) - } */ /* We need to map parents of non-specialized inline traits (see tests/pos/specialized-trait-partial-complete-specialization-with-return-type.scala, we need to map the A[Int] reference to A$sp$Int in B's parents) */ @@ -394,9 +322,9 @@ class DesugarSpecializedTraits extends MacroTransform: val specializations2 = specializations1.installNewInterfaceSymbols.installNewImplementationSymbols - // We have Vec$sp$Int extends Vec[Int] in order to do the inlining, but then remove this parent - // afterwards to avoid interface implementation problems (see tests/run/specialized-trait-as-parameter.scala, - // tests/run/specialized-trait-as-return-type.scala) + /* We have Vec$sp$Int extends Vec[Int] in order to do the inlining, but then remove this parent + afterwards to avoid interface implementation problems (see tests/run/specialized-trait-as-parameter.scala, + tests/run/specialized-trait-as-return-type.scala) */ extension (classTree: Tree) def updateParents(parentUpdater: List[Type] => List[Type]) = (classTree: @unchecked) match { case td@TypeDef(name, t@Template(constr, preParentsOrDerived, self, preBody)) => @@ -448,7 +376,7 @@ class DesugarSpecializedTraits extends MacroTransform: } override def transform(tree: Tree)(using Context): Tree = tree - match { // TODO: Is Package level processing really what we want? Given we are not going to output the classes somewhere else do we not really want either to deepFold the whole tree directly or do a more direct transform? + match { // TODO: Is Package level processing really what we want? Given we are going to output the classes somewhere else do we not really want either to deepFold the whole tree directly or do a more direct transform? case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. def checkType(t: Type, pos: SrcPos) = t.widen.dealias match { @@ -467,12 +395,6 @@ class DesugarSpecializedTraits extends MacroTransform: cpy.PackageDef(pkg)(pid, stats1) } - // TODO: There is a case where recursive expansion causes something to need an implementation where it didn't before. - - - // TODO: Try with just generating new Foo(100) with no function to pass it to and no other references to Foo. this may not work because we might not - // correctly detect it. - private def collectReferencedSpecializations(stats: List[Tree], specializations: SpecializedTraitCache)(using Context): SpecializedTraitCache = stats.foldLeft(specializations)((specializations, tree) => { tree.deepFold(specializations)((specializations, tree) => tree match @@ -514,8 +436,6 @@ class DesugarSpecializedTraits extends MacroTransform: }) } end DesugarSpecializedTraits - // TODO: Need to think carefully about the behaviour when we are integrating libraries - should the library generate the implementation classes or the user? - // In any case we need to read back in either the $sp$ classes or the $impl$ traits to be able to work with them. object DesugarSpecializedTraits: val name: String = "desugarSpecializedTraits" @@ -565,11 +485,11 @@ end DesugarSpecializedTraits Model: Contains two levels: - interface/implementation symbols we have found since the last installNewInterface/ImplementationSymbols call (i.e. typically on this iteration) ("new") - - Those we found prior to that call + - Those we found prior to that call, that were thus installed by it or previously - Invariant: (newImplementationSymbols \cup implementationSymbols) \subseteq (interfaceSymbols \cup newInterfaceSymbols). + Invariant: (newImplementationSymbols ∪ implementationSymbols) ⊆ (interfaceSymbols ∪ newInterfaceSymbols). This is enforced by only providing addInterface and addInterfaceAndImplementation, and allows the unchecked get in - getNeWImplementationSymbols. + getNewImplementationSymbols. */ @@ -649,8 +569,8 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(usi def hasSpecializedParams: Boolean = specializedTypeParams.nonEmpty - // If inline trait Foo[T] has a method taking another Foo[T] there's no point specializing the reference - // since the resulting sp$T$ would be the same as the starting trait. + /* If inline trait Foo[T] has a method taking another Foo[T] there's no point specializing the reference + since the resulting sp$T$ would be the same as the starting trait. */ def isSpecialized: Boolean = hasSpecializedParams && typeArguments.exists(!_.tpe.existsPart(_.typeSymbol.isTypeParam)) //) !tpt.symbol.isTypeParam) // .zip(traitSymbol.typeParams).forall((t, s) => t.tpe =:= s.typeRef)) @@ -699,28 +619,7 @@ object Specialization: } end Specialization -// Would be nice to define a Specialization class I think -// -> Map the specialized type params to Int etc -// -> Map the non-specialized type params to new type params -// -> Be a canonical representation so we can store that in a set -// -> Generate a name / string representation for use in new traits -// -> Get the specialized list to apply - - -// TODO: Fix name generation which doesn't work if the tpye isn't provided explicitly - - - - -// Generate impl instead of generating anonymous classes every time to avoid insane code bloat - // Do we really want the method definitions to live in the implementation classes or in the trait?| - // I think in the trait is fine but note that this only actually saves any space if we don't use anonymous classes (because those copy parent members automatically it seems) -// Need to make sure all my examples are up to date, consistent with what we do and what we want to do so that they are actually useful for the future. // Need to somehow make my naming a lot more consistent as well. -// Correctly generate names -// generate classes as well -// do we actually want to generate Iteratorsp$Int -// should we be worried about the results that we generate causing more stuff to be generated? // figure out why we generate the T version. // Try to see if we can do with only types and not trees // Synthesise Specialized instances so that people can't do stupid stuff like Specialized[Array[T]]. type x = Specialized[Array[Array[Int]]] @@ -730,76 +629,23 @@ end Specialization // TODO: Think carefully about use of primaryConstructor and the other appropriateConstructors call or whatever it was. -// Probably (tree)typemap - -// Would it be better to just copy rather than creating everything from scratch? I think this is right - -// 1. Figure out which specializations we need to generate -// 2. Generate ArrayIterator$sp$Int and ArrayIterator$impl$Int wherever they live -// 3. Replace ArrayIterator[Int] with ArrayIterator$sp$Int -// 4. Replace new ArrayIterator[Int](xs) {} with new ArrayIterator$impl$Int(xs) {} -// 5. Somehow figure out the caching -// 6. Delete references to Specialized I guess - -// Synthesize Specialized[T] instances. // TODO: Need to try with a bigger project with multiple packages later on to see if we get the behaviour that we are expecting to get in terms of the classes that we generate. -// Need to ban all of these but we will do that earlier I guess? -// Vec[Vec[Int]] hehe <- fine -// Vec[S, S[T]: Specialized] <- banned -// Vec[S, T[T]: Specialized] <- banned -// Vec[Array[T]: Specialized] <- banned - - // TODO: Prune the generated anonymous classes. - // need to test with explicit evidence / our own custom type classes -// TODO: Make sure name encoding is fully qualified - e.g. potential for conflicts if we define our own class Int. - // // TODO: check that we have a single type var only +// TODO: check that we have a single type var only // trait Vec$Sp[S] extends Vec[S, Int, Int, Int, Int] // inline trait Two[S: Specialized] extends Vec$sp[S] -// does mean that any methods in the original trait lose their specialization - maybe we /should/ make the generated traits inline? -// hmm but we can't do that because we need the methods called on the Vec$Sp trait to be the specialized ones - that is really important. // Could potentially copy over the inline based on whether Two is inline or not? Needs some thought. - // In the case of foo[S](a: Vec[S, Int, Int, Int, Int]) I think we ideally do want this because we should be able to get speed gains by accessing the specialized members -// Should we allow these? I think they are all fine -// inline trait Two[S: Specialized] extends Vec[S, Int, Int, Int, Int] -// inline trait Two[S] extends Vec[S, Int, Int, Int, Int] // Maybe worth warning? Perhaps behind an extra flag -// trait Two[S] extends Vec[S, Int, Int, Int, Int] -// TODO: We want a self reference case where Vec[T] has some method that takes a Vec[Int] for example. -// TODO: Fix broken "inline" tests // TODO: Only specialize if there is some material increase in specialization - I think only if at least one new parameter gets fully specialized // Maybe it is better to not allow partial specializations -- we can think about that. -// TODO: Need to add the rule that we need to directly extend inline traits to pass parameters in the same way as for normal traits -// See inline-trait-param-no-shadow.scala -// could ban normal traits from mixing in inline traits on the basis that -// it's a very unlikely usecase and would fix the mixin problem - although -// we could just try and fix the mixin problem properly. - -// TOOD: probably need to do this from Timothée: - // - private members are not renamed, only private parameter accessors are; -// this needs to be changed so that all overridable private members are renamed -// Implement postphase checks for inline traits // TODO: Don't synthesize specialized instances for random generic types probably - as Hamza said we want to be able to control the specialization -// TODO: If we are to ban trait extends inline trait then need to fix some tests. -// TODO: Check that when we extend parents we actually do so in the specialized forms. -// TODO: Fix specialized-trait-collections-example.scala -// TODO: Test extensively with inline methods and inline traits. - -// TODO: - // Need to enforce these: -// - can extend only a single specialized trait, -// - cannot mix in further classes or traits, and -// - cannot contain member definitions. -// I'm not sure we strictly need them though. - -// TODO: Put classes onto the classpath as desired. // Concerns: // - The superclass of `C` is a top class, or `C` itself is a top class. @@ -807,21 +653,7 @@ end Specialization // If we can manage to get rid of the inheritance there that could be helpful in terms of avoiding multiple values // BUT: generate a version which is with just inline traits that has this problem as well. -// Need to deal with the caching at some point // These implementation classes are type correct as long as we inject the knowledge that a specialization trait // like `Seq$sp$Int` is equal to its parameterized version `Seq[Int]` -// Warning for dropping Specialized qualifier or it doesn't compile? -// TODO: Make name consistent for tests. -// TODO: In order to fix Foo extends Bar (banned for now) -// // case impl@Template(constr, preParentsOrDerived, self, _) => -// cpy.Template(impl)(body = impl.body.map(transform(_)), -// parents = // CAN POTENTIALLY MOVE THE OWNER CALL UP HERE. -// impl.parents.map(p => if isSpecializationOf(impl.symbol.typeRef, p.tpe, allowImplementationClass = true) then {println(impl.symbol.typeRef); println(p.tpe); p} else transform(p))) -// case tree => super.transform(tree) -// } -// Plus need another case in the normal map where you add sp trait as a parent as well as the original trait, AND update symbols. (or maybe switch to impl calss also possibhle). - -// end DesugarSpecializedTraits // Also delete the other members that already got inlined or maybe we don't care. -// extend both traits From fcbcfaee69b91b99a9b77323daab0e1dc02acac5 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 28 Apr 2026 14:11:34 +0200 Subject: [PATCH 161/254] Add a couple more tests --- ...lized-trait-specialized-incorrect-usage.scala | 2 ++ ...rait-argument-specializes-current-trait.scala | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 tests/neg/specialized-trait-specialized-incorrect-usage.scala create mode 100644 tests/run/specialized-trait-argument-specializes-current-trait.scala diff --git a/tests/neg/specialized-trait-specialized-incorrect-usage.scala b/tests/neg/specialized-trait-specialized-incorrect-usage.scala new file mode 100644 index 000000000000..8e7487afbff5 --- /dev/null +++ b/tests/neg/specialized-trait-specialized-incorrect-usage.scala @@ -0,0 +1,2 @@ +//> using options -language:experimental.specializedTraits +def x: Specialized[Int] = new Specialized[Int] {} // error: Cannot extend sealed trait Specialized in a different source file diff --git a/tests/run/specialized-trait-argument-specializes-current-trait.scala b/tests/run/specialized-trait-argument-specializes-current-trait.scala new file mode 100644 index 000000000000..b3e067793f2c --- /dev/null +++ b/tests/run/specialized-trait-argument-specializes-current-trait.scala @@ -0,0 +1,16 @@ +//> using options -language:experimental.specializedTraits + +inline trait Vec[T: Specialized](elems: Array[T]): + def length = elems.length + + def apply(i: Int): T = elems(i) + + def lengthOfOtherVector(other: Vec[Int]): Int = + other.length + +object Test: + def main(args: Array[String]) = + val x = new Vec[Int](Array(1, 2, 3, 4, 5)) {} + val y = new Vec[Int](Array(3, 4, 5, 6, 7, 9)) {} + assert(x.lengthOfOtherVector(y) == 6) + assert(y.lengthOfOtherVector(x) == 5) From 2461bd49d70e85bf705eba9c451f153103705bfd Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 29 Apr 2026 12:18:27 +0200 Subject: [PATCH 162/254] fix: Add missing flag --- .../dotty/tools/dotc/transform/DesugarSpecializedTraits.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 5a174dabed8c..27008d9eff8c 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -182,7 +182,7 @@ class DesugarSpecializedTraits extends MacroTransform: init.setParamss(typeParams :: valueParams) - val paramAccessorss = valueParams.map(params => params.map(_.copy(owner = classSymbol, flags= Flags.LocalParamAccessor))) + val paramAccessorss = valueParams.map(params => params.map(s => s.copy(owner = classSymbol, flags=s.flags|Flags.LocalParamAccessor))) paramAccessorss.foreach(_.foreach(classSymbol.enter(_))) init.info = tm2(specialization.traitSymbol.primaryConstructor.info.appliedTo(specialization.typeArguments.map(_.tpe))) From f76526d57d2d26ef2ba8061f9bfb48c0943fcabc Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 29 Apr 2026 12:20:10 +0200 Subject: [PATCH 163/254] Add comment to benchmark --- .../dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala index 749ca0fec274..7ee46bb5f9c4 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala @@ -1,7 +1,9 @@ // Run with: scala-cli --power --jmh bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala // May have to run it again / delete .scala-build and rerun if you get a class not found // error from scala-cli first time - the --jmh flag is still experimental. -// Don't forget to publish the compiler first and check that the version below corresponds to the generated version. +// Don't forget to publish the compiler first and check that the version below corresponds to the generated version, +// as well as to kill the bloop server if you are republishing the same version as before. +// scala-cli --power bloop exit //> using scala 3.8.4-RC1-bin-SNAPSHOT-nonbootstrapped //> using options -language:experimental.specializedTraits From 430b64621182d2a19c607f53feae0a5c698eaad0 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 3 May 2026 17:26:32 +0200 Subject: [PATCH 164/254] Make sure RefChecks is actually fully after PruneInlineTraits --- compiler/src/dotty/tools/dotc/Compiler.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 67392d7c7fc2..998ce45fffe4 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -73,9 +73,9 @@ class Compiler { new InlineVals, // Check right hand-sides of an `inline val`s new ExpandSAMs, // Expand single abstract method closures to anonymous classes new ElimRepeated, // Rewrite vararg parameters and arguments - new RefChecks, // Various checks mostly related to abstract members and overriding new DropForMap, // Drop unused trailing map calls in for comprehensions new PruneInlineTraits) :: // Remove right-hand side of definitions in inline traits + List(new RefChecks) :: // Various checks mostly related to abstract members and overriding List(new init.Checker) :: // Check initialization of objects List(new ProtectedAccessors, // Add accessors for protected members new ExtensionMethods, // Expand methods of value classes with extension methods From 02a4e56b6290374ed5a61888afbb72f0d15a37ba Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 3 May 2026 17:45:23 +0200 Subject: [PATCH 165/254] Explicitly deal with retained inline methods in inline traits --- compiler/src/dotty/tools/dotc/transform/PostTyper.scala | 2 +- .../dotty/tools/dotc/transform/PruneInlineTraits.scala | 2 +- ...ait-inline-overrides-non-inline-retained-inline.scala | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 tests/neg/inline-trait-inline-overrides-non-inline-retained-inline.scala diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 6f9444fb7a21..8e760dcf8df3 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -292,7 +292,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => private def checkInlTraitPrivateMemberIsLocal(tree: Tree)(using Context): Unit = if tree.symbol.owner.isInlineTrait && tree.symbol.isAllOf(Private, butNot = Local) then - report.error(em"implementation restriction: inline traits cannot have non-local private members", tree.srcPos) + report.error(em"implementation restriction: inline traits cannot have non-local private members. This also means no retained inline methods.", tree.srcPos) private def transformSelect(tree: Select, targs: List[Tree])(using Context): Tree = { val qual = tree.qualifier diff --git a/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala index 611c195f6088..9aefcad316dc 100644 --- a/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala @@ -50,7 +50,7 @@ class PruneInlineTraits extends MiniPhase with SymTransformer { thisTransform => private def isDeletable(sym: SymDenotation)(using Context): Boolean = !sym.isType && sym.owner.isInlineTrait - && (sym.is(Local) || sym.is(Inline)) + && (sym.is(Local) || (sym.is(Inline) && !sym.isRetainedInline)) // isRetainedInline not an issue while we block non-local privates as retainedBody is non local private. && !sym.is(Param) && !sym.is(ParamAccessor) } diff --git a/tests/neg/inline-trait-inline-overrides-non-inline-retained-inline.scala b/tests/neg/inline-trait-inline-overrides-non-inline-retained-inline.scala new file mode 100644 index 000000000000..3d400831c81b --- /dev/null +++ b/tests/neg/inline-trait-inline-overrides-non-inline-retained-inline.scala @@ -0,0 +1,9 @@ +inline trait A: + def x: Int + +inline trait B extends A: + override inline def x = 10 // error: implementation restriction: inline traits cannot have non-local private members. This also means no retained inline methods. + +@main def Test = + val x: A = new B() {} + assert(x.x == 10) From 3fc202afc6a79cd2286880bd664edfc22121d82e Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 3 May 2026 17:48:44 +0200 Subject: [PATCH 166/254] Add isSpecializedTrait --- compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 5dd7da032552..c50c07e7f2de 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -27,6 +27,7 @@ import cc.{CapturingType, derivedCapturingType, stripCapturing} import scala.annotation.internal.sharable import scala.compiletime.uninitialized +import dotty.tools.dotc.transform.Specialization object SymDenotations { @@ -1057,6 +1058,9 @@ object SymDenotations { def isInlineTrait(using Context): Boolean = isAllOf(InlineTrait) + + def isSpecializedTrait(using Context): Boolean = + Specialization.isSpecializedTrait(symbol) /** Does this method or field need to be retained at runtime */ def isRetainedInline(using Context): Boolean = From 003d28e5af87faf7f4085e4f4dfa50bc18c7991f Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 3 May 2026 17:56:15 +0200 Subject: [PATCH 167/254] Add another illegal trait inlining cycle case --- .../neg/inline-trait-infinite-inline-triangle-cycle.scala | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/neg/inline-trait-infinite-inline-triangle-cycle.scala diff --git a/tests/neg/inline-trait-infinite-inline-triangle-cycle.scala b/tests/neg/inline-trait-infinite-inline-triangle-cycle.scala new file mode 100644 index 000000000000..a5bfb9dd8c70 --- /dev/null +++ b/tests/neg/inline-trait-infinite-inline-triangle-cycle.scala @@ -0,0 +1,7 @@ +inline trait C[S]: + def v(x: S): S = x + def w: Unit = + val x = new D[S] {} // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. + println("w") + +inline trait D[S] extends C[S] From 9c8f2f8402135e66e4e43ea16079bf742df9e08d Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 3 May 2026 17:57:04 +0200 Subject: [PATCH 168/254] Actually we do allow loops if the traits involved are specialized --- ...lementation-required-loop-bad-manual.scala | 19 +++++++++++++++++++ ...ses-implementation-required-loop-bad.scala | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad-manual.scala rename tests/{neg => pos}/specialized-trait-inlining-causes-implementation-required-loop-bad.scala (68%) diff --git a/tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad-manual.scala b/tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad-manual.scala new file mode 100644 index 000000000000..9c7f271f47fa --- /dev/null +++ b/tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad-manual.scala @@ -0,0 +1,19 @@ +//> using options -language:experimental.specializedTraits + +// Contrast with tests/neg/specialized-trait-inlining-causes-implementation-required-loop-bad.scala. +// This one is not allowed because it will loop forever when specializing. + +// A[Char] => A$sp$Char => C$sp$Char, but only once we inline the body of C$sp$Char do we realise that we need +// C$impl$Char as well. + +inline trait C[S: Specialized]: + def v(x: S): S = x + def w: Unit = + class D extends C[S] // ok: This one is actually also fine because we can make D extend C$sp$Char once we've inlined it into C$impl$Int + println("w") + +inline trait A[T: Specialized]: + def x(y: C[T]): Unit = println("x") + +def main = + val y = new A[Char] {} diff --git a/tests/neg/specialized-trait-inlining-causes-implementation-required-loop-bad.scala b/tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad.scala similarity index 68% rename from tests/neg/specialized-trait-inlining-causes-implementation-required-loop-bad.scala rename to tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad.scala index 17f20331557c..281c1beee93a 100644 --- a/tests/neg/specialized-trait-inlining-causes-implementation-required-loop-bad.scala +++ b/tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad.scala @@ -6,7 +6,7 @@ inline trait C[S: Specialized]: def v(x: S): S = x def w: Unit = - val x = new C[S] {} // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. + val x = new C[S] {} // Actually ok because $impl$ classes are generated outside of the inline trait; class C$impl$Char will just create an instance of itself (this is allowed) println("w") inline trait A[T: Specialized]: From ce93c09a36187be61ad82cda57eec523caf46892 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 3 May 2026 18:31:24 +0200 Subject: [PATCH 169/254] Break out loop checks --- .../dotty/tools/dotc/inlines/Inlines.scala | 86 ++++++++++--------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index f2e91c454bd7..60c494268434 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -341,47 +341,55 @@ object Inlines: case cls @ tpd.TypeDef(_, impl: Template) => checkInlineTraitOverrides(cls.symbol.asClass) val clsOverriddenSyms = cls.symbol.info.decls.toList.flatMap(_.allOverriddenSymbols).toSet - val newDefs = inContext(ctx.withOwner(cls.symbol)) { - inlineTraitAncestors(cls).foldLeft((List.empty[Tree], impl.body)){ - case ((inlineDefs, childDefs), parent) => - if cls.symbol.ownersIterator.contains(symbolFromParent(parent)) then - // TODO: This appears at the inline trait D line rather than the line corresponding to the inlining - should we be worried ? - report.error("Inlining of inline traits looped, which will create an infinitely long program. This is not allowed.", cls.sourcePos) - (inlineDefs, childDefs) - else - val parentTraitInliner = InlineParentTrait(parent) - - // Update self type - val newSelfType = cls.symbol.asClass.classDenot.givenSelfType & parentTraitInliner.inlinedSelfType.extractAnnotationsAndOpaqueTypeAliases - cls.symbol.info = cls.symbol.asClass.classInfo.derivedClassInfo(selfInfo=newSelfType) - - // Inline body - val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) - val inlinedDefs1 = inlineDefs ::: parentTraitInliner.expandDefs(overriddenSymbols) - cls.symbol.flags = updateFlagsFromInlinedParent(cls.symbol.flags, parent.symbol.flags) - - val childDefs1 = parentTraitInliner.adaptSuperCalls(childDefs) - (parentTraitInliner.adaptSuperCalls(inlinedDefs1), childDefs1) + val ancestors = inlineTraitAncestors(cls) + val cycleFound = ancestors.exists { parent => + if cls.symbol.ownersIterator.contains(symbolFromParent(parent)) then + // TODO: This appears at the inline trait D line rather than the line corresponding to the inlining - should we be worried ? + report.error("Inlining of inline traits looped, which will create an infinitely long program. This is not allowed.", cls.sourcePos) + cls.symbol.ownersIterator.contains(symbolFromParent(parent)) + } + + if cycleFound then cls + else { + val newDefs = inContext(ctx.withOwner(cls.symbol)) { + ancestors.foldLeft((List.empty[Tree], impl.body)){ + case ((inlineDefs, childDefs), parent) => + val parentTraitInliner = InlineParentTrait(parent) + + // Update self type + val newSelfType = cls.symbol.asClass.classDenot.givenSelfType & parentTraitInliner.inlinedSelfType.extractAnnotationsAndOpaqueTypeAliases + cls.symbol.info = cls.symbol.asClass.classInfo.derivedClassInfo(selfInfo=newSelfType) + + // Inline body + val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) + val inlinedDefs1 = inlineDefs ::: parentTraitInliner.expandDefs(overriddenSymbols) + cls.symbol.flags = updateFlagsFromInlinedParent(cls.symbol.flags, parent.symbol.flags) + + val childDefs1 = parentTraitInliner.adaptSuperCalls(childDefs) + (parentTraitInliner.adaptSuperCalls(inlinedDefs1), childDefs1) + } } + + val newbody = newDefs._1 ::: newDefs._2 + val paramAccessors = newbody.filter(_.symbol.is(ParamAccessor)) + + for pacc <- paramAccessors + otherstat <- newbody if !otherstat.symbol.is(ParamAccessor) && otherstat.denot.matches(pacc.denot.asSingleDenotation) + do report.error(s"Inlining of inline trait created name conflict on ${pacc.denot.name}. Constructor parameters of inline receivers may not collide with members of inline traits.", pacc.srcPos) + + val impl1 = cpy.Template(impl)(body = newbody, + self= + if cls.symbol.asClass.classDenot.givenSelfType.exists then + cpy.ValDef(impl.self)(tpt= + TypeTree(cls.symbol.asClass.classDenot.givenSelfType) + .withSpan(impl.self.tpt.span.orElse(cls.symbol.span))) + .withSpan(impl.self.span.orElse(cls.symbol.span)) + .cloneIn(cls.symbol.source) + else impl.self + ) + + cpy.TypeDef(cls)(rhs = impl1) } - val newbody = newDefs._1 ::: newDefs._2 - val paramAccessors = newbody.filter(_.symbol.is(ParamAccessor)) - - for pacc <- paramAccessors - otherstat <- newbody if !otherstat.symbol.is(ParamAccessor) && otherstat.denot.matches(pacc.denot.asSingleDenotation) - do report.error(s"Inlining of inline trait created name conflict on ${pacc.denot.name}. Constructor parameters of inline receivers may not collide with members of inline traits.", pacc.srcPos) - - val impl1 = cpy.Template(impl)(body = newbody, - self= - if cls.symbol.asClass.classDenot.givenSelfType.exists then - cpy.ValDef(impl.self)(tpt= - TypeTree(cls.symbol.asClass.classDenot.givenSelfType) - .withSpan(impl.self.tpt.span.orElse(cls.symbol.span))) - .withSpan(impl.self.span.orElse(cls.symbol.span)) - .cloneIn(cls.symbol.source) - else impl.self - ) - cpy.TypeDef(cls)(rhs = impl1) case _ => cls } From e51284cc54ce520c1b62ee09e655fe3a235f8c6f Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 3 May 2026 18:48:58 +0200 Subject: [PATCH 170/254] Specify rules for specialized trait extension, allowing object/class/trait, and banning illegal anonymous class inheritances --- .../dotty/tools/dotc/inlines/Inlines.scala | 15 +- .../transform/DesugarSpecializedTraits.scala | 227 +++++++++++------- docs/_docs/internals/specialized-traits.md | 40 ++- ...d-trait-anonymous-class-breaks-rules.scala | 27 +++ ...ait-class-extends-specialized-trait.scala} | 3 +- ...ait-object-extends-specialized-trait.scala | 9 + ...rait-trait-extends-specialized-trait.scala | 12 + 7 files changed, 230 insertions(+), 103 deletions(-) create mode 100644 tests/neg/specialized-trait-anonymous-class-breaks-rules.scala rename tests/{neg/specialized-trait-inheritance.scala => pos/specialized-trait-class-extends-specialized-trait.scala} (69%) create mode 100644 tests/pos/specialized-trait-object-extends-specialized-trait.scala create mode 100644 tests/pos/specialized-trait-trait-extends-specialized-trait.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 60c494268434..c54ca346f03e 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -86,7 +86,7 @@ object Inlines: // && !member.symbol.is(Deferred) // Also inline interfaces (see specialized-trait-collections-example.scala) /** Should call be inlined in this context? */ - def needsInlining(tree: Tree)(using Context): Boolean = + def needsInlining(tree: Tree, allowSpecializedTraits: Boolean = false)(using Context): Boolean = def isInlineableInCtx = StagingLevel.level == 0 && ( @@ -101,10 +101,10 @@ object Inlines: tree match case Block(_, expr) => - needsInlining(expr) + needsInlining(expr, allowSpecializedTraits) case tdef @ TypeDef(_, impl: Template) => // !tdef.symbol.isInlineTrait && - impl.parents.map(symbolFromParent).exists(_.isInlineTrait) && isInlineableInCtx + impl.parents.map(symbolFromParent).exists(sym => sym.isInlineTrait && (allowSpecializedTraits || !sym.isSpecializedTrait)) && isInlineableInCtx case _ => def isUnapplyExpressionWithDummy: Boolean = // The first step of typing an `unapply` consists in typing the call @@ -120,10 +120,10 @@ object Inlines: private[dotc] def symbolFromParent(parent: Tree)(using Context): Symbol = if parent.symbol.isConstructor then parent.symbol.owner else parent.tpe.typeSymbol - private def inlineTraitAncestors(cls: TypeDef)(using Context): List[Tree] = cls match { + private def inlineTraitAncestors(cls: TypeDef, allowSpecialized: Boolean)(using Context): List[Tree] = cls match { case tpd.TypeDef(_, tmpl: Template) => val parentTrees: Map[Symbol, Tree] = tmpl.parents.map(par => symbolFromParent(par) -> par).toMap.filter(_._1.isInlineTrait) - val ancestors: List[ClassSymbol] = cls.tpe.baseClasses.filter(sym => sym.isInlineTrait && sym != cls.symbol) + val ancestors: List[ClassSymbol] = cls.tpe.baseClasses.filter(sym => sym.isInlineTrait && sym != cls.symbol && (allowSpecialized || !sym.isSpecializedTrait) ) ancestors.flatMap(ancestor => def baseTree = cls.tpe.baseType(ancestor) match @@ -333,7 +333,7 @@ object Inlines: ) OverridingPairsChecker(clsSym, clsSym.thisType).checkAll(checkInlineTraitOverride) - def inlineParentInlineTraits(cls: Tree)(using Context): Tree = + def inlineParentInlineTraits(cls: Tree, allowSpecialized: Boolean=false)(using Context): Tree = cls match { // case cls @ tpd.TypeDef(_, impl: Template) if cls.symbol.owner.ownersIterator.exists(_.isInlineTrait) => // TODO: We can relax this if we use a seen list to avoid cycles // report.error("May not inline an inline trait into a class defined inside another inline trait. If you really need to do this, make the inline trait Specialized or move the class definition outside the trait.", cls.srcPos) @@ -341,7 +341,7 @@ object Inlines: case cls @ tpd.TypeDef(_, impl: Template) => checkInlineTraitOverrides(cls.symbol.asClass) val clsOverriddenSyms = cls.symbol.info.decls.toList.flatMap(_.allOverriddenSymbols).toSet - val ancestors = inlineTraitAncestors(cls) + val ancestors = inlineTraitAncestors(cls, allowSpecialized) val cycleFound = ancestors.exists { parent => if cls.symbol.ownersIterator.contains(symbolFromParent(parent)) then // TODO: This appears at the inline trait D line rather than the line corresponding to the inlining - should we be worried ? @@ -1162,7 +1162,6 @@ object Inlines: // Check if oldSym has been inlined into childClasslike def inlinedSymbolIsRegistered(oldSym: Symbol, childClasslike: Type) = inlinedTraitSymbols.contains((oldSym, childClasslike)) - end InlineTraitState end Inlines diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 27008d9eff8c..e5e3aea2d2e1 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -41,8 +41,13 @@ import dotty.tools.dotc.core.Constants.Constant import dotty.tools.dotc.util.SrcPos import dotty.tools.dotc.core.Decorators.nestedMap import dotty.tools.dotc.core.NameOps.expandedName +import dotty.tools.dotc.core.DenotTransformers.DenotTransformer +import dotty.tools.dotc.core.Denotations.SingleDenotation -class DesugarSpecializedTraits extends MacroTransform: +class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: + + + override def transform(ref: SingleDenotation)(using Context): SingleDenotation = ref override def phaseName: String = DesugarSpecializedTraits.name override def description: String = DesugarSpecializedTraits.description @@ -190,16 +195,7 @@ class DesugarSpecializedTraits extends MacroTransform: fixConstructor(init, classSymbol) val typer = Typer(ctx.nestingLevel + 1) // TODO: actually get these from the user. - val newParamss = - specialization.traitSymbol.primaryConstructor.paramSymss.tail.zip(paramAccessorss.map(_.map(ref))) // skip the type params - .map((paramSyms, paramAccessors) => - paramSyms.zip(paramAccessors).map( - (paramSym, accessor) => - if paramSym.name.asTermName.is(ContextBoundParamName) - then typer.implicitArgTree(tm(paramSym.info), paramSym.span) // TODO: Fix spans throughout - else accessor - ) - ) + val newParamss = paramAccessorss.nestedMap(ref(_)) val newParams1 = if (newParamss.length == 1) then newParamss ++ List(List()) else newParamss @@ -225,7 +221,7 @@ class DesugarSpecializedTraits extends MacroTransform: classDef } - private def replaceSpecializedSymbolsMap(specializations: SpecializedTraitCache) = + private def replaceSpecializedSymbolsMap(specializations: SpecializedTraitCache)(using Context) = val typeMap = new TypeMap: def apply(t: Type) = t match { case Specialization(spec) => @@ -238,27 +234,58 @@ class DesugarSpecializedTraits extends MacroTransform: def treeMap(tree: Tree): Tree = tree match { // Replace (anonymous class version of) new Foo[Int] {} with new Foo$impl$Int.asInstanceOf[Foo$sp$Int] - case Block(List(an@TypeDef(anon, Template(_, parentCalls: List[Tree], _, _))), + case Block(List(an@TypeDef(anon, tmpl@Template(_, parentCalls: List[Tree], _, _))), Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => - parentCalls match { - case _ :+ Apply(Apply(tpe, ctorArgs), ev) => // extends Object, parents of spec trait, spec trait - val spec = Specialization.unapply(t.tpe).get - { // We don't replace non-specialized anonymous class instantiations e.g. new Foo[T] where T is defined in the enclosing scope. - for (specializedSymbol <- specializations.getImplementationSymbol(spec)) - yield Typed(Apply(Apply(Select(New(ref(specializedSymbol)),ctor).appliedToTypeTrees(spec.unspecializedTypeArgs), ctorArgs.map(_.changeNonLocalOwners(an.symbol.owner))), ev), t) - }.getOrElse(tree) - case _ => tree - } - - // Replace class Bar extends Foo[Int](params) with class Bar extends Foo$sp$Int(params) - // TODO: Why do we still have this case if we don't allow this pattern? - // Note: We always drop the evidence params when creating these new specialized traits so we know that there are none, but we may need to revisit this if we decide we do want to copy the evidence parameters over - case Apply(TypeApply(fun@Select(New(tpt), init), args), ev) if fun.symbol.isConstructor => - val spec = Specialization(fun.symbol.owner, args) - { - for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) - yield New(ref(specializedSymbol)).select(init).appliedToTypeTrees(spec.unspecializedTypeArgs) - }.getOrElse(tree) + + def deandify(tp: Type): Iterator[Type] = tp match + case AndType(l, r) => deandify(l) ++ deandify(r) + case _ => Iterator.single(tp) + + t.tpe match { + case a: AndType => /* Multiple mixed in traits will be typed as an AndType */ + deandify(a).foreach(trt => + Specialization.unapply(trt).foreach {spec => + if spec.hasSpecializedParams then + report.error("Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like.", an.srcPos) + } + ) + tree + case tpe => + Specialization.unapply(tpe).map(spec => + { + if spec.hasSpecializedParams then + if tmpl.body.filterNot(x => x.symbol.name.is(ContextBoundParamName)).nonEmpty then // Only allowed to contain evidence parameters + report.error("Anonymous classes acting as instances of Specialized traits may not have additional members; you can make a named object instead if you like.", an.srcPos) + + parentCalls match { + case (obj :: parentsOfSpecTrait) :+ Apply(Apply(tpe, ctorArgs), ev) if (obj.symbol.owner == ctx.definitions.ObjectClass) && (parentsOfSpecTrait.forall(x => spec.traitSymbol.asClass.parentSyms.exists(p => p == x.symbol.owner))) => + specializations.getImplementationSymbol(spec).map( specializedSymbol => + Typed(Apply(Apply(Select(New(ref(specializedSymbol)),ctor).appliedToTypeTrees(spec.unspecializedTypeArgs), ctorArgs.map(_.changeNonLocalOwners(an.symbol.owner))), ev), t) + ).getOrElse(tree) // We don't replace non-specialized anonymous class instantiations e.g. new Foo[T] where T is defined in the enclosing scope. + case _ => + report.error("Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like.", an.srcPos) + tree + } + else + tree + }).getOrElse(tree) + } + + // Replace class/object Bar extends Foo[Int](params) with class/object Bar extends Foo$sp$Int(params) + case app @ Apply(_, _) => tpd.methPart(app) match { + case fun @ Select(New(tpt), init) if fun.symbol.isConstructor => + val argss = tpd.allArgss(tree) + argss match { + case typeArgs :: valueArgss => + val spec = Specialization(fun.symbol.owner, typeArgs) + { + for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) + yield New(ref(specializedSymbol)).select(init).appliedToTypeTrees(spec.unspecializedTypeArgs).appliedToNone + }.getOrElse(tree) + case _ => tree + } + case _ => tree + } // Replace AppliedTypeTree instances in code case Specialization(spec) => { @@ -275,45 +302,47 @@ class DesugarSpecializedTraits extends MacroTransform: case dd@DefDef(name, paramss, tpt, preRhs) => val transformedDef = super.transform(dd) transformedDef.symbol.info = mapType(transformedDef.symbol.info) - if transformedDef.symbol.allOverriddenSymbols.isEmpty && (transformedDef.symbol.owner.isSpecializedTraitInterface || transformedDef.symbol.owner.isSpecializedTraitImplementationClass) then - transformedDef.symbol.flags = transformedDef.symbol.flags &~ Flags.Override transformedDef case vd@ValDef(name, tpt, preRhs) => val transformedDef = super.transform(vd) transformedDef.symbol.info = mapType(transformedDef.symbol.info) - if transformedDef.symbol.allOverriddenSymbols.isEmpty && (transformedDef.symbol.owner.isSpecializedTraitInterface || transformedDef.symbol.owner.isSpecializedTraitImplementationClass) then - transformedDef.symbol.flags = transformedDef.symbol.flags &~ Flags.Override transformedDef - case impl@Template(constr, preParentsOrDerived, self, _) => - impl.parents.foreach(p => - p.tpe match { - case Specialization(spec) if - spec.hasSpecializedParams - && !impl.symbol.owner.isAnonymousClass /* impl.symbol is the dummy local class; owner is the actual class. */ - && !isSpecializationOf(impl.symbol.typeRef, p.tpe, allowImplementationClass = true) - && !isImplementationOf(impl.symbol.owner.name, p.tpe.typeSymbol.name) - && !impl.symbol.owner.isOneOf(InlineTrait) => - report.error("Specialized traits may only be extended by anonymous class instances or inline traits.", impl.srcPos) - case _ => - } - ) - + case impl@Template(constr, preParentsOrDerived, self, _) => val mappedbody = impl.body.map(transform(_)) + val mappedconstr = transform(impl.constr).asInstanceOf[DefDef] /* We need to map parents of non-specialized inline traits (see tests/pos/specialized-trait-partial-complete-specialization-with-return-type.scala, we need - to map the A[Int] reference to A$sp$Int in B's parents) */ - val mappedparents = impl.parents.map(transform(_)) + to map the A[Int] reference to A$sp$Int in B's parents). For our implementation classes and interface traits we don't want to map as we will delete parents after. */ + val mappedparents = if impl.symbol.owner.isSpecializedTraitImplementationClass || impl.symbol.owner.isSpecializedTraitInterface then impl.parents else impl.parents.map(transform(_)) val oldInfo = impl.symbol.owner.info.asInstanceOf[ClassInfo] - impl.symbol.owner.info = oldInfo.derivedClassInfo(declaredParents = oldInfo.declaredParents.map(mapType(_))) + impl.symbol.owner.info = oldInfo.derivedClassInfo(declaredParents = if impl.symbol.owner.isSpecializedTraitImplementationClass || impl.symbol.owner.isSpecializedTraitInterface then oldInfo.declaredParents else oldInfo.declaredParents.map(mapType(_))) - cpy.Template(impl)(body = mappedbody, parents = mappedparents) + cpy.Template(impl)(body = mappedbody, parents = mappedparents, constr = mappedconstr) case tree => super.transform(tree) } } end replaceSpecializedSymbolsMap + /* Override flags can be generated by inline trait inlining, but after removing the Foo[Int] parent the corresponding members no longer override members in their parents. + Therefore we need to remove them. */ + def removeRedundantOverridesMap = new TreeTypeMap(treeMap = + tree => tree match { + case dd@DefDef(name, paramss, tpt, preRhs) => + if dd.symbol.exists && dd.symbol.allOverriddenSymbols.isEmpty && (dd.symbol.owner.isSpecializedTraitInterface || dd.symbol.owner.isSpecializedTraitImplementationClass) then + dd.symbol.flags = dd.symbol.flags &~ Flags.Override + dd + + case vd@ValDef(name, tpt, preRhs) => + if vd.symbol.exists && vd.symbol.allOverriddenSymbols.isEmpty && (vd.symbol.owner.isSpecializedTraitInterface || vd.symbol.owner.isSpecializedTraitImplementationClass) then + vd.symbol.flags = vd.symbol.flags &~ Flags.Override + vd + + case tree => tree + } + ) + // Returns (new stmts including original, new symbols including original) private def transformStatements(stats: List[Tree], span: Span, specializations: SpecializedTraitCache): (List[Tree], SpecializedTraitCache) = { val specializations1 = collectReferencedSpecializations(stats, specializations) @@ -328,9 +357,12 @@ class DesugarSpecializedTraits extends MacroTransform: extension (classTree: Tree) def updateParents(parentUpdater: List[Type] => List[Type]) = (classTree: @unchecked) match { case td@TypeDef(name, t@Template(constr, preParentsOrDerived, self, preBody)) => - td.symbol.info = td.symbol.info match { - case ci: ClassInfo => ci.derivedClassInfo(declaredParents=parentUpdater(ci.declaredParents)) - } + + val cls = td.symbol.asClass + val oldInfo = cls.classInfo + val newInfo = oldInfo.derivedClassInfo(declaredParents = parentUpdater(oldInfo.declaredParents)) + cls.info = newInfo + cls.copySymDenotation(info = newInfo).installAfter(DesugarSpecializedTraits.this) } def refreshClassDef = (classTree: @unchecked) match { @@ -340,7 +372,7 @@ class DesugarSpecializedTraits extends MacroTransform: /* We need to inline recursively throughout generated specialized traits - see tests/run/specialized-trait-requires-inline-trait-inlining.scala */ // TODO: How do we calculate the spans correctly? - val ttmap = new TreeTypeMap(treeMap = { + val inlineInlineTraits = new TreeTypeMap(treeMap = (tree: Tree) => tree match { case tree: TypeDef if tree.symbol.isInlineTrait => val tree1 = Inlines.checkAndTransformInlineTrait(tree) val tree2 = if Inlines.needsInlining(tree1) then Inlines.inlineParentInlineTraits(tree1) else tree1 @@ -350,29 +382,54 @@ class DesugarSpecializedTraits extends MacroTransform: case t => t }) - val generatedTraitStats1 = generatedTraitStats.map(trtDef => ttmap(trtDef.withSpan(span))) - val generatedClassStats1 = generatedClassStats.map(clsDef => ttmap(clsDef.withSpan(span))) - .tapEach: + val generatedTraitStats1 = generatedTraitStats.map { + case tree: TypeDef => + assert(tree.symbol.isInlineTrait) + val inlined = Inlines.inlineParentInlineTraits(Inlines.checkAndTransformInlineTrait(tree.withSpan(span)),allowSpecialized=true).asInstanceOf[TypeDef] + cpy.TypeDef(inlined)(name = inlined.name, rhs = inlineInlineTraits(inlined.rhs)).withSpan(inlined.span) + } + + val generatedClassStats1 = generatedClassStats.map { + case tree: TypeDef => + assert(Inlines.needsInlining(tree, allowSpecializedTraits=true)) + val inlined = Inlines.inlineParentInlineTraits(tree.withSpan(span), allowSpecialized=true).asInstanceOf[TypeDef] + cpy.TypeDef(inlined)(name = inlined.name, rhs = inlineInlineTraits(inlined.rhs)).withSpan(inlined.span) + }.tapEach: // We can do parent removal earlier for $impl$ classes as we don't depend on the parents later. _.updateParents { parents => (parents: @unchecked) match case obj :: traitSp :: originalSpec :: Nil => obj :: traitSp :: Nil } .map(refreshClassDef) - // We need to do this after inlining into the $impl$ classes otherwise we break - // overriding/interface implementation rules during the inlining. - val generatedTraitStats1a = generatedTraitStats1 - .tapEach: - _.updateParents { parents => (parents: @unchecked) match - case obj :: original :: parents => obj :: parents - } - .map(refreshClassDef) - - if (generatedTraitStats1a.isEmpty && generatedClassStats1.isEmpty) + if (generatedTraitStats1.isEmpty && generatedClassStats1.isEmpty) (stats.map(replaceSpecializedSymbolsMap(specializations2)(_)), specializations2) else - val (generatedTraitStats2, specializations3) = transformStatements(generatedTraitStats1a, span, specializations2) + val (generatedTraitStats2, specializations3) = transformStatements(generatedTraitStats1, span, specializations2) val (generatedClassStats2, specializations4) = transformStatements(generatedClassStats1, span, specializations3) - (generatedTraitStats2 ++ generatedClassStats2 ++ stats.map(replaceSpecializedSymbolsMap(specializations4)(_)), specializations4) + + /* We need to do the parent removal after inlining into the $impl$ classes otherwise we break + overriding/interface implementation rules during the inlining. The $impl$ inlining + can also happen in the recursive calls, and so we need to do this right at the end (after the recursive calls): */ + val generatedTraitStats3 = + generatedTraitStats2.tapEach: stat => + if stat.symbol.isSpecializedTraitInterface then // We could have $impl$ classes from recursive calls as well. + stat.updateParents { parents => (parents: @unchecked) match + case obj :: Specialization(originalSpec) :: parents if specializations4.getInterfaceSymbol(originalSpec).get == stat.symbol.asClass => + obj :: parents + case obj :: parents => obj :: parents // We already removed the relevant parent. + } + .map(refreshClassDef) + + val stats2 = generatedTraitStats3 ++ + generatedClassStats2 ++ + stats.map(stat => + replaceSpecializedSymbolsMap(specializations4)( // Foo[Int] -> Foo$sp$Int in user code. + if (!stat.symbol.isSpecializedTraitImplementationClass && !stat.symbol.isSpecializedTraitInterface) then // We already processed these in an earlier recursive call + Inlines.inlineParentInlineTraits(stat, allowSpecialized = true) // Perform inlining into class Bar extends Foo[Int] from user code. + else + stat + )) + + (stats2.map(removeRedundantOverridesMap(_)), specializations4) } override def transform(tree: Tree)(using Context): Tree = tree @@ -607,16 +664,20 @@ object Specialization: def classSpecializedTypeParams(classSym: Symbol)(using Context): List[Type] = classSym.unforcedDecls.implicitDecls.collect(_.info match { case SpecializedEvidence(typeVar) => typeVar }) - def anonymousClassIsSpecialized(tree: Tree)(using Context) = tree match { - case TypeDef(anon, Template(_, parentCalls: List[Tree], _, _)) => - parentCalls match { - case _ :+ Apply(Apply(t@tpe, ctorArgs), ev) => // extends Object, parents of spec trait, spec trait - val spec = Specialization.unapply(t.tpe.resultType.resultType) - spec.get.hasSpecializedParams - case _ => true - } - case _ => true - } + // TODO: These methods are used in other phases; probably move them to the phase object? + def anonymousClassIsSpecialized(tree: Tree)(using Context) = + tree match { + case TypeDef(anon, Template(_, parentCalls: List[Tree], _, _)) => + parentCalls match { + case _ :+ Apply(Apply(t@tpe, ctorArgs), ev) => // extends Object, parents of spec trait, spec trait + val spec = Specialization.unapply(t.tpe.resultType.resultType) + spec.get.hasSpecializedParams + case _ => false + } + case _ => false + } + + def isSpecializedTrait(sym: Symbol)(using Context) = classSpecializedTypeParams(sym).nonEmpty end Specialization // Need to somehow make my naming a lot more consistent as well. diff --git a/docs/_docs/internals/specialized-traits.md b/docs/_docs/internals/specialized-traits.md index daf945c315d2..6a43f941f512 100644 --- a/docs/_docs/internals/specialized-traits.md +++ b/docs/_docs/internals/specialized-traits.md @@ -82,18 +82,35 @@ A specialized context bound (or its expansion to a context parameter) is only al type parameters of inline methods and inline traits. Regular methods or traits or classes cannot take `Specialized[T]` parameters. -Hence, the only way to create a specialized trait is using an anonymous class instance, like in the `Vec.apply` method above. What's more, -we require that each such anonymous class instance - - - can extend only a single specialized trait, - - cannot mix in further classes or traits, and - - cannot contain member definitions. - -So each such class instance is of the form `new A[Ts](ps1)...(psN) {}` where -`A` is a specialized trait and the type parameters `Ts` and term parameters `ps1, ,,, psN` can also be absent. +## Creating Specialized Trait Instances +Creation of an object with specialized behaviour can occur in one of two ways: + - Instantiating a `class` or using an `object` which extends the specialized trait, specializing its type parameters. E.g.: +```scala +inline trait Foo[T: Specialized](x: T): + def foo: T = x -The restrictions ensure that each time we create an instance of a specialized trait we know statically the classes of all `Specialized` type arguments. +class Bar extends Foo[Int](10): // Type parameter does of course not need to be specified explicitly here (it can be inferred from the value type of 10) + def myMethod = "Hello I am a method" +// Both Baz and myBar will have specialized instances of foo. +object Baz extends Foo(12) +val myBar = Bar() +``` + - Instantiation of an anonymous class instance directly from the Specialized trait: `new A[Ts](ps1)...(psN) {}` where `A` is a specialized trait and the type parameters `Ts` and term parameters `ps1, ,,, psN` can also be absent. This has a special meaning, as it desugars to instantiating a _specialized instance class_ (see Expansion of Specialized Traits). This comes with the twin advantages that: + - there is no need for the boilerplate of manually defining an object/class to extend the specialized trait + - this specialized instance class is reused every time such an instance is created (there is no proliferation of anonymous classes). + + However, to facilitate this reuse, we must impose the following restrictions. Anonymous class instances acting as instances of Specialized traits: + - can extend only a single specialized trait [0], + - cannot mix in further classes or traits, and + - cannot contain member definitions. + + Should these restrictions be undesirable, the user can always create their own named `object` or `class` extending from a specialized trait (i.e. the first case for creation of a specialized object just above), which does not induce these restrictions. + +[0] Note that of course an anonymous class instance such as `new Foo[Int] {}` where `Foo` extends some other trait `Bar` desugars in the compiler to `new Bar[Int] with Foo[Int] {}`, which means we can't distinguish these two cases. Therefore we begrudgingly allow +`new Bar[Int] with Foo[Int] {}` although there is really no reason to use this in source code because it's exactly the same as writing `new Foo[Int] {}`. We disallow `new Foo[Int] with Bar[Int] {}` however. + + @@ -137,7 +154,7 @@ The specialized instance traits are created on demand the first time they are me inline trait Vec$sp$Int extends Vec[Int] ``` -In general a specialized instance trait that specializes an inline trait `A[T]` with a specialization type `S`: +In general a specialized interface trait that specializes an inline trait `A[T]` with a specialization type `S`: - drops all `Specialized` trait parameters of `A` - adds `A[S]` as first parent trait @@ -145,6 +162,7 @@ In general a specialized instance trait that specializes an inline trait `A[T]` - contains declarations from the body of `A` specialized to the type(s) in question (after inlining) - Is an `inline trait`. This is for consistency; see [1]. - Maintains type parameters for type params not marked with `Specialized` in the original trait, and also for specializations where the type *argument* is`T: Specialized', in the case of partial specializations. + - Does not take value parameters (including evidence parameters) A specialized instance class for an inline trait `A` at specialized argument `S` diff --git a/tests/neg/specialized-trait-anonymous-class-breaks-rules.scala b/tests/neg/specialized-trait-anonymous-class-breaks-rules.scala new file mode 100644 index 000000000000..99a03b3912a2 --- /dev/null +++ b/tests/neg/specialized-trait-anonymous-class-breaks-rules.scala @@ -0,0 +1,27 @@ +//> using options -language:experimental.specializedTraits + +inline trait Baz[T: Specialized]: + def foo = "hello world" + +inline trait Foo[T: Specialized] extends Baz[T]: + val bar = 10 + +inline trait Bar[T: Specialized]: + val baz = 100 + +trait MyTrait[T](val x: T) + +@main def main = + val w = new Foo[Int] {} // ok + + val x = new Foo[Int] { // error: Anonymous classes acting as instances of Specialized traits may not have additional members; you can make a named object instead if you like. + val y = 10 + } + + val y = new Foo[Int] with MyTrait(10) with Bar {} // error: Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like. + + val z = new Foo[Int] with Bar {} // error: Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like. + + val a = new Foo[Int] with Baz[Int] {} // error: Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like. + + val b = new Baz[Int] with Foo[Int] {} // ok: We have to allow this because it desugars to the same thing as the new Foo[Int] {} case; it will have the same behaviour when compiled; diff --git a/tests/neg/specialized-trait-inheritance.scala b/tests/pos/specialized-trait-class-extends-specialized-trait.scala similarity index 69% rename from tests/neg/specialized-trait-inheritance.scala rename to tests/pos/specialized-trait-class-extends-specialized-trait.scala index 9431253d64e8..f2ba081e5ecc 100644 --- a/tests/neg/specialized-trait-inheritance.scala +++ b/tests/pos/specialized-trait-class-extends-specialized-trait.scala @@ -2,7 +2,8 @@ inline trait Foo[T: Specialized](x: T): def foo = x -class Bar extends Foo(10) // error: Specialized traits may only be extended by anonymous class instances. +class Bar extends Foo(10): + def myMethod = "Hello I am a method" def f(b: Foo[Int]) = println(s"We found the following value of foo ${b.foo}") diff --git a/tests/pos/specialized-trait-object-extends-specialized-trait.scala b/tests/pos/specialized-trait-object-extends-specialized-trait.scala new file mode 100644 index 000000000000..0ba1fa4a42e8 --- /dev/null +++ b/tests/pos/specialized-trait-object-extends-specialized-trait.scala @@ -0,0 +1,9 @@ +//> using options -language:experimental.specializedTraits + +inline trait MyInterface[T: Specialized]: + def fromInt(x: Int): T + def plus(x: T, y: T): T + +implicit object Implementation extends MyInterface[Int]: + override def fromInt(x: Int): Int = x + override def plus(x: Int, y: Int): Int = x + y diff --git a/tests/pos/specialized-trait-trait-extends-specialized-trait.scala b/tests/pos/specialized-trait-trait-extends-specialized-trait.scala new file mode 100644 index 000000000000..f69439983ded --- /dev/null +++ b/tests/pos/specialized-trait-trait-extends-specialized-trait.scala @@ -0,0 +1,12 @@ +//> using options -language:experimental.specializedTraits +inline trait Foo[T: Specialized](x: T): + def foo = x + +trait Bar extends Foo[Int]: + def myMethod = "Hello I am a method" + +def f(b: Foo[Int]) = println(s"We found the following value of foo ${b.foo}") + +@main def main = + val x = new Bar with Foo(19) {} + f(x) From f10e40f2ea73116881a2680df6198ab722c39108 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 3 May 2026 19:03:15 +0200 Subject: [PATCH 171/254] Switch from copying RHS type to our own specialized Numeric --- .../benchmarks/SpecializedTraitsBenchmark.scala | 17 +++++++++++++++-- .../src/dotty/tools/dotc/inlines/Inlines.scala | 6 +----- .../specialized-trait-vector-dot-product.scala | 17 +++++++++++++++-- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala index 7ee46bb5f9c4..11f77d0e7b3e 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala @@ -41,8 +41,8 @@ class VecGeneric[T: Numeric](elems: Array[T]): result = num.plus(result, num.times(this(i), other(i))) result -inline trait VecSpecialized[T: {Specialized, Numeric}](elems: Array[T]): - private val num = summon[Numeric[T]] +inline trait VecSpecialized[T: {Specialized, Numeric2}](elems: Array[T]): + private val num = summon[Numeric2[T]] def length = elems.length @@ -85,3 +85,16 @@ class VecBench: val x = new VecSpecialized[Int](arr.arr1) {} val y = new VecSpecialized[Int](arr.arr2) {} assert(x.scalarProduct(y) == arr.target) + +// You can really see the impact of Specialized on the interface usage here +// Remove Specialized and see that the generated code gets much more boxing and unboxing +// which slows it down substantially. +inline trait Numeric2[T: Specialized]: + def fromInt(x: Int): T + def plus(x: T, y: T): T + def times(x: T, y: T): T + +implicit object IntIsIntegral extends Numeric2[Int]: + override def fromInt(x: Int): Int = x + override def plus(x: Int, y: Int): Int = x + y + override def times(x: Int, y: Int): Int = x * y diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index c54ca346f03e..6c06493567ad 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -996,11 +996,7 @@ object Inlines: paramAccessorsMapper .getParamAccessorRhs(vdef.symbol.owner, vdef.symbol.name) .getOrElse(inlinedRhs(vdef, inlinedSym)) - - // TODO: We might only need to do this to evidence params but tbh I can't see much harm in applying it when we want to? - if (rhs.tpe.exists && !vdef.symbol.isMutableVar) // we can't narrow vars because e.g. var current = 0 would be narrowed to type 0 but someone may letter set i - inlinedSym.info = rhs.tpe - + val rhs1 = rhs.changeNonLocalOwners(inlinedSym) // if rhs.symbol.exists then rhs.changeOwner(rhs.symbol.owner, inlinedSym) else rhs tpd.ValDef(inlinedSym.asTerm, rhs1).withSpan(parent.span) diff --git a/tests/run/specialized-trait-vector-dot-product.scala b/tests/run/specialized-trait-vector-dot-product.scala index 9228fa2030b8..122b44df5c8c 100644 --- a/tests/run/specialized-trait-vector-dot-product.scala +++ b/tests/run/specialized-trait-vector-dot-product.scala @@ -1,7 +1,7 @@ //> using options -language:experimental.specializedTraits -inline trait Vec[T: {Specialized, Numeric}](elems: Array[T]): - private val num = summon[Numeric[T]] +inline trait Vec[T: {Specialized, Numeric2}](elems: Array[T]): + private val num = summon[Numeric2[T]] def length = elems.length @@ -16,7 +16,20 @@ inline trait Vec[T: {Specialized, Numeric}](elems: Array[T]): object Test: def main(args: Array[String]) = + implicit val v: Numeric2[Int] = new IntIsIntegral() {} val x = new Vec[Int](Array(1, 2, 3, 4, 5)) {} val y = new Vec[Int](Array(3, 4, 5, 6, 7)) {} val z = x.scalarProduct(y) assert(z == 85) + + +inline trait Numeric2[T: Specialized]: + def fromInt(x: Int): T + def plus(x: T, y: T): T + def times(x: T, y: T): T + +class IntIsIntegral extends Numeric2[Int]: + override def fromInt(x: Int): Int = x + override def plus(x: Int, y: Int): Int = x + y + override def times(x: Int, y: Int): Int = x * y + From 081b5913928cd089328fb8643bcbbc43680e5942 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 5 May 2026 13:35:37 +0200 Subject: [PATCH 172/254] Fix partial parameter types for specialization --- .../transform/DesugarSpecializedTraits.scala | 76 ++++++++++--------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index e5e3aea2d2e1..700c38a5b69b 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -140,7 +140,7 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: private def generateImplementationClassParents(specialization: Specialization, interfaceSymbol: ClassSymbol) = val objectParent = defn.ObjectType val traitSpParent = interfaceSymbol.typeRef.appliedTo(specialization.unspecializedTypeParams) // Set using old unspecializedTypeParams and replace after. - val originalTraitSpecializedParent = AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.typeArguments).tpe + val originalTraitSpecializedParent = AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.mapUnspecializedArgs(specialization.unspecializedTypeParams.map(TypeTree(_)))).tpe (objectParent, traitSpParent, originalTraitSpecializedParent) private def newImplementationClass(specialization: Specialization, interfaceSymbol: ClassSymbol) = @@ -166,40 +166,41 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: // TODO: Standardise a bit so that we either generate the symbols and later the classes or not. // TODO: Tidy this up a bit with functions private def buildImplementationClassTree(specialization: Specialization, interfaceSymbol: ClassSymbol, classSymbol: ClassSymbol)(using Context) = { - val (objectParent, traitSpParent, originalTraitSpecializedParent) = generateImplementationClassParents(specialization, interfaceSymbol) + val (objectParent, traitSpParent_, originalTraitSpecializedParent_) = generateImplementationClassParents(specialization, interfaceSymbol) + + // Apply Type Param Fix: TODO : This really ought to be done more cleanly somewhere else. + val tpMap: Map[Type, Type] = specialization.unspecializedTypeParams.zip(classSymbol.typeParams.map(_.typeRef)).toMap + val freshTypeVarMap = new TypeMap: + def apply(t: Type) = tpMap.applyOrElse(t, mapOver) + val traitSpParent = freshTypeVarMap(traitSpParent_) + val originalTraitSpecializedParent = freshTypeVarMap(originalTraitSpecializedParent_) + val init = newDefaultConstructor(classSymbol) val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. - def apply(t: Type) = specialization.constructorParamToArgumentTypeMap.view.applyOrElse(t, mapOver) // TODO: IF we can do just types we can get rid fo this + def apply(t: Type) = specialization.specializedConstructorParamToArgumentTypeMap.view.applyOrElse(t, mapOver) // TODO: IF we can do just types we can get rid fo this - val tm2 = new TypeMap: - def apply(t: Type) = t match { - case Specialization(spec) if spec.traitSymbol eq specialization.traitSymbol => - classSymbol.typeRef - case _ => mapOver(t) - } - + /* Create constructor and setup constructor type */ val nonTypeParams = specialization.traitSymbol.primaryConstructor.rawParamss.tail + val oldTypeParams = specialization.unspecializedConstructorParams + val initTypeParams = classSymbol.typeParams.map(s => s.copy(owner = init, flags = (s.flags &~ (Flags.Private | Flags.Deferred)))) + val valueParams = nonTypeParams.map(_.map(param => param.copy(owner = init, info = tm(param.info).substSym(oldTypeParams, initTypeParams), name=param.name.expandedName(classSymbol)))) // We need to map the parameter names to avoid a name clash with val params from parents (see tests/pos/specialized-trait-val-parameter.scala) + + initTypeParams.foreach(_.entered) + init.setParamss(initTypeParams :: valueParams) + init.info = specialization.traitSymbol.primaryConstructor.info.appliedTo( // Type Arg if specialized; otherwise we want our type param. + specialization.constructorTypeParams.map(par => specialization.specializedConstructorParamToArgumentTypeMap.applyOrElse(par, _.subst(oldTypeParams, classSymbol.typeParams.map(_.typeRef)))) + ) + fixConstructor(init, classSymbol) - // We need to map the parameter names to avoid a name clash with val params from parents (see tests/pos/specialized-trait-val-parameter.scala) - val valueParams = nonTypeParams.map(_.map(param => param.copy(owner = init, info = tm(param.info), name=param.name.expandedName(classSymbol)))) // .map(_.filterNot(isSyntheticEvidence) - val typeParams = classSymbol.typeParams.map(_.copy()) - - init.setParamss(typeParams :: valueParams) - - val paramAccessorss = valueParams.map(params => params.map(s => s.copy(owner = classSymbol, flags=s.flags|Flags.LocalParamAccessor))) + /* Build param accessors */ + val paramAccessorss = valueParams.map(params => params.map(s => s.copy(owner = classSymbol, flags=(s.flags|Flags.LocalParamAccessor) &~ Flags.Param, info = s.info.subst(initTypeParams, classSymbol.typeParams.map(_.typeRef))))) paramAccessorss.foreach(_.foreach(classSymbol.enter(_))) - init.info = tm2(specialization.traitSymbol.primaryConstructor.info.appliedTo(specialization.typeArguments.map(_.tpe))) - - fixConstructor(init, classSymbol) - val typer = Typer(ctx.nestingLevel + 1) // TODO: actually get these from the user. - + /* Build class def tree */ val newParamss = paramAccessorss.nestedMap(ref(_)) - val newParams1 = if (newParamss.length == 1) then newParamss ++ List(List()) else newParamss - - // TODO: Clean adn robust + // TODO: Clean and robust val classDef = ClassDefWithParents( classSymbol, DefDef(init.asTerm.entered), @@ -209,14 +210,10 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: New(originalTraitSpecializedParent.typeConstructor) .select(TermRef(originalTraitSpecializedParent.typeConstructor, specialization.traitSymbol.primaryConstructor.asTerm)) // TODO: Check for other constructors .appliedToTypes(originalTraitSpecializedParent.argTypes) - // .appliedToArgss(paramAccessors.map(_.map(ref))) .appliedToArgss(newParams1) - - // TODO: What about potential custom typeclass instances? How do we balance that with generating another version of the class every time? Probably just generate the basic version and then let them apply their own version want (based on some kind of hashing). Then we generate a whole new impl class / or anon class which is still specialised to their instances that they provided, at the time that we see it? - // To be honest if our assumption is that we aren't very often going to do anything weird we can just always generate the class at the point of use, with the evidences specialized (but only if we don't ahve that one already - i.e. effectively consider the evidences as part of the name) ), // Put into body of class - paramAccessorss.flatMap(syms => syms.map(sym => tpd.ValDef(sym.asTerm))) // .withFlags(Flags.LocalParamAccessor).withType(sym.info) + paramAccessorss.flatMap(syms => syms.map(sym => tpd.ValDef(sym.asTerm))) ) classDef } @@ -233,7 +230,9 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: } def treeMap(tree: Tree): Tree = tree match { - // Replace (anonymous class version of) new Foo[Int] {} with new Foo$impl$Int.asInstanceOf[Foo$sp$Int] + /* Replace new Foo[Int] {} with new Foo$impl$Int.asInstanceOf[Foo$sp$Int] + This has already been desugared to an anonymous class instance with parents: + Objects, Parents of Foo, Foo. */ case Block(List(an@TypeDef(anon, tmpl@Template(_, parentCalls: List[Tree], _, _))), Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => @@ -618,13 +617,18 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(usi val specializedTypeParamsToTypeArgumentsMap: Map[Type, Tree] = paramToArgList.toMap.filter((k, v) => specializedTypeParamsSet(k)) val specialization: List[Tree] = traitSymbol.typeParams.map(_.typeRef).map(specializedTypeParamsToTypeArgumentsMap.applyOrElse(_, TypeTree(_))) // TODO: Don't really like this name - // val constructorParamToArgumentTypeMap: Map[Type, Type] = traitSymbol.primaryConstructor.typeParams.zip(paramToArgList).filter((constrParam, paramArg) => specializedTypeParamsSet(paramArg._1)).map((constrParam, paramArg) => (constrParam.typeRef, paramArg._1)).toMap - // TODO: Potentially can get this out of the specialization.specialization directly given we make the same assumption about one primary constructor and param ordering. - def constructorParamToArgumentTypeMap: Map[Type, Type] = - traitSymbol.primaryConstructor.rawParamss.head.map(_.typeRef).zip(typeArguments.map(_.tpe)).toMap + def constructorTypeParams: List[Type] = traitSymbol.primaryConstructor.rawParamss.head.map(_.typeRef) + def unspecializedConstructorParams: List[Symbol] = traitSymbol.primaryConstructor.rawParamss.head.zip(traitSymbol.typeParams).filterNot((constrParam, typeParam) => specializedTypeParamsSet(typeParam.typeRef)).map((constrParam, typeParam) => constrParam) + def specializedConstructorParamToArgumentTypeMap: Map[Type, Type] = + traitSymbol.primaryConstructor.rawParamss.head.map(_.typeRef).zip(paramToArgList).filter((constrParam, paramArg) => specializedTypeParamsSet(paramArg._1)).map((constrParam, paramArg) => (constrParam, paramArg._2.tpe)).toMap + + val hasSpecializedParams: Boolean = specializedTypeParams.nonEmpty - def hasSpecializedParams: Boolean = specializedTypeParams.nonEmpty + def mapUnspecializedArgs(unspec: List[Tree]): List[Tree] = paramToArgList.foldLeft((List.empty[Tree], unspec))((resUnspec, paramArg) => ((resUnspec, paramArg): @unchecked) match { + case ((result, unspec), (param, arg)) if specializedTypeParamsSet(param) => (arg :: result, unspec) + case ((result, head :: rest), (param, arg)) => (head :: result, rest) + })._1.reverse /* If inline trait Foo[T] has a method taking another Foo[T] there's no point specializing the reference since the resulting sp$T$ would be the same as the starting trait. */ From 16c6183177436afefeab0e4fc1d1ab68cc16d2de Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 5 May 2026 15:44:49 +0200 Subject: [PATCH 173/254] Add isSpecializedMethod --- compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 3 +++ .../dotty/tools/dotc/transform/DesugarSpecializedTraits.scala | 3 +++ 2 files changed, 6 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index c50c07e7f2de..0a619641bfc1 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -1059,6 +1059,9 @@ object SymDenotations { def isInlineTrait(using Context): Boolean = isAllOf(InlineTrait) + def isSpecializedMethod(using Context): Boolean = + Specialization.isSpecializedMethod(symbol) + def isSpecializedTrait(using Context): Boolean = Specialization.isSpecializedTrait(symbol) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 700c38a5b69b..5807bd8a3d64 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -43,6 +43,7 @@ import dotty.tools.dotc.core.Decorators.nestedMap import dotty.tools.dotc.core.NameOps.expandedName import dotty.tools.dotc.core.DenotTransformers.DenotTransformer import dotty.tools.dotc.core.Denotations.SingleDenotation +import dotty.tools.dotc.core.Flags.InlineMethod class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: @@ -667,6 +668,7 @@ object Specialization: } def classSpecializedTypeParams(classSym: Symbol)(using Context): List[Type] = classSym.unforcedDecls.implicitDecls.collect(_.info match { case SpecializedEvidence(typeVar) => typeVar }) + def methodSpecializedTypeParams(methodSym: Symbol)(using Context): List[Type] = methodSym.paramSymss.flatten.collect(_.info match { case SpecializedEvidence(typeVar) => typeVar }) // TODO: These methods are used in other phases; probably move them to the phase object? def anonymousClassIsSpecialized(tree: Tree)(using Context) = @@ -682,6 +684,7 @@ object Specialization: } def isSpecializedTrait(sym: Symbol)(using Context) = classSpecializedTypeParams(sym).nonEmpty + def isSpecializedMethod(sym: Symbol)(using Context) = sym.isAllOf(InlineMethod) && methodSpecializedTypeParams(sym).nonEmpty end Specialization // Need to somehow make my naming a lot more consistent as well. From b8f0731f7acbfe7a99a0d929e20542dbfede3734 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 5 May 2026 17:03:32 +0200 Subject: [PATCH 174/254] Fix multiple parameter lists in specialized trait anonymous class instances --- .../tools/dotc/transform/DesugarSpecializedTraits.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 5807bd8a3d64..8cb847e4e706 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -258,9 +258,12 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: report.error("Anonymous classes acting as instances of Specialized traits may not have additional members; you can make a named object instead if you like.", an.srcPos) parentCalls match { - case (obj :: parentsOfSpecTrait) :+ Apply(Apply(tpe, ctorArgs), ev) if (obj.symbol.owner == ctx.definitions.ObjectClass) && (parentsOfSpecTrait.forall(x => spec.traitSymbol.asClass.parentSyms.exists(p => p == x.symbol.owner))) => + case (obj :: parentsOfSpecTrait) :+ (app@Apply(_, _)) if (obj.symbol.owner == ctx.definitions.ObjectClass) && (parentsOfSpecTrait.forall(x => spec.traitSymbol.asClass.parentSyms.exists(p => p == x.symbol.owner))) => specializations.getImplementationSymbol(spec).map( specializedSymbol => - Typed(Apply(Apply(Select(New(ref(specializedSymbol)),ctor).appliedToTypeTrees(spec.unspecializedTypeArgs), ctorArgs.map(_.changeNonLocalOwners(an.symbol.owner))), ev), t) + Typed( + Select(New(ref(specializedSymbol)),ctor).appliedToTypeTrees(spec.unspecializedTypeArgs) + .appliedToArgss(tpd.allArgss(app).tail.nestedMap(_.changeNonLocalOwners(an.symbol.owner))) // Remove the type params which are not needed + , t) ).getOrElse(tree) // We don't replace non-specialized anonymous class instantiations e.g. new Foo[T] where T is defined in the enclosing scope. case _ => report.error("Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like.", an.srcPos) From c2f6b1fbd79c888310c06cbc4ddc4ab237fe4e45 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 5 May 2026 18:25:50 +0200 Subject: [PATCH 175/254] Fix unspecialized AppliedTypeTree bug --- .../tools/dotc/transform/DesugarSpecializedTraits.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 8cb847e4e706..98501c53cda7 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -293,7 +293,11 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: // Replace AppliedTypeTree instances in code case Specialization(spec) => { for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) - yield AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? + yield + if spec.unspecializedTypeArgs.nonEmpty then + AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? + else + TypeTree(specializedSymbol.typeRef) }.getOrElse(tree) case tree => tree From 29a4377df25d74d2d84e4aa185a36d076315c688 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 5 May 2026 18:26:16 +0200 Subject: [PATCH 176/254] Add comment --- .../dotty/tools/dotc/transform/DesugarSpecializedTraits.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 98501c53cda7..4dde2667016d 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -408,7 +408,7 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: .map(refreshClassDef) if (generatedTraitStats1.isEmpty && generatedClassStats1.isEmpty) - (stats.map(replaceSpecializedSymbolsMap(specializations2)(_)), specializations2) + (stats.map(replaceSpecializedSymbolsMap(specializations2)(_)), specializations2) // TODO: Check if this shouldn't be the same as the one belwo!? else val (generatedTraitStats2, specializations3) = transformStatements(generatedTraitStats1, span, specializations2) val (generatedClassStats2, specializations4) = transformStatements(generatedClassStats1, span, specializations3) From 7ae124873ae281481871861113702011e8fec705 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 5 May 2026 18:26:52 +0200 Subject: [PATCH 177/254] Support specialized trait inline "factory" methods --- compiler/src/dotty/tools/dotc/Compiler.scala | 1 + .../transform/DesugarSpecializedTraits.scala | 15 ++++++- .../dotc/transform/PruneInlineTraits.scala | 1 + .../transform/PruneSpecializedMethods.scala | 45 +++++++++++++++++++ .../run/specialized-trait-inlining-loop.scala | 25 +++++++++++ 5 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/transform/PruneSpecializedMethods.scala create mode 100644 tests/run/specialized-trait-inlining-loop.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 998ce45fffe4..2ed5c5be8c0b 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -74,6 +74,7 @@ class Compiler { new ExpandSAMs, // Expand single abstract method closures to anonymous classes new ElimRepeated, // Rewrite vararg parameters and arguments new DropForMap, // Drop unused trailing map calls in for comprehensions + new PruneSpecializedMethods, // Drop specialized methods which have already been inlined new PruneInlineTraits) :: // Remove right-hand side of definitions in inline traits List(new RefChecks) :: // Various checks mostly related to abstract members and overriding List(new init.Checker) :: // Check initialization of objects diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 4dde2667016d..e2ae8901c160 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -351,7 +351,18 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: ) // Returns (new stmts including original, new symbols including original) - private def transformStatements(stats: List[Tree], span: Span, specializations: SpecializedTraitCache): (List[Tree], SpecializedTraitCache) = { + private def transformStatements(stats1: List[Tree], span: Span, specializations: SpecializedTraitCache): (List[Tree], SpecializedTraitCache) = { + + val inlineSpecializedMethods = new TreeMapWithPreciseStatContexts { + override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? + case app: Apply if app.symbol.isSpecializedMethod => + super.transform(Inlines.inlineCall(tree)) + case tree => super.transform(tree) + } + } + + val stats = inlineSpecializedMethods.transform(stats1) + val specializations1 = collectReferencedSpecializations(stats, specializations) val generatedTraitStats = specializations1.getNewInterfaceSymbols.toList.map(buildInterfaceTraitTree) val generatedClassStats = specializations1.getNewImplementationSymbols.toList.map(buildImplementationClassTree) @@ -388,7 +399,7 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: Inlines.inlineParentInlineTraits(tree) case t => t }) - + val generatedTraitStats1 = generatedTraitStats.map { case tree: TypeDef => assert(tree.symbol.isInlineTrait) diff --git a/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala index 9aefcad316dc..db191c4080ed 100644 --- a/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/PruneInlineTraits.scala @@ -18,6 +18,7 @@ class PruneInlineTraits extends MiniPhase with SymTransformer { thisTransform => override def phaseName: String = PruneInlineTraits.name override def description: String = PruneInlineTraits.description + override def runsAfter: Set[String] = Set(PruneSpecializedMethods.name) override def transformSym(sym: SymDenotation)(using Context): SymDenotation = if isEraseable(sym) then sym.copySymDenotation(initFlags = sym.flags | Deferred) diff --git a/compiler/src/dotty/tools/dotc/transform/PruneSpecializedMethods.scala b/compiler/src/dotty/tools/dotc/transform/PruneSpecializedMethods.scala new file mode 100644 index 000000000000..53fb016eeafd --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/PruneSpecializedMethods.scala @@ -0,0 +1,45 @@ +package dotty.tools.dotc +package transform + +import core._ +import Contexts._ +import DenotTransformers.SymTransformer +import Flags._ +import SymDenotations._ +import Symbols._ +import MegaPhase.MiniPhase +import ast.tpd +import dotty.tools.dotc.core.StdNames.str + +class PruneSpecializedMethods extends MiniPhase with SymTransformer { thisTransform => + import tpd._ + import PruneSpecializedMethods._ + + override def phaseName: String = PruneSpecializedMethods.name + + override def description: String = PruneSpecializedMethods.description + + override def transformSym(sym: SymDenotation)(using Context): SymDenotation = + if sym.isClass && !sym.is(Package) then + val clsInfo = sym.asClass.classInfo + val clsInfo2 = clsInfo.derivedClassInfo(decls = + clsInfo.decls.filteredScope(!isDeletable(_)) + ) + sym.copySymDenotation(info = clsInfo2) + else sym + + override def transformTemplate(tree: Template)(using Context): Tree = + cpy.Template(tree)(body = tree.body.flatMap({ + case stmt: DefDef if isDeletable(stmt.symbol) => None + case stmt => Some(stmt) + })) + + private def isDeletable(sym: SymDenotation)(using Context): Boolean = sym.isSpecializedMethod +} + +object PruneSpecializedMethods { + import tpd._ + + val name: String = "pruneSpecializedMethods" + val description: String = "drop specialized methods which have already been inlined; we can't wait until erasure because they can be broken by pruneInlineTraits removing members from the specialized traits they instantiate" +} diff --git a/tests/run/specialized-trait-inlining-loop.scala b/tests/run/specialized-trait-inlining-loop.scala new file mode 100644 index 000000000000..034c1cc256b1 --- /dev/null +++ b/tests/run/specialized-trait-inlining-loop.scala @@ -0,0 +1,25 @@ +//> using options -language:experimental.specializedTraits +inline trait T1[T: Specialized]: + def bar1 = myInlineMethod1[T] + +inline trait T2[T: Specialized]: + def bar2 = myInlineMethod2[T] + +inline trait T3[T: Specialized]: + def foo(x: T): T = x + +inline def myInlineMethod1[T: Specialized] = new T2[T]() {} +inline def myInlineMethod2[T: Specialized] = new T3[T]() {} + +@main def Test = + val x = new T1[Int]() {} + assert(x.bar1.bar2.foo(10) == 10) + +// Need to: +// 1) Create T1$impl$Int$ and inline `def bar1 = myInlineMethod1[Int]` (desugarSpecializedTraits) +// 2) Inline myInlineMethod1 into bar1 inside T1$impl$Int (inlining) +// 3) Create T2$impl$Int and fix the inlined definition of bar1/myInlineMethod1 (desugarSpecializedTraits) +// 4) Inline myInlineMethod2 into bar2 inside T2$impl$Int +// 5) Create T3$impl$Int and fix the inlined definition of bar2/myInlineMethod2 (desugarSpecializedTraits) + +// This can continue for arbitrarily many inlines and you don't know in advance that any of these specializations need to be created. From d58145ef8f57def9640dd9d7fef31bfd02847106 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 5 May 2026 18:29:45 +0200 Subject: [PATCH 178/254] Switch vector dot product example to use factory method --- tests/run/specialized-trait-vector-dot-product.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/run/specialized-trait-vector-dot-product.scala b/tests/run/specialized-trait-vector-dot-product.scala index 122b44df5c8c..4d2112f2b185 100644 --- a/tests/run/specialized-trait-vector-dot-product.scala +++ b/tests/run/specialized-trait-vector-dot-product.scala @@ -14,15 +14,18 @@ inline trait Vec[T: {Specialized, Numeric2}](elems: Array[T]): result = num.plus(result, num.times(this(i), other(i))) result +object Vec: + inline def apply[T: {Specialized, Numeric2}](elems: Array[T]) = new Vec[T](elems) {} +end Vec + object Test: def main(args: Array[String]) = implicit val v: Numeric2[Int] = new IntIsIntegral() {} - val x = new Vec[Int](Array(1, 2, 3, 4, 5)) {} - val y = new Vec[Int](Array(3, 4, 5, 6, 7)) {} + val x = Vec[Int](Array(1, 2, 3, 4, 5)) + val y = Vec[Int](Array(3, 4, 5, 6, 7)) val z = x.scalarProduct(y) assert(z == 85) - inline trait Numeric2[T: Specialized]: def fromInt(x: Int): T def plus(x: T, y: T): T From 9b0d9dec9fb140bd5e2ab6e89c0fcafa65bfacde Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 6 May 2026 12:43:35 +0200 Subject: [PATCH 179/254] Ban unsupported uses of Specialized --- .../tools/dotc/reporting/ErrorMessageID.scala | 1 + .../dotty/tools/dotc/reporting/messages.scala | 17 ++++++ .../tools/dotc/transform/PostTyper.scala | 7 +++ docs/_docs/reference/error-codes/E228.md | 58 +++++++++++++++++++ ...ed-trait-specialized-incorrect-usage.scala | 9 ++- ...d-trait-specialized-incorrect-usage2.scala | 24 ++++++++ ...d-trait-specialized-incorrect-usage3.scala | 3 + 7 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 docs/_docs/reference/error-codes/E228.md create mode 100644 tests/neg/specialized-trait-specialized-incorrect-usage2.scala create mode 100644 tests/neg/specialized-trait-specialized-incorrect-usage3.scala diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index da69926286e9..f3505cd909f1 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -243,6 +243,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case InferUnionWarningID // errorNumber: 225 case TypeParameterShadowsTypeID // errorNumber: 226 case PrivateShadowsTypeID // errorNumber: 227 + case IllegalUseOfSpecializedID // errorNumber: 228 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 83eb076f329d..5e67fab34210 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -3861,3 +3861,20 @@ final class PrivateShadowsType(shadow: Symbol, shadowed: Symbol)(using Context) i"""A private field shadows an inherited field with the same name. |This can lead to confusion as the inherited field becomes inaccessible. |Consider renaming the private field to avoid the shadowing.""" + +final class IllegalUseOfSpecialized(using Context) + extends SyntaxMsg(IllegalUseOfSpecializedID): + override protected def msg(using Context): String = + i"Specialized may only be used as a context bound" + override protected def explain(using Context): String = + i"""Specialized allows for a performance improvement by specializing generic type parameters + to avoid boxing/unboxing. It should only be used as a context (typeclass) bound on a + generic type in inline traits or inline methods: + + inline trait Vec[T: Specialized](val x: T) + + inline def foo[T: Specialized](v: Vec[T]) = v.x + + In this instance it was used in a way which is unsupported, such as + trying to create a type synonym or a value with explicit type Specialized[X]. + """ diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 8e760dcf8df3..62e798344d97 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -26,6 +26,7 @@ import cc.* import dotty.tools.dotc.transform.MacroAnnotations.hasMacroAnnotation import dotty.tools.dotc.core.NameKinds.DefaultGetterName import ast.TreeInfo +import dotty.tools.dotc.core.NameKinds.ContextBoundParamName object PostTyper { val name: String = "posttyper" @@ -495,6 +496,8 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => if tree.isType then checkNotPackage(tree) else + if tree.symbol == defn.SpecializedModule then + report.error(IllegalUseOfSpecialized(), tree.srcPos) registerNeedsInlining(tree) val tree1 = checkUsableAsValue(tree) tree1.tpe match { @@ -599,6 +602,8 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => case tree: TypeDef => if tree.symbol.isInlineTrait then ctx.compilationUnit.needsInlining = true // Transform inner classes to traits + if tree.rhs.tpe.existsPart(t => t.typeSymbol == defn.SpecializedClass.asType) then + report.error(IllegalUseOfSpecialized(), tree.srcPos) registerIfHasMacroAnnotations(tree) val sym = tree.symbol if (sym.isClass) @@ -671,6 +676,8 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => else if (tree.tpt.symbol == defn.orType) () // nothing to do else + if tree.tpt.symbol == defn.SpecializedClass && !ctx.owner.name.is(ContextBoundParamName) then + report.error(IllegalUseOfSpecialized(), tree.srcPos) Checking.checkAppliedType(tree) super.transform(tree) case SingletonTypeTree(ref) => diff --git a/docs/_docs/reference/error-codes/E228.md b/docs/_docs/reference/error-codes/E228.md new file mode 100644 index 000000000000..62e38853bd4f --- /dev/null +++ b/docs/_docs/reference/error-codes/E228.md @@ -0,0 +1,58 @@ +--- +title: E228: Illegal Use of Specialized Error +kind: Error +--- +# E228: Illegal Use of Specialized Error + +This error occurs when Specialized is used outside of a context bound. + +--- + +## Example + +```scala sc:fail +def bar(x: Specialized[Int]): Int // error: Specialized may only be used as a context bound +class Foo(x: Specialized[Int]) // error: Specialized may only be used as a context bound +class Bar(val x: Specialized[Int]) // error: Specialized may only be used as a context bound +val y: Specialized[Char] // error: Specialized may only be used as a context bound +val v = Specialized.apply[Int] // error: Specialized may only be used as a context bound +def z = println(Specialized.apply[Float]) // error: Specialized may only be used as a context bound +def a = Specialized.apply // error: Specialized may only be used as a context bound +type V = Specialized // error: Specialized may only be used as a context bound +type W = Specialized[Int] // error: Specialized may only be used as a context bound +val b = Specialized // error: Specialized may only be used as a context bound +``` + +### Error + +```scala sc:nocompile +-- [E228] Syntax Error: tests/neg/specialized-trait-specialized-incorrect-usage2.scala:24:12 +24 | val b = Specialized // error: Specialized may only be used as a context bound + | ^^^^^^^^^^^ + | Specialized may only be used as a context bound + |---------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Specialized allows for a performance improvement by specializing generic type parameters + | to avoid boxing/unboxing. It should only be used as a context (typeclass) bound on a + | generic type in inline traits or inline methods: + | + | inline trait Vec[T: Specialized](val x: T) + | + | inline def foo[T: Specialized](v: Vec[T]) = v.x + | + | In this instance it was used in a way which is unsupported, such as + | trying to create a type synonym or a value with explicit type Specialized[X]. + | + ---------------------------------------------------------------------------- +``` + +### Solution +Use Specialized correctly, i.e. as a context bound in an inline trait or method: + + +```scala sc:compile sc-opts:-Werror +inline trait Vec[T: Specialized](val x: T) + +inline def foo[T: Specialized](v: Vec[T]) = v.x +``` diff --git a/tests/neg/specialized-trait-specialized-incorrect-usage.scala b/tests/neg/specialized-trait-specialized-incorrect-usage.scala index 8e7487afbff5..629b8379a96f 100644 --- a/tests/neg/specialized-trait-specialized-incorrect-usage.scala +++ b/tests/neg/specialized-trait-specialized-incorrect-usage.scala @@ -1,2 +1,9 @@ //> using options -language:experimental.specializedTraits -def x: Specialized[Int] = new Specialized[Int] {} // error: Cannot extend sealed trait Specialized in a different source file +trait Illegal: + def x: Int = + new Specialized[Int] {} // error: Cannot extend sealed trait Specialized in a different source file + 10 + + class Baz extends Specialized[Int] // error: Cannot extend sealed trait Specialized in a different source file + + trait A[T] extends Specialized[T] // error: Cannot extend sealed trait Specialized in a different source file diff --git a/tests/neg/specialized-trait-specialized-incorrect-usage2.scala b/tests/neg/specialized-trait-specialized-incorrect-usage2.scala new file mode 100644 index 000000000000..7bcecede6038 --- /dev/null +++ b/tests/neg/specialized-trait-specialized-incorrect-usage2.scala @@ -0,0 +1,24 @@ +//> using options -language:experimental.specializedTraits +trait Illegal: + def foo: Specialized[Int] // error: Specialized may only be used as a context bound + + def bar(x: Specialized[Int]): Int // error: Specialized may only be used as a context bound + + class Foo(x: Specialized[Int]) // error: Specialized may only be used as a context bound + + class Bar(val x: Specialized[Int]) // error: Specialized may only be used as a context bound + + val y: Specialized[Char] // error: Specialized may only be used as a context bound + + val v = Specialized.apply[Int] // error: Specialized may only be used as a context bound + + def z = + println(Specialized.apply[Float]) // error: Specialized may only be used as a context bound + + def a = Specialized.apply // error: Specialized may only be used as a context bound + + type V = Specialized // error: Specialized may only be used as a context bound + + type W = Specialized[Int] // error: Specialized may only be used as a context bound + + val b = Specialized // error: Specialized may only be used as a context bound diff --git a/tests/neg/specialized-trait-specialized-incorrect-usage3.scala b/tests/neg/specialized-trait-specialized-incorrect-usage3.scala new file mode 100644 index 000000000000..011390a3fb70 --- /dev/null +++ b/tests/neg/specialized-trait-specialized-incorrect-usage3.scala @@ -0,0 +1,3 @@ +//> using options -language:experimental.specializedTraits +trait Illegal: + def x(v: Specialized) = 10 // error: Missing type parameter for Specialized From e2a7e536d30d6d783edb2f886c74715801d7bb62 Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 6 May 2026 12:49:46 +0200 Subject: [PATCH 180/254] Add multiple stages specialized numeric case --- ...-multiple-stages-specialized-numeric.scala | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/pos/specialized-trait-multiple-stages-specialized-numeric.scala diff --git a/tests/pos/specialized-trait-multiple-stages-specialized-numeric.scala b/tests/pos/specialized-trait-multiple-stages-specialized-numeric.scala new file mode 100644 index 000000000000..40211f2b8729 --- /dev/null +++ b/tests/pos/specialized-trait-multiple-stages-specialized-numeric.scala @@ -0,0 +1,22 @@ +//> using options -language:experimental.specializedTraits +// This shouldn't generate any boxing thanks to the specialized version of Numeric + +inline def foo[T: {Specialized, Numeric2}](x: T): T = + val num = summon[Numeric2[T]] + num.plus(x, num.fromInt(1)) + +inline trait A[T: {Numeric2, Specialized}]: + def bar(x: T): T = foo(x) + +class B extends A[Int]: + def baz(x: Int): Int = foo(x) + +inline trait Numeric2[T: Specialized]: + def fromInt(x: Int): T + def plus(x: T, y: T): T + def times(x: T, y: T): T + +implicit object IntIsIntegral extends Numeric2[Int]: + override def fromInt(x: Int): Int = x + override def plus(x: Int, y: Int): Int = x + y + override def times(x: Int, y: Int): Int = x * y From a5e23c6330ad7b08c0b40699803b1ff869566bc2 Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 6 May 2026 13:06:26 +0200 Subject: [PATCH 181/254] Delete some dead code and comments --- .../transform/DesugarSpecializedTraits.scala | 53 +++---------------- 1 file changed, 6 insertions(+), 47 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index e2ae8901c160..d8052336f853 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -32,9 +32,7 @@ import dotty.tools.dotc.core.Flags.GivenOrImplicit import dotty.tools.dotc.core.NameKinds.ContextBoundParamName import dotty.tools.dotc.inlines.Inlines import dotty.tools.dotc.util.Spans.Span -import dotty.tools.dotc.transform.DesugarSpecializedTraits.isSpecializationOf import dotty.tools.dotc.report -import dotty.tools.dotc.transform.DesugarSpecializedTraits.isImplementationOf import dotty.tools.dotc.core.Flags.InlineTrait import dotty.tools.dotc.core.Annotations.Annotation import dotty.tools.dotc.core.Constants.Constant @@ -45,8 +43,7 @@ import dotty.tools.dotc.core.DenotTransformers.DenotTransformer import dotty.tools.dotc.core.Denotations.SingleDenotation import dotty.tools.dotc.core.Flags.InlineMethod -class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: - +class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: // We need DenotTransformer for installAfter (even though we implement transform as id) override def transform(ref: SingleDenotation)(using Context): SingleDenotation = ref @@ -54,7 +51,6 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: override def description: String = DesugarSpecializedTraits.description override def changesMembers: Boolean = false override def changesParents: Boolean = true - override def runsAfter: Set[String] = Set("specializeInlineTraits") override def allowsImplicitSearch: Boolean = true override def run(using Context): Unit = @@ -65,7 +61,7 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: private def newInterfaceTrait(specialization: Specialization, specializations: SpecializedTraitCache): (ClassSymbol, SpecializedTraitCache) = { val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. - def apply(t: Type) = specialization.specializedTypeParamsToTypeArgumentsMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) // TODO: IF we can do just types we can get rid fo this + def apply(t: Type) = specialization.specializedTypeParamsToTypeArgumentsMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) // TODO: If we can do just types we can get rid of this val inheritedParents = specialization.traitSymbol.denot.info.parents.filterNot(_ eq defn.ObjectType).map(tm(_)) // Parents may be specializable and so we need to specialize them as well @@ -99,11 +95,6 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: private def buildInterfaceTraitTree(interfaceSymbol: ClassSymbol)(using Context) = { val init = newDefaultConstructor(interfaceSymbol) - - // TODO: Confirm that we don't need to worry about copying the evidence parameters over from the old constructor - // These should be dealt with when we instantiate the original trait as a parent of this one. Otherwise we should be - // able to copy them over, apply the specialization (keeping e.g. Numeric[Int] that arises from this) and - // pruning any that belong to Specialized. fixConstructor(init, interfaceSymbol) ClassDef(interfaceSymbol, DefDef(init.entered), Nil) } @@ -164,7 +155,6 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: newImplementationClassSymbol.entered // TODO: Do we want to share some code with the newSpecializedInterfaceTrait and buildInterfaceTraitTree? - // TODO: Standardise a bit so that we either generate the symbols and later the classes or not. // TODO: Tidy this up a bit with functions private def buildImplementationClassTree(specialization: Specialization, interfaceSymbol: ClassSymbol, classSymbol: ClassSymbol)(using Context) = { val (objectParent, traitSpParent_, originalTraitSpecializedParent_) = generateImplementationClassParents(specialization, interfaceSymbol) @@ -179,7 +169,7 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: val init = newDefaultConstructor(classSymbol) val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. - def apply(t: Type) = specialization.specializedConstructorParamToArgumentTypeMap.view.applyOrElse(t, mapOver) // TODO: IF we can do just types we can get rid fo this + def apply(t: Type) = specialization.specializedConstructorParamToArgumentTypeMap.applyOrElse(t, mapOver) /* Create constructor and setup constructor type */ val nonTypeParams = specialization.traitSymbol.primaryConstructor.rawParamss.tail @@ -303,7 +293,6 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: case tree => tree } - // TODO: Do we acvtually need to worry about these cases if we have enough limitations? new TreeTypeMap(typeMap, treeMap) { override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? case dd@DefDef(name, paramss, tpt, preRhs) => @@ -526,32 +515,8 @@ object DesugarSpecializedTraits: private[transform] def newImplementationClassName(specialization: Specialization)(using Context): TypeName = generateName(specialization, str.SPECIALIZED_TRAIT_IMPL_SUFFIX) - - // TODO: Put this somewhere else; consider if we want to do it like this? - def isSpecializationOf(type1: Type, type2: Type, allowImplementationClass: Boolean = false)(using Context) = - type2 match { - case Specialization(spec) => type1 match { - case AppliedType(tp, args) => - tp.typeSymbol.name == newSpecializedTraitName(spec) - || (allowImplementationClass && tp.typeSymbol.name == newImplementationClassName(spec)) - case tp: TypeRef => - (tp.typeSymbol.name.toString.contains(newSpecializedTraitName(spec).toString) && - tp.symbol.owner.name == newSpecializedTraitName(spec)) - || - (allowImplementationClass && - tp.typeSymbol.name.toString.contains(newImplementationClassName(spec).toString) && - tp.symbol.owner.name == newImplementationClassName(spec) - ) - case _ => false - } - case _ => false - } - - // TODO: Maybe make consistent with the isSpecializationOf function - def isImplementationOf(name1: Name, name2: Name)(using Context) = - name1.toString().replace(str.SPECIALIZED_TRAIT_IMPL_SUFFIX, str.SPECIALIZED_TRAIT_SUFFIX) == name2.toString() - end DesugarSpecializedTraits + /* Stores the specializations we have found in the program and the symbols for the interface traits and implementation classes that will replace them. We generate these symbols when we enter the specializations into the cache, via the functions @@ -652,7 +617,7 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(usi /* If inline trait Foo[T] has a method taking another Foo[T] there's no point specializing the reference since the resulting sp$T$ would be the same as the starting trait. */ def isSpecialized: Boolean = - hasSpecializedParams && typeArguments.exists(!_.tpe.existsPart(_.typeSymbol.isTypeParam)) //) !tpt.symbol.isTypeParam) // .zip(traitSymbol.typeParams).forall((t, s) => t.tpe =:= s.typeRef)) + hasSpecializedParams && typeArguments.exists(!_.tpe.existsPart(_.typeSymbol.isTypeParam)) // Note: We only care about the specialized arguments for equality; a specialization of Vec[A: Specialized, B] with B = Int and one // with B = String can be considered to be the same as they use the same specialized trait @@ -717,10 +682,7 @@ end Specialization // TODO: Need to try with a bigger project with multiple packages later on to see if we get the behaviour that we are expecting to get in terms of the classes that we generate. -// TODO: Prune the generated anonymous classes. - -// need to test with explicit evidence / our own custom type classes -// TODO: check that we have a single type var only +// TODO: need to test with explicit evidence / our own custom type classes // trait Vec$Sp[S] extends Vec[S, Int, Int, Int, Int] // inline trait Two[S: Specialized] extends Vec$sp[S] @@ -735,11 +697,8 @@ end Specialization // Concerns: // - The superclass of `C` is a top class, or `C` itself is a top class. -// Drop all specialized trait parameters of A // If we can manage to get rid of the inheritance there that could be helpful in terms of avoiding multiple values // BUT: generate a version which is with just inline traits that has this problem as well. // These implementation classes are type correct as long as we inject the knowledge that a specialization trait // like `Seq$sp$Int` is equal to its parameterized version `Seq[Int]` - -// Also delete the other members that already got inlined or maybe we don't care. From bbaf7546f2d017fe487e3074ea5003f7e0834133 Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 6 May 2026 15:50:36 +0200 Subject: [PATCH 182/254] Switch to identity denot transformer --- .../dotc/transform/DesugarSpecializedTraits.scala | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index d8052336f853..c6e70fc722f2 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -42,10 +42,9 @@ import dotty.tools.dotc.core.NameOps.expandedName import dotty.tools.dotc.core.DenotTransformers.DenotTransformer import dotty.tools.dotc.core.Denotations.SingleDenotation import dotty.tools.dotc.core.Flags.InlineMethod +import dotty.tools.dotc.core.DenotTransformers.IdentityDenotTransformer -class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: // We need DenotTransformer for installAfter (even though we implement transform as id) - - override def transform(ref: SingleDenotation)(using Context): SingleDenotation = ref +class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: override def phaseName: String = DesugarSpecializedTraits.name override def description: String = DesugarSpecializedTraits.description @@ -53,11 +52,9 @@ class DesugarSpecializedTraits extends MacroTransform, DenotTransformer: // We n override def changesParents: Boolean = true override def allowsImplicitSearch: Boolean = true - override def run(using Context): Unit = - try super.run - catch case _: CompilationUnit.SuspendException => () - override def newTransformer(using Context): Transformer = new Transformer { + + private def newInterfaceTrait(specialization: Specialization, specializations: SpecializedTraitCache): (ClassSymbol, SpecializedTraitCache) = { val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. From 7a6dac1a807f857b75ac3e4fa26099438efade72 Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 6 May 2026 15:52:27 +0200 Subject: [PATCH 183/254] Move transform functions outside of Transformer --- .../transform/DesugarSpecializedTraits.scala | 806 +++++++++--------- 1 file changed, 401 insertions(+), 405 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index c6e70fc722f2..4ca929719b66 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -52,392 +52,389 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: override def changesParents: Boolean = true override def allowsImplicitSearch: Boolean = true + private def newInterfaceTrait(specialization: Specialization, specializations: SpecializedTraitCache)(using Context): (ClassSymbol, SpecializedTraitCache) = { + val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. + def apply(t: Type) = specialization.specializedTypeParamsToTypeArgumentsMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) // TODO: If we can do just types we can get rid of this + + val inheritedParents = specialization.traitSymbol.denot.info.parents.filterNot(_ eq defn.ObjectType).map(tm(_)) + // Parents may be specializable and so we need to specialize them as well + // See ArrayIterator extends Iterator in specialized-trait-collections-example.scala + val specializations1 = inheritedParents.foldLeft(specializations)((specializations, parent) => + parent match { + case Specialization(spec) if spec.isSpecialized => specializations.addInterface(spec) + case _ => specializations + } + ) + // Create new trait + val parents = defn.ObjectType + :: AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization).tpe // original trait, specialized to Foo[Int] + :: inheritedParents.map(replaceSpecializedSymbolsMap(specializations1).typeMap(_)) // parents of the original trait, specialized to Foo$sp$Int + + val traitSymbol = newNormalizedClassSymbol( + specialization.traitSymbol.owner, + DesugarSpecializedTraits.newSpecializedTraitName(specialization), + Flags.Synthetic | Flags.Trait | Flags.Inline, + parents, + NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? + specialization.traitSymbol.privateWithin, + specialization.traitSymbol.coord, + specialization.traitSymbol.compilationUnitInfo + ) + buildTypeParameters(traitSymbol, specialization) + (traitSymbol.entered, specializations1) + } + private def buildInterfaceTraitTree(interfaceSymbol: ClassSymbol)(using Context) = { + val init = newDefaultConstructor(interfaceSymbol) + fixConstructor(init, interfaceSymbol) + ClassDef(interfaceSymbol, DefDef(init.entered), Nil) + } - private def newInterfaceTrait(specialization: Specialization, specializations: SpecializedTraitCache): (ClassSymbol, SpecializedTraitCache) = { - val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. - def apply(t: Type) = specialization.specializedTypeParamsToTypeArgumentsMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) // TODO: If we can do just types we can get rid of this - - val inheritedParents = specialization.traitSymbol.denot.info.parents.filterNot(_ eq defn.ObjectType).map(tm(_)) - // Parents may be specializable and so we need to specialize them as well - // See ArrayIterator extends Iterator in specialized-trait-collections-example.scala - val specializations1 = inheritedParents.foldLeft(specializations)((specializations, parent) => - parent match { - case Specialization(spec) if spec.isSpecialized => specializations.addInterface(spec) - case _ => specializations - } - ) - - // Create new trait - val parents = defn.ObjectType - :: AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization).tpe // original trait, specialized to Foo[Int] - :: inheritedParents.map(replaceSpecializedSymbolsMap(specializations1).typeMap(_)) // parents of the original trait, specialized to Foo$sp$Int - - val traitSymbol = newNormalizedClassSymbol( - specialization.traitSymbol.owner, - DesugarSpecializedTraits.newSpecializedTraitName(specialization), - Flags.Synthetic | Flags.Trait | Flags.Inline, - parents, - NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? - specialization.traitSymbol.privateWithin, - specialization.traitSymbol.coord, - specialization.traitSymbol.compilationUnitInfo - ) - - buildTypeParameters(traitSymbol, specialization) - (traitSymbol.entered, specializations1) - } - - private def buildInterfaceTraitTree(interfaceSymbol: ClassSymbol)(using Context) = { - val init = newDefaultConstructor(interfaceSymbol) - fixConstructor(init, interfaceSymbol) - ClassDef(interfaceSymbol, DefDef(init.entered), Nil) + /* Fix constructor so that it: + 1) Has correct generic type parameters + 2) Returns the correct type corresponding to those type parameters applied */ + private def fixConstructor(init: Symbol, traitOrClassSymbol: ClassSymbol)(using Context) = + val rt = traitOrClassSymbol.typeRef.appliedTo(traitOrClassSymbol.typeParams.map(_.typeRef)) + def resultType(tpe: Type): Option[Type] = tpe match { + case mt @ MethodType(paramNames) => Some(mt.derivedLambdaType(paramNames, mt.paramInfos, resultType(mt.resultType).getOrElse(rt))) + case pt : PolyType => Some(pt.derivedLambdaType(pt.paramNames, pt.paramInfos, resultType(pt.resType).get)) + case _ => None } + init.info = resultType(init.info).get + init.info = PolyType.fromParams(init.owner.typeParams, init.info) + + private def buildTypeParameters(traitOrClassSymbol: ClassSymbol, specialization: Specialization)(using Context) = + val tps = newTypeParams(traitOrClassSymbol, + specialization.unspecializedTypeParams.map(_.typeSymbol.name.asTypeName), + EmptyFlags, + targets => targets.map(t => specialization.traitSymbol.typeParams.find(_.name == t.name).get.info.bounds) + ) + tps.foreach(traitOrClassSymbol.enter(_, EmptyScope)) + + // Replace old type parameters that were copied from original trait with new ones + // inside the parents of the new trait + val tpMap: Map[Type, Type] = specialization.unspecializedTypeParams.zip(tps.map(_.typeRef)).toMap + val freshTypeVarMap = new TypeMap: + def apply(t: Type) = tpMap.applyOrElse(t, mapOver) + + // TODO: What happens if the creator of the specialized inline trait provides a self type? + traitOrClassSymbol.info = ClassInfo(traitOrClassSymbol.owner.thisType, traitOrClassSymbol, traitOrClassSymbol.info.parents.map(freshTypeVarMap(_)), traitOrClassSymbol.info.decls) + + private def generateImplementationClassParents(specialization: Specialization, interfaceSymbol: ClassSymbol)(using Context) = + val objectParent = defn.ObjectType + val traitSpParent = interfaceSymbol.typeRef.appliedTo(specialization.unspecializedTypeParams) // Set using old unspecializedTypeParams and replace after. + val originalTraitSpecializedParent = AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.mapUnspecializedArgs(specialization.unspecializedTypeParams.map(TypeTree(_)))).tpe + (objectParent, traitSpParent, originalTraitSpecializedParent) + + private def newImplementationClass(specialization: Specialization, interfaceSymbol: ClassSymbol)(using Context) = + val (objectParent, traitSpParent, originalTraitSpecializedParent) = generateImplementationClassParents(specialization, interfaceSymbol) + val parents = List(objectParent, traitSpParent, originalTraitSpecializedParent) + + val newImplementationClassSymbol = newNormalizedClassSymbol( + specialization.traitSymbol.owner, + DesugarSpecializedTraits.newImplementationClassName(specialization), + Flags.Synthetic, + parents, + NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? + specialization.traitSymbol.privateWithin, + specialization.traitSymbol.coord, + specialization.traitSymbol.compilationUnitInfo + ) - /* Fix constructor so that it: - 1) Has correct generic type parameters - 2) Returns the correct type corresponding to those type parameters applied */ - private def fixConstructor(init: Symbol, traitOrClassSymbol: ClassSymbol) = - val rt = traitOrClassSymbol.typeRef.appliedTo(traitOrClassSymbol.typeParams.map(_.typeRef)) - def resultType(tpe: Type): Option[Type] = tpe match { - case mt @ MethodType(paramNames) => Some(mt.derivedLambdaType(paramNames, mt.paramInfos, resultType(mt.resultType).getOrElse(rt))) - case pt : PolyType => Some(pt.derivedLambdaType(pt.paramNames, pt.paramInfos, resultType(pt.resType).get)) - case _ => None - } - init.info = resultType(init.info).get - init.info = PolyType.fromParams(init.owner.typeParams, init.info) - - private def buildTypeParameters(traitOrClassSymbol: ClassSymbol, specialization: Specialization) = - val tps = newTypeParams(traitOrClassSymbol, - specialization.unspecializedTypeParams.map(_.typeSymbol.name.asTypeName), - EmptyFlags, - targets => targets.map(t => specialization.traitSymbol.typeParams.find(_.name == t.name).get.info.bounds) - ) - tps.foreach(traitOrClassSymbol.enter(_, EmptyScope)) - - // Replace old type parameters that were copied from original trait with new ones - // inside the parents of the new trait - val tpMap: Map[Type, Type] = specialization.unspecializedTypeParams.zip(tps.map(_.typeRef)).toMap - val freshTypeVarMap = new TypeMap: - def apply(t: Type) = tpMap.applyOrElse(t, mapOver) - - // TODO: What happens if the creator of the specialized inline trait provides a self type? - traitOrClassSymbol.info = ClassInfo(traitOrClassSymbol.owner.thisType, traitOrClassSymbol, traitOrClassSymbol.info.parents.map(freshTypeVarMap(_)), traitOrClassSymbol.info.decls) - - private def generateImplementationClassParents(specialization: Specialization, interfaceSymbol: ClassSymbol) = - val objectParent = defn.ObjectType - val traitSpParent = interfaceSymbol.typeRef.appliedTo(specialization.unspecializedTypeParams) // Set using old unspecializedTypeParams and replace after. - val originalTraitSpecializedParent = AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.mapUnspecializedArgs(specialization.unspecializedTypeParams.map(TypeTree(_)))).tpe - (objectParent, traitSpParent, originalTraitSpecializedParent) - - private def newImplementationClass(specialization: Specialization, interfaceSymbol: ClassSymbol) = - val (objectParent, traitSpParent, originalTraitSpecializedParent) = generateImplementationClassParents(specialization, interfaceSymbol) - val parents = List(objectParent, traitSpParent, originalTraitSpecializedParent) - - val newImplementationClassSymbol = newNormalizedClassSymbol( - specialization.traitSymbol.owner, - DesugarSpecializedTraits.newImplementationClassName(specialization), - Flags.Synthetic, - parents, - NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? - specialization.traitSymbol.privateWithin, - specialization.traitSymbol.coord, - specialization.traitSymbol.compilationUnitInfo - ) - - buildTypeParameters(newImplementationClassSymbol, specialization) + buildTypeParameters(newImplementationClassSymbol, specialization) - newImplementationClassSymbol.entered + newImplementationClassSymbol.entered - // TODO: Do we want to share some code with the newSpecializedInterfaceTrait and buildInterfaceTraitTree? - // TODO: Tidy this up a bit with functions - private def buildImplementationClassTree(specialization: Specialization, interfaceSymbol: ClassSymbol, classSymbol: ClassSymbol)(using Context) = { - val (objectParent, traitSpParent_, originalTraitSpecializedParent_) = generateImplementationClassParents(specialization, interfaceSymbol) + // TODO: Do we want to share some code with the newSpecializedInterfaceTrait and buildInterfaceTraitTree? + // TODO: Tidy this up a bit with functions + private def buildImplementationClassTree(specialization: Specialization, interfaceSymbol: ClassSymbol, classSymbol: ClassSymbol)(using Context) = { + val (objectParent, traitSpParent_, originalTraitSpecializedParent_) = generateImplementationClassParents(specialization, interfaceSymbol) - // Apply Type Param Fix: TODO : This really ought to be done more cleanly somewhere else. - val tpMap: Map[Type, Type] = specialization.unspecializedTypeParams.zip(classSymbol.typeParams.map(_.typeRef)).toMap - val freshTypeVarMap = new TypeMap: - def apply(t: Type) = tpMap.applyOrElse(t, mapOver) - val traitSpParent = freshTypeVarMap(traitSpParent_) - val originalTraitSpecializedParent = freshTypeVarMap(originalTraitSpecializedParent_) + // Apply Type Param Fix: TODO : This really ought to be done more cleanly somewhere else. + val tpMap: Map[Type, Type] = specialization.unspecializedTypeParams.zip(classSymbol.typeParams.map(_.typeRef)).toMap + val freshTypeVarMap = new TypeMap: + def apply(t: Type) = tpMap.applyOrElse(t, mapOver) + val traitSpParent = freshTypeVarMap(traitSpParent_) + val originalTraitSpecializedParent = freshTypeVarMap(originalTraitSpecializedParent_) - val init = newDefaultConstructor(classSymbol) - - val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. - def apply(t: Type) = specialization.specializedConstructorParamToArgumentTypeMap.applyOrElse(t, mapOver) - - /* Create constructor and setup constructor type */ - val nonTypeParams = specialization.traitSymbol.primaryConstructor.rawParamss.tail - val oldTypeParams = specialization.unspecializedConstructorParams - val initTypeParams = classSymbol.typeParams.map(s => s.copy(owner = init, flags = (s.flags &~ (Flags.Private | Flags.Deferred)))) - val valueParams = nonTypeParams.map(_.map(param => param.copy(owner = init, info = tm(param.info).substSym(oldTypeParams, initTypeParams), name=param.name.expandedName(classSymbol)))) // We need to map the parameter names to avoid a name clash with val params from parents (see tests/pos/specialized-trait-val-parameter.scala) - - initTypeParams.foreach(_.entered) - init.setParamss(initTypeParams :: valueParams) - init.info = specialization.traitSymbol.primaryConstructor.info.appliedTo( // Type Arg if specialized; otherwise we want our type param. - specialization.constructorTypeParams.map(par => specialization.specializedConstructorParamToArgumentTypeMap.applyOrElse(par, _.subst(oldTypeParams, classSymbol.typeParams.map(_.typeRef)))) + val init = newDefaultConstructor(classSymbol) + + val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. + def apply(t: Type) = specialization.specializedConstructorParamToArgumentTypeMap.applyOrElse(t, mapOver) + + /* Create constructor and setup constructor type */ + val nonTypeParams = specialization.traitSymbol.primaryConstructor.rawParamss.tail + val oldTypeParams = specialization.unspecializedConstructorParams + val initTypeParams = classSymbol.typeParams.map(s => s.copy(owner = init, flags = (s.flags &~ (Flags.Private | Flags.Deferred)))) + val valueParams = nonTypeParams.map(_.map(param => param.copy(owner = init, info = tm(param.info).substSym(oldTypeParams, initTypeParams), name=param.name.expandedName(classSymbol)))) // We need to map the parameter names to avoid a name clash with val params from parents (see tests/pos/specialized-trait-val-parameter.scala) + + initTypeParams.foreach(_.entered) + init.setParamss(initTypeParams :: valueParams) + init.info = specialization.traitSymbol.primaryConstructor.info.appliedTo( // Type Arg if specialized; otherwise we want our type param. + specialization.constructorTypeParams.map(par => specialization.specializedConstructorParamToArgumentTypeMap.applyOrElse(par, _.subst(oldTypeParams, classSymbol.typeParams.map(_.typeRef)))) + ) + fixConstructor(init, classSymbol) + + /* Build param accessors */ + val paramAccessorss = valueParams.map(params => params.map(s => s.copy(owner = classSymbol, flags=(s.flags|Flags.LocalParamAccessor) &~ Flags.Param, info = s.info.subst(initTypeParams, classSymbol.typeParams.map(_.typeRef))))) + paramAccessorss.foreach(_.foreach(classSymbol.enter(_))) + + /* Build class def tree */ + val newParamss = paramAccessorss.nestedMap(ref(_)) + val newParams1 = if (newParamss.length == 1) then newParamss ++ List(List()) else newParamss + // TODO: Clean and robust + val classDef = ClassDefWithParents( + classSymbol, + DefDef(init.asTerm.entered), + List( + New(objectParent, objectParent.classSymbol.primaryConstructor.asTerm, Nil), + New(traitSpParent, traitSpParent.classSymbol.primaryConstructor.asTerm, Nil), + New(originalTraitSpecializedParent.typeConstructor) + .select(TermRef(originalTraitSpecializedParent.typeConstructor, specialization.traitSymbol.primaryConstructor.asTerm)) // TODO: Check for other constructors + .appliedToTypes(originalTraitSpecializedParent.argTypes) + .appliedToArgss(newParams1) + ), + // Put into body of class + paramAccessorss.flatMap(syms => syms.map(sym => tpd.ValDef(sym.asTerm))) ) - fixConstructor(init, classSymbol) - - /* Build param accessors */ - val paramAccessorss = valueParams.map(params => params.map(s => s.copy(owner = classSymbol, flags=(s.flags|Flags.LocalParamAccessor) &~ Flags.Param, info = s.info.subst(initTypeParams, classSymbol.typeParams.map(_.typeRef))))) - paramAccessorss.foreach(_.foreach(classSymbol.enter(_))) - - /* Build class def tree */ - val newParamss = paramAccessorss.nestedMap(ref(_)) - val newParams1 = if (newParamss.length == 1) then newParamss ++ List(List()) else newParamss - // TODO: Clean and robust - val classDef = ClassDefWithParents( - classSymbol, - DefDef(init.asTerm.entered), - List( - New(objectParent, objectParent.classSymbol.primaryConstructor.asTerm, Nil), - New(traitSpParent, traitSpParent.classSymbol.primaryConstructor.asTerm, Nil), - New(originalTraitSpecializedParent.typeConstructor) - .select(TermRef(originalTraitSpecializedParent.typeConstructor, specialization.traitSymbol.primaryConstructor.asTerm)) // TODO: Check for other constructors - .appliedToTypes(originalTraitSpecializedParent.argTypes) - .appliedToArgss(newParams1) - ), - // Put into body of class - paramAccessorss.flatMap(syms => syms.map(sym => tpd.ValDef(sym.asTerm))) - ) - classDef - } - - private def replaceSpecializedSymbolsMap(specializations: SpecializedTraitCache)(using Context) = - val typeMap = new TypeMap: - def apply(t: Type) = t match { - case Specialization(spec) => - { - for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) - yield specializedSymbol.typeRef.appliedTo(spec.unspecializedTypeArgs.map(_.tpe)) - }.getOrElse(mapOver(t)) - case _ => mapOver(t) - } + classDef + } - def treeMap(tree: Tree): Tree = tree match { - /* Replace new Foo[Int] {} with new Foo$impl$Int.asInstanceOf[Foo$sp$Int] - This has already been desugared to an anonymous class instance with parents: - Objects, Parents of Foo, Foo. */ - case Block(List(an@TypeDef(anon, tmpl@Template(_, parentCalls: List[Tree], _, _))), - Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => - - def deandify(tp: Type): Iterator[Type] = tp match - case AndType(l, r) => deandify(l) ++ deandify(r) - case _ => Iterator.single(tp) + private def replaceSpecializedSymbolsMap(specializations: SpecializedTraitCache)(using Context) = + val typeMap = new TypeMap: + def apply(t: Type) = t match { + case Specialization(spec) => + { + for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) + yield specializedSymbol.typeRef.appliedTo(spec.unspecializedTypeArgs.map(_.tpe)) + }.getOrElse(mapOver(t)) + case _ => mapOver(t) + } - t.tpe match { - case a: AndType => /* Multiple mixed in traits will be typed as an AndType */ - deandify(a).foreach(trt => - Specialization.unapply(trt).foreach {spec => - if spec.hasSpecializedParams then - report.error("Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like.", an.srcPos) - } - ) - tree - case tpe => - Specialization.unapply(tpe).map(spec => - { - if spec.hasSpecializedParams then - if tmpl.body.filterNot(x => x.symbol.name.is(ContextBoundParamName)).nonEmpty then // Only allowed to contain evidence parameters - report.error("Anonymous classes acting as instances of Specialized traits may not have additional members; you can make a named object instead if you like.", an.srcPos) - - parentCalls match { - case (obj :: parentsOfSpecTrait) :+ (app@Apply(_, _)) if (obj.symbol.owner == ctx.definitions.ObjectClass) && (parentsOfSpecTrait.forall(x => spec.traitSymbol.asClass.parentSyms.exists(p => p == x.symbol.owner))) => - specializations.getImplementationSymbol(spec).map( specializedSymbol => - Typed( - Select(New(ref(specializedSymbol)),ctor).appliedToTypeTrees(spec.unspecializedTypeArgs) - .appliedToArgss(tpd.allArgss(app).tail.nestedMap(_.changeNonLocalOwners(an.symbol.owner))) // Remove the type params which are not needed - , t) - ).getOrElse(tree) // We don't replace non-specialized anonymous class instantiations e.g. new Foo[T] where T is defined in the enclosing scope. - case _ => - report.error("Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like.", an.srcPos) - tree - } - else - tree - }).getOrElse(tree) - } - - // Replace class/object Bar extends Foo[Int](params) with class/object Bar extends Foo$sp$Int(params) - case app @ Apply(_, _) => tpd.methPart(app) match { - case fun @ Select(New(tpt), init) if fun.symbol.isConstructor => - val argss = tpd.allArgss(tree) - argss match { - case typeArgs :: valueArgss => - val spec = Specialization(fun.symbol.owner, typeArgs) + def treeMap(tree: Tree): Tree = tree match { + /* Replace new Foo[Int] {} with new Foo$impl$Int.asInstanceOf[Foo$sp$Int] + This has already been desugared to an anonymous class instance with parents: + Objects, Parents of Foo, Foo. */ + case Block(List(an@TypeDef(anon, tmpl@Template(_, parentCalls: List[Tree], _, _))), + Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => + + def deandify(tp: Type): Iterator[Type] = tp match + case AndType(l, r) => deandify(l) ++ deandify(r) + case _ => Iterator.single(tp) + + t.tpe match { + case a: AndType => /* Multiple mixed in traits will be typed as an AndType */ + deandify(a).foreach(trt => + Specialization.unapply(trt).foreach {spec => + if spec.hasSpecializedParams then + report.error("Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like.", an.srcPos) + } + ) + tree + case tpe => + Specialization.unapply(tpe).map(spec => { - for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) - yield New(ref(specializedSymbol)).select(init).appliedToTypeTrees(spec.unspecializedTypeArgs).appliedToNone - }.getOrElse(tree) - case _ => tree - } - case _ => tree - } - - // Replace AppliedTypeTree instances in code - case Specialization(spec) => { - for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) - yield - if spec.unspecializedTypeArgs.nonEmpty then - AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? - else - TypeTree(specializedSymbol.typeRef) - }.getOrElse(tree) - - case tree => tree - } - - new TreeTypeMap(typeMap, treeMap) { - override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? - case dd@DefDef(name, paramss, tpt, preRhs) => - val transformedDef = super.transform(dd) - transformedDef.symbol.info = mapType(transformedDef.symbol.info) - transformedDef - - case vd@ValDef(name, tpt, preRhs) => - val transformedDef = super.transform(vd) - transformedDef.symbol.info = mapType(transformedDef.symbol.info) - transformedDef - - case impl@Template(constr, preParentsOrDerived, self, _) => - val mappedbody = impl.body.map(transform(_)) - val mappedconstr = transform(impl.constr).asInstanceOf[DefDef] - - /* We need to map parents of non-specialized inline traits (see tests/pos/specialized-trait-partial-complete-specialization-with-return-type.scala, we need - to map the A[Int] reference to A$sp$Int in B's parents). For our implementation classes and interface traits we don't want to map as we will delete parents after. */ - val mappedparents = if impl.symbol.owner.isSpecializedTraitImplementationClass || impl.symbol.owner.isSpecializedTraitInterface then impl.parents else impl.parents.map(transform(_)) - val oldInfo = impl.symbol.owner.info.asInstanceOf[ClassInfo] - impl.symbol.owner.info = oldInfo.derivedClassInfo(declaredParents = if impl.symbol.owner.isSpecializedTraitImplementationClass || impl.symbol.owner.isSpecializedTraitInterface then oldInfo.declaredParents else oldInfo.declaredParents.map(mapType(_))) - - cpy.Template(impl)(body = mappedbody, parents = mappedparents, constr = mappedconstr) - case tree => super.transform(tree) - } + if spec.hasSpecializedParams then + if tmpl.body.filterNot(x => x.symbol.name.is(ContextBoundParamName)).nonEmpty then // Only allowed to contain evidence parameters + report.error("Anonymous classes acting as instances of Specialized traits may not have additional members; you can make a named object instead if you like.", an.srcPos) + + parentCalls match { + case (obj :: parentsOfSpecTrait) :+ (app@Apply(_, _)) if (obj.symbol.owner == ctx.definitions.ObjectClass) && (parentsOfSpecTrait.forall(x => spec.traitSymbol.asClass.parentSyms.exists(p => p == x.symbol.owner))) => + specializations.getImplementationSymbol(spec).map( specializedSymbol => + Typed( + Select(New(ref(specializedSymbol)),ctor).appliedToTypeTrees(spec.unspecializedTypeArgs) + .appliedToArgss(tpd.allArgss(app).tail.nestedMap(_.changeNonLocalOwners(an.symbol.owner))) // Remove the type params which are not needed + , t) + ).getOrElse(tree) // We don't replace non-specialized anonymous class instantiations e.g. new Foo[T] where T is defined in the enclosing scope. + case _ => + report.error("Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like.", an.srcPos) + tree + } + else + tree + }).getOrElse(tree) + } + + // Replace class/object Bar extends Foo[Int](params) with class/object Bar extends Foo$sp$Int(params) + case app @ Apply(_, _) => tpd.methPart(app) match { + case fun @ Select(New(tpt), init) if fun.symbol.isConstructor => + val argss = tpd.allArgss(tree) + argss match { + case typeArgs :: valueArgss => + val spec = Specialization(fun.symbol.owner, typeArgs) + { + for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) + yield New(ref(specializedSymbol)).select(init).appliedToTypeTrees(spec.unspecializedTypeArgs).appliedToNone + }.getOrElse(tree) + case _ => tree + } + case _ => tree } - end replaceSpecializedSymbolsMap - - /* Override flags can be generated by inline trait inlining, but after removing the Foo[Int] parent the corresponding members no longer override members in their parents. - Therefore we need to remove them. */ - def removeRedundantOverridesMap = new TreeTypeMap(treeMap = - tree => tree match { - case dd@DefDef(name, paramss, tpt, preRhs) => - if dd.symbol.exists && dd.symbol.allOverriddenSymbols.isEmpty && (dd.symbol.owner.isSpecializedTraitInterface || dd.symbol.owner.isSpecializedTraitImplementationClass) then - dd.symbol.flags = dd.symbol.flags &~ Flags.Override - dd - - case vd@ValDef(name, tpt, preRhs) => - if vd.symbol.exists && vd.symbol.allOverriddenSymbols.isEmpty && (vd.symbol.owner.isSpecializedTraitInterface || vd.symbol.owner.isSpecializedTraitImplementationClass) then - vd.symbol.flags = vd.symbol.flags &~ Flags.Override - vd + + // Replace AppliedTypeTree instances in code + case Specialization(spec) => { + for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) + yield + if spec.unspecializedTypeArgs.nonEmpty then + AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? + else + TypeTree(specializedSymbol.typeRef) + }.getOrElse(tree) + + case tree => tree + } + + new TreeTypeMap(typeMap, treeMap) { + override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? + case dd@DefDef(name, paramss, tpt, preRhs) => + val transformedDef = super.transform(dd) + transformedDef.symbol.info = mapType(transformedDef.symbol.info) + transformedDef + + case vd@ValDef(name, tpt, preRhs) => + val transformedDef = super.transform(vd) + transformedDef.symbol.info = mapType(transformedDef.symbol.info) + transformedDef + + case impl@Template(constr, preParentsOrDerived, self, _) => + val mappedbody = impl.body.map(transform(_)) + val mappedconstr = transform(impl.constr).asInstanceOf[DefDef] - case tree => tree + /* We need to map parents of non-specialized inline traits (see tests/pos/specialized-trait-partial-complete-specialization-with-return-type.scala, we need + to map the A[Int] reference to A$sp$Int in B's parents). For our implementation classes and interface traits we don't want to map as we will delete parents after. */ + val mappedparents = if impl.symbol.owner.isSpecializedTraitImplementationClass || impl.symbol.owner.isSpecializedTraitInterface then impl.parents else impl.parents.map(transform(_)) + val oldInfo = impl.symbol.owner.info.asInstanceOf[ClassInfo] + impl.symbol.owner.info = oldInfo.derivedClassInfo(declaredParents = if impl.symbol.owner.isSpecializedTraitImplementationClass || impl.symbol.owner.isSpecializedTraitInterface then oldInfo.declaredParents else oldInfo.declaredParents.map(mapType(_))) + + cpy.Template(impl)(body = mappedbody, parents = mappedparents, constr = mappedconstr) + case tree => super.transform(tree) } - ) + } + end replaceSpecializedSymbolsMap + + /* Override flags can be generated by inline trait inlining, but after removing the Foo[Int] parent the corresponding members no longer override members in their parents. + Therefore we need to remove them. */ + def removeRedundantOverridesMap(using Context) = new TreeTypeMap(treeMap = + tree => tree match { + case dd@DefDef(name, paramss, tpt, preRhs) => + if dd.symbol.exists && dd.symbol.allOverriddenSymbols.isEmpty && (dd.symbol.owner.isSpecializedTraitInterface || dd.symbol.owner.isSpecializedTraitImplementationClass) then + dd.symbol.flags = dd.symbol.flags &~ Flags.Override + dd + + case vd@ValDef(name, tpt, preRhs) => + if vd.symbol.exists && vd.symbol.allOverriddenSymbols.isEmpty && (vd.symbol.owner.isSpecializedTraitInterface || vd.symbol.owner.isSpecializedTraitImplementationClass) then + vd.symbol.flags = vd.symbol.flags &~ Flags.Override + vd + + case tree => tree + } + ) - // Returns (new stmts including original, new symbols including original) - private def transformStatements(stats1: List[Tree], span: Span, specializations: SpecializedTraitCache): (List[Tree], SpecializedTraitCache) = { + // Returns (new stmts including original, new symbols including original) + private def transformStatements(stats1: List[Tree], span: Span, specializations: SpecializedTraitCache)(using Context): (List[Tree], SpecializedTraitCache) = { - val inlineSpecializedMethods = new TreeMapWithPreciseStatContexts { - override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? - case app: Apply if app.symbol.isSpecializedMethod => - super.transform(Inlines.inlineCall(tree)) - case tree => super.transform(tree) - } + val inlineSpecializedMethods = new TreeMapWithPreciseStatContexts { + override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? + case app: Apply if app.symbol.isSpecializedMethod => + super.transform(Inlines.inlineCall(tree)) + case tree => super.transform(tree) + } + } + + val stats = inlineSpecializedMethods.transform(stats1) + + val specializations1 = collectReferencedSpecializations(stats, specializations) + val generatedTraitStats = specializations1.getNewInterfaceSymbols.toList.map(buildInterfaceTraitTree) + val generatedClassStats = specializations1.getNewImplementationSymbols.toList.map(buildImplementationClassTree) + + val specializations2 = specializations1.installNewInterfaceSymbols.installNewImplementationSymbols + + /* We have Vec$sp$Int extends Vec[Int] in order to do the inlining, but then remove this parent + afterwards to avoid interface implementation problems (see tests/run/specialized-trait-as-parameter.scala, + tests/run/specialized-trait-as-return-type.scala) */ + extension (classTree: Tree) + def updateParents(parentUpdater: List[Type] => List[Type]) = (classTree: @unchecked) match { + case td@TypeDef(name, t@Template(constr, preParentsOrDerived, self, preBody)) => + + val cls = td.symbol.asClass + val oldInfo = cls.classInfo + val newInfo = oldInfo.derivedClassInfo(declaredParents = parentUpdater(oldInfo.declaredParents)) + cls.info = newInfo + cls.copySymDenotation(info = newInfo).installAfter(DesugarSpecializedTraits.this) + } + + def refreshClassDef = (classTree: @unchecked) match { + case td@TypeDef(name, t@Template(constr, preParentsOrDerived, self, preBody)) => + ClassDef(td.symbol.asClass, constr, t.body) } - - val stats = inlineSpecializedMethods.transform(stats1) - - val specializations1 = collectReferencedSpecializations(stats, specializations) - val generatedTraitStats = specializations1.getNewInterfaceSymbols.toList.map(buildInterfaceTraitTree) - val generatedClassStats = specializations1.getNewImplementationSymbols.toList.map(buildImplementationClassTree) - - val specializations2 = specializations1.installNewInterfaceSymbols.installNewImplementationSymbols - - /* We have Vec$sp$Int extends Vec[Int] in order to do the inlining, but then remove this parent - afterwards to avoid interface implementation problems (see tests/run/specialized-trait-as-parameter.scala, - tests/run/specialized-trait-as-return-type.scala) */ - extension (classTree: Tree) - def updateParents(parentUpdater: List[Type] => List[Type]) = (classTree: @unchecked) match { - case td@TypeDef(name, t@Template(constr, preParentsOrDerived, self, preBody)) => - - val cls = td.symbol.asClass - val oldInfo = cls.classInfo - val newInfo = oldInfo.derivedClassInfo(declaredParents = parentUpdater(oldInfo.declaredParents)) - cls.info = newInfo - cls.copySymDenotation(info = newInfo).installAfter(DesugarSpecializedTraits.this) - } - def refreshClassDef = (classTree: @unchecked) match { - case td@TypeDef(name, t@Template(constr, preParentsOrDerived, self, preBody)) => - ClassDef(td.symbol.asClass, constr, t.body) + /* We need to inline recursively throughout generated specialized traits - see tests/run/specialized-trait-requires-inline-trait-inlining.scala */ + // TODO: How do we calculate the spans correctly? + val inlineInlineTraits = new TreeTypeMap(treeMap = (tree: Tree) => tree match { + case tree: TypeDef if tree.symbol.isInlineTrait => + val tree1 = Inlines.checkAndTransformInlineTrait(tree) + val tree2 = if Inlines.needsInlining(tree1) then Inlines.inlineParentInlineTraits(tree1) else tree1 + tree2 + case tree: TypeDef if Inlines.needsInlining(tree) => + Inlines.inlineParentInlineTraits(tree) + case t => t + }) + + val generatedTraitStats1 = generatedTraitStats.map { + case tree: TypeDef => + assert(tree.symbol.isInlineTrait) + val inlined = Inlines.inlineParentInlineTraits(Inlines.checkAndTransformInlineTrait(tree.withSpan(span)),allowSpecialized=true).asInstanceOf[TypeDef] + cpy.TypeDef(inlined)(name = inlined.name, rhs = inlineInlineTraits(inlined.rhs)).withSpan(inlined.span) + } + + val generatedClassStats1 = generatedClassStats.map { + case tree: TypeDef => + assert(Inlines.needsInlining(tree, allowSpecializedTraits=true)) + val inlined = Inlines.inlineParentInlineTraits(tree.withSpan(span), allowSpecialized=true).asInstanceOf[TypeDef] + cpy.TypeDef(inlined)(name = inlined.name, rhs = inlineInlineTraits(inlined.rhs)).withSpan(inlined.span) + }.tapEach: // We can do parent removal earlier for $impl$ classes as we don't depend on the parents later. + _.updateParents { parents => (parents: @unchecked) match + case obj :: traitSp :: originalSpec :: Nil => obj :: traitSp :: Nil } + .map(refreshClassDef) - /* We need to inline recursively throughout generated specialized traits - see tests/run/specialized-trait-requires-inline-trait-inlining.scala */ - // TODO: How do we calculate the spans correctly? - val inlineInlineTraits = new TreeTypeMap(treeMap = (tree: Tree) => tree match { - case tree: TypeDef if tree.symbol.isInlineTrait => - val tree1 = Inlines.checkAndTransformInlineTrait(tree) - val tree2 = if Inlines.needsInlining(tree1) then Inlines.inlineParentInlineTraits(tree1) else tree1 - tree2 - case tree: TypeDef if Inlines.needsInlining(tree) => - Inlines.inlineParentInlineTraits(tree) - case t => t - }) - - val generatedTraitStats1 = generatedTraitStats.map { - case tree: TypeDef => - assert(tree.symbol.isInlineTrait) - val inlined = Inlines.inlineParentInlineTraits(Inlines.checkAndTransformInlineTrait(tree.withSpan(span)),allowSpecialized=true).asInstanceOf[TypeDef] - cpy.TypeDef(inlined)(name = inlined.name, rhs = inlineInlineTraits(inlined.rhs)).withSpan(inlined.span) - } - - val generatedClassStats1 = generatedClassStats.map { - case tree: TypeDef => - assert(Inlines.needsInlining(tree, allowSpecializedTraits=true)) - val inlined = Inlines.inlineParentInlineTraits(tree.withSpan(span), allowSpecialized=true).asInstanceOf[TypeDef] - cpy.TypeDef(inlined)(name = inlined.name, rhs = inlineInlineTraits(inlined.rhs)).withSpan(inlined.span) - }.tapEach: // We can do parent removal earlier for $impl$ classes as we don't depend on the parents later. - _.updateParents { parents => (parents: @unchecked) match - case obj :: traitSp :: originalSpec :: Nil => obj :: traitSp :: Nil + if (generatedTraitStats1.isEmpty && generatedClassStats1.isEmpty) + (stats.map(replaceSpecializedSymbolsMap(specializations2)(_)), specializations2) // TODO: Check if this shouldn't be the same as the one belwo!? + else + val (generatedTraitStats2, specializations3) = transformStatements(generatedTraitStats1, span, specializations2) + val (generatedClassStats2, specializations4) = transformStatements(generatedClassStats1, span, specializations3) + + /* We need to do the parent removal after inlining into the $impl$ classes otherwise we break + overriding/interface implementation rules during the inlining. The $impl$ inlining + can also happen in the recursive calls, and so we need to do this right at the end (after the recursive calls): */ + val generatedTraitStats3 = + generatedTraitStats2.tapEach: stat => + if stat.symbol.isSpecializedTraitInterface then // We could have $impl$ classes from recursive calls as well. + stat.updateParents { parents => (parents: @unchecked) match + case obj :: Specialization(originalSpec) :: parents if specializations4.getInterfaceSymbol(originalSpec).get == stat.symbol.asClass => + obj :: parents + case obj :: parents => obj :: parents // We already removed the relevant parent. } .map(refreshClassDef) - - if (generatedTraitStats1.isEmpty && generatedClassStats1.isEmpty) - (stats.map(replaceSpecializedSymbolsMap(specializations2)(_)), specializations2) // TODO: Check if this shouldn't be the same as the one belwo!? - else - val (generatedTraitStats2, specializations3) = transformStatements(generatedTraitStats1, span, specializations2) - val (generatedClassStats2, specializations4) = transformStatements(generatedClassStats1, span, specializations3) - - /* We need to do the parent removal after inlining into the $impl$ classes otherwise we break - overriding/interface implementation rules during the inlining. The $impl$ inlining - can also happen in the recursive calls, and so we need to do this right at the end (after the recursive calls): */ - val generatedTraitStats3 = - generatedTraitStats2.tapEach: stat => - if stat.symbol.isSpecializedTraitInterface then // We could have $impl$ classes from recursive calls as well. - stat.updateParents { parents => (parents: @unchecked) match - case obj :: Specialization(originalSpec) :: parents if specializations4.getInterfaceSymbol(originalSpec).get == stat.symbol.asClass => - obj :: parents - case obj :: parents => obj :: parents // We already removed the relevant parent. - } - .map(refreshClassDef) - - val stats2 = generatedTraitStats3 ++ - generatedClassStats2 ++ - stats.map(stat => - replaceSpecializedSymbolsMap(specializations4)( // Foo[Int] -> Foo$sp$Int in user code. - if (!stat.symbol.isSpecializedTraitImplementationClass && !stat.symbol.isSpecializedTraitInterface) then // We already processed these in an earlier recursive call - Inlines.inlineParentInlineTraits(stat, allowSpecialized = true) // Perform inlining into class Bar extends Foo[Int] from user code. - else - stat - )) - - (stats2.map(removeRedundantOverridesMap(_)), specializations4) - } - - override def transform(tree: Tree)(using Context): Tree = tree - match { // TODO: Is Package level processing really what we want? Given we are going to output the classes somewhere else do we not really want either to deepFold the whole tree directly or do a more direct transform? + + val stats2 = generatedTraitStats3 ++ + generatedClassStats2 ++ + stats.map(stat => + replaceSpecializedSymbolsMap(specializations4)( // Foo[Int] -> Foo$sp$Int in user code. + if (!stat.symbol.isSpecializedTraitImplementationClass && !stat.symbol.isSpecializedTraitInterface) then // We already processed these in an earlier recursive call + Inlines.inlineParentInlineTraits(stat, allowSpecialized = true) // Perform inlining into class Bar extends Foo[Int] from user code. + else + stat + )) + + (stats2.map(removeRedundantOverridesMap(_)), specializations4) + } + + override protected def newTransformer(using Context): Transformer = new Transformer: + override def transform(tree: Tree)(using Context): Tree = + tree match { // TODO: Is Package level processing really what we want? Given we are going to output the classes somewhere else do we not really want either to deepFold the whole tree directly or do a more direct transform? case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. def checkType(t: Type, pos: SrcPos) = t.widen.dealias match { @@ -456,46 +453,45 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: cpy.PackageDef(pkg)(pid, stats1) } - private def collectReferencedSpecializations(stats: List[Tree], specializations: SpecializedTraitCache)(using Context): SpecializedTraitCache = - stats.foldLeft(specializations)((specializations, tree) => { - tree.deepFold(specializations)((specializations, tree) => tree match - case Typed(Apply(Select(New(anon),ctor),List()), t: TypeTree) if anon.symbol.isAnonymousClass => - t.tpe match { - case Specialization(spec) if spec.isSpecialized => specializations.addInterfaceAndImplementation(spec) - case _ => specializations - } - case Specialization(spec) => - if (spec.isSpecialized) { - // Block Vec[?] and similar - spec.specializedTypeArgs.filter { - case t: TypeBoundsTree => true - case _ => false - }.foreach: tr => - report.error("Wildcard types may not be substituted for Specialized type parameters.", tr.srcPos) - - specializations.addInterface(spec) - } else { - // Check foo[S: Specialized] <= Vec[S: Specialized] - spec.specializedTypeArgs.flatMap(arg => { // For each type we are using in a Specialized position - arg.tpe.widen.dealias.namedPartsWith(part => // Find all type params within that type that are not marked as Specialized so we can error - part.typeSymbol.isTypeParam && - (!(if part.typeSymbol.owner.isClass then part.typeSymbol.owner.primaryConstructor else part.typeSymbol.owner).paramSymss.flatten.exists( - d => d.info match { - case Specialization.SpecializedEvidence(tpeArg) => - tpeArg.typeSymbol.isTypeParam && tpeArg.typeSymbol.name == part.name - case _ => false - } - )) - ) - }).foreach: tr => - if tr.denot.symbol.srcPos.span.exists then - report.error(s"${tr.typeSymbol} used in a Specialized position, so it must be marked as Specialized at its definition.", tr.denot.symbol.srcPos) - specializations - } - case _ => specializations - ) - }) - } + private def collectReferencedSpecializations(stats: List[Tree], specializations: SpecializedTraitCache)(using Context): SpecializedTraitCache = + stats.foldLeft(specializations)((specializations, tree) => { + tree.deepFold(specializations)((specializations, tree) => tree match + case Typed(Apply(Select(New(anon),ctor),List()), t: TypeTree) if anon.symbol.isAnonymousClass => + t.tpe match { + case Specialization(spec) if spec.isSpecialized => specializations.addInterfaceAndImplementation(spec) + case _ => specializations + } + case Specialization(spec) => + if (spec.isSpecialized) { + // Block Vec[?] and similar + spec.specializedTypeArgs.filter { + case t: TypeBoundsTree => true + case _ => false + }.foreach: tr => + report.error("Wildcard types may not be substituted for Specialized type parameters.", tr.srcPos) + + specializations.addInterface(spec) + } else { + // Check foo[S: Specialized] <= Vec[S: Specialized] + spec.specializedTypeArgs.flatMap(arg => { // For each type we are using in a Specialized position + arg.tpe.widen.dealias.namedPartsWith(part => // Find all type params within that type that are not marked as Specialized so we can error + part.typeSymbol.isTypeParam && + (!(if part.typeSymbol.owner.isClass then part.typeSymbol.owner.primaryConstructor else part.typeSymbol.owner).paramSymss.flatten.exists( + d => d.info match { + case Specialization.SpecializedEvidence(tpeArg) => + tpeArg.typeSymbol.isTypeParam && tpeArg.typeSymbol.name == part.name + case _ => false + } + )) + ) + }).foreach: tr => + if tr.denot.symbol.srcPos.span.exists then + report.error(s"${tr.typeSymbol} used in a Specialized position, so it must be marked as Specialized at its definition.", tr.denot.symbol.srcPos) + specializations + } + case _ => specializations + ) + }) end DesugarSpecializedTraits object DesugarSpecializedTraits: @@ -532,8 +528,8 @@ end DesugarSpecializedTraits object SpecializedTraitCache: type SymbolMap = Map[Specialization, ClassSymbol] - type GenInterfaceSymbol = (Specialization, SpecializedTraitCache) => (ClassSymbol, SpecializedTraitCache) - type GenImplementationSymbol = (Specialization, ClassSymbol) => ClassSymbol + type GenInterfaceSymbol = (Specialization, SpecializedTraitCache) => Context ?=> (ClassSymbol, SpecializedTraitCache) + type GenImplementationSymbol = (Specialization, ClassSymbol) => Context ?=> ClassSymbol class SpecializedTraitCache( @@ -560,13 +556,13 @@ class SpecializedTraitCache( def getNewInterfaceSymbols = newInterfaceSymbols.values def getNewImplementationSymbols: List[(Specialization, ClassSymbol, ClassSymbol)] = newImplementationSymbols.map((k, v) => (k, getInterfaceSymbol(k).get, v)).toList - def addInterface(spec: Specialization): SpecializedTraitCache = + def addInterface(spec: Specialization)(using Context): SpecializedTraitCache = if (newInterfaceSymbols.contains(spec) || interfaceSymbols.contains(spec)) then this else val (targetSymbol, resultingCache) = genInterfaceSymbol(spec, this) resultingCache.copy(newInterfaceSymbols = resultingCache.newInterfaceSymbols + (spec -> targetSymbol)) - def addInterfaceAndImplementation(spec: Specialization): SpecializedTraitCache = + def addInterfaceAndImplementation(spec: Specialization)(using Context): SpecializedTraitCache = if (newImplementationSymbols.contains(spec) || implementationSymbols.contains(spec)) then this else From beed85907a1455473ede70d15797bf6e01238b1f Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 8 May 2026 17:15:40 +0200 Subject: [PATCH 184/254] Share specialized trait cache between comp units in a run --- .../tools/dotc/transform/DesugarSpecializedTraits.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 4ca929719b66..4fa41e19a4d0 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -52,6 +52,8 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: override def changesParents: Boolean = true override def allowsImplicitSearch: Boolean = true + private var specializedTraitCache = SpecializedTraitCache(genInterfaceSymbol = newInterfaceTrait, genImplementationSymbol = newImplementationClass) + private def newInterfaceTrait(specialization: Specialization, specializations: SpecializedTraitCache)(using Context): (ClassSymbol, SpecializedTraitCache) = { val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. def apply(t: Type) = specialization.specializedTypeParamsToTypeArgumentsMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) // TODO: If we can do just types we can get rid of this @@ -449,7 +451,8 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: case _ => } - val (stats1, _) = transformStatements(stats, tree.span, SpecializedTraitCache(genInterfaceSymbol = newInterfaceTrait, genImplementationSymbol = newImplementationClass)) // TODO: Fix span + val (stats1, specializedTraitCache2) = transformStatements(stats, tree.span, specializedTraitCache) // TODO: Fix span + specializedTraitCache = specializedTraitCache2 // TODO: Maybe avoid mutation here cpy.PackageDef(pkg)(pid, stats1) } From b2dc9348d798c95922e1255ee0d377a45bd3cc56 Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 8 May 2026 17:16:13 +0200 Subject: [PATCH 185/254] Fix outstanding bug due to putting types in a set in Specialization --- .../transform/DesugarSpecializedTraits.scala | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 4fa41e19a4d0..0c800b2b245c 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -588,26 +588,26 @@ end SpecializedTraitCache class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(using Context): // TODO: Can we get away with List[Type] val specializedTypeParams: List[Type] = Specialization.classSpecializedTypeParams(traitSymbol) // Type parameters marked with Specialized - private val specializedTypeParamsSet = specializedTypeParams.toSet + // private val specializedTypeParamsSet = specializedTypeParams.toSet // TODO: We can bring this back if we manage to get the =:= type hashing but it's really not a big deal given the expected number of type parameters. private val paramToArgList = traitSymbol.typeParams.map(_.typeRef.asInstanceOf[Type]).zip(typeArguments) - val unspecializedTypeParams: List[Type] = paramToArgList.filterNot((tParam, tArg) => specializedTypeParamsSet(tParam)).map(_._1) // Type parameters not marked with Specialized - val specializedTypeArgs: List[Tree] = paramToArgList.filter((tParam, tArg) => specializedTypeParamsSet(tParam)).map(_._2) // Type arguments provided to parameters that are marked with Specialized at their definition - val unspecializedTypeArgs: List[Tree] = paramToArgList.filterNot((tParam, tArg) => specializedTypeParamsSet(tParam)).map(_._2) // Type arguments provided to parameters that are not marked with Specialized at their definition + val unspecializedTypeParams: List[Type] = paramToArgList.filterNot((tParam, tArg) => specializedTypeParams.exists(_ =:= tParam)).map(_._1) // Type parameters not marked with Specialized + val specializedTypeArgs: List[Tree] = paramToArgList.filter((tParam, tArg) => specializedTypeParams.exists(_ =:= tParam)).map(_._2) // Type arguments provided to parameters that are marked with Specialized at their definition + val unspecializedTypeArgs: List[Tree] = paramToArgList.filterNot((tParam, tArg) => specializedTypeParams.exists(_ =:= tParam)).map(_._2) // Type arguments provided to parameters that are not marked with Specialized at their definition - val specializedTypeParamsToTypeArgumentsMap: Map[Type, Tree] = paramToArgList.toMap.filter((k, v) => specializedTypeParamsSet(k)) + val specializedTypeParamsToTypeArgumentsMap: Map[Type, Tree] = paramToArgList.toMap.filter((k, v) => specializedTypeParams.exists(_ =:= k)) val specialization: List[Tree] = traitSymbol.typeParams.map(_.typeRef).map(specializedTypeParamsToTypeArgumentsMap.applyOrElse(_, TypeTree(_))) // TODO: Don't really like this name def constructorTypeParams: List[Type] = traitSymbol.primaryConstructor.rawParamss.head.map(_.typeRef) - def unspecializedConstructorParams: List[Symbol] = traitSymbol.primaryConstructor.rawParamss.head.zip(traitSymbol.typeParams).filterNot((constrParam, typeParam) => specializedTypeParamsSet(typeParam.typeRef)).map((constrParam, typeParam) => constrParam) + def unspecializedConstructorParams: List[Symbol] = traitSymbol.primaryConstructor.rawParamss.head.zip(traitSymbol.typeParams).filterNot((constrParam, typeParam) => specializedTypeParams.exists(_ =:= typeParam.typeRef)).map((constrParam, typeParam) => constrParam) def specializedConstructorParamToArgumentTypeMap: Map[Type, Type] = - traitSymbol.primaryConstructor.rawParamss.head.map(_.typeRef).zip(paramToArgList).filter((constrParam, paramArg) => specializedTypeParamsSet(paramArg._1)).map((constrParam, paramArg) => (constrParam, paramArg._2.tpe)).toMap + traitSymbol.primaryConstructor.rawParamss.head.map(_.typeRef).zip(paramToArgList).filter((constrParam, paramArg) => specializedTypeParams.exists(_ =:= paramArg._1)).map((constrParam, paramArg) => (constrParam, paramArg._2.tpe)).toMap val hasSpecializedParams: Boolean = specializedTypeParams.nonEmpty def mapUnspecializedArgs(unspec: List[Tree]): List[Tree] = paramToArgList.foldLeft((List.empty[Tree], unspec))((resUnspec, paramArg) => ((resUnspec, paramArg): @unchecked) match { - case ((result, unspec), (param, arg)) if specializedTypeParamsSet(param) => (arg :: result, unspec) - case ((result, head :: rest), (param, arg)) => (head :: result, rest) + case ((result, unspec), (param, arg)) if specializedTypeParams.exists(_ =:= param) => (arg :: result, unspec) + case ((result, head :: rest), (param, arg)) => (head :: result, rest) })._1.reverse /* If inline trait Foo[T] has a method taking another Foo[T] there's no point specializing the reference From 91ca6bb1793604db5d582d503214f422eb76446c Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 8 May 2026 17:17:20 +0200 Subject: [PATCH 186/254] Make two branches the same in transformStatements --- .../dotty/tools/dotc/inlines/Inlines.scala | 11 ++-- .../transform/DesugarSpecializedTraits.scala | 63 ++++++++++--------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 6c06493567ad..7b3afc9b7a19 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -120,10 +120,13 @@ object Inlines: private[dotc] def symbolFromParent(parent: Tree)(using Context): Symbol = if parent.symbol.isConstructor then parent.symbol.owner else parent.tpe.typeSymbol - private def inlineTraitAncestors(cls: TypeDef, allowSpecialized: Boolean)(using Context): List[Tree] = cls match { + private def inlineTraitAncestors(cls: TypeDef, allowSpecialized: Boolean, allowNonSpecialized: Boolean)(using Context): List[Tree] = cls match { case tpd.TypeDef(_, tmpl: Template) => val parentTrees: Map[Symbol, Tree] = tmpl.parents.map(par => symbolFromParent(par) -> par).toMap.filter(_._1.isInlineTrait) - val ancestors: List[ClassSymbol] = cls.tpe.baseClasses.filter(sym => sym.isInlineTrait && sym != cls.symbol && (allowSpecialized || !sym.isSpecializedTrait) ) + val ancestors: List[ClassSymbol] = cls.tpe.baseClasses.filter(sym => sym != cls.symbol && + ((sym.isInlineTrait && !sym.isSpecializedTrait && allowNonSpecialized) || + (sym.isInlineTrait && sym.isSpecializedTrait && allowSpecialized)) + ) ancestors.flatMap(ancestor => def baseTree = cls.tpe.baseType(ancestor) match @@ -333,7 +336,7 @@ object Inlines: ) OverridingPairsChecker(clsSym, clsSym.thisType).checkAll(checkInlineTraitOverride) - def inlineParentInlineTraits(cls: Tree, allowSpecialized: Boolean=false)(using Context): Tree = + def inlineParentInlineTraits(cls: Tree, allowSpecialized: Boolean=false, allowNonSpecialized: Boolean=true)(using Context): Tree = cls match { // case cls @ tpd.TypeDef(_, impl: Template) if cls.symbol.owner.ownersIterator.exists(_.isInlineTrait) => // TODO: We can relax this if we use a seen list to avoid cycles // report.error("May not inline an inline trait into a class defined inside another inline trait. If you really need to do this, make the inline trait Specialized or move the class definition outside the trait.", cls.srcPos) @@ -341,7 +344,7 @@ object Inlines: case cls @ tpd.TypeDef(_, impl: Template) => checkInlineTraitOverrides(cls.symbol.asClass) val clsOverriddenSyms = cls.symbol.info.decls.toList.flatMap(_.allOverriddenSymbols).toSet - val ancestors = inlineTraitAncestors(cls, allowSpecialized) + val ancestors = inlineTraitAncestors(cls, allowSpecialized, allowNonSpecialized) val cycleFound = ancestors.exists { parent => if cls.symbol.ownersIterator.contains(symbolFromParent(parent)) then // TODO: This appears at the inline trait D line rather than the line corresponding to the inlining - should we be worried ? diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 0c800b2b245c..75f3ec5f1c80 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -402,36 +402,39 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: } .map(refreshClassDef) - if (generatedTraitStats1.isEmpty && generatedClassStats1.isEmpty) - (stats.map(replaceSpecializedSymbolsMap(specializations2)(_)), specializations2) // TODO: Check if this shouldn't be the same as the one belwo!? - else - val (generatedTraitStats2, specializations3) = transformStatements(generatedTraitStats1, span, specializations2) - val (generatedClassStats2, specializations4) = transformStatements(generatedClassStats1, span, specializations3) - - /* We need to do the parent removal after inlining into the $impl$ classes otherwise we break - overriding/interface implementation rules during the inlining. The $impl$ inlining - can also happen in the recursive calls, and so we need to do this right at the end (after the recursive calls): */ - val generatedTraitStats3 = - generatedTraitStats2.tapEach: stat => - if stat.symbol.isSpecializedTraitInterface then // We could have $impl$ classes from recursive calls as well. - stat.updateParents { parents => (parents: @unchecked) match - case obj :: Specialization(originalSpec) :: parents if specializations4.getInterfaceSymbol(originalSpec).get == stat.symbol.asClass => - obj :: parents - case obj :: parents => obj :: parents // We already removed the relevant parent. - } - .map(refreshClassDef) - - val stats2 = generatedTraitStats3 ++ - generatedClassStats2 ++ - stats.map(stat => - replaceSpecializedSymbolsMap(specializations4)( // Foo[Int] -> Foo$sp$Int in user code. - if (!stat.symbol.isSpecializedTraitImplementationClass && !stat.symbol.isSpecializedTraitInterface) then // We already processed these in an earlier recursive call - Inlines.inlineParentInlineTraits(stat, allowSpecialized = true) // Perform inlining into class Bar extends Foo[Int] from user code. - else - stat - )) - - (stats2.map(removeRedundantOverridesMap(_)), specializations4) + + val (generatedTraitStatsFinal, generatedClassStatsFinal, specializationsFinal) = + if (generatedTraitStats1.isEmpty && generatedClassStats1.isEmpty) + (generatedTraitStats1, generatedClassStats1, specializations2) + else + val (generatedTraitStats2, specializations3) = transformStatements(generatedTraitStats1, span, specializations2) + val (generatedClassStats2, specializations4) = transformStatements(generatedClassStats1, span, specializations3) + + /* We need to do the parent removal after inlining into the $impl$ classes otherwise we break + overriding/interface implementation rules during the inlining. The $impl$ inlining + can also happen in the recursive calls, and so we need to do this right at the end (after the recursive calls): */ + val generatedTraitStats3 = + generatedTraitStats2.tapEach: stat => + if stat.symbol.isSpecializedTraitInterface then // We could have $impl$ classes from recursive calls as well. + stat.updateParents { parents => (parents: @unchecked) match + case obj :: Specialization(originalSpec) :: parents if specializations4.getInterfaceSymbol(originalSpec).get == stat.symbol.asClass => + obj :: parents + case obj :: parents => obj :: parents // We already removed the relevant parent. + } + .map(refreshClassDef) + (generatedTraitStats3, generatedClassStats2, specializations4) + + val statsFinal = generatedTraitStatsFinal ++ + generatedClassStatsFinal ++ + stats.map(stat => + replaceSpecializedSymbolsMap(specializationsFinal)( // Foo[Int] -> Foo$sp$Int in user code. + if (!stat.symbol.isSpecializedTraitImplementationClass && !stat.symbol.isSpecializedTraitInterface) then // We already processed these in an earlier recursive call + Inlines.inlineParentInlineTraits(stat, allowSpecialized = true, allowNonSpecialized = false) // Perform inlining into class Bar extends Foo[Int] from user code. // TODO: I don't really like this gating. + else + stat + )) + + (statsFinal.map(removeRedundantOverridesMap(_)), specializationsFinal) } override protected def newTransformer(using Context): Transformer = new Transformer: From d041e88707d370c39d38186074b4a6e217fb1d73 Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 6 May 2026 15:52:27 +0200 Subject: [PATCH 187/254] Move transform functions outside of Transformer --- .../dotty/tools/dotc/transform/DesugarSpecializedTraits.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 75f3ec5f1c80..93d7aaf27ddc 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -402,7 +402,6 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: } .map(refreshClassDef) - val (generatedTraitStatsFinal, generatedClassStatsFinal, specializationsFinal) = if (generatedTraitStats1.isEmpty && generatedClassStats1.isEmpty) (generatedTraitStats1, generatedClassStats1, specializations2) @@ -435,7 +434,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: )) (statsFinal.map(removeRedundantOverridesMap(_)), specializationsFinal) - } +} override protected def newTransformer(using Context): Transformer = new Transformer: override def transform(tree: Tree)(using Context): Tree = From 5adac5c864ac52a39df01da44252e1c3b8ee405d Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 8 May 2026 16:30:49 +0200 Subject: [PATCH 188/254] Save deftrees when copying symbols for ttmap --- compiler/src/dotty/tools/dotc/core/Symbols.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/core/Symbols.scala b/compiler/src/dotty/tools/dotc/core/Symbols.scala index 852c6ce2cc04..5e0ae17f6ca4 100644 --- a/compiler/src/dotty/tools/dotc/core/Symbols.scala +++ b/compiler/src/dotty/tools/dotc/core/Symbols.scala @@ -973,6 +973,12 @@ object Symbols extends SymUtils { copies.foreach(_.ensureCompleted()) // avoid memory leak + copies.zip(originals).foreach { (copied, original) => + if copied.retainsDefTree then + copied.defTree = original.defTree + } + + // Update Child annotations of classes encountered previously to new values // if some child is among the mapped symbols for orig <- ttmap1.substFrom do From a054958eb9bbcd288a8955d1ae6df093345913c4 Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 8 May 2026 16:39:11 +0200 Subject: [PATCH 189/254] Treat opaque types better based on standard approach --- .../src/dotty/tools/dotc/core/Types.scala | 6 ---- .../dotty/tools/dotc/inlines/Inlines.scala | 30 +++++++------------ 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 76492ea0f962..e3d1412516d3 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1357,12 +1357,6 @@ object Types extends TypeUtils { case _ => this } - /* Extract annotations and opaque type laiases; removing the internal type. */ - def extractAnnotationsAndOpaqueTypeAliases(using Context): Type = this match { - case AnnotatedType(tp1, annot) => AnnotatedType(tp1.extractAnnotationsAndOpaqueTypeAliases, annot) - case RefinedType(parent, rname, TypeAlias(alias)) => RefinedType(parent.extractAnnotationsAndOpaqueTypeAliases, rname, TypeAlias(alias)) - case _ => NoType - } /** Strip PolyType prefixes */ def stripPoly(using Context): Type = this match { diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 7b3afc9b7a19..219cb3427f5b 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -358,11 +358,6 @@ object Inlines: ancestors.foldLeft((List.empty[Tree], impl.body)){ case ((inlineDefs, childDefs), parent) => val parentTraitInliner = InlineParentTrait(parent) - - // Update self type - val newSelfType = cls.symbol.asClass.classDenot.givenSelfType & parentTraitInliner.inlinedSelfType.extractAnnotationsAndOpaqueTypeAliases - cls.symbol.info = cls.symbol.asClass.classInfo.derivedClassInfo(selfInfo=newSelfType) - // Inline body val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) val inlinedDefs1 = inlineDefs ::: parentTraitInliner.expandDefs(overriddenSymbols) @@ -380,16 +375,7 @@ object Inlines: otherstat <- newbody if !otherstat.symbol.is(ParamAccessor) && otherstat.denot.matches(pacc.denot.asSingleDenotation) do report.error(s"Inlining of inline trait created name conflict on ${pacc.denot.name}. Constructor parameters of inline receivers may not collide with members of inline traits.", pacc.srcPos) - val impl1 = cpy.Template(impl)(body = newbody, - self= - if cls.symbol.asClass.classDenot.givenSelfType.exists then - cpy.ValDef(impl.self)(tpt= - TypeTree(cls.symbol.asClass.classDenot.givenSelfType) - .withSpan(impl.self.tpt.span.orElse(cls.symbol.span))) - .withSpan(impl.self.span.orElse(cls.symbol.span)) - .cloneIn(cls.symbol.source) - else impl.self - ) + val impl1 = cpy.Template(impl)(body = newbody) cpy.TypeDef(cls)(rhs = impl1) } @@ -857,10 +843,12 @@ object Inlines: paramAccessorsMapper.registerParamValuesOf(parent) val stats = Inlines.defsToInline(parentSym).filterNot(stat => overriddenDecls.contains(stat.symbol) && stat.symbol.is(Deferred)) - stats.map{ // Private symbols must be entered before the RHSs are inlined + val stats1 = stats.map{ // Private symbols must be entered before the RHSs are inlined case member: MemberDef => Left((member, inlinedSym(member.symbol, overriddenDecls))) case stat => Right(stat) - }.map{ + } + ctx.owner.info = ctx.owner.asClass.classInfo.integrateOpaqueMembers + stats1.map{ case Left((tree, inlinedSym)) => expandStat(tree, inlinedSym) case Right(tree) => inlinedRhs(tree) } @@ -1041,11 +1029,13 @@ object Inlines: */ private def inlinedTypeDef(tdef: TypeDef, inlinedSym: Symbol)(using Context): TypeDef = - val tdef2 = tpd.TypeDef(inlinedSym.asType).withSpan(parent.span) if inlinedSym.isOpaqueAlias then - cpy.TypeDef(tdef2)(rhs=TypeTree(inlinedSym.opaqueAlias)) + val inlinedRhsType = inlinerTypeMap(tdef.rhs.tpe) + inlinedSym.info = inlinedSym.opaqueToBounds(TypeAlias(inlinedRhsType), tdef.rhs, List()) + inlinedSym.typeRef.recomputeDenot() + ctx.typeAssigner.assignType(untpd.TypeDef(inlinedSym.name.asTypeName, TypeTree(inlinedRhsType)), inlinedSym).withSpan(parent.span) else - tdef2 + tpd.TypeDef(inlinedSym.asType).withSpan(parent.span) private def inlinedRhs(vddef: ValOrDefDef, inlinedSym: Symbol)(using Context): Tree = From a79100b656964c0e07fcf8fdd95c8968d1c21fa1 Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 8 May 2026 16:39:27 +0200 Subject: [PATCH 190/254] Fix coords in inline traits --- compiler/src/dotty/tools/dotc/inlines/Inlines.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 219cb3427f5b..502b9a380b50 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -1049,14 +1049,14 @@ object Inlines: val symbolMap = mutable.Map[Symbol, Symbol]() // TODO make version of inlined that does not return bindings? - val rhs1 = Inlined(tpd.ref(parentSym).withSpan(parentSym.span), Nil, inlined(rhs)._2).withSpan(parent.span) + val rhs1 = Inlined(tpd.ref(parentSym).withSpan(parentSym.span), Nil, inlined(rhs)._2).withSpan(parent.span) // TODO: This inlines also calls to inline defs that were made in the inline trait body, is that desirable? // In case of nested inline trait inlines, because BodyAnnotation is out of date, // body inlined misses nested expansion, but we have the symbols for the items that should be there // Remove them so that they can be inlined prperly later. val ttmap = TreeTypeMap(treeMap = { case tree@TypeDef(name, tmpl: Template) if Inlines.needsInlining(tree) => - val newSym = tree.symbol.copy() + val newSym = tree.symbol.copy(coord = spanCoord(tree.span)) // Coord should correspond to original location because we will inline from there. newSym.info = ClassInfo(tree.symbol.owner.thisType, newSym.asClass, tree.symbol.asClass.parentTypes, Scopes.newScope) val newConstructorSymbol = tree.symbol.primaryConstructor.copy(owner = newSym) @@ -1072,7 +1072,7 @@ object Inlines: val childSyms = tree.symbol.info.decls .filter(sym => tmpl.body.exists(vddef => vddef.symbol == sym)) - .tapEach(sym => symbolMap(sym) = sym.copy(owner = newSym)) + .tapEach(sym => symbolMap(sym) = sym.copy(owner = newSym, coord=sym.coord)) .map(symbolMap) childSyms.foreach(p => p.entered) From 4212cec9ae65d9a097f893345b76db6405eab71a Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 8 May 2026 16:39:45 +0200 Subject: [PATCH 191/254] Fix qualifier on select in replaceInlinedTraitSymbols --- .../tools/dotc/transform/ReplaceInlinedTraitSymbols.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala b/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala index cf9779723abd..22aeae67377b 100644 --- a/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala +++ b/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala @@ -34,7 +34,8 @@ class ReplaceInlinedTraitSymbols extends MiniPhase: if ctx.inlineTraitState.inlinedSymbolIsRegistered(tree.symbol, qualType) then val newSym = ctx.inlineTraitState.lookupInlinedSymbol(tree.symbol, qualType) assert(tree.symbol.isTerm) - tree.withType(newSym.termRef) + tpd.Select(tree.qualifier, tree.name) + tree.withType(tree.qualifier.tpe.select(newSym)) else tree From 52cb4e77544da975d054606e86ebe50c900da6b8 Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 8 May 2026 16:41:44 +0200 Subject: [PATCH 192/254] Fix param names in generated constructor --- .../transform/DesugarSpecializedTraits.scala | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 93d7aaf27ddc..6b9f9407a4b4 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -96,15 +96,20 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: /* Fix constructor so that it: 1) Has correct generic type parameters - 2) Returns the correct type corresponding to those type parameters applied */ - private def fixConstructor(init: Symbol, traitOrClassSymbol: ClassSymbol)(using Context) = + 2) Returns the correct type corresponding to those type parameters applied + 3) Has correct parameter names corresponding to targetParamNames */ + private def fixConstructor(init: Symbol, traitOrClassSymbol: ClassSymbol, targetParamNames: List[List[TermName]] = List())(using Context) = val rt = traitOrClassSymbol.typeRef.appliedTo(traitOrClassSymbol.typeParams.map(_.typeRef)) - def resultType(tpe: Type): Option[Type] = tpe match { - case mt @ MethodType(paramNames) => Some(mt.derivedLambdaType(paramNames, mt.paramInfos, resultType(mt.resultType).getOrElse(rt))) - case pt : PolyType => Some(pt.derivedLambdaType(pt.paramNames, pt.paramInfos, resultType(pt.resType).get)) + def resultType(tpe: Type, targetParamNames: List[List[TermName]]): Option[Type] = + tpe match { + case mt @ MethodType(paramNames) => targetParamNames match { + case head :: tail => Some(mt.derivedLambdaType(head, mt.paramInfos, resultType(mt.resultType, tail).getOrElse(rt))) + case Nil => Some(mt.derivedLambdaType(paramNames, mt.paramInfos, resultType(mt.resultType, targetParamNames).getOrElse(rt))) + } + case pt : PolyType => Some(pt.derivedLambdaType(pt.paramNames, pt.paramInfos, resultType(pt.resType, targetParamNames).get)) case _ => None } - init.info = resultType(init.info).get + init.info = resultType(init.info, targetParamNames).get init.info = PolyType.fromParams(init.owner.typeParams, init.info) private def buildTypeParameters(traitOrClassSymbol: ClassSymbol, specialization: Specialization)(using Context) = @@ -177,7 +182,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: init.info = specialization.traitSymbol.primaryConstructor.info.appliedTo( // Type Arg if specialized; otherwise we want our type param. specialization.constructorTypeParams.map(par => specialization.specializedConstructorParamToArgumentTypeMap.applyOrElse(par, _.subst(oldTypeParams, classSymbol.typeParams.map(_.typeRef)))) ) - fixConstructor(init, classSymbol) + fixConstructor(init, classSymbol, valueParams.map(_.map(_.name.asTermName))) /* Build param accessors */ val paramAccessorss = valueParams.map(params => params.map(s => s.copy(owner = classSymbol, flags=(s.flags|Flags.LocalParamAccessor) &~ Flags.Param, info = s.info.subst(initTypeParams, classSymbol.typeParams.map(_.typeRef))))) From 8754ef9d6f1dd89ce47d83673121d66450a9a501 Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 8 May 2026 16:43:36 +0200 Subject: [PATCH 193/254] Fix spans and coords in specialized traits --- .../transform/DesugarSpecializedTraits.scala | 62 ++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 6b9f9407a4b4..cfb0d67e2336 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -43,6 +43,9 @@ import dotty.tools.dotc.core.DenotTransformers.DenotTransformer import dotty.tools.dotc.core.Denotations.SingleDenotation import dotty.tools.dotc.core.Flags.InlineMethod import dotty.tools.dotc.core.DenotTransformers.IdentityDenotTransformer +import dotty.tools.dotc.core.Names.TermName +import dotty.tools.dotc.util.Spans.spanCoord +import dotty.tools.dotc.util.Spans.NoSpan class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: @@ -62,7 +65,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: // Parents may be specializable and so we need to specialize them as well // See ArrayIterator extends Iterator in specialized-trait-collections-example.scala val specializations1 = inheritedParents.foldLeft(specializations)((specializations, parent) => - parent match { + (parent, specialization.span) match { case Specialization(spec) if spec.isSpecialized => specializations.addInterface(spec) case _ => specializations } @@ -80,7 +83,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: parents, NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? specialization.traitSymbol.privateWithin, - specialization.traitSymbol.coord, + spanCoord(specialization.span), specialization.traitSymbol.compilationUnitInfo ) @@ -88,10 +91,10 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: (traitSymbol.entered, specializations1) } - private def buildInterfaceTraitTree(interfaceSymbol: ClassSymbol)(using Context) = { - val init = newDefaultConstructor(interfaceSymbol) + private def buildInterfaceTraitTree(specialization: Specialization, interfaceSymbol: ClassSymbol)(using Context) = { + val init = newConstructor(interfaceSymbol, EmptyFlags, Nil, Nil, coord=spanCoord(specialization.span)) fixConstructor(init, interfaceSymbol) - ClassDef(interfaceSymbol, DefDef(init.entered), Nil) + ClassDef(interfaceSymbol, DefDef(init.entered), Nil).withSpan(specialization.span) } /* Fix constructor so that it: @@ -146,7 +149,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: parents, NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? specialization.traitSymbol.privateWithin, - specialization.traitSymbol.coord, + spanCoord(specialization.span), specialization.traitSymbol.compilationUnitInfo ) @@ -166,8 +169,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: val traitSpParent = freshTypeVarMap(traitSpParent_) val originalTraitSpecializedParent = freshTypeVarMap(originalTraitSpecializedParent_) - val init = newDefaultConstructor(classSymbol) - + val init = newConstructor(classSymbol, EmptyFlags, Nil, Nil, coord=spanCoord(specialization.span)) val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. def apply(t: Type) = specialization.specializedConstructorParamToArgumentTypeMap.applyOrElse(t, mapOver) @@ -192,7 +194,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: val newParamss = paramAccessorss.nestedMap(ref(_)) val newParams1 = if (newParamss.length == 1) then newParamss ++ List(List()) else newParamss // TODO: Clean and robust - val classDef = ClassDefWithParents( + ClassDefWithParents( classSymbol, DefDef(init.asTerm.entered), List( @@ -205,8 +207,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: ), // Put into body of class paramAccessorss.flatMap(syms => syms.map(sym => tpd.ValDef(sym.asTerm))) - ) - classDef + ).withSpan(specialization.span) } private def replaceSpecializedSymbolsMap(specializations: SpecializedTraitCache)(using Context) = @@ -234,14 +235,14 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: t.tpe match { case a: AndType => /* Multiple mixed in traits will be typed as an AndType */ deandify(a).foreach(trt => - Specialization.unapply(trt).foreach {spec => + Specialization.unapply(trt, t.span).foreach {spec => if spec.hasSpecializedParams then report.error("Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like.", an.srcPos) } ) tree case tpe => - Specialization.unapply(tpe).map(spec => + Specialization.unapply(tpe, t.span).map(spec => { if spec.hasSpecializedParams then if tmpl.body.filterNot(x => x.symbol.name.is(ContextBoundParamName)).nonEmpty then // Only allowed to contain evidence parameters @@ -270,7 +271,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: val argss = tpd.allArgss(tree) argss match { case typeArgs :: valueArgss => - val spec = Specialization(fun.symbol.owner, typeArgs) + val spec = Specialization(fun.symbol.owner, typeArgs, app.span) { for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) yield New(ref(specializedSymbol)).select(init).appliedToTypeTrees(spec.unspecializedTypeArgs).appliedToNone @@ -340,7 +341,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: ) // Returns (new stmts including original, new symbols including original) - private def transformStatements(stats1: List[Tree], span: Span, specializations: SpecializedTraitCache)(using Context): (List[Tree], SpecializedTraitCache) = { + private def transformStatements(stats1: List[Tree], specializations: SpecializedTraitCache)(using Context): (List[Tree], SpecializedTraitCache) = { val inlineSpecializedMethods = new TreeMapWithPreciseStatContexts { override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? @@ -392,14 +393,14 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: val generatedTraitStats1 = generatedTraitStats.map { case tree: TypeDef => assert(tree.symbol.isInlineTrait) - val inlined = Inlines.inlineParentInlineTraits(Inlines.checkAndTransformInlineTrait(tree.withSpan(span)),allowSpecialized=true).asInstanceOf[TypeDef] + val inlined = Inlines.inlineParentInlineTraits(Inlines.checkAndTransformInlineTrait(tree),allowSpecialized=true).asInstanceOf[TypeDef] cpy.TypeDef(inlined)(name = inlined.name, rhs = inlineInlineTraits(inlined.rhs)).withSpan(inlined.span) } val generatedClassStats1 = generatedClassStats.map { case tree: TypeDef => assert(Inlines.needsInlining(tree, allowSpecializedTraits=true)) - val inlined = Inlines.inlineParentInlineTraits(tree.withSpan(span), allowSpecialized=true).asInstanceOf[TypeDef] + val inlined = Inlines.inlineParentInlineTraits(tree, allowSpecialized=true).asInstanceOf[TypeDef] cpy.TypeDef(inlined)(name = inlined.name, rhs = inlineInlineTraits(inlined.rhs)).withSpan(inlined.span) }.tapEach: // We can do parent removal earlier for $impl$ classes as we don't depend on the parents later. _.updateParents { parents => (parents: @unchecked) match @@ -411,8 +412,8 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: if (generatedTraitStats1.isEmpty && generatedClassStats1.isEmpty) (generatedTraitStats1, generatedClassStats1, specializations2) else - val (generatedTraitStats2, specializations3) = transformStatements(generatedTraitStats1, span, specializations2) - val (generatedClassStats2, specializations4) = transformStatements(generatedClassStats1, span, specializations3) + val (generatedTraitStats2, specializations3) = transformStatements(generatedTraitStats1, specializations2) + val (generatedClassStats2, specializations4) = transformStatements(generatedClassStats1, specializations3) /* We need to do the parent removal after inlining into the $impl$ classes otherwise we break overriding/interface implementation rules during the inlining. The $impl$ inlining @@ -458,7 +459,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: case _ => } - val (stats1, specializedTraitCache2) = transformStatements(stats, tree.span, specializedTraitCache) // TODO: Fix span + val (stats1, specializedTraitCache2) = transformStatements(stats, specializedTraitCache) // TODO: Fix span specializedTraitCache = specializedTraitCache2 // TODO: Maybe avoid mutation here cpy.PackageDef(pkg)(pid, stats1) } @@ -467,7 +468,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: stats.foldLeft(specializations)((specializations, tree) => { tree.deepFold(specializations)((specializations, tree) => tree match case Typed(Apply(Select(New(anon),ctor),List()), t: TypeTree) if anon.symbol.isAnonymousClass => - t.tpe match { + (t.tpe, t.span) match { case Specialization(spec) if spec.isSpecialized => specializations.addInterfaceAndImplementation(spec) case _ => specializations } @@ -563,7 +564,7 @@ class SpecializedTraitCache( def getInterfaceSymbol(spec: Specialization): Option[ClassSymbol] = newInterfaceSymbols.orElse(interfaceSymbols).lift(spec) def getImplementationSymbol(spec: Specialization): Option[ClassSymbol] = newImplementationSymbols.orElse(implementationSymbols).lift(spec) - def getNewInterfaceSymbols = newInterfaceSymbols.values + def getNewInterfaceSymbols: List[(Specialization, ClassSymbol)] = newInterfaceSymbols.toList def getNewImplementationSymbols: List[(Specialization, ClassSymbol, ClassSymbol)] = newImplementationSymbols.map((k, v) => (k, getInterfaceSymbol(k).get, v)).toList def addInterface(spec: Specialization)(using Context): SpecializedTraitCache = @@ -592,7 +593,7 @@ class SpecializedTraitCache( end SpecializedTraitCache /* Represents an application traitSymbol[typeArguments] */ -class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree])(using Context): // TODO: Can we get away with List[Type] +class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree], val span: Span)(using Context): // TODO: Can we get away with List[Type] val specializedTypeParams: List[Type] = Specialization.classSpecializedTypeParams(traitSymbol) // Type parameters marked with Specialized // private val specializedTypeParamsSet = specializedTypeParams.toSet // TODO: We can bring this back if we manage to get the =:= type hashing but it's really not a big deal given the expected number of type parameters. @@ -643,13 +644,18 @@ object Specialization: } def unapply(tpt: Tree)(using Context): Option[Specialization] = tpt match { - case AppliedTypeTree(specializedTrait: Ident, concreteTypeTrees: List[Tree]) => Some(Specialization(specializedTrait.denot.symbol, concreteTypeTrees)) - case t: TypeTree => Specialization.unapply(t.tpe) + case AppliedTypeTree(specializedTrait: Ident, concreteTypeTrees: List[Tree]) => Some(Specialization(specializedTrait.denot.symbol, concreteTypeTrees, tpt.span)) + case t: TypeTree => Specialization.unapply(t.tpe, t.span) case _ => None } + def unapply(typeSpan: (Type, Span))(using Context): Option[Specialization] = typeSpan match { + case (AppliedType(tycon: Type, args: List[Type]), span) => Some(Specialization(tycon.typeSymbol, args.map(TypeTree(_)), span)) + case _ => None + } + def unapply(tpe: Type)(using Context): Option[Specialization] = tpe match { - case AppliedType(tycon: Type, args: List[Type]) => Some(Specialization(tycon.typeSymbol, args.map(TypeTree(_)))) + case AppliedType(tycon: Type, args: List[Type]) => Some(Specialization(tycon.typeSymbol, args.map(TypeTree(_)), NoSpan)) case _ => None } @@ -661,8 +667,8 @@ object Specialization: tree match { case TypeDef(anon, Template(_, parentCalls: List[Tree], _, _)) => parentCalls match { - case _ :+ Apply(Apply(t@tpe, ctorArgs), ev) => // extends Object, parents of spec trait, spec trait - val spec = Specialization.unapply(t.tpe.resultType.resultType) + case _ :+ Apply(Apply(t, ctorArgs), ev) => // extends Object, parents of spec trait, spec trait + val spec = Specialization.unapply(t.tpe.resultType.resultType, t.span) spec.get.hasSpecializedParams case _ => false } From 7cf5ec5beba352ef330b1c297ea7c13dfde1b140 Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 8 May 2026 16:45:11 +0200 Subject: [PATCH 194/254] Remove old comment --- .../dotty/tools/dotc/transform/DesugarSpecializedTraits.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index cfb0d67e2336..d96515d7a707 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -344,7 +344,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: private def transformStatements(stats1: List[Tree], specializations: SpecializedTraitCache)(using Context): (List[Tree], SpecializedTraitCache) = { val inlineSpecializedMethods = new TreeMapWithPreciseStatContexts { - override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? + override def transform(tree: Tree)(using Context): Tree = tree match { case app: Apply if app.symbol.isSpecializedMethod => super.transform(Inlines.inlineCall(tree)) case tree => super.transform(tree) From 17f8dbc2a3773a9d1fb0d6ff526cda5239bd99d1 Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 8 May 2026 16:46:14 +0200 Subject: [PATCH 195/254] Fix positions and sources when prematurely inlining specialized inline methods --- .../tools/dotc/transform/DesugarSpecializedTraits.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index d96515d7a707..03cdb1855c44 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -346,7 +346,10 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: val inlineSpecializedMethods = new TreeMapWithPreciseStatContexts { override def transform(tree: Tree)(using Context): Tree = tree match { case app: Apply if app.symbol.isSpecializedMethod => - super.transform(Inlines.inlineCall(tree)) + val inlinedTree = Inlines.inlineCall(tree).asInstanceOf[Inlined] + val callTrace = Inlines.inlineCallTrace(tree.symbol, inlinedTree.sourcePos)(using ctx.withSource(inlinedTree.source)) + val flattenedTree = cpy.Inlined(inlinedTree)(callTrace, inlinedTree.bindings, inlinedTree.expansion)(using inlineContext(inlinedTree)) + super.transform(flattenedTree) case tree => super.transform(tree) } } From 95dfe2a25e7d1b8acddddb3fbec540e037a2e5de Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 8 May 2026 16:46:52 +0200 Subject: [PATCH 196/254] Process inline and specialized traits before pickling --- compiler/src/dotty/tools/dotc/Compiler.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 2ed5c5be8c0b..1f6d14dfbf86 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -44,15 +44,15 @@ class Compiler { List(new UnrollDefinitions) :: // Unroll annotated methods if detected in PostTyper List(new sjs.PrepJSInterop) :: // Additional checks and transformations for Scala.js (Scala.js only) List(new SetRootTree) :: // Set the `rootTreeOrProvider` on class symbols + List(new SpecializeInlineTraits) :: // Inline the code of inline traits into their children + List(new DesugarSpecializedTraits) :: // Process the Specialized annotation + List(new ReplaceInlinedTraitSymbols) :: // Replace symbols referring to inline trait members with resulting inlined member symbols Nil /** Phases dealing with TASTY tree pickling and unpickling */ protected def picklerPhases: List[List[Phase]] = List(new Pickler) :: // Generate TASTY info List(new sbt.ExtractAPI) :: // Sends a representation of the API of classes to sbt via callbacks - List(new SpecializeInlineTraits) :: // Inline the code of inline traits into their children - List(new DesugarSpecializedTraits) :: // Process the Specialized annotation - List(new ReplaceInlinedTraitSymbols) :: // Replace symbols referring to inline trait members with resulting inlined member symbols List(new Inlining) :: // Inline and execute macros List(new PostInlining) :: // Add mirror support for inlined code List(new Staging) :: // Check staging levels and heal staged types From 5d9b532a8e9ba4634fb89a1e0401c905aec7cee9 Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 8 May 2026 18:56:21 +0200 Subject: [PATCH 197/254] Add convenience toString method --- .../dotty/tools/dotc/transform/DesugarSpecializedTraits.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 03cdb1855c44..95047dd7d41b 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -636,6 +636,9 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree], val override def hashCode(): Int = (traitSymbol, specializedTypeArgs.map(_.tpe.widen.dealias.show)).hashCode() // TODO: Consider not using show for this for performance reasons (correctness also?) + + override def toString(): String = + s"Specialization(${traitSymbol}, ${typeArguments}, ${span})" end Specialization object Specialization: From cac75201d5183a84b13d3a77b9bd3d5f3dccfd5b Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 8 May 2026 18:56:43 +0200 Subject: [PATCH 198/254] Fix $ at end of name confuses class loader --- .../dotty/tools/dotc/transform/DesugarSpecializedTraits.scala | 2 +- .../run/specialized-trait-check-specialized-method-called.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 95047dd7d41b..c67dafb9e745 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -515,7 +515,7 @@ object DesugarSpecializedTraits: // TODO: What happens with this name generation if we have Vec[Vec[T]] for example? We potentially don't have an Ident // TODO: Check what happens here when we have a case where the types being specialized into are user defined instead of primitives or type vars. private def generateName(specialization: Specialization, suffix: String)(using Context) = // TODO: Probably don't use show - specialization.specializedTypeArgs.collect(t => t.tpe.show ++ str.SPECIALIZED_TRAIT_TYPE_SEP).foldLeft((specialization.traitSymbol.name ++ suffix).asTypeName)((n1, n2) => n1 ++ n2) + (specialization.traitSymbol.name ++ suffix).asTypeName ++ specialization.specializedTypeArgs.map(t => t.tpe.show).mkString(str.SPECIALIZED_TRAIT_TYPE_SEP) private[transform] def newSpecializedTraitName(specialization: Specialization)(using Context): TypeName = generateName(specialization, str.SPECIALIZED_TRAIT_SUFFIX) diff --git a/tests/run/specialized-trait-check-specialized-method-called.scala b/tests/run/specialized-trait-check-specialized-method-called.scala index 796c0809a35e..a7914494422f 100644 --- a/tests/run/specialized-trait-check-specialized-method-called.scala +++ b/tests/run/specialized-trait-check-specialized-method-called.scala @@ -13,7 +13,7 @@ inline trait Foo[T: Specialized](x: T): assert(stackTrace.toList.tail.takeWhile(call => call.getMethodName().startsWith("foo")).length == 1) // We call this method on the correct impl class - assert(Thread.currentThread.getStackTrace()(1).getClassName() == "Foo$impl$Int$") + assert(Thread.currentThread.getStackTrace()(1).getClassName() == "Foo$impl$Int") x def f(b: Foo[Int]) = From dcf7e42e3fb8af730d691354741615223989d33c Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 11 May 2026 09:56:09 +0200 Subject: [PATCH 199/254] Add specialized trait one parent is inline --- .../specialized-trait-one-parent-is-inline.scala | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/run/specialized-trait-one-parent-is-inline.scala diff --git a/tests/run/specialized-trait-one-parent-is-inline.scala b/tests/run/specialized-trait-one-parent-is-inline.scala new file mode 100644 index 000000000000..e9a6fd56f831 --- /dev/null +++ b/tests/run/specialized-trait-one-parent-is-inline.scala @@ -0,0 +1,15 @@ +//> using options -language:experimental.specializedTraits + +inline trait Foo[T: Specialized] +inline trait A[T](x: T): + def foo: T = x + +class B extends A[Int](15), Foo: + val y = 1 + +def h(x: B) = x.foo + +@main def Test = + val b = B() + assert(h(b) == 15) + assert(b.y == 1) From 9fc6f8e7adcea9ec54ddcc30bae6b1a73a35e80b Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 11 May 2026 11:04:38 +0200 Subject: [PATCH 200/254] Add missing span / source file --- compiler/src/dotty/tools/dotc/inlines/Inlines.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 502b9a380b50..89e347fb26c8 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -1049,7 +1049,7 @@ object Inlines: val symbolMap = mutable.Map[Symbol, Symbol]() // TODO make version of inlined that does not return bindings? - val rhs1 = Inlined(tpd.ref(parentSym).withSpan(parentSym.span), Nil, inlined(rhs)._2).withSpan(parent.span) // TODO: This inlines also calls to inline defs that were made in the inline trait body, is that desirable? + val rhs1 = Inlined(tpd.ref(parentSym).withSpan(parentSym.span), Nil, inlined(rhs)._2.withSpan(parent.span).cloneIn(parentSym.source)).withSpan(parent.span) // TODO: This inlines also calls to inline defs that were made in the inline trait body, is that desirable? // In case of nested inline trait inlines, because BodyAnnotation is out of date, // body inlined misses nested expansion, but we have the symbols for the items that should be there From 0046c0a6ebf3c0abc3dba9fd660255d0168181ef Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 11 May 2026 11:05:00 +0200 Subject: [PATCH 201/254] Special case treechecker for duplicate impl and sp classes as discussed with Solal --- compiler/src/dotty/tools/dotc/transform/TreeChecker.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala index 0117e636b173..a3a79af6ce88 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -75,7 +75,11 @@ class TreeChecker extends Phase with SymTransformer { sym.isRefinementClass assert(validSuperclass, i"$sym has no superclass set") - testDuplicate(sym, seenClasses, "class") + + // Multiple references to specialized traits will specialize multiple times, but they lead to the same + // interface and implementation classes every time, so we can allow duplicates and pick one arbitrarily. + if !(sym.isSpecializedTraitInterface || sym.isSpecializedTraitImplementationClass) then + testDuplicate(sym, seenClasses, "class") } val badDeferredAndPrivate = From e950ecf943e4e4b456488d67bda0ced7045dd5b5 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 11 May 2026 11:05:15 +0200 Subject: [PATCH 202/254] Add multiple files separate compilation units test --- tests/run/specialized-trait-multiple-files/A_1.scala | 6 ++++++ tests/run/specialized-trait-multiple-files/B_2.scala | 10 ++++++++++ 2 files changed, 16 insertions(+) create mode 100644 tests/run/specialized-trait-multiple-files/A_1.scala create mode 100644 tests/run/specialized-trait-multiple-files/B_2.scala diff --git a/tests/run/specialized-trait-multiple-files/A_1.scala b/tests/run/specialized-trait-multiple-files/A_1.scala new file mode 100644 index 000000000000..9fb078f6bccf --- /dev/null +++ b/tests/run/specialized-trait-multiple-files/A_1.scala @@ -0,0 +1,6 @@ +//> using options -language:experimental.specializedTraits + +inline trait A[T: Specialized]: + def foo(x: T):T = x + +class B extends A[Int] diff --git a/tests/run/specialized-trait-multiple-files/B_2.scala b/tests/run/specialized-trait-multiple-files/B_2.scala new file mode 100644 index 000000000000..7843ebadf48f --- /dev/null +++ b/tests/run/specialized-trait-multiple-files/B_2.scala @@ -0,0 +1,10 @@ +//> using options -language:experimental.specializedTraits + +class C extends A[Int] + +@main def Test = + val b = B() + val c = C() + + println(b.foo(10)) + println(c.foo(10)) \ No newline at end of file From f4f3b1f31fe520f3c3539db1879dd8071ea0813d Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 11 May 2026 11:08:52 +0200 Subject: [PATCH 203/254] Add specialized trait body macro --- tests/pos/specialized-trait-body-macro/Macro_1.scala | 11 +++++++++++ tests/pos/specialized-trait-body-macro/Test_2.scala | 2 ++ 2 files changed, 13 insertions(+) create mode 100644 tests/pos/specialized-trait-body-macro/Macro_1.scala create mode 100644 tests/pos/specialized-trait-body-macro/Test_2.scala diff --git a/tests/pos/specialized-trait-body-macro/Macro_1.scala b/tests/pos/specialized-trait-body-macro/Macro_1.scala new file mode 100644 index 000000000000..9bc4f7301e70 --- /dev/null +++ b/tests/pos/specialized-trait-body-macro/Macro_1.scala @@ -0,0 +1,11 @@ +//> using options -language:experimental.specializedTraits +import scala.quoted.* + +inline def foo(): Int = + ${fooImpl} + +def fooImpl(using Quotes): Expr[Int] = + '{3} + +inline trait A[T: Specialized]: + val i: Int = foo() diff --git a/tests/pos/specialized-trait-body-macro/Test_2.scala b/tests/pos/specialized-trait-body-macro/Test_2.scala new file mode 100644 index 000000000000..ba4e4b643cf8 --- /dev/null +++ b/tests/pos/specialized-trait-body-macro/Test_2.scala @@ -0,0 +1,2 @@ +//> using options -language:experimental.specializedTraits +class B extends A[Int] \ No newline at end of file From 388da41ed0d5bfb260f6188dfc063f1b4583510f Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 11 May 2026 11:16:31 +0200 Subject: [PATCH 204/254] Add specialized-trait-inline-specialized-instance --- .../specialized-trait-inline-specialized-instance/A_1.scala | 6 ++++++ .../specialized-trait-inline-specialized-instance/B_2.scala | 4 ++++ 2 files changed, 10 insertions(+) create mode 100644 tests/run/specialized-trait-inline-specialized-instance/A_1.scala create mode 100644 tests/run/specialized-trait-inline-specialized-instance/B_2.scala diff --git a/tests/run/specialized-trait-inline-specialized-instance/A_1.scala b/tests/run/specialized-trait-inline-specialized-instance/A_1.scala new file mode 100644 index 000000000000..469c6ff22692 --- /dev/null +++ b/tests/run/specialized-trait-inline-specialized-instance/A_1.scala @@ -0,0 +1,6 @@ +//> using options -language:experimental.specializedTraits + +inline trait A[T: Specialized]: + def foo() = assert(Thread.currentThread.getStackTrace()(1).getClassName() == "A$impl$Int") + +inline def bar = new A[Int]() {} diff --git a/tests/run/specialized-trait-inline-specialized-instance/B_2.scala b/tests/run/specialized-trait-inline-specialized-instance/B_2.scala new file mode 100644 index 000000000000..b9cb802a15a1 --- /dev/null +++ b/tests/run/specialized-trait-inline-specialized-instance/B_2.scala @@ -0,0 +1,4 @@ +//> using options -language:experimental.specializedTraits + +@main def Test = + bar.foo() From 2f6fb01de48012aca81ba7ab43a1d8e78e361391 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 11 May 2026 12:33:48 +0200 Subject: [PATCH 205/254] Fix collection of specializations as parent types --- .../dotc/transform/DesugarSpecializedTraits.scala | 11 +++++++++++ ...-extends-specialized-trait-check-specialized.scala | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 tests/run/specialized-trait-class-extends-specialized-trait-check-specialized.scala diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index c67dafb9e745..109e16d6af97 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -503,6 +503,17 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: report.error(s"${tr.typeSymbol} used in a Specialized position, so it must be marked as Specialized at its definition.", tr.denot.symbol.srcPos) specializations } + + case app @ Apply(_, _) => tpd.methPart(app) match { // class / object Bar extends Foo[Int](params) + case fun @ Select(New(tpt), init) if fun.symbol.isConstructor => tpd.allArgss(tree) match { + case typeArgs :: valueArgss => + val spec = Specialization(fun.symbol.owner, typeArgs, app.span) + if spec.isSpecialized then specializations.addInterface(spec) else specializations + case _ => specializations + } + case _ => specializations + } + case _ => specializations ) }) diff --git a/tests/run/specialized-trait-class-extends-specialized-trait-check-specialized.scala b/tests/run/specialized-trait-class-extends-specialized-trait-check-specialized.scala new file mode 100644 index 000000000000..618c72859748 --- /dev/null +++ b/tests/run/specialized-trait-class-extends-specialized-trait-check-specialized.scala @@ -0,0 +1,11 @@ +//> using options -language:experimental.specializedTraits +inline trait Foo[T: Specialized](x: T): + def foo = x + +class Bar extends Foo(10): + def myMethod = "Hello I am a method" + +@main def Test = + val x = Bar() + val traits = classOf[Bar].getInterfaces() + assert(traits.exists(cl => cl.getName() == "Foo$sp$Int")) From f155e6488e400d63b5d64529cd47d8bc92d92314 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 11 May 2026 12:35:05 +0200 Subject: [PATCH 206/254] Add specialized-trait-list-of-specialized-trait.scala --- .../specialized-trait-list-of-specialized-trait.scala | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/run/specialized-trait-list-of-specialized-trait.scala diff --git a/tests/run/specialized-trait-list-of-specialized-trait.scala b/tests/run/specialized-trait-list-of-specialized-trait.scala new file mode 100644 index 000000000000..a6d9d964a097 --- /dev/null +++ b/tests/run/specialized-trait-list-of-specialized-trait.scala @@ -0,0 +1,9 @@ +//> using options -language:experimental.specializedTraits +inline trait Foo[T: Specialized]: + def foo = Thread.currentThread.getStackTrace()(1).getClassName() + +def bar(xs: List[Foo[Int]]) = xs.head + +@main def Test = + val myList = List(new Foo[Int]() {}, new Foo[Int]() {}) + assert(bar(myList).foo == "Foo$impl$Int") From 61b6d02df97dd5882756dd42943bd81a9bd8b98d Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 14 May 2026 16:33:18 +0200 Subject: [PATCH 207/254] Add some more tests --- ...cialized-trait-processing-order-matters.scala | 16 ++++++++++++++++ .../specialized-trait-scoped-inside-object.scala | 9 +++++++++ ...specialized-trait-very-specialized-list.scala | 8 ++++++++ ...ialized-trait-masked-inline-specialized.scala | 12 ++++++++++++ 4 files changed, 45 insertions(+) create mode 100644 tests/pos/specialized-trait-processing-order-matters.scala create mode 100644 tests/pos/specialized-trait-scoped-inside-object.scala create mode 100644 tests/pos/specialized-trait-very-specialized-list.scala create mode 100644 tests/run/specialized-trait-masked-inline-specialized.scala diff --git a/tests/pos/specialized-trait-processing-order-matters.scala b/tests/pos/specialized-trait-processing-order-matters.scala new file mode 100644 index 000000000000..132652152c7e --- /dev/null +++ b/tests/pos/specialized-trait-processing-order-matters.scala @@ -0,0 +1,16 @@ +//> using options -language:experimental.specializedTraits + +// It's important to update the symbol infos for methods whose type interface changes due to specialization +// before we update any code that uses these symbols so that we type their uses' Apply nodes correctly. +// This only becomes clear when the user is in another object (and not a top-level main method) +// because otherwise the processing follows source code ordering. + +inline trait Trait[T: Specialized]: + def bar = "bar" + +def foo(v: Trait[Int]) = v + +object Test: + def main(args: Array[String]): Unit = + val a = new Trait[Int] {} + foo(a).bar diff --git a/tests/pos/specialized-trait-scoped-inside-object.scala b/tests/pos/specialized-trait-scoped-inside-object.scala new file mode 100644 index 000000000000..589b08a89154 --- /dev/null +++ b/tests/pos/specialized-trait-scoped-inside-object.scala @@ -0,0 +1,9 @@ +//> using options -language:experimental.specializedTraits + +object MySpecializedStuff: + inline trait Foo[T: Specialized]: + def bar = "Bar" + + def foo = new Foo[Int] {} + +@main def main = MySpecializedStuff.foo.bar diff --git a/tests/pos/specialized-trait-very-specialized-list.scala b/tests/pos/specialized-trait-very-specialized-list.scala new file mode 100644 index 000000000000..7bef9c3786a0 --- /dev/null +++ b/tests/pos/specialized-trait-very-specialized-list.scala @@ -0,0 +1,8 @@ +//> using options -language:experimental.specializedTraits +inline trait Foo[T: Specialized] + +abstract class VerySpecializedList extends Seq[Foo[Int]] + +def main = + val x: VerySpecializedList = null + val y: Foo[Int] = x(1) diff --git a/tests/run/specialized-trait-masked-inline-specialized.scala b/tests/run/specialized-trait-masked-inline-specialized.scala new file mode 100644 index 000000000000..fcd03b104b76 --- /dev/null +++ b/tests/run/specialized-trait-masked-inline-specialized.scala @@ -0,0 +1,12 @@ +//> using options -language:experimental.specializedTraits + +inline trait A[T: Specialized]: + def foo() = assert(Thread.currentThread.getStackTrace()(1).getClassName() == "A$impl$Int") + +inline def myMethod1 = new A[Int]() {} +inline def myMethod2 = myMethod1 +inline def MyMethod3 = myMethod2 + +@main def Test = + val v = myMethod1 + v.foo() From af088756f322d88e1fc80ca1656e3f7e2724c5ee Mon Sep 17 00:00:00 2001 From: Hamish Date: Sat, 16 May 2026 12:15:29 +0200 Subject: [PATCH 208/254] Stop using dollar signs in source in inline-trait-specialized-desugar --- tests/run/inline-trait-specialized-desugar.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/run/inline-trait-specialized-desugar.scala b/tests/run/inline-trait-specialized-desugar.scala index 00ad49b109c3..ee8f57ddc91d 100644 --- a/tests/run/inline-trait-specialized-desugar.scala +++ b/tests/run/inline-trait-specialized-desugar.scala @@ -11,15 +11,15 @@ inline trait ArrayIterator[T](elems: Array[T]) extends Iterator[T]: // Specialized traits generates these signatures: -inline trait Iterator$sp$Int extends Iterator[Int] -inline trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator$sp$Int -class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator[Int](elems) +inline trait Iterator_sp_Int extends Iterator[Int] +inline trait ArrayIterator_sp_Int extends ArrayIterator[Int], Iterator_sp_Int +class ArrayIterator_impl_Int(elems: Array[Int]) extends ArrayIterator_sp_Int, ArrayIterator[Int](elems) // User code does this: def foo(x: ArrayIterator[Int]): Int = x.next() // Specialized traits converts this to -def foo(x: ArrayIterator$sp$Int): Int = x.next() +def foo(x: ArrayIterator_sp_Int): Int = x.next() // User code does this: /* class MyClassA @@ -27,7 +27,7 @@ def foo(x: ArrayIterator$sp$Int): Int = x.next() // We convert this to: class MyClassA -class MyClassB extends MyClassA, ArrayIterator$sp$Int, ArrayIterator[Int](Array.from(Seq(1, 5))) +class MyClassB extends MyClassA, ArrayIterator_sp_Int, ArrayIterator[Int](Array.from(Seq(1, 5))) @main def Test = val xs: Array[Int] = Array(1, 2, 3) @@ -36,7 +36,7 @@ class MyClassB extends MyClassA, ArrayIterator$sp$Int, ArrayIterator[Int](Array. /* val ai = new ArrayIterator[Int](xs) {} */ // We convert this to: - val ai = ArrayIterator$impl$Int(xs) + val ai = ArrayIterator_impl_Int(xs) val mcb = MyClassB() assert(mcb.hasNext) From f4dcfb021750ed2fe92ecc7bbe73114fbf4a9fca Mon Sep 17 00:00:00 2001 From: Hamish Date: Sat, 16 May 2026 16:37:31 +0200 Subject: [PATCH 209/254] Check info exists before pruning --- .../dotty/tools/dotc/transform/PruneSpecializedMethods.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/PruneSpecializedMethods.scala b/compiler/src/dotty/tools/dotc/transform/PruneSpecializedMethods.scala index 53fb016eeafd..22d76f1dcee3 100644 --- a/compiler/src/dotty/tools/dotc/transform/PruneSpecializedMethods.scala +++ b/compiler/src/dotty/tools/dotc/transform/PruneSpecializedMethods.scala @@ -20,7 +20,7 @@ class PruneSpecializedMethods extends MiniPhase with SymTransformer { thisTransf override def description: String = PruneSpecializedMethods.description override def transformSym(sym: SymDenotation)(using Context): SymDenotation = - if sym.isClass && !sym.is(Package) then + if sym.isClass && !sym.is(Package) && sym.info.exists then val clsInfo = sym.asClass.classInfo val clsInfo2 = clsInfo.derivedClassInfo(decls = clsInfo.decls.filteredScope(!isDeletable(_)) From b1570f56d1415b2cfb7d21b12fc70e966a0ab856 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sat, 16 May 2026 16:38:05 +0200 Subject: [PATCH 210/254] Add test for bug discovered in inlining --- tests/neg/inline-def-into-inline-val.scala | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tests/neg/inline-def-into-inline-val.scala diff --git a/tests/neg/inline-def-into-inline-val.scala b/tests/neg/inline-def-into-inline-val.scala new file mode 100644 index 000000000000..50a5b921e2b6 --- /dev/null +++ b/tests/neg/inline-def-into-inline-val.scala @@ -0,0 +1,5 @@ +object O1: + inline def x = 10 + +object O2: + inline val y = O1.x // error: inline value must have a literal constant type From f3cb5bec800ae2866193de6f44cb4d7d5123b9e6 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sat, 16 May 2026 16:39:23 +0200 Subject: [PATCH 211/254] Add smaller version of tests/pos/specialized-trait-multiple-stages-specialized-numeric.scala --- .../specialized-trait-specialized-context-bound.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/pos/specialized-trait-specialized-context-bound.scala diff --git a/tests/pos/specialized-trait-specialized-context-bound.scala b/tests/pos/specialized-trait-specialized-context-bound.scala new file mode 100644 index 000000000000..fb2022eaee63 --- /dev/null +++ b/tests/pos/specialized-trait-specialized-context-bound.scala @@ -0,0 +1,11 @@ +//> using options -language:experimental.specializedTraits + +inline trait Numeric2[T: Specialized] + +inline trait A[T: {Numeric2, Specialized}]: + def bar(x: T): T = x + +given Numeric2[Int] = new Numeric2[Int] {} + +class B extends A[Int]: + def baz(x: Int): Int = x From 82240c946bf1e79325cdf1dbc0c6ebae974a1370 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sat, 16 May 2026 16:40:34 +0200 Subject: [PATCH 212/254] Sharpen rules on inline vals and inline defs inside inline traits --- docs/_docs/internals/inline-traits.md | 59 ++++++++++++++----- ...line-trait-inline-val-constant-value.scala | 2 + .../A_1.scala | 1 + .../A_1.scala | 1 - 4 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 tests/neg/inline-trait-inline-val-constant-value.scala diff --git a/docs/_docs/internals/inline-traits.md b/docs/_docs/internals/inline-traits.md index af108975fce9..3b912715cb0d 100644 --- a/docs/_docs/internals/inline-traits.md +++ b/docs/_docs/internals/inline-traits.md @@ -168,21 +168,33 @@ method on A or B. Furthermore if we allowed this, specialization would be lost. e.g. `A$$foo$`, `B$$foo` and the `override def foo` in `C` will delegate to one of these methods. Super calls to non-overridden methods are also supported. These are transformed to point directly to the corresponding inlined methods with no need for name mangling. -- inline traits may define inline members (e.g. `inline def`, `inline val`). References to these are inlined as the body of the trait is inlined into the inline receiver, but the members themselves are not inlined and are deleted from the parent trait. E.g.: -```scala -inline trait A: - inline val x = 1 +- **Interaction with other types of inline**: -class B extends A: - def f = x -``` -becomes: -```scala -inline trait A -class B extends A: - def f = 1 -``` - + - Inline traits may define inline members (e.g. `inline def`, `inline val`). References to these are inlined as the body of the trait is inlined into the inline receiver, but the members themselves are not inlined and are deleted from the parent trait. E.g.: + + ```scala + inline trait A: + inline val x = 1 + + class B extends A: + def f = x + ``` + + becomes: + + ```scala + inline trait A + class B extends A: + def f = 1 + ``` + - As is usual, `inline val`s must have constant value types. In particular this means that they may not take the value of a parameter to the inline trait: + + ```scala + inline trait A[T](x: T): + inline val y = 1 + inline val a = y // ok + inline val z = x // Not ok + ``` - There is the potential for name clashes between members / parameters of inline traits and parameters / members of inline receivers. These are handled in the following way: @@ -393,7 +405,7 @@ This behaviour is the same as that in Timothée's thesis except for the followin - We now do replacement of member accesses to point to the inlined versions throughout the whole code, not just in the bodies of inner classes - He allows inline traits to contain inner classes in principle, however in practice they don't work which is why we ban them. - We specialize types of member accesses on e.g. Numeric - - He in principle allows traits to extend inline traits although it doesn't work that well; we impose concrete rules on this: + - He in practice allows traits to extend inline traits although it doesn't work that well and there was some suggestion it should have been banned; we tighten/specify the rules on this: - Trait extends inline trait is only allowed if the inline trait is parameterless - Inline trait extends trait is always allowed - We modify some of the rules around overrides and conflicting members in order to make the behaviour more consistent with ordinary traits. @@ -403,3 +415,20 @@ This behaviour is the same as that in Timothée's thesis except for the followin - We enforce a number of rules that were previously implicit, with proper errors. - We do the RHS type narrowing for vals (not vars) described above as an optimisation - We change the handling of private members in pruning of inline traits (we now delete them completely) + - We change the phase ordering since we conclude that inline trait inlining must happen before pickling to get the benefit of specialization + across compilation units. Otherwise with the following under separate compilation we will induce boxing: + +```scala +// File A.scala +inline trait IT[T]: + def foo(x: T): T = x + +class A extends IT[Int] + +// File B.scala +def main = + val a = new A() + val x: Int = a.foo(10) // leads to Int.unbox(a.foo(Int.box(10))) +``` + + This happens because when B.scala is compiled separately against the interface of A (derived from the pickled A.tasty) it will appear that A only supports the generic T interface (erasing to Object and so boxed), whereas it actually also has a specialized Int interface from inline trait inlining. If instead we inline before pickling we solve this problem as the generated interface is present in the pickle. There is precedent for some inlining before pickling in e.g. transparent inlines, and if one uses inline traits one expects code duplication (that's why it is opt-in) and therefore we argue this is not a problem in terms of the increased tasty file size that it leads to. diff --git a/tests/neg/inline-trait-inline-val-constant-value.scala b/tests/neg/inline-trait-inline-val-constant-value.scala new file mode 100644 index 000000000000..7984686e9d98 --- /dev/null +++ b/tests/neg/inline-trait-inline-val-constant-value.scala @@ -0,0 +1,2 @@ +inline trait A[T](x: T): + inline val property = x // error: inline value must have a literal constant type diff --git a/tests/pos/inline-trait-multiple-stages-defs/A_1.scala b/tests/pos/inline-trait-multiple-stages-defs/A_1.scala index 97532bc38ae5..28781c194dd1 100644 --- a/tests/pos/inline-trait-multiple-stages-defs/A_1.scala +++ b/tests/pos/inline-trait-multiple-stages-defs/A_1.scala @@ -7,4 +7,5 @@ inline trait A(x: Int): var k: Int = 4 inline val a = 5 + inline val b = a inline def b(a: Int): Int = 6 diff --git a/tests/pos/inline-trait-multiple-stages-generic-defs/A_1.scala b/tests/pos/inline-trait-multiple-stages-generic-defs/A_1.scala index c033e5471d4a..3660b1e933ce 100644 --- a/tests/pos/inline-trait-multiple-stages-generic-defs/A_1.scala +++ b/tests/pos/inline-trait-multiple-stages-generic-defs/A_1.scala @@ -6,5 +6,4 @@ inline trait A[T](x: T): val j: T var k: T = x - inline val property = x inline def method(a: T): T = x From 49ac34e12621ce37071b30d3a5f20c0416be02ef Mon Sep 17 00:00:00 2001 From: Hamish Date: Sat, 16 May 2026 16:41:22 +0200 Subject: [PATCH 213/254] Fix inline-trait-y-equals-x-inlined-nowarn by special case in error rules --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 5 +++-- tests/pos/inline-trait-y-equals-x-inlined-nowarn.scala | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index a8a855b6ae5b..12e93f7e1f3b 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -635,8 +635,9 @@ object Semantic: reporter.report(error) Hot else - val error = AccessNonInit(target)(trace) - reporter.report(error) + if !receiver.classSymbol.isInlineTrait then // See tests/pos/inline-trait-y-equals-x-inlined-nowarn.scala + val error = AccessNonInit(target)(trace) + reporter.report(error) Hot else report.warning("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", field = " + field.show + Trace.show, Trace.position) diff --git a/tests/pos/inline-trait-y-equals-x-inlined-nowarn.scala b/tests/pos/inline-trait-y-equals-x-inlined-nowarn.scala index 4eb0d1d684ab..f0712d6518f6 100644 --- a/tests/pos/inline-trait-y-equals-x-inlined-nowarn.scala +++ b/tests/pos/inline-trait-y-equals-x-inlined-nowarn.scala @@ -1,7 +1,8 @@ //> using options -Werror -Wsafe-init inline trait A(val x: Int): - val y = x // We need to be careful not to warn on this when we inline it but leave the body in the parent. Pruning early enough avoids this. + val y = x // We need to be careful not to warn on this when we inline it but leave the body in the parent, because in the child the access is fine as we inline the parameter value, + // while the rhs of val y in the parent inline trait is going to be pruned out later so the illegal access won't be possible. class C extends A(10) @main def Test = From fa0bb8b0225b7b809f46ea82ff8e77f8d23d466d Mon Sep 17 00:00:00 2001 From: Hamish Date: Sat, 16 May 2026 16:46:29 +0200 Subject: [PATCH 214/254] Switch to using erasure for specialization and inline trait symbol replacement --- compiler/src/dotty/tools/dotc/Compiler.scala | 8 +- .../src/dotty/tools/dotc/core/Contexts.scala | 13 ++ .../src/dotty/tools/dotc/core/Phases.scala | 3 - .../dotty/tools/dotc/core/TypeErasure.scala | 27 ++- .../dotty/tools/dotc/inlines/Inlines.scala | 16 +- .../transform/DesugarSpecializedTraits.scala | 173 ++++++++---------- .../dotty/tools/dotc/transform/Erasure.scala | 11 +- .../ReplaceInlinedTraitSymbols.scala | 49 ----- 8 files changed, 125 insertions(+), 175 deletions(-) delete mode 100644 compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 1f6d14dfbf86..4408998f5212 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -9,7 +9,6 @@ import Phases.Phase import transform.* import backend.jvm.GenBCode import localopt.{StringInterpolatorOpt, DropForMap} -import dotty.tools.dotc.transform.ReplaceInlinedTraitSymbols /** The central class of the dotc compiler. The job of a compiler is to create * runs, which process given `phases` in a given `rootContext`. @@ -46,7 +45,6 @@ class Compiler { List(new SetRootTree) :: // Set the `rootTreeOrProvider` on class symbols List(new SpecializeInlineTraits) :: // Inline the code of inline traits into their children List(new DesugarSpecializedTraits) :: // Process the Specialized annotation - List(new ReplaceInlinedTraitSymbols) :: // Replace symbols referring to inline trait members with resulting inlined member symbols Nil /** Phases dealing with TASTY tree pickling and unpickling */ @@ -74,8 +72,7 @@ class Compiler { new ExpandSAMs, // Expand single abstract method closures to anonymous classes new ElimRepeated, // Rewrite vararg parameters and arguments new DropForMap, // Drop unused trailing map calls in for comprehensions - new PruneSpecializedMethods, // Drop specialized methods which have already been inlined - new PruneInlineTraits) :: // Remove right-hand side of definitions in inline traits + new PruneSpecializedMethods) :: // Remove right-hand side of definitions in inline traits List(new RefChecks) :: // Various checks mostly related to abstract members and overriding List(new init.Checker) :: // Check initialization of objects List(new ProtectedAccessors, // Add accessors for protected members @@ -87,7 +84,8 @@ class Compiler { new ForwardDepChecks, // Check that there are no forward references to local vals new SpecializeApplyMethods, // Adds specialized methods to FunctionN new TryCatchPatterns, // Compile cases in try/catch - new PatternMatcher) :: // Compile pattern matches + new PatternMatcher, // Compile pattern matches + new PruneInlineTraits) :: // Remove right-hand side of definitions in inline traits List(new TestRecheck.Pre) :: // Test only: run rechecker, enabled under -Yrecheck-test List(new TestRecheck) :: // Test only: run rechecker, enabled under -Yrecheck-test List(new cc.Setup) :: // Preparations for check captures phase, enabled under captureChecking diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index f772995736fc..caf1c02b39ec 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -44,6 +44,7 @@ import java.nio.file.InvalidPathException import dotty.tools.dotc.coverage.Coverage import scala.annotation.tailrec import dotty.tools.dotc.inlines.Inlines.InlineTraitState +import dotty.tools.dotc.transform.SpecializedTraitState object Contexts { @@ -149,6 +150,7 @@ object Contexts { def gadt: GadtConstraint = gadtState.gadt def gadtState: GadtState def inlineTraitState: InlineTraitState + def specializedTraitState: SpecializedTraitState def searchHistory: SearchHistory def source: SourceFile @@ -440,6 +442,7 @@ object Contexts { .setTyperState(typerState) .setGadtState(gadtState) .setInlineTraitState(inlineTraitState) + .setSpecializedTraitState(specializedTraitState) .fresh .setScope(this.scope) } @@ -611,6 +614,9 @@ object Contexts { private var _inlineTraitState: InlineTraitState = uninitialized final def inlineTraitState: InlineTraitState = _inlineTraitState + private var _specializedTraitState: SpecializedTraitState = uninitialized + final def specializedTraitState: SpecializedTraitState = _specializedTraitState + private var _searchHistory: SearchHistory = uninitialized final def searchHistory: SearchHistory = _searchHistory @@ -636,6 +642,7 @@ object Contexts { _scope = origin.scope _gadtState = origin.gadtState _inlineTraitState = origin.inlineTraitState + _specializedTraitState = origin.specializedTraitState _searchHistory = origin.searchHistory _source = origin.source _moreProperties = origin.moreProperties @@ -704,6 +711,11 @@ object Contexts { this._inlineTraitState = inlineTraitState this + def setSpecializedTraitState(specializedTraitState: SpecializedTraitState): this.type = + util.Stats.record("Context.setSpecializedTraitState") + this._specializedTraitState = specializedTraitState + this + def setSearchHistory(searchHistory: SearchHistory): this.type = util.Stats.record("Context.setSearchHistory") this._searchHistory = searchHistory @@ -799,6 +811,7 @@ object Contexts { c._searchHistory = new SearchRoot c._gadtState = GadtState(GadtConstraint.empty) c._inlineTraitState = InlineTraitState() + c._specializedTraitState = SpecializedTraitState() c end FreshContext diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index 2aca93bd1cdf..404740d84c95 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -239,7 +239,6 @@ object Phases { private var myPicklerPhase: Phase = uninitialized private var mySetRootTreePhase: Phase = uninitialized private var mySpecializeInlineTraitsPhase: Phase = uninitialized - private var myReplaceInlinedTraitSymbolsPhase: Phase = uninitialized private var myDesugarSpecializedTraitsPhase: Phase = uninitialized private var myInliningPhase: Phase = uninitialized private var myStagingPhase: Phase = uninitialized @@ -275,7 +274,6 @@ object Phases { final def picklerPhase: Phase = myPicklerPhase final def setRootTreePhase: Phase = mySetRootTreePhase final def specializeInlineTraitsPhase: Phase = mySpecializeInlineTraitsPhase - final def replaceInlinedTraitSymbolsPhase: Phase = myReplaceInlinedTraitSymbolsPhase final def desugarSpecializedTraitsPhase: Phase = myDesugarSpecializedTraitsPhase final def inliningPhase: Phase = myInliningPhase final def stagingPhase: Phase = myStagingPhase @@ -311,7 +309,6 @@ object Phases { mySetRootTreePhase = phaseOfClass(classOf[SetRootTree]) myPicklerPhase = phaseOfClass(classOf[Pickler]) mySpecializeInlineTraitsPhase = phaseOfClass(classOf[SpecializeInlineTraits]) - myReplaceInlinedTraitSymbolsPhase = phaseOfClass(classOf[ReplaceInlinedTraitSymbols]) myDesugarSpecializedTraitsPhase = phaseOfClass(classOf[DesugarSpecializedTraits]) myInliningPhase = phaseOfClass(classOf[Inlining]) myStagingPhase = phaseOfClass(classOf[Staging]) diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index f9a4ecd3ab94..0c0e30ba5365 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -7,6 +7,7 @@ import Flags.JavaDefined import Uniques.unique import backend.sjs.JSDefinitions import transform.ExplicitOuter.* +import transform.Specialization import transform.ValueClasses.* import transform.ContextFunctionResults.* import unpickleScala2.Scala2Erasure @@ -77,7 +78,7 @@ end SourceLanguage object TypeErasure: private def erasureDependsOnArgs(sym: Symbol)(using Context) = - sym == defn.ArrayClass || sym == defn.PairClass || sym.isDerivedValueClass + sym == defn.ArrayClass || sym == defn.PairClass || sym.isDerivedValueClass || sym.isSpecializedTrait /** The arity of this tuple type, which can be made up of EmptyTuple, TupleX and `*:` pairs. * @@ -770,6 +771,10 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else if semiEraseVCs && sym.isDerivedValueClass then eraseDerivedValueClass(tp) else if defn.isSyntheticFunctionClass(sym) then defn.functionTypeErasure(sym) else eraseNormalClassRef(tp) + case Specialization(spec) if ((ctx.phase == erasurePhase || ctx.erasedTypes) && spec.isSpecialized) => + val interfaceSymbol = ctx.specializedTraitState.specializedTraitCache.get.getInterfaceSymbol(spec) + assert(interfaceSymbol.nonEmpty) // This is a specialized trait; we should have a specialization we can swap in for it + this(interfaceSymbol.get.typeRef.appliedTo(spec.unspecializedTypeArgs.map(_.tpe))) case tp: AppliedType => val tycon = tp.tycon if (tycon.isRef(defn.ArrayClass)) eraseArray(tp) @@ -866,13 +871,19 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst } val erasedParents: List[Type] = if ((cls eq defn.ObjectClass) || cls.isPrimitiveValueClass) Nil - else parents.mapConserve(eraseParent) match { - case tr :: trs1 => - assert(!tr.classSymbol.is(Trait), i"$cls has bad parents $parents%, %") - val tr1 = if (cls.is(Trait)) defn.ObjectType else tr - tr1 :: trs1.filterNot(_.isAnyRef) - case nil => nil - } + else + val parents1 = parents.mapConserve(eraseParent) + // drop duplicate Foo$sp$Int arising from erasure of Foo[Int] + val parents2 = parents1.filterNot( + p => Specialization.unapply(p).exists(s => ctx.specializedTraitState.specializedTraitCache.get.getInterfaceSymbol(s).nonEmpty) + ) + parents2 match { + case tr :: trs1 => + assert(!tr.classSymbol.is(Trait), i"$cls has bad parents $parents%, %") + val tr1 = if (cls.is(Trait)) defn.ObjectType else tr + tr1 :: trs1.filterNot(_.isAnyRef) + case nil => nil + } val erasedDecls = decls.filteredScope( keep = sym => !sym.isType || sym.isClass, rename = sym => diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 89e347fb26c8..fb0d9150140c 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -123,7 +123,7 @@ object Inlines: private def inlineTraitAncestors(cls: TypeDef, allowSpecialized: Boolean, allowNonSpecialized: Boolean)(using Context): List[Tree] = cls match { case tpd.TypeDef(_, tmpl: Template) => val parentTrees: Map[Symbol, Tree] = tmpl.parents.map(par => symbolFromParent(par) -> par).toMap.filter(_._1.isInlineTrait) - val ancestors: List[ClassSymbol] = cls.tpe.baseClasses.filter(sym => sym != cls.symbol && + val ancestors: List[ClassSymbol] = cls.tpe.baseClasses.filter(sym => sym != cls.symbol && // TODO: Do we not need to stop if there is a non-inline trait somewhere in the hierarchy? It should block the inlining right? ((sym.isInlineTrait && !sym.isSpecializedTrait && allowNonSpecialized) || (sym.isInlineTrait && sym.isSpecializedTrait && allowSpecialized)) ) @@ -934,7 +934,7 @@ object Inlines: private def inlinedSym(sym: Symbol, overriddenDecls: Set[Symbol], withoutFlags: FlagSet = EmptyFlags)(using Context): Symbol = val newSym = if sym.isClass then inlinedClassSym(sym.asClass, withoutFlags) else inlinedMemberSym(sym, overriddenDecls, withoutFlags) - ctx.inlineTraitState.registerInlinedSymbol(sym, newSym, ctx.owner.thisType.widenDealias) + ctx.inlineTraitState.registerInlinedSymbol(sym, newSym, ctx.owner.thisType.classSymbol) newSym private def inlinedClassSym(sym: ClassSymbol, withoutFlags: FlagSet = EmptyFlags)(using Context): ClassSymbol = @@ -958,7 +958,7 @@ object Inlines: spanCoord(parent.span) ) // ctx.inlineTraitState.registerInlinedInnerClassSymbol(sym, inlinedSym, childThisType) - ctx.inlineTraitState.registerInlinedSymbol(sym, inlinedSym, childThisType.widenDealias) + ctx.inlineTraitState.registerInlinedSymbol(sym, inlinedSym, childThisType.classSymbol) inlinedSym.entered case _ => report.error(s"Class symbol ${sym.show} does not have class info") @@ -1133,23 +1133,23 @@ object Inlines: class InlineTraitState: // Map representing all symbols we have inlined from inline traits, - // from the symbol in the parent trait, and the type of the child class-like + // from the symbol in the parent trait, and the symbol of the child class-like // to the inlined symbol in that child class-like. // E.g. inline trait A {def foo#1000}; trait B extends A {def foo#2000 // created by inlining} // The map has (foo#1000, trait B) => foo#2000 - private val inlinedTraitSymbols = mutable.HashMap[(Symbol, Type), Symbol]() + val inlinedTraitSymbols = mutable.HashMap[(Symbol, Symbol), Symbol]() // Record that we just inlined oldSym into childClasslike which created // childClassLike.newSym - def registerInlinedSymbol(oldSym: Symbol, newSym: Symbol, childClasslike: Type) = + def registerInlinedSymbol(oldSym: Symbol, newSym: Symbol, childClasslike: Symbol) = inlinedTraitSymbols((oldSym, childClasslike)) = newSym // Map (e.g.) B.foo#1000 into foo#2000 - def lookupInlinedSymbol(oldSym: Symbol, childClasslike: Type) = + def lookupInlinedSymbol(oldSym: Symbol, childClasslike: Symbol) = inlinedTraitSymbols((oldSym, childClasslike)) // Check if oldSym has been inlined into childClasslike - def inlinedSymbolIsRegistered(oldSym: Symbol, childClasslike: Type) = + def inlinedSymbolIsRegistered(oldSym: Symbol, childClasslike: Symbol) = inlinedTraitSymbols.contains((oldSym, childClasslike)) end InlineTraitState diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 109e16d6af97..5e26186510e5 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -26,7 +26,6 @@ import tpd._ import scala.collection.mutable import scala.annotation.unspecialized import dotty.tools.dotc.typer.Synthesizer -import dotty.tools.dotc.typer.Typer import dotty.tools.dotc.core.NameKinds import dotty.tools.dotc.core.Flags.GivenOrImplicit import dotty.tools.dotc.core.NameKinds.ContextBoundParamName @@ -55,8 +54,6 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: override def changesParents: Boolean = true override def allowsImplicitSearch: Boolean = true - private var specializedTraitCache = SpecializedTraitCache(genInterfaceSymbol = newInterfaceTrait, genImplementationSymbol = newImplementationClass) - private def newInterfaceTrait(specialization: Specialization, specializations: SpecializedTraitCache)(using Context): (ClassSymbol, SpecializedTraitCache) = { val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. def apply(t: Type) = specialization.specializedTypeParamsToTypeArgumentsMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) // TODO: If we can do just types we can get rid of this @@ -74,7 +71,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: // Create new trait val parents = defn.ObjectType :: AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization).tpe // original trait, specialized to Foo[Int] - :: inheritedParents.map(replaceSpecializedSymbolsMap(specializations1).typeMap(_)) // parents of the original trait, specialized to Foo$sp$Int + :: inheritedParents // parents of the original trait in the form Foo[Int] (later specialized to Foo$sp$Int) val traitSymbol = newNormalizedClassSymbol( specialization.traitSymbol.owner, @@ -210,117 +207,66 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: ).withSpan(specialization.span) } - private def replaceSpecializedSymbolsMap(specializations: SpecializedTraitCache)(using Context) = - val typeMap = new TypeMap: - def apply(t: Type) = t match { - case Specialization(spec) => - { - for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) - yield specializedSymbol.typeRef.appliedTo(spec.unspecializedTypeArgs.map(_.tpe)) - }.getOrElse(mapOver(t)) - case _ => mapOver(t) - } + private def replaceImplementationClassesMap(specializations: SpecializedTraitCache)(using Context) = + val specializeTypeTree: Tree => Tree = tree => + tree match { + case Specialization(spec) => + ctx.specializedTraitState.specializedTraitCache.get.getInterfaceSymbol(spec).map: + specializedSymbol => + if spec.unspecializedTypeArgs.nonEmpty then + AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? + else + TypeTree(specializedSymbol.typeRef) + .getOrElse(tree) + case tree => tree + } def treeMap(tree: Tree): Tree = tree match { /* Replace new Foo[Int] {} with new Foo$impl$Int.asInstanceOf[Foo$sp$Int] This has already been desugared to an anonymous class instance with parents: Objects, Parents of Foo, Foo. */ - case Block(List(an@TypeDef(anon, tmpl@Template(_, parentCalls: List[Tree], _, _))), - Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => - + + case AnonymousSpecializationInstance(anon) => def deandify(tp: Type): Iterator[Type] = tp match case AndType(l, r) => deandify(l) ++ deandify(r) case _ => Iterator.single(tp) - - t.tpe match { + anon.typeTree.tpe match { case a: AndType => /* Multiple mixed in traits will be typed as an AndType */ deandify(a).foreach(trt => - Specialization.unapply(trt, t.span).foreach {spec => + Specialization.unapply(trt, anon.typeTree.span).foreach {spec => if spec.hasSpecializedParams then - report.error("Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like.", an.srcPos) + report.error("Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like.", anon.srcPos) } ) tree case tpe => - Specialization.unapply(tpe, t.span).map(spec => + Specialization.unapply(tpe, anon.typeTree.span).map(spec => { if spec.hasSpecializedParams then - if tmpl.body.filterNot(x => x.symbol.name.is(ContextBoundParamName)).nonEmpty then // Only allowed to contain evidence parameters - report.error("Anonymous classes acting as instances of Specialized traits may not have additional members; you can make a named object instead if you like.", an.srcPos) + if anon.body.filterNot(x => x.symbol.name.is(ContextBoundParamName)).nonEmpty then // Only allowed to contain evidence parameters + report.error("Anonymous classes acting as instances of Specialized traits may not have additional members; you can make a named object instead if you like.", anon.srcPos) - parentCalls match { + anon.parentCalls match { case (obj :: parentsOfSpecTrait) :+ (app@Apply(_, _)) if (obj.symbol.owner == ctx.definitions.ObjectClass) && (parentsOfSpecTrait.forall(x => spec.traitSymbol.asClass.parentSyms.exists(p => p == x.symbol.owner))) => + specializations.getImplementationSymbol(spec).map( specializedSymbol => Typed( - Select(New(ref(specializedSymbol)),ctor).appliedToTypeTrees(spec.unspecializedTypeArgs) - .appliedToArgss(tpd.allArgss(app).tail.nestedMap(_.changeNonLocalOwners(an.symbol.owner))) // Remove the type params which are not needed - , t) + Select(New(ref(specializedSymbol)),anon.ctor).appliedToTypeTrees(spec.unspecializedTypeArgs) + .appliedToArgss(tpd.allArgss(app).tail.nestedMap(_.changeNonLocalOwners(anon.symbol.owner))) // Remove the type params which are not needed + , specializeTypeTree(anon.typeTree)) ).getOrElse(tree) // We don't replace non-specialized anonymous class instantiations e.g. new Foo[T] where T is defined in the enclosing scope. case _ => - report.error("Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like.", an.srcPos) + report.error("Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like.", anon.srcPos) tree } else tree }).getOrElse(tree) - } - - // Replace class/object Bar extends Foo[Int](params) with class/object Bar extends Foo$sp$Int(params) - case app @ Apply(_, _) => tpd.methPart(app) match { - case fun @ Select(New(tpt), init) if fun.symbol.isConstructor => - val argss = tpd.allArgss(tree) - argss match { - case typeArgs :: valueArgss => - val spec = Specialization(fun.symbol.owner, typeArgs, app.span) - { - for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) - yield New(ref(specializedSymbol)).select(init).appliedToTypeTrees(spec.unspecializedTypeArgs).appliedToNone - }.getOrElse(tree) - case _ => tree - } - case _ => tree - } - - // Replace AppliedTypeTree instances in code - case Specialization(spec) => { - for (specializedSymbol <- specializations.getInterfaceSymbol(spec)) - yield - if spec.unspecializedTypeArgs.nonEmpty then - AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? - else - TypeTree(specializedSymbol.typeRef) - }.getOrElse(tree) - + } case tree => tree } - - new TreeTypeMap(typeMap, treeMap) { - override def transform(tree: Tree)(using Context): Tree = tree match { // HACK: This seems to do what we want but I don't understand why we don't do this by default? Surely we should apply transformDefs over template body? - case dd@DefDef(name, paramss, tpt, preRhs) => - val transformedDef = super.transform(dd) - transformedDef.symbol.info = mapType(transformedDef.symbol.info) - transformedDef - - case vd@ValDef(name, tpt, preRhs) => - val transformedDef = super.transform(vd) - transformedDef.symbol.info = mapType(transformedDef.symbol.info) - transformedDef - - case impl@Template(constr, preParentsOrDerived, self, _) => - val mappedbody = impl.body.map(transform(_)) - val mappedconstr = transform(impl.constr).asInstanceOf[DefDef] - - /* We need to map parents of non-specialized inline traits (see tests/pos/specialized-trait-partial-complete-specialization-with-return-type.scala, we need - to map the A[Int] reference to A$sp$Int in B's parents). For our implementation classes and interface traits we don't want to map as we will delete parents after. */ - val mappedparents = if impl.symbol.owner.isSpecializedTraitImplementationClass || impl.symbol.owner.isSpecializedTraitInterface then impl.parents else impl.parents.map(transform(_)) - val oldInfo = impl.symbol.owner.info.asInstanceOf[ClassInfo] - impl.symbol.owner.info = oldInfo.derivedClassInfo(declaredParents = if impl.symbol.owner.isSpecializedTraitImplementationClass || impl.symbol.owner.isSpecializedTraitInterface then oldInfo.declaredParents else oldInfo.declaredParents.map(mapType(_))) - - cpy.Template(impl)(body = mappedbody, parents = mappedparents, constr = mappedconstr) - case tree => super.transform(tree) - } - } - end replaceSpecializedSymbolsMap + new TreeTypeMap(treeMap = treeMap) + end replaceImplementationClassesMap /* Override flags can be generated by inline trait inlining, but after removing the Foo[Int] parent the corresponding members no longer override members in their parents. Therefore we need to remove them. */ @@ -405,11 +351,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: assert(Inlines.needsInlining(tree, allowSpecializedTraits=true)) val inlined = Inlines.inlineParentInlineTraits(tree, allowSpecialized=true).asInstanceOf[TypeDef] cpy.TypeDef(inlined)(name = inlined.name, rhs = inlineInlineTraits(inlined.rhs)).withSpan(inlined.span) - }.tapEach: // We can do parent removal earlier for $impl$ classes as we don't depend on the parents later. - _.updateParents { parents => (parents: @unchecked) match - case obj :: traitSp :: originalSpec :: Nil => obj :: traitSp :: Nil - } - .map(refreshClassDef) + } val (generatedTraitStatsFinal, generatedClassStatsFinal, specializationsFinal) = if (generatedTraitStats1.isEmpty && generatedClassStats1.isEmpty) @@ -429,13 +371,12 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: obj :: parents case obj :: parents => obj :: parents // We already removed the relevant parent. } - .map(refreshClassDef) + .map(stat => if stat.symbol.isSpecializedTraitInterface then refreshClassDef(stat) else stat) (generatedTraitStats3, generatedClassStats2, specializations4) - val statsFinal = generatedTraitStatsFinal ++ + val statsFinal = generatedTraitStatsFinal ++ generatedClassStatsFinal ++ - stats.map(stat => - replaceSpecializedSymbolsMap(specializationsFinal)( // Foo[Int] -> Foo$sp$Int in user code. + stats.map(stat => replaceImplementationClassesMap(specializationsFinal)( if (!stat.symbol.isSpecializedTraitImplementationClass && !stat.symbol.isSpecializedTraitInterface) then // We already processed these in an earlier recursive call Inlines.inlineParentInlineTraits(stat, allowSpecialized = true, allowNonSpecialized = false) // Perform inlining into class Bar extends Foo[Int] from user code. // TODO: I don't really like this gating. else @@ -462,8 +403,11 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: case _ => } - val (stats1, specializedTraitCache2) = transformStatements(stats, specializedTraitCache) // TODO: Fix span - specializedTraitCache = specializedTraitCache2 // TODO: Maybe avoid mutation here + if ctx.specializedTraitState.specializedTraitCache.isEmpty then + ctx.specializedTraitState.specializedTraitCache = Some(SpecializedTraitCache(genInterfaceSymbol = newInterfaceTrait, genImplementationSymbol = newImplementationClass)) + + val (stats1, specializedTraitCache2) = transformStatements(stats, ctx.specializedTraitState.specializedTraitCache.get) + ctx.specializedTraitState.specializedTraitCache = Some(specializedTraitCache2) // TODO: Avoid mutation here - we will make the cache mutable instead I think. Makes more sense cpy.PackageDef(pkg)(pid, stats1) } @@ -475,15 +419,16 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: case Specialization(spec) if spec.isSpecialized => specializations.addInterfaceAndImplementation(spec) case _ => specializations } - case Specialization(spec) => - if (spec.isSpecialized) { + case Specialization(spec) => + if (spec.hasSpecializedParams) { // Block Vec[?] and similar spec.specializedTypeArgs.filter { case t: TypeBoundsTree => true case _ => false }.foreach: tr => report.error("Wildcard types may not be substituted for Specialized type parameters.", tr.srcPos) - + } + if (spec.isSpecialized) { specializations.addInterface(spec) } else { // Check foo[S: Specialized] <= Vec[S: Specialized] @@ -635,7 +580,10 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree], val /* If inline trait Foo[T] has a method taking another Foo[T] there's no point specializing the reference since the resulting sp$T$ would be the same as the starting trait. */ def isSpecialized: Boolean = - hasSpecializedParams && typeArguments.exists(!_.tpe.existsPart(_.typeSymbol.isTypeParam)) + hasSpecializedParams && typeArguments.exists(!_.tpe.existsPart(part => (part.typeSymbol.isTypeParam) || + (part.typeSymbol eq defn.AnyClass) || + (part.typeSymbol eq defn.ObjectClass) || + (part.typeSymbol eq defn.AnyValClass))) // Note: We only care about the specialized arguments for equality; a specialization of Vec[A: Specialized, B] with B = Int and one // with B = String can be considered to be the same as they use the same specialized trait @@ -692,10 +640,33 @@ object Specialization: case _ => false } - def isSpecializedTrait(sym: Symbol)(using Context) = classSpecializedTypeParams(sym).nonEmpty + def isSpecializedTrait(sym: Symbol)(using Context) = sym.isClass && sym.isAllOf(InlineTrait) && classSpecializedTypeParams(sym).nonEmpty def isSpecializedMethod(sym: Symbol)(using Context) = sym.isAllOf(InlineMethod) && methodSpecializedTypeParams(sym).nonEmpty end Specialization +class AnonymousSpecializationInstance( + val srcPos: SrcPos, + val symbol: Symbol, + val body: List[Tree], + val parentCalls: List[Tree], + val ctor: Name, + val typeTree: TypeTree +) + +object AnonymousSpecializationInstance: + def unapply(tree: Tree)(using Context) = tree match { + case Block(List(an@TypeDef(anon, tmpl@Template(_, parentCalls: List[Tree], _, _))), + Typed(Apply(Select(New(anon1),ctor), _), t: TypeTree)) if anon1.symbol.isAnonymousClass => + Some(AnonymousSpecializationInstance(an.srcPos, an.symbol, tmpl.body, parentCalls, ctor, t)) + case _ => None + } +end AnonymousSpecializationInstance + +class SpecializedTraitState: + var specializedTraitCache: Option[SpecializedTraitCache] = None +end SpecializedTraitState + + // Need to somehow make my naming a lot more consistent as well. // figure out why we generate the T version. // Try to see if we can do with only types and not trees diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index ed2a88519ca0..4014c21d9699 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -743,6 +743,9 @@ object Erasure { adaptIfSuper(qual) match case qual1: Super => select(qual1, sym) + case qual1 if ctx.inlineTraitState.inlinedSymbolIsRegistered(sym, qual1.tpe.widenDealias.classSymbol) => + val newSym = ctx.inlineTraitState.lookupInlinedSymbol(sym, qual1.tpe.widenDealias.classSymbol) + untpd.cpy.Select(tree)(qual, sym.name).withType(qual1.tpe.select(newSym)) // TODO: Maybe we could just do this earlier also; maybe we don't need this cache case qual1 if !isJvmAccessible(qual1.tpe.typeSymbol) || !qual1.tpe.derivesFrom(sym.owner) => val castTarget = // Avoid inaccessible cast targets, see i8661 @@ -1017,7 +1020,13 @@ object Erasure { EmptyTree override def typedClassDef(cdef: untpd.TypeDef, cls: ClassSymbol)(using Context): Tree = - val typedTree@TypeDef(name, impl @ Template(constr, _, self, _)) = super.typedClassDef(cdef, cls): @unchecked + // drop Foo[Int] leading to duplicate Foo$sp$Int + val TypeDef(_, implInit: Template) = cdef: @unchecked + val cdef1 = cpy.TypeDef(cdef.asInstanceOf[TypeDef])(rhs = cpy.Template(implInit.asInstanceOf[Template])(parents = implInit.asInstanceOf[Template].parents.filterNot( + p => Specialization.unapply(p.tpe).exists(s => ctx.specializedTraitState.specializedTraitCache.get.getInterfaceSymbol(s).nonEmpty) + ))) + + val typedTree@TypeDef(name, impl @ Template(constr, _, self, _)) = super.typedClassDef(cdef1, cls): @unchecked // In the case where a trait extends a class, we need to strip any non trait class from the signature // and accept the first one (see tests/run/mixins.scala) val newTraits = impl.parents.tail.filterConserve: tree => diff --git a/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala b/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala deleted file mode 100644 index 22aeae67377b..000000000000 --- a/compiler/src/dotty/tools/dotc/transform/ReplaceInlinedTraitSymbols.scala +++ /dev/null @@ -1,49 +0,0 @@ -package dotty.tools.dotc -package transform - -import ast.*, core._ -import Flags._ -import Contexts._ -import Symbols._ -import dotty.tools.dotc.ast.tpd -import dotty.tools.dotc.ast.Trees._ -import dotty.tools.dotc.quoted._ -import dotty.tools.dotc.inlines.Inlines -import dotty.tools.dotc.ast.TreeMapWithImplicits -import dotty.tools.dotc.core.DenotTransformers.SymTransformer -import dotty.tools.dotc.staging.StagingLevel -import dotty.tools.dotc.core.SymDenotations.SymDenotation -import dotty.tools.dotc.core.StdNames.{str, nme} -import dotty.tools.dotc.core.Types.* -import dotty.tools.dotc.core.Names.{Name, TermName} - -import scala.collection.mutable.ListBuffer -import dotty.tools.dotc.transform.MegaPhase.MiniPhase - -class ReplaceInlinedTraitSymbols extends MiniPhase: - import tpd._ - - override def phaseName: String = ReplaceInlinedTraitSymbols.name - override def description: String = ReplaceInlinedTraitSymbols.description - override def changesMembers: Boolean = true - override def changesParents: Boolean = true - override def runsAfter: Set[String] = Set("desugarSpecializedTraits", "specializeInlineTraits") - - override def transformSelect(tree: Select)(using Context): Tree = - val qualType = tree.qualifier.tpe.widenDealias - if ctx.inlineTraitState.inlinedSymbolIsRegistered(tree.symbol, qualType) then - val newSym = ctx.inlineTraitState.lookupInlinedSymbol(tree.symbol, qualType) - assert(tree.symbol.isTerm) - tpd.Select(tree.qualifier, tree.name) - tree.withType(tree.qualifier.tpe.select(newSym)) - else - tree - - override def runsAfterGroupsOf: Set[String] = Set("specializeInlineTraits") -object ReplaceInlinedTraitSymbols: - val name: String = "replaceInlinedTraitSymbols" - val description: String = "Replace symbols referring to inline trait members with resulting inlined member symbols. Also replace bridge method calls with specialized method calls for specialized traits." - /* We need to replace symbols referring to inlined methods / members because otherwise we will still point - to the parent symbol (this was resolved before we generated the new symbols) and so we won't get the efficiency gain. - See tests/pos/inline-trait-return-ref.scala. We also need to do this outside the inline traits themselves (i.e. in - the whole program - see tests/pos/inline-trait-parent-ref.scala) */ From 2c2b6a0c948f99c2c65cc3e0af8602763a1f8f92 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sat, 16 May 2026 16:53:56 +0200 Subject: [PATCH 215/254] Update comment --- .../tools/dotc/transform/DesugarSpecializedTraits.scala | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 5e26186510e5..86803015e36e 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -207,6 +207,11 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: ).withSpan(specialization.span) } + /* We can replace implementation class already because they implement both Foo[Int] and Foo$sp$Int so they are + a drop in replacement for the anonymous classes that implement Foo[Int], and so we can swap them in without + modifying signatures / interfaces, which we have to do later at erasure. We don't really want to wait with + this replacement because it's easier to detect the anonymous classes earlier before they undergo too many transforms, + and doing it at erasure would be strange given it's a tree transform and not a type transform. */ private def replaceImplementationClassesMap(specializations: SpecializedTraitCache)(using Context) = val specializeTypeTree: Tree => Tree = tree => tree match { From 25ad6b4f5aecb37b22ce1283f561e81c3ab913bb Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 25 May 2026 12:00:11 +0200 Subject: [PATCH 216/254] Specialized traits may not be extended by ordinary traits (they always take parameters) Add missing test --- .../dotty/tools/dotc/transform/SpecializeInlineTraits.scala | 5 ++++- ...ized-trait-trait-extends-specialized-trait-no-param.scala | 5 +++++ .../specialized-trait-trait-extends-specialized-trait.scala | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 tests/neg/specialized-trait-trait-extends-specialized-trait-no-param.scala rename tests/{pos => neg}/specialized-trait-trait-extends-specialized-trait.scala (63%) diff --git a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala index 9556ba60855d..7e6467a8d5c4 100644 --- a/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/SpecializeInlineTraits.scala @@ -49,7 +49,10 @@ class SpecializeInlineTraits extends MacroTransform, SymTransformer { && p.classSymbol.primaryConstructor.paramSymss.exists(paramList => paramList.nonEmpty && paramList.head.isTerm) ) problemParents.foreach( p => - report.error(s"Only parameterless inline traits may be extended by ordinary traits. Make ${tree.symbol} inline or remove inline ${p.typeSymbol}'s parameter list.", tree.srcPos) + val message = if p.typeSymbol.isSpecializedTrait then "Specialized traits may not be extended by ordinary traits. They may only be extended by classes, objects or inline/specialized traits." + else s"Only parameterless inline traits may be extended by ordinary traits. Make ${tree.symbol} inline or remove inline ${p.typeSymbol}'s parameter list." + + report.error(message, tree.srcPos) ) val tree1 = if tree.symbol.isInlineTrait then diff --git a/tests/neg/specialized-trait-trait-extends-specialized-trait-no-param.scala b/tests/neg/specialized-trait-trait-extends-specialized-trait-no-param.scala new file mode 100644 index 000000000000..9a747d7daf79 --- /dev/null +++ b/tests/neg/specialized-trait-trait-extends-specialized-trait-no-param.scala @@ -0,0 +1,5 @@ +//> using options -language:experimental.specializedTraits +inline trait Foo[T: Specialized] + +trait Bar extends Foo[Int]: // error: Specialized traits may not be extended by ordinary traits. They may only be extended by classes, objects or inline/specialized traits. + def myMethod = "Hello I am a method" diff --git a/tests/pos/specialized-trait-trait-extends-specialized-trait.scala b/tests/neg/specialized-trait-trait-extends-specialized-trait.scala similarity index 63% rename from tests/pos/specialized-trait-trait-extends-specialized-trait.scala rename to tests/neg/specialized-trait-trait-extends-specialized-trait.scala index f69439983ded..246da494f134 100644 --- a/tests/pos/specialized-trait-trait-extends-specialized-trait.scala +++ b/tests/neg/specialized-trait-trait-extends-specialized-trait.scala @@ -2,7 +2,7 @@ inline trait Foo[T: Specialized](x: T): def foo = x -trait Bar extends Foo[Int]: +trait Bar extends Foo[Int]: // error: Specialized traits may not be extended by ordinary traits. They may only be extended by classes, objects or inline/specialized traits. def myMethod = "Hello I am a method" def f(b: Foo[Int]) = println(s"We found the following value of foo ${b.foo}") From 6b7eed9eecce63bd3e00d1d55d779ce6b4cfca66 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 25 May 2026 12:05:08 +0200 Subject: [PATCH 217/254] Fix object class bug manifesting across files --- .../dotty/tools/dotc/transform/DesugarSpecializedTraits.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 86803015e36e..dca99343b765 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -58,7 +58,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: val tm = new TypeMap: // TODO: Can we get this into the specialization ideally. def apply(t: Type) = specialization.specializedTypeParamsToTypeArgumentsMap.view.mapValues(_.tpe).applyOrElse(t, mapOver) // TODO: If we can do just types we can get rid of this - val inheritedParents = specialization.traitSymbol.denot.info.parents.filterNot(_ eq defn.ObjectType).map(tm(_)) + val inheritedParents = specialization.traitSymbol.denot.info.parents.filterNot(_.typeSymbol == defn.ObjectClass).map(tm(_)) // Parents may be specializable and so we need to specialize them as well // See ArrayIterator extends Iterator in specialized-trait-collections-example.scala val specializations1 = inheritedParents.foldLeft(specializations)((specializations, parent) => From a70eda5d6352ab32702a53f74f88e03f5898b041 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 25 May 2026 12:06:35 +0200 Subject: [PATCH 218/254] Fix source file and spans for anonymous class instances --- .../transform/DesugarSpecializedTraits.scala | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index dca99343b765..2c216408346b 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -219,9 +219,9 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: ctx.specializedTraitState.specializedTraitCache.get.getInterfaceSymbol(spec).map: specializedSymbol => if spec.unspecializedTypeArgs.nonEmpty then - AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? + AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs).withSpan(tree.span) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? else - TypeTree(specializedSymbol.typeRef) + TypeTree(specializedSymbol.typeRef).withSpan(tree.span) .getOrElse(tree) case tree => tree } @@ -253,12 +253,14 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: anon.parentCalls match { case (obj :: parentsOfSpecTrait) :+ (app@Apply(_, _)) if (obj.symbol.owner == ctx.definitions.ObjectClass) && (parentsOfSpecTrait.forall(x => spec.traitSymbol.asClass.parentSyms.exists(p => p == x.symbol.owner))) => - - specializations.getImplementationSymbol(spec).map( specializedSymbol => - Typed( - Select(New(ref(specializedSymbol)),anon.ctor).appliedToTypeTrees(spec.unspecializedTypeArgs) - .appliedToArgss(tpd.allArgss(app).tail.nestedMap(_.changeNonLocalOwners(anon.symbol.owner))) // Remove the type params which are not needed + specializations.getImplementationSymbol(spec).map( specializedSymbol => + inContext(ctx.withSource(anon.typeTree.source)) { + Typed( + Select(New(ref(specializedSymbol)), anon.ctor) + .appliedToTypeTrees(spec.unspecializedTypeArgs) + .appliedToArgss(tpd.allArgss(app).tail.nestedMap(_.changeNonLocalOwners(anon.symbol.owner))) // Skip the type params which are not needed , specializeTypeTree(anon.typeTree)) + }.withSpan(anon.typeTree.span) ).getOrElse(tree) // We don't replace non-specialized anonymous class instantiations e.g. new Foo[T] where T is defined in the enclosing scope. case _ => report.error("Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like.", anon.srcPos) From b1b9f399299b818c98ff568bf78b7532035f56e3 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 25 May 2026 12:10:56 +0200 Subject: [PATCH 219/254] Support packages and objects containing specialized traits Add import Add tests for package/object Fix names for canonicalName --- .../transform/DesugarSpecializedTraits.scala | 46 +++++++++++++++---- .../specialized-trait-no-path-dependent.scala | 4 ++ ...it-scoped-inside-object-deep-nesting.scala | 18 ++++++++ ...rait-check-specialized-method-called.scala | 2 +- ...-specialized-trait-check-specialized.scala | 12 ++++- .../A_1.scala | 2 +- ...ized-trait-list-of-specialized-trait.scala | 2 +- ...ized-trait-masked-inline-specialized.scala | 2 +- ...it-multiple-package-same-file-nested.scala | 30 ++++++++++++ ...zed-trait-multiple-package-same-file.scala | 28 +++++++++++ .../A_1.scala | 11 +++++ .../B_2.scala | 11 +++++ ...alized-trait-package-contains-object.scala | 20 ++++++++ tests/run/specialized-trait-package/A_1.scala | 8 ++++ tests/run/specialized-trait-package/A_2.scala | 7 +++ tests/run/specialized-trait-package/B_3.scala | 21 +++++++++ ...ecialized-trait-scoped-inside-object.scala | 2 +- 17 files changed, 211 insertions(+), 15 deletions(-) create mode 100644 tests/neg/specialized-trait-no-path-dependent.scala create mode 100644 tests/neg/specialized-trait-scoped-inside-object-deep-nesting.scala create mode 100644 tests/run/specialized-trait-multiple-package-same-file-nested.scala create mode 100644 tests/run/specialized-trait-multiple-package-same-file.scala create mode 100644 tests/run/specialized-trait-package-contains-object-multi-file/A_1.scala create mode 100644 tests/run/specialized-trait-package-contains-object-multi-file/B_2.scala create mode 100644 tests/run/specialized-trait-package-contains-object.scala create mode 100644 tests/run/specialized-trait-package/A_1.scala create mode 100644 tests/run/specialized-trait-package/A_2.scala create mode 100644 tests/run/specialized-trait-package/B_3.scala rename tests/{pos => run}/specialized-trait-scoped-inside-object.scala (80%) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 2c216408346b..c7670b8c7fcf 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -29,6 +29,7 @@ import dotty.tools.dotc.typer.Synthesizer import dotty.tools.dotc.core.NameKinds import dotty.tools.dotc.core.Flags.GivenOrImplicit import dotty.tools.dotc.core.NameKinds.ContextBoundParamName +import dotty.tools.dotc.core.NameKinds.FlatName import dotty.tools.dotc.inlines.Inlines import dotty.tools.dotc.util.Spans.Span import dotty.tools.dotc.report @@ -74,7 +75,10 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: :: inheritedParents // parents of the original trait in the form Foo[Int] (later specialized to Foo$sp$Int) val traitSymbol = newNormalizedClassSymbol( - specialization.traitSymbol.owner, + specialization.traitSymbol.owner.enclosingPackageClass, // For specialized traits defined inside objects/classes etc, pre-Flatten the $sp$ and $impl$ def trees (i.e. + // make them live in the enclosing package with the flattened name). We do this because it's easier than + // finding the defining tree of the object, which would require scanning the whole file, and it + // might be in another compilation unit / already compiled. DesugarSpecializedTraits.newSpecializedTraitName(specialization), Flags.Synthetic | Flags.Trait | Flags.Inline, parents, @@ -140,7 +144,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: val parents = List(objectParent, traitSpParent, originalTraitSpecializedParent) val newImplementationClassSymbol = newNormalizedClassSymbol( - specialization.traitSymbol.owner, + specialization.traitSymbol.owner.enclosingPackageClass, DesugarSpecializedTraits.newImplementationClassName(specialization), Flags.Synthetic, parents, @@ -396,7 +400,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: override protected def newTransformer(using Context): Transformer = new Transformer: override def transform(tree: Tree)(using Context): Tree = tree match { // TODO: Is Package level processing really what we want? Given we are going to output the classes somewhere else do we not really want either to deepFold the whole tree directly or do a more direct transform? - case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method. + case pkg@PackageDef(pid, stats) => // TODO: If we do everything ourselves and match only on the package then we can get rid of the MacroTransform aspect and just have a Phase with the transformPackageDef method or even transformStats ideally def checkType(t: Type, pos: SrcPos) = t.widen.dealias match { case Specialization.SpecializedEvidence(_) => @@ -415,12 +419,25 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: val (stats1, specializedTraitCache2) = transformStatements(stats, ctx.specializedTraitState.specializedTraitCache.get) ctx.specializedTraitState.specializedTraitCache = Some(specializedTraitCache2) // TODO: Avoid mutation here - we will make the cache mutable instead I think. Makes more sense - cpy.PackageDef(pkg)(pid, stats1) + + val grouped = stats1.groupBy(tree => tree.symbol.enclosingPackageClass) + + tpd.PackageDef(Ident(defn.EmptyPackageVal.namedType), + grouped.getOrElse(defn.RootClass, List()) ::: + grouped.getOrElse(defn.EmptyPackageClass, List()) ::: + grouped.toList.filter((pk, stmts) => pk != defn.RootClass && pk != defn.EmptyPackageClass).map((pkg, stmts) => tpd.PackageDef(Ident(pkg.sourceModule.namedType), stmts)) + ) } private def collectReferencedSpecializations(stats: List[Tree], specializations: SpecializedTraitCache)(using Context): SpecializedTraitCache = stats.foldLeft(specializations)((specializations, tree) => { tree.deepFold(specializations)((specializations, tree) => tree match + case t@TypeDef(name, tmpl: Template) if t.symbol.isSpecializedTrait => + if !t.symbol.isStatic then + // The approach we use for flattening makes this quite tricky: see e.g. tests/neg/specialized-trait-scoped-inside-object-deep-nesting.scala. + // In theory can scan the tree to find where to put the generated traits instead, but this still doesn't work cross-CU, so for now we ban. + report.error("Specialized traits may not be defined inside classes or traits (this would make them path-dependent which is not currently supported); they may be defined inside objects.", t.symbol.srcPos) + specializations case Typed(Apply(Select(New(anon),ctor),List()), t: TypeTree) if anon.symbol.isAnonymousClass => (t.tpe, t.span) match { case Specialization(spec) if spec.isSpecialized => specializations.addInterfaceAndImplementation(spec) @@ -475,15 +492,28 @@ object DesugarSpecializedTraits: val name: String = "desugarSpecializedTraits" val description: String = "Replaces traits having type parameters that have the Specialized annotation with specialized versions" + // TODO: Do we want to compress this more by adopting e.g. specializedTypeNames from scala 2? + // TODO: NameKind? + def canonicalName(tp: Type)(using Context): String = tp.dealias match + case AppliedType(tycon, args) => + canonicalName(tycon) + args.map(canonicalName).mkString("$_$") + case other => + other.typeSymbol.fullName.toString.replace('.', '$') + + // TODO: What happens with this name generation if we have Vec[Vec[T]] for example? We potentially don't have an Ident // TODO: Check what happens here when we have a case where the types being specialized into are user defined instead of primitives or type vars. - private def generateName(specialization: Specialization, suffix: String)(using Context) = // TODO: Probably don't use show - (specialization.traitSymbol.name ++ suffix).asTypeName ++ specialization.specializedTypeArgs.map(t => t.tpe.show).mkString(str.SPECIALIZED_TRAIT_TYPE_SEP) + private def generateName(specialization: Specialization, suffix: String)(using Context) = + val name = (specialization.traitSymbol.name ++ suffix).asTypeName ++ specialization.specializedTypeArgs.map(t => canonicalName(t.tpe)).mkString(str.SPECIALIZED_TRAIT_TYPE_SEP) + if specialization.traitSymbol.owner.is(Flags.Package) then + name + else + FlatName(specialization.traitSymbol.owner.flatName.toTermName, name.toTermName).toSimpleName.toTypeName - private[transform] def newSpecializedTraitName(specialization: Specialization)(using Context): TypeName = + /*private[transform]*/ def newSpecializedTraitName(specialization: Specialization)(using Context): TypeName = generateName(specialization, str.SPECIALIZED_TRAIT_SUFFIX) - private[transform] def newImplementationClassName(specialization: Specialization)(using Context): TypeName = + /*private[transform]*/ def newImplementationClassName(specialization: Specialization)(using Context): TypeName = generateName(specialization, str.SPECIALIZED_TRAIT_IMPL_SUFFIX) end DesugarSpecializedTraits diff --git a/tests/neg/specialized-trait-no-path-dependent.scala b/tests/neg/specialized-trait-no-path-dependent.scala new file mode 100644 index 000000000000..04c2f4f14d87 --- /dev/null +++ b/tests/neg/specialized-trait-no-path-dependent.scala @@ -0,0 +1,4 @@ +//> using options -language:experimental.specializedTraits +trait T1: + inline trait Foo[T: Specialized]: // error: May not define specialized traits inside traits or classes so as to make them path-dependent + def bar = "Bar" diff --git a/tests/neg/specialized-trait-scoped-inside-object-deep-nesting.scala b/tests/neg/specialized-trait-scoped-inside-object-deep-nesting.scala new file mode 100644 index 000000000000..b3dff60d4e4d --- /dev/null +++ b/tests/neg/specialized-trait-scoped-inside-object-deep-nesting.scala @@ -0,0 +1,18 @@ +//> using options -language:experimental.specializedTraits + +package p1: + package p2: + object O1: + class C1: + trait T1: + object O2: + inline trait Foo[T: Specialized]: // error: May not define specialized traits inside traits or classes so as to make them path-dependent + def bar = "Bar" + + class F extends Foo + + val v = O1.C1() + val w = new v.T1 {} + def foo = new w.O2.Foo[Int] {} + +@main def Test = p1.p2.foo.bar diff --git a/tests/run/specialized-trait-check-specialized-method-called.scala b/tests/run/specialized-trait-check-specialized-method-called.scala index a7914494422f..16f2b0714016 100644 --- a/tests/run/specialized-trait-check-specialized-method-called.scala +++ b/tests/run/specialized-trait-check-specialized-method-called.scala @@ -13,7 +13,7 @@ inline trait Foo[T: Specialized](x: T): assert(stackTrace.toList.tail.takeWhile(call => call.getMethodName().startsWith("foo")).length == 1) // We call this method on the correct impl class - assert(Thread.currentThread.getStackTrace()(1).getClassName() == "Foo$impl$Int") + assert(Thread.currentThread.getStackTrace()(1).getClassName() == "Foo$impl$scala$Int") x def f(b: Foo[Int]) = diff --git a/tests/run/specialized-trait-class-extends-specialized-trait-check-specialized.scala b/tests/run/specialized-trait-class-extends-specialized-trait-check-specialized.scala index 618c72859748..bd717c0545f8 100644 --- a/tests/run/specialized-trait-class-extends-specialized-trait-check-specialized.scala +++ b/tests/run/specialized-trait-class-extends-specialized-trait-check-specialized.scala @@ -2,10 +2,18 @@ inline trait Foo[T: Specialized](x: T): def foo = x -class Bar extends Foo(10): +class Bar extends Foo[Int](10): + def myMethod = "Hello I am a method" + +class Bar2 extends Foo(10): def myMethod = "Hello I am a method" @main def Test = + val y = new Foo(10) {} val x = Bar() val traits = classOf[Bar].getInterfaces() - assert(traits.exists(cl => cl.getName() == "Foo$sp$Int")) + + assert(traits.exists(cl => cl.getName() == "Foo$sp$scala$Int")) + + val traits2 = classOf[Bar2].getInterfaces() + assert(traits2.exists(cl => cl.getName() == "Foo$sp$scala$Int")) diff --git a/tests/run/specialized-trait-inline-specialized-instance/A_1.scala b/tests/run/specialized-trait-inline-specialized-instance/A_1.scala index 469c6ff22692..35220628cef2 100644 --- a/tests/run/specialized-trait-inline-specialized-instance/A_1.scala +++ b/tests/run/specialized-trait-inline-specialized-instance/A_1.scala @@ -1,6 +1,6 @@ //> using options -language:experimental.specializedTraits inline trait A[T: Specialized]: - def foo() = assert(Thread.currentThread.getStackTrace()(1).getClassName() == "A$impl$Int") + def foo() = assert(Thread.currentThread.getStackTrace()(1).getClassName() == "A$impl$scala$Int") inline def bar = new A[Int]() {} diff --git a/tests/run/specialized-trait-list-of-specialized-trait.scala b/tests/run/specialized-trait-list-of-specialized-trait.scala index a6d9d964a097..446276c0e8c7 100644 --- a/tests/run/specialized-trait-list-of-specialized-trait.scala +++ b/tests/run/specialized-trait-list-of-specialized-trait.scala @@ -6,4 +6,4 @@ def bar(xs: List[Foo[Int]]) = xs.head @main def Test = val myList = List(new Foo[Int]() {}, new Foo[Int]() {}) - assert(bar(myList).foo == "Foo$impl$Int") + assert(bar(myList).foo == "Foo$impl$scala$Int") diff --git a/tests/run/specialized-trait-masked-inline-specialized.scala b/tests/run/specialized-trait-masked-inline-specialized.scala index fcd03b104b76..7b1b49eadef0 100644 --- a/tests/run/specialized-trait-masked-inline-specialized.scala +++ b/tests/run/specialized-trait-masked-inline-specialized.scala @@ -1,7 +1,7 @@ //> using options -language:experimental.specializedTraits inline trait A[T: Specialized]: - def foo() = assert(Thread.currentThread.getStackTrace()(1).getClassName() == "A$impl$Int") + def foo() = assert(Thread.currentThread.getStackTrace()(1).getClassName() == "A$impl$scala$Int") inline def myMethod1 = new A[Int]() {} inline def myMethod2 = myMethod1 diff --git a/tests/run/specialized-trait-multiple-package-same-file-nested.scala b/tests/run/specialized-trait-multiple-package-same-file-nested.scala new file mode 100644 index 000000000000..d44ba22053cc --- /dev/null +++ b/tests/run/specialized-trait-multiple-package-same-file-nested.scala @@ -0,0 +1,30 @@ +//> using options -language:experimental.specializedTraits +package owner { + package package1 { + inline trait A[T: Specialized]: + def foo(x: T) = "Package 1!" + def bar = Thread.currentThread.getStackTrace()(1).getClassName() + class B extends A[Int] + class C extends A[String] + } + + package package2 { + inline trait A[T: Specialized]: + def foo(x: T) = "Package 2!" + def bar = Thread.currentThread.getStackTrace()(1).getClassName() + class B extends A[Int] + class C extends A[String] + } +} + +@main def Test = + val b = owner.package1.B() + val c = owner.package2.C() + assert(b.foo(10) == "Package 1!") + assert(c.foo("Hello World") == "Package 2!") + + val d = new owner.package1.A[Int]() {} + val e = new owner.package2.A[Int]() {} + assert(d.bar == "owner.package1.A$impl$scala$Int") + assert(e.bar == "owner.package2.A$impl$scala$Int") + diff --git a/tests/run/specialized-trait-multiple-package-same-file.scala b/tests/run/specialized-trait-multiple-package-same-file.scala new file mode 100644 index 000000000000..be6b83aa5465 --- /dev/null +++ b/tests/run/specialized-trait-multiple-package-same-file.scala @@ -0,0 +1,28 @@ +//> using options -language:experimental.specializedTraits + +package package1 { + inline trait A[T: Specialized]: + def foo(x: T) = "Package 1!" + def bar = Thread.currentThread.getStackTrace()(1).getClassName() + class B extends A[Int] + class C extends A[String] +} + +package package2 { + inline trait A[T: Specialized]: + def foo(x: T) = "Package 2!" + def bar = Thread.currentThread.getStackTrace()(1).getClassName() + class B extends A[Int] + class C extends A[String] +} + +@main def Test = + val b = package1.B() + val c = package2.C() + assert(b.foo(10) == "Package 1!") + assert(c.foo("Hello World") == "Package 2!") + + val d = new package1.A[Int]() {} + val e = new package2.A[Int]() {} + assert(d.bar == "package1.A$impl$scala$Int") + assert(e.bar == "package2.A$impl$scala$Int") diff --git a/tests/run/specialized-trait-package-contains-object-multi-file/A_1.scala b/tests/run/specialized-trait-package-contains-object-multi-file/A_1.scala new file mode 100644 index 000000000000..0810149ba75e --- /dev/null +++ b/tests/run/specialized-trait-package-contains-object-multi-file/A_1.scala @@ -0,0 +1,11 @@ +//> using options -language:experimental.specializedTraits +package package1 { + object Outer: + object Inner: + inline trait A[T: Specialized]: + def foo(x: T) = "Package 1!" + def bar = Thread.currentThread.getStackTrace()(1).getClassName() + class B extends Outer.Inner.A[Int] + class C extends Outer.Inner.A[String] +} + diff --git a/tests/run/specialized-trait-package-contains-object-multi-file/B_2.scala b/tests/run/specialized-trait-package-contains-object-multi-file/B_2.scala new file mode 100644 index 000000000000..b7984b4d8708 --- /dev/null +++ b/tests/run/specialized-trait-package-contains-object-multi-file/B_2.scala @@ -0,0 +1,11 @@ +//> using options -language:experimental.specializedTraits + +@main def Test = + val b = package1.B() + val c = package1.C() + assert(b.foo(10) == "Package 1!") + assert(c.foo("Hello World") == "Package 1!") + + val d = new package1.Outer.Inner.A[Int]() {} + println(d.bar) + assert(d.bar == "package1.Outer$Inner$$A$impl$scala$Int") diff --git a/tests/run/specialized-trait-package-contains-object.scala b/tests/run/specialized-trait-package-contains-object.scala new file mode 100644 index 000000000000..0489a1e9baa8 --- /dev/null +++ b/tests/run/specialized-trait-package-contains-object.scala @@ -0,0 +1,20 @@ +//> using options -language:experimental.specializedTraits +package package1 { + object Outer: + object Inner: + inline trait A[T: Specialized]: + def foo(x: T) = "Package 1!" + def bar = Thread.currentThread.getStackTrace()(1).getClassName() + class B extends Outer.Inner.A[Int] + class C extends Outer.Inner.A[String] +} + +@main def Test = + val b = package1.B() + val c = package1.C() + assert(b.foo(10) == "Package 1!") + assert(c.foo("Hello World") == "Package 1!") + + val d = new package1.Outer.Inner.A[Int]() {} + assert(d.bar == "package1.Outer$Inner$$A$impl$scala$Int") + diff --git a/tests/run/specialized-trait-package/A_1.scala b/tests/run/specialized-trait-package/A_1.scala new file mode 100644 index 000000000000..57eecffa6c17 --- /dev/null +++ b/tests/run/specialized-trait-package/A_1.scala @@ -0,0 +1,8 @@ +//> using options -language:experimental.specializedTraits + +package package1 + +inline trait A[T: Specialized]: + def foo(x: T) = "Package 1!" + def bar = Thread.currentThread.getStackTrace()(1).getClassName() + \ No newline at end of file diff --git a/tests/run/specialized-trait-package/A_2.scala b/tests/run/specialized-trait-package/A_2.scala new file mode 100644 index 000000000000..268ee13bffe8 --- /dev/null +++ b/tests/run/specialized-trait-package/A_2.scala @@ -0,0 +1,7 @@ +//> using options -language:experimental.specializedTraits + +package package2 + +inline trait A[T: Specialized]: + def foo(x: T) = "Package 2!" + def bar = Thread.currentThread.getStackTrace()(1).getClassName() diff --git a/tests/run/specialized-trait-package/B_3.scala b/tests/run/specialized-trait-package/B_3.scala new file mode 100644 index 000000000000..93a5c9ba801b --- /dev/null +++ b/tests/run/specialized-trait-package/B_3.scala @@ -0,0 +1,21 @@ +//> using options -language:experimental.specializedTraits +class B extends package1.A[Int] +class C extends package2.A[Int] + +@main def Test = + val b = B() + val c = C() + assert(b.foo(10) == "Package 1!") + assert(c.foo(11) == "Package 2!") + + val d = new package1.A[Int]() {} + val e = new package2.A[Int]() {} + assert(d.bar == "package1.A$impl$scala$Int") + assert(e.bar == "package2.A$impl$scala$Int") + + import package1.A + import package2.A as A2 + val f = new A[Int]() {} + val g = new A2[Int]() {} + assert(d.bar == "package1.A$impl$scala$Int") + assert(e.bar == "package2.A$impl$scala$Int") diff --git a/tests/pos/specialized-trait-scoped-inside-object.scala b/tests/run/specialized-trait-scoped-inside-object.scala similarity index 80% rename from tests/pos/specialized-trait-scoped-inside-object.scala rename to tests/run/specialized-trait-scoped-inside-object.scala index 589b08a89154..ec14264a72a7 100644 --- a/tests/pos/specialized-trait-scoped-inside-object.scala +++ b/tests/run/specialized-trait-scoped-inside-object.scala @@ -6,4 +6,4 @@ object MySpecializedStuff: def foo = new Foo[Int] {} -@main def main = MySpecializedStuff.foo.bar +@main def Test = MySpecializedStuff.foo.bar From fd64930a0a211b9fec6fadf0bcecb34ff4fda06d Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 25 May 2026 12:19:43 +0200 Subject: [PATCH 220/254] Remove nasty gating and allow specializeInlineTraits to inline into Bar extends Foo[spec] --- compiler/src/dotty/tools/dotc/Compiler.scala | 4 +-- .../dotty/tools/dotc/inlines/Inlines.scala | 33 ++++++++++++------- .../transform/DesugarSpecializedTraits.scala | 31 ++++++----------- docs/_docs/internals/specialized-traits.md | 2 +- 4 files changed, 34 insertions(+), 36 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 4408998f5212..f18206949917 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -43,8 +43,8 @@ class Compiler { List(new UnrollDefinitions) :: // Unroll annotated methods if detected in PostTyper List(new sjs.PrepJSInterop) :: // Additional checks and transformations for Scala.js (Scala.js only) List(new SetRootTree) :: // Set the `rootTreeOrProvider` on class symbols - List(new SpecializeInlineTraits) :: // Inline the code of inline traits into their children - List(new DesugarSpecializedTraits) :: // Process the Specialized annotation + List(new DesugarSpecializedTraits) :: // Process Specialized traits + List(new SpecializeInlineTraits) :: // Inline the code of inline traits into their children Nil /** Phases dealing with TASTY tree pickling and unpickling */ diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index fb0d9150140c..eb041fc99f48 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -86,14 +86,14 @@ object Inlines: // && !member.symbol.is(Deferred) // Also inline interfaces (see specialized-trait-collections-example.scala) /** Should call be inlined in this context? */ - def needsInlining(tree: Tree, allowSpecializedTraits: Boolean = false)(using Context): Boolean = + def needsInlining(tree: Tree)(using Context): Boolean = def isInlineableInCtx = StagingLevel.level == 0 && ( ctx.phase == Phases.inliningPhase || (ctx.phase == Phases.typerPhase && needsTransparentInlining(tree)) - || (ctx.phase == Phases.specializeInlineTraitsPhase && !tree.symbol.is(Macro)) - || (ctx.phase == Phases.desugarSpecializedTraitsPhase && !tree.symbol.is(Macro)) + || (ctx.phase == Phases.specializeInlineTraitsPhase && !tree.symbol.is(Macro) && !(tree.symbol.isSpecializedTraitImplementationClass || tree.symbol.isSpecializedTraitInterface)) + || (ctx.phase == Phases.desugarSpecializedTraitsPhase && !tree.symbol.is(Macro) && (tree.symbol.isSpecializedTraitImplementationClass || tree.symbol.isSpecializedTraitInterface) ) ) && !ctx.typer.hasInliningErrors && !ctx.base.stopInlining @@ -101,10 +101,10 @@ object Inlines: tree match case Block(_, expr) => - needsInlining(expr, allowSpecializedTraits) + needsInlining(expr) case tdef @ TypeDef(_, impl: Template) => // !tdef.symbol.isInlineTrait && - impl.parents.map(symbolFromParent).exists(sym => sym.isInlineTrait && (allowSpecializedTraits || !sym.isSpecializedTrait)) && isInlineableInCtx + impl.parents.map(symbolFromParent).exists(sym => sym.isInlineTrait) && isInlineableInCtx case _ => def isUnapplyExpressionWithDummy: Boolean = // The first step of typing an `unapply` consists in typing the call @@ -120,12 +120,12 @@ object Inlines: private[dotc] def symbolFromParent(parent: Tree)(using Context): Symbol = if parent.symbol.isConstructor then parent.symbol.owner else parent.tpe.typeSymbol - private def inlineTraitAncestors(cls: TypeDef, allowSpecialized: Boolean, allowNonSpecialized: Boolean)(using Context): List[Tree] = cls match { + // Inline trait ancestors in linearization order. + private def inlineTraitAncestors(cls: TypeDef)(using Context): List[Tree] = cls match { case tpd.TypeDef(_, tmpl: Template) => val parentTrees: Map[Symbol, Tree] = tmpl.parents.map(par => symbolFromParent(par) -> par).toMap.filter(_._1.isInlineTrait) - val ancestors: List[ClassSymbol] = cls.tpe.baseClasses.filter(sym => sym != cls.symbol && // TODO: Do we not need to stop if there is a non-inline trait somewhere in the hierarchy? It should block the inlining right? - ((sym.isInlineTrait && !sym.isSpecializedTrait && allowNonSpecialized) || - (sym.isInlineTrait && sym.isSpecializedTrait && allowSpecialized)) + val ancestors: List[ClassSymbol] = cls.tpe.baseClasses.filter(sym => sym != cls.symbol && sym.isInlineTrait // && // TODO: Do we not need to stop if there is a non-inline trait somewhere in the hierarchy? It should block the inlining right? + && !(cls.symbol.asClass.ownersIterator.toList.tail.exists(p => p.isInlineTrait)) // We can skip anything that would be inlined nested into an inline trait because it must be pruned out later ) ancestors.flatMap(ancestor => def baseTree = @@ -138,7 +138,16 @@ object Inlines: report.error(s"unknown base type ${baseTpe.show} for ancestor ${ancestor.show} of ${cls.symbol.show}") None parentTrees.get(ancestor).orElse(baseTree.map(_.withSpan(cls.span))) - ) + ).flatMap { tree => + tree.tpe match { + case Specialization(spec) if spec.hasSpecializedParams && !spec.isFullySpecialized => None // these can only exist in cases where we don't want to inline because: + // 1) they will be pruned out later anyway and if we inline them we will create a loop (as in tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad.scala) + // 2) they are covered by another specialized type parameter in inlining and therefore we will specialize later when the inline function is called + // tests/run/specialized-trait-inline-specialized-instance-with-specialization + // 3) we already dealt with them in desugarSpecializedTraits + case other => Some(tree) + } + } case _ => Nil } @@ -336,7 +345,7 @@ object Inlines: ) OverridingPairsChecker(clsSym, clsSym.thisType).checkAll(checkInlineTraitOverride) - def inlineParentInlineTraits(cls: Tree, allowSpecialized: Boolean=false, allowNonSpecialized: Boolean=true)(using Context): Tree = + def inlineParentInlineTraits(cls: Tree)(using Context): Tree = cls match { // case cls @ tpd.TypeDef(_, impl: Template) if cls.symbol.owner.ownersIterator.exists(_.isInlineTrait) => // TODO: We can relax this if we use a seen list to avoid cycles // report.error("May not inline an inline trait into a class defined inside another inline trait. If you really need to do this, make the inline trait Specialized or move the class definition outside the trait.", cls.srcPos) @@ -344,7 +353,7 @@ object Inlines: case cls @ tpd.TypeDef(_, impl: Template) => checkInlineTraitOverrides(cls.symbol.asClass) val clsOverriddenSyms = cls.symbol.info.decls.toList.flatMap(_.allOverriddenSymbols).toSet - val ancestors = inlineTraitAncestors(cls, allowSpecialized, allowNonSpecialized) + val ancestors = inlineTraitAncestors(cls) val cycleFound = ancestors.exists { parent => if cls.symbol.ownersIterator.contains(symbolFromParent(parent)) then // TODO: This appears at the inline trait D line rather than the line corresponding to the inlining - should we be worried ? diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index c7670b8c7fcf..4a5c9f8203b0 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -339,29 +339,19 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: } /* We need to inline recursively throughout generated specialized traits - see tests/run/specialized-trait-requires-inline-trait-inlining.scala */ - // TODO: How do we calculate the spans correctly? - val inlineInlineTraits = new TreeTypeMap(treeMap = (tree: Tree) => tree match { - case tree: TypeDef if tree.symbol.isInlineTrait => - val tree1 = Inlines.checkAndTransformInlineTrait(tree) - val tree2 = if Inlines.needsInlining(tree1) then Inlines.inlineParentInlineTraits(tree1) else tree1 - tree2 - case tree: TypeDef if Inlines.needsInlining(tree) => - Inlines.inlineParentInlineTraits(tree) - case t => t - }) val generatedTraitStats1 = generatedTraitStats.map { case tree: TypeDef => assert(tree.symbol.isInlineTrait) - val inlined = Inlines.inlineParentInlineTraits(Inlines.checkAndTransformInlineTrait(tree),allowSpecialized=true).asInstanceOf[TypeDef] - cpy.TypeDef(inlined)(name = inlined.name, rhs = inlineInlineTraits(inlined.rhs)).withSpan(inlined.span) + val inlined = Inlines.inlineParentInlineTraits(Inlines.checkAndTransformInlineTrait(tree)).asInstanceOf[TypeDef] + cpy.TypeDef(inlined)(name = inlined.name, rhs = inlined.rhs).withSpan(inlined.span) } val generatedClassStats1 = generatedClassStats.map { case tree: TypeDef => - assert(Inlines.needsInlining(tree, allowSpecializedTraits=true)) - val inlined = Inlines.inlineParentInlineTraits(tree, allowSpecialized=true).asInstanceOf[TypeDef] - cpy.TypeDef(inlined)(name = inlined.name, rhs = inlineInlineTraits(inlined.rhs)).withSpan(inlined.span) + assert(Inlines.needsInlining(tree)) + val inlined = Inlines.inlineParentInlineTraits(tree).asInstanceOf[TypeDef] + cpy.TypeDef(inlined)(name = inlined.name, rhs = inlined.rhs).withSpan(inlined.span) } val (generatedTraitStatsFinal, generatedClassStatsFinal, specializationsFinal) = @@ -387,12 +377,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: val statsFinal = generatedTraitStatsFinal ++ generatedClassStatsFinal ++ - stats.map(stat => replaceImplementationClassesMap(specializationsFinal)( - if (!stat.symbol.isSpecializedTraitImplementationClass && !stat.symbol.isSpecializedTraitInterface) then // We already processed these in an earlier recursive call - Inlines.inlineParentInlineTraits(stat, allowSpecialized = true, allowNonSpecialized = false) // Perform inlining into class Bar extends Foo[Int] from user code. // TODO: I don't really like this gating. - else - stat - )) + stats.map(stat => replaceImplementationClassesMap(specializationsFinal)(stat)) (statsFinal.map(removeRedundantOverridesMap(_)), specializationsFinal) } @@ -622,6 +607,10 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree], val (part.typeSymbol eq defn.ObjectClass) || (part.typeSymbol eq defn.AnyValClass))) + // Only works before erasure. + def isFullySpecialized: Boolean = + !specializedTypeArgs.exists(_.tpe.existsPart(part => (part.typeSymbol.isTypeParam))) + // Note: We only care about the specialized arguments for equality; a specialization of Vec[A: Specialized, B] with B = Int and one // with B = String can be considered to be the same as they use the same specialized trait // TODO: I don't really like this logic being in Specialization because they are really different diff --git a/docs/_docs/internals/specialized-traits.md b/docs/_docs/internals/specialized-traits.md index 6a43f941f512..b48c99967644 100644 --- a/docs/_docs/internals/specialized-traits.md +++ b/docs/_docs/internals/specialized-traits.md @@ -273,7 +273,7 @@ it is possible to create arbitrarily long chains requiring alternating between s this problem arises not only with the `$sp$` traits, but also the `$impl$` classes (see `specialized-trait-inlining-causes-implementation-required.scala`). To resolve this problem without alternating between and looping the `specializeInlineTraits` and `desugarSpecializedTraits` phases in an inconvenient way, we opt to make: -- `specializeInlineTraits` responsible for inlining inline traits written directly in user code. +- `specializeInlineTraits` responsible for inlining inline traits written directly in user code. If a specialized trait creates an inline trait inlining opportunity which is not specialized, this is dealt with by specializeInlineTraits. Further if a user writes `class Bar extends Foo[Int]` where Foo is declared Specialized, `specializeInlineTraits` will do the inlining. - `desugarSpecializedTraits` responsible for finding specializations and generating the required `$sp$` traits and `$impl$` classes, inlining the parent specialized traits into these classes, and repeating until no more inlining can be performed and no more `$sp$` traits and `$impl$` classes are needed. This phase also performs replacement of e.g. `Vec[Int]` with `Vec$sp$Int` and `new Vec[Int]` with `new Vec$impl$Int`. - `pruneInlineTraits` and `replaceInlinedTraitSymbols` responsible respectively for converting inline traits to pure interfaces, and for replacing members accessed on inline receivers with the corresponding inlined symbols. This is *whether the inline traits in question come from inline traits in source code or specialized trait expansion, in both cases.* (see the document on inline traits for a more detailed description of these phases). From b7cc8994d0e6e96187a2d86fc43dc332c96c566b Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 25 May 2026 12:26:54 +0200 Subject: [PATCH 221/254] Redo inline trait cycle check detection - catch previously missing cases and don't block if only an intermediate state Fix error message in inline-trait docs for inlining looping --- .../dotty/tools/dotc/inlines/Inlines.scala | 31 ++++++++++++++----- docs/_docs/internals/inline-traits.md | 6 ++-- ...trait-infinite-inline-triangle-cycle.scala | 4 ++- ...ne-trait-self-inline-anonymous-class.scala | 4 ++- ...inline-trait-self-inline-three-cycle.scala | 13 +++++--- .../inline-trait-self-inline-two-cycle.scala | 7 +++-- ...nline-trait-self-specialization-loop.scala | 10 ++++++ ...lementation-required-loop-bad-manual.scala | 23 ++++++++++++++ ...e-trait-self-inline-two-cycle-no-use.scala | 15 +++++++++ ...on-required-loop-bad-manual-no-usage.scala | 12 +++++++ ...lementation-required-loop-bad-manual.scala | 19 ------------ ...red-loop-without-implementation-fine.scala | 15 +++++++++ 12 files changed, 121 insertions(+), 38 deletions(-) create mode 100644 tests/neg/inline-trait-self-specialization-loop.scala create mode 100644 tests/neg/specialized-trait-inlining-causes-implementation-required-loop-bad-manual.scala create mode 100644 tests/pos/inline-trait-self-inline-two-cycle-no-use.scala create mode 100644 tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad-manual-no-usage.scala delete mode 100644 tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad-manual.scala create mode 100644 tests/pos/specialized-trait-inlining-causes-implementation-required-loop-without-implementation-fine.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index eb041fc99f48..880f10317536 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -347,18 +347,22 @@ object Inlines: def inlineParentInlineTraits(cls: Tree)(using Context): Tree = cls match { - // case cls @ tpd.TypeDef(_, impl: Template) if cls.symbol.owner.ownersIterator.exists(_.isInlineTrait) => // TODO: We can relax this if we use a seen list to avoid cycles - // report.error("May not inline an inline trait into a class defined inside another inline trait. If you really need to do this, make the inline trait Specialized or move the class definition outside the trait.", cls.srcPos) - // cls case cls @ tpd.TypeDef(_, impl: Template) => checkInlineTraitOverrides(cls.symbol.asClass) val clsOverriddenSyms = cls.symbol.info.decls.toList.flatMap(_.allOverriddenSymbols).toSet val ancestors = inlineTraitAncestors(cls) val cycleFound = ancestors.exists { parent => - if cls.symbol.ownersIterator.contains(symbolFromParent(parent)) then - // TODO: This appears at the inline trait D line rather than the line corresponding to the inlining - should we be worried ? - report.error("Inlining of inline traits looped, which will create an infinitely long program. This is not allowed.", cls.sourcePos) - cls.symbol.ownersIterator.contains(symbolFromParent(parent)) + val parentSym = symbolFromParent(parent) + val errorPos = if cls.symbol.ownersIterator.contains(parentSym) then Some(cls.srcPos) // Trying to inline into the tree which defines parentSym (need to catch this separately + // as need to catch it before we inline the second time to avoid tripping an assertion) + else if ctx.inlineTraitState.inlineOrigins(cls.symbol).contains(parentSym) then + val userPos = tpd.enclosingInlineds.last.srcPos // Select the user code that caused this error so we get two errors if there are two problematic inlines, not one + Some(userPos) // Trying to inline into the inlined body of parentSym not in the defn tree + else None // Fine + errorPos.foreach(pos => + report.error(s"Inlining of inline traits looped. Tried to inline ${parentSym} into its own body.", pos) + ) + errorPos.nonEmpty } if cycleFound then cls @@ -842,6 +846,7 @@ object Inlines: private val parentSym = symbolFromParent(parent) private val paramAccessorsMapper = ParamAccessorsMapper() + private val child = ctx.owner private val childThisType = ctx.owner.thisType private val childThisTree = This(ctx.owner.asClass).withSpan(parent.span) @@ -1058,7 +1063,7 @@ object Inlines: val symbolMap = mutable.Map[Symbol, Symbol]() // TODO make version of inlined that does not return bindings? - val rhs1 = Inlined(tpd.ref(parentSym).withSpan(parentSym.span), Nil, inlined(rhs)._2.withSpan(parent.span).cloneIn(parentSym.source)).withSpan(parent.span) // TODO: This inlines also calls to inline defs that were made in the inline trait body, is that desirable? + val rhs1 = Inlined(tpd.ref(parentSym).withSpan(parent.span), Nil, inlined(rhs)._2.withSpan(parent.span).cloneIn(parentSym.source)).withSpan(parent.span) // TODO: This inlines also calls to inline defs that were made in the inline trait body, is that desirable? // In case of nested inline trait inlines, because BodyAnnotation is out of date, // body inlined misses nested expansion, but we have the symbols for the items that should be there @@ -1097,6 +1102,9 @@ object Inlines: val ctor = tpd.DefDef(newConstructorSymbol.asTerm, rhsFun) symbolMap(tree.symbol) = newSym + + ctx.inlineTraitState.registerInlineOrigin(newSym, child, parentSym) + tpd.ClassDefWithParents(newSym.asClass, ctor, tmpl1.parents, tmpl1.body) case tree => tree }) @@ -1148,6 +1156,10 @@ object Inlines: // The map has (foo#1000, trait B) => foo#2000 val inlinedTraitSymbols = mutable.HashMap[(Symbol, Symbol), Symbol]() + // For a class symbol created during inlining of an inline trait, + // the chain of inlined traits which produced it. We don't actually care about the order. + val inlineOrigins = mutable.HashMap[Symbol, Set[Symbol]]().withDefaultValue(Set.empty) + // Record that we just inlined oldSym into childClasslike which created // childClassLike.newSym def registerInlinedSymbol(oldSym: Symbol, newSym: Symbol, childClasslike: Symbol) = @@ -1160,6 +1172,9 @@ object Inlines: // Check if oldSym has been inlined into childClasslike def inlinedSymbolIsRegistered(oldSym: Symbol, childClasslike: Symbol) = inlinedTraitSymbols.contains((oldSym, childClasslike)) + + def registerInlineOrigin(newSym: Symbol, owner: Symbol, parentSym: Symbol): Unit = + inlineOrigins(newSym) = inlineOrigins(owner) + parentSym end InlineTraitState end Inlines diff --git a/docs/_docs/internals/inline-traits.md b/docs/_docs/internals/inline-traits.md index 3b912715cb0d..3371491c6363 100644 --- a/docs/_docs/internals/inline-traits.md +++ b/docs/_docs/internals/inline-traits.md @@ -335,17 +335,19 @@ Local private members (members with the same access patterns as the former `priv [4] As long as this doesn't create a cycle e.g.: ```scala -inline trait C[S]: // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. +inline trait C[S]: def v(x: S): S = x def w: Unit = val x = new D[S] {} println("w") -inline trait D[S]: // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. +inline trait D[S]: def v(x: S): S = x def w: Unit = val x = new C[S] {} println("w") + +class T extends D // error: Inlining of inline traits looped. Tried to inline trait D into its own body. ``` [5] Supported with same behaviour as in normal traits. In particular, the following is completely fine, and will be inlined into B. diff --git a/tests/neg/inline-trait-infinite-inline-triangle-cycle.scala b/tests/neg/inline-trait-infinite-inline-triangle-cycle.scala index a5bfb9dd8c70..2f506c96871f 100644 --- a/tests/neg/inline-trait-infinite-inline-triangle-cycle.scala +++ b/tests/neg/inline-trait-infinite-inline-triangle-cycle.scala @@ -1,7 +1,9 @@ inline trait C[S]: def v(x: S): S = x def w: Unit = - val x = new D[S] {} // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. + val x = new D[S] {} println("w") inline trait D[S] extends C[S] + +class Cl[T] extends D[T] // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. diff --git a/tests/neg/inline-trait-self-inline-anonymous-class.scala b/tests/neg/inline-trait-self-inline-anonymous-class.scala index 784ceaab9dec..b916ac476992 100644 --- a/tests/neg/inline-trait-self-inline-anonymous-class.scala +++ b/tests/neg/inline-trait-self-inline-anonymous-class.scala @@ -1,5 +1,7 @@ inline trait C[S]: def v(x: S): S = x def w: Unit = - val x = new C[S] {} // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. + val x = new C[S] {} println("w") + +class T extends C[Int] // error: Inlining of inline traits looped. diff --git a/tests/neg/inline-trait-self-inline-three-cycle.scala b/tests/neg/inline-trait-self-inline-three-cycle.scala index 551333cd3abf..4f54945e81ec 100644 --- a/tests/neg/inline-trait-self-inline-three-cycle.scala +++ b/tests/neg/inline-trait-self-inline-three-cycle.scala @@ -1,17 +1,20 @@ -inline trait C[S]: // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. +inline trait C[S]: def v(x: S): S = x def w: Unit = - val x = new D[S] {} + val x = new D[S] {} println("w") -inline trait D[S]: // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. +inline trait D[S]: def v(x: S): S = x def w: Unit = - val x = new E[S] {} + val x = new E[S] {} println("w") -inline trait E[S]: // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. +inline trait E[S]: def v(x: S): S = x def w: Unit = val x = new C[S] {} println("w") + +def main = + val x = new C[Int] {} // error: Inlining of inline traits looped. diff --git a/tests/neg/inline-trait-self-inline-two-cycle.scala b/tests/neg/inline-trait-self-inline-two-cycle.scala index a638da31bc01..98da6c2455a1 100644 --- a/tests/neg/inline-trait-self-inline-two-cycle.scala +++ b/tests/neg/inline-trait-self-inline-two-cycle.scala @@ -1,11 +1,14 @@ -inline trait C[S]: // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. +inline trait C[S]: def v(x: S): S = x def w: Unit = val x = new D[S] {} println("w") -inline trait D[S]: // error: Inlining of inline traits looped, which will create an infinitely long program. This is not allowed. +inline trait D[S]: def v(x: S): S = x def w: Unit = val x = new C[S] {} println("w") + +def main = + val x = new C[Int] {} // error: Inlining of inline traits looped. diff --git a/tests/neg/inline-trait-self-specialization-loop.scala b/tests/neg/inline-trait-self-specialization-loop.scala new file mode 100644 index 000000000000..6c986eb241dc --- /dev/null +++ b/tests/neg/inline-trait-self-specialization-loop.scala @@ -0,0 +1,10 @@ +//> using options -language:experimental.specializedTraits + +inline trait C[S]: + def v(x: S): S = x + def w: Unit = + val x = new C[Int] {} + println("w") + +def main = + val y = new C[Int] {} // error: Inlining of inline traits loops which would create an infinitely long program. This is not allowed. diff --git a/tests/neg/specialized-trait-inlining-causes-implementation-required-loop-bad-manual.scala b/tests/neg/specialized-trait-inlining-causes-implementation-required-loop-bad-manual.scala new file mode 100644 index 000000000000..5201e6a602e3 --- /dev/null +++ b/tests/neg/specialized-trait-inlining-causes-implementation-required-loop-bad-manual.scala @@ -0,0 +1,23 @@ +//> using options -language:experimental.specializedTraits + +// Contrast with specialized-trait-inlining-causes-implementation-required-loop-bad.scala. +// and specialized-trait-inlining-causes-implementation-required-loop-without-implementation-fine.scala +// This one is not allowed because it will loop forever when specializing. The $impl$ class will +// get a loop when the body of w is inlined. + +// A[Char] => A$sp$Char, but only once we inline the body of A$sp$Char do we realise that we need +// C$impl$Char as well. + +inline trait C[S: Specialized]: + def v(x: S): S = x + def w: Unit = + class D extends C[S] + println("w") + +inline trait A[T: Specialized]: + def x(y: C[T]): Unit = + val x = new C[T]() {} // error: Inlining of inline traits looped + println("x") + +def main = + val y = new A[Char] {} diff --git a/tests/pos/inline-trait-self-inline-two-cycle-no-use.scala b/tests/pos/inline-trait-self-inline-two-cycle-no-use.scala new file mode 100644 index 000000000000..6185497a39de --- /dev/null +++ b/tests/pos/inline-trait-self-inline-two-cycle-no-use.scala @@ -0,0 +1,15 @@ +// We throw the error only when someone tries to use the traits, because +// the theoretical infinite inlining is only an intermediate state before pruning anyway. +// At the end these traits can be pruned. + +inline trait C[S]: + def v(x: S): S = x + def w: Unit = + val x = new D[S] {} + println("w") + +inline trait D[S]: + def v(x: S): S = x + def w: Unit = + val x = new C[S] {} + println("w") diff --git a/tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad-manual-no-usage.scala b/tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad-manual-no-usage.scala new file mode 100644 index 000000000000..87948cffb8c9 --- /dev/null +++ b/tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad-manual-no-usage.scala @@ -0,0 +1,12 @@ +//> using options -language:experimental.specializedTraits + +// No usage of the spec traits so this is fine. + +inline trait C[S: Specialized]: + def v(x: S): S = x + def w: Unit = + class D extends C[S] + println("w") + +inline trait A[T: Specialized]: + def x(y: C[T]): Unit = println("x") diff --git a/tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad-manual.scala b/tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad-manual.scala deleted file mode 100644 index 9c7f271f47fa..000000000000 --- a/tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad-manual.scala +++ /dev/null @@ -1,19 +0,0 @@ -//> using options -language:experimental.specializedTraits - -// Contrast with tests/neg/specialized-trait-inlining-causes-implementation-required-loop-bad.scala. -// This one is not allowed because it will loop forever when specializing. - -// A[Char] => A$sp$Char => C$sp$Char, but only once we inline the body of C$sp$Char do we realise that we need -// C$impl$Char as well. - -inline trait C[S: Specialized]: - def v(x: S): S = x - def w: Unit = - class D extends C[S] // ok: This one is actually also fine because we can make D extend C$sp$Char once we've inlined it into C$impl$Int - println("w") - -inline trait A[T: Specialized]: - def x(y: C[T]): Unit = println("x") - -def main = - val y = new A[Char] {} diff --git a/tests/pos/specialized-trait-inlining-causes-implementation-required-loop-without-implementation-fine.scala b/tests/pos/specialized-trait-inlining-causes-implementation-required-loop-without-implementation-fine.scala new file mode 100644 index 000000000000..61c4e83cba56 --- /dev/null +++ b/tests/pos/specialized-trait-inlining-causes-implementation-required-loop-without-implementation-fine.scala @@ -0,0 +1,15 @@ +//> using options -language:experimental.specializedTraits + +// This one is fine because we only ask for an $sp$ trait interface in C and not an $impl$ class + +inline trait C[S: Specialized]: + def v(x: S): S = x + def w: Unit = + class D extends C[S] + println("w") + +inline trait A[T: Specialized]: + def x(y: C[T]): Unit = println("x") + +def main = + val y = new A[Char] {} From d2b0deead8fa90da923c769f8ebcf420a542a1cd Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 25 May 2026 12:31:09 +0200 Subject: [PATCH 222/254] Fix inline trait accesses parent val param --- compiler/src/dotty/tools/dotc/inlines/Inlines.scala | 6 ++++-- tests/pos/inline-trait-accesses-parent-val-param.scala | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 tests/pos/inline-trait-accesses-parent-val-param.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 880f10317536..96a3804222ef 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -373,7 +373,10 @@ object Inlines: val parentTraitInliner = InlineParentTrait(parent) // Inline body val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) - val inlinedDefs1 = inlineDefs ::: parentTraitInliner.expandDefs(overriddenSymbols) + // Need to put the new defs first because we process in linearization order to make overridees correct, + // but we want parent definitions to come first so that if child inline traits refer to values defined in a parent + // inline trait these are defined. + val inlinedDefs1 = parentTraitInliner.expandDefs(overriddenSymbols) ::: inlineDefs cls.symbol.flags = updateFlagsFromInlinedParent(cls.symbol.flags, parent.symbol.flags) val childDefs1 = parentTraitInliner.adaptSuperCalls(childDefs) @@ -1060,7 +1063,6 @@ object Inlines: if rhs.isEmpty then rhs else - val symbolMap = mutable.Map[Symbol, Symbol]() // TODO make version of inlined that does not return bindings? val rhs1 = Inlined(tpd.ref(parentSym).withSpan(parent.span), Nil, inlined(rhs)._2.withSpan(parent.span).cloneIn(parentSym.source)).withSpan(parent.span) // TODO: This inlines also calls to inline defs that were made in the inline trait body, is that desirable? diff --git a/tests/pos/inline-trait-accesses-parent-val-param.scala b/tests/pos/inline-trait-accesses-parent-val-param.scala new file mode 100644 index 000000000000..9e337435e728 --- /dev/null +++ b/tests/pos/inline-trait-accesses-parent-val-param.scala @@ -0,0 +1,9 @@ +//> using options -Werror -Wsafe-init + +inline trait A[T](val x: T): + val v = x + +inline trait B extends A[Int]: + val z = x + +class C extends B, A[Int](10) From db9a6a952f72f567c5d9e7bc6a4b972ac46b4da1 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 25 May 2026 12:39:02 +0200 Subject: [PATCH 223/254] Do replaceInlineTraitSymbols by symbol lookup rather than manually maintaining a cache --- .../dotty/tools/dotc/inlines/Inlines.scala | 24 +------------------ .../dotty/tools/dotc/transform/Erasure.scala | 23 +++++++++++++++--- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 96a3804222ef..98ce7238be73 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -951,7 +951,6 @@ object Inlines: private def inlinedSym(sym: Symbol, overriddenDecls: Set[Symbol], withoutFlags: FlagSet = EmptyFlags)(using Context): Symbol = val newSym = if sym.isClass then inlinedClassSym(sym.asClass, withoutFlags) else inlinedMemberSym(sym, overriddenDecls, withoutFlags) - ctx.inlineTraitState.registerInlinedSymbol(sym, newSym, ctx.owner.thisType.classSymbol) newSym private def inlinedClassSym(sym: ClassSymbol, withoutFlags: FlagSet = EmptyFlags)(using Context): ClassSymbol = @@ -974,8 +973,6 @@ object Inlines: sym.privateWithin, spanCoord(parent.span) ) - // ctx.inlineTraitState.registerInlinedInnerClassSymbol(sym, inlinedSym, childThisType) - ctx.inlineTraitState.registerInlinedSymbol(sym, inlinedSym, childThisType.classSymbol) inlinedSym.entered case _ => report.error(s"Class symbol ${sym.show} does not have class info") @@ -1151,30 +1148,11 @@ object Inlines: end InlineParentTrait class InlineTraitState: - // Map representing all symbols we have inlined from inline traits, - // from the symbol in the parent trait, and the symbol of the child class-like - // to the inlined symbol in that child class-like. - // E.g. inline trait A {def foo#1000}; trait B extends A {def foo#2000 // created by inlining} - // The map has (foo#1000, trait B) => foo#2000 - val inlinedTraitSymbols = mutable.HashMap[(Symbol, Symbol), Symbol]() - // For a class symbol created during inlining of an inline trait, // the chain of inlined traits which produced it. We don't actually care about the order. + // Used as a "seen list" for cycle checking. Persists across invocations of InlineParentTrait val inlineOrigins = mutable.HashMap[Symbol, Set[Symbol]]().withDefaultValue(Set.empty) - // Record that we just inlined oldSym into childClasslike which created - // childClassLike.newSym - def registerInlinedSymbol(oldSym: Symbol, newSym: Symbol, childClasslike: Symbol) = - inlinedTraitSymbols((oldSym, childClasslike)) = newSym - - // Map (e.g.) B.foo#1000 into foo#2000 - def lookupInlinedSymbol(oldSym: Symbol, childClasslike: Symbol) = - inlinedTraitSymbols((oldSym, childClasslike)) - - // Check if oldSym has been inlined into childClasslike - def inlinedSymbolIsRegistered(oldSym: Symbol, childClasslike: Symbol) = - inlinedTraitSymbols.contains((oldSym, childClasslike)) - def registerInlineOrigin(newSym: Symbol, owner: Symbol, parentSym: Symbol): Unit = inlineOrigins(newSym) = inlineOrigins(owner) + parentSym end InlineTraitState diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index 4014c21d9699..5e975d4ca07f 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -743,9 +743,26 @@ object Erasure { adaptIfSuper(qual) match case qual1: Super => select(qual1, sym) - case qual1 if ctx.inlineTraitState.inlinedSymbolIsRegistered(sym, qual1.tpe.widenDealias.classSymbol) => - val newSym = ctx.inlineTraitState.lookupInlinedSymbol(sym, qual1.tpe.widenDealias.classSymbol) - untpd.cpy.Select(tree)(qual, sym.name).withType(qual1.tpe.select(newSym)) // TODO: Maybe we could just do this earlier also; maybe we don't need this cache + case qual1 if owner.isInlineTrait && + (qual1.tpe.widenDealias.classSymbol ne sym.owner) && + qual1.tpe.widenDealias.classSymbol.derivesFrom(sym.owner) => + + // If A is an inline trait and A.foo was inlined into B, references to b.foo (val b = B()) will still + // point to A.foo until now. We want them to point to B.foo so we get the benefit of specialization. + // We fix that here rather than in a separate phase because + // it needs to happen coordinated with erasure of Specialized traits, so that: + // a) we see the erased A$sp$Int traits and can point at their members + // b) we make the replacement before boxing in case A.foo is typed with T and B.foo specializes this to e.g. Int + // Otherwise we will end up with Int.unbox(A.foo) instead of directly B.foo + val specializedInterfaceType = qual1.tpe.widenDealias + val newSym = inContext(preErasureCtx) { + val desiredType = tree.asInstanceOf[Select].tpe.widen.dealias + val name = tree.name + specializedInterfaceType.classSymbol.info.findMember(name, specializedInterfaceType) + .matchingDenotation(specializedInterfaceType, desiredType, name).symbol + } + + qual1.select(newSym) case qual1 if !isJvmAccessible(qual1.tpe.typeSymbol) || !qual1.tpe.derivesFrom(sym.owner) => val castTarget = // Avoid inaccessible cast targets, see i8661 From c593708a67929e23d6f6469bcf046c46b7b3d675 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 25 May 2026 13:21:01 +0200 Subject: [PATCH 224/254] Fix erasure of class parents when these parents are specialized traits --- .../dotty/tools/dotc/core/TypeErasure.scala | 48 ++++++++++++++----- .../transform/DesugarSpecializedTraits.scala | 17 ++----- .../dotty/tools/dotc/transform/Erasure.scala | 34 +++++++++++-- 3 files changed, 72 insertions(+), 27 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 0c0e30ba5365..ea3ec083485c 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -14,6 +14,8 @@ import unpickleScala2.Scala2Erasure import Decorators.* import Definitions.MaxImplementedFunctionArity import scala.annotation.tailrec +import dotty.tools.dotc.transform.DesugarSpecializedTraits +import dotty.tools.dotc.util.Property /** The language in which the definition being erased was written. */ enum SourceLanguage: @@ -77,6 +79,8 @@ end SourceLanguage */ object TypeErasure: + private val DisallowSpecialized = Property.Key[Unit] + private def erasureDependsOnArgs(sym: Symbol)(using Context) = sym == defn.ArrayClass || sym == defn.PairClass || sym.isDerivedValueClass || sym.isSpecializedTrait @@ -210,6 +214,14 @@ object TypeErasure: def preErasureCtx(using Context) = if (ctx.erasedTypes) ctx.withPhase(erasurePhase) else ctx + /** The current context but with Foo[Int] erasing to Foo instead of + * Foo$sp$Int when Foo is a specialized trait. */ + def disallowSpecializedCtx(using Context) = ctx.fresh.setProperty(DisallowSpecialized, ()) + + /** The current context but with Foo[Int] erasing to Foo$sp$Int instead of + * Foo when Foo is a specialized trait. */ + def allowSpecializedCtx(using Context) = ctx.fresh.dropProperty(DisallowSpecialized) + /** The standard erasure of a Scala type. Value classes are erased as normal classes. * * @param tp The type to erase. @@ -771,10 +783,11 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else if semiEraseVCs && sym.isDerivedValueClass then eraseDerivedValueClass(tp) else if defn.isSyntheticFunctionClass(sym) then defn.functionTypeErasure(sym) else eraseNormalClassRef(tp) - case Specialization(spec) if ((ctx.phase == erasurePhase || ctx.erasedTypes) && spec.isSpecialized) => - val interfaceSymbol = ctx.specializedTraitState.specializedTraitCache.get.getInterfaceSymbol(spec) - assert(interfaceSymbol.nonEmpty) // This is a specialized trait; we should have a specialization we can swap in for it - this(interfaceSymbol.get.typeRef.appliedTo(spec.unspecializedTypeArgs.map(_.tpe))) + case Specialization(spec) if ((ctx.phase == erasurePhase || ctx.erasedTypes) && spec.isSpecialized && ctx.property(DisallowSpecialized).isEmpty) => + val specName = DesugarSpecializedTraits.newSpecializedTraitName(spec) // TODO: Maybe better as method on spec + val interfaceSymbol = spec.traitSymbol.owner.enclosingPackageClass.info.decls.lookup(specName) + assert(interfaceSymbol.exists && interfaceSymbol.isClass) + this(interfaceSymbol.typeRef.appliedTo(spec.unspecializedTypeArgs.map(_.tpe))) case tp: AppliedType => val tycon = tp.tycon if (tycon.isRef(defn.ArrayClass)) eraseArray(tp) @@ -865,19 +878,30 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst case tp @ ClassInfo(pre, cls, parents, decls, _) => if (cls.is(Package)) tp else { - def eraseParent(tp: Type) = tp.dealias match { // note: can't be opaque, since it's a class parent + def eraseParent(tp: Type)(using Context) = tp.dealias match { // note: can't be opaque, since it's a class parent case tp: AppliedType if tp.tycon.isRef(defn.PairClass) => defn.ObjectType case _ => apply(tp) } val erasedParents: List[Type] = if ((cls eq defn.ObjectClass) || cls.isPrimitiveValueClass) Nil - else - val parents1 = parents.mapConserve(eraseParent) - // drop duplicate Foo$sp$Int arising from erasure of Foo[Int] - val parents2 = parents1.filterNot( - p => Specialization.unapply(p).exists(s => ctx.specializedTraitState.specializedTraitCache.get.getInterfaceSymbol(s).nonEmpty) - ) - parents2 match { + else + // Match corresponding tree erasure in Erasure::typedClassDef + val parents1 = + if cls.isSpecializedTraitInterface then // {source: Bar, Foo both specialized traits} inline trait Bar$sp$Int extends Object, Bar, Foo$sp$Int + val (obj :: originalTrait :: inheritedParents) = parents : @unchecked + eraseParent(obj) :: apply(originalTrait)(using disallowSpecializedCtx) :: inheritedParents.mapConserve(eraseParent(_)(using allowSpecializedCtx)) + else if cls.isSpecializedTraitImplementationClass then // {source: Bar, Foo both specialized traits} class Bar$impl$Int extends Object, Bar$sp$Int, Bar(10) + val (objectParent :: traitSpParent :: originalTraitSpecializedParent :: Nil) = parents : @unchecked + eraseParent(objectParent) :: eraseParent(traitSpParent)(using allowSpecializedCtx) :: apply(originalTraitSpecializedParent)(using disallowSpecializedCtx) :: Nil + else + val originalSpecializedTraits = parents.filter(p => p.typeSymbol.isSpecializedTrait).map(eraseParent(_)(using allowSpecializedCtx)) + + // {source: class Bar extends Foo[Int](10) with Baz[Int](10)} + // class Bar extends Object, Foo(10), Bar(10), Foo$sp$Int, Bar$sp$Int + parents.mapConserve(p => if p.typeSymbol.isSpecializedTrait then + apply(p)(using disallowSpecializedCtx) + else eraseParent(p)) ::: originalSpecializedTraits + parents1 match { case tr :: trs1 => assert(!tr.classSymbol.is(Trait), i"$cls has bad parents $parents%, %") val tr1 = if (cls.is(Trait)) defn.ObjectType else tr diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 4a5c9f8203b0..ac41673d6299 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -69,11 +69,12 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: } ) - // Create new trait + // Order is depended on in Erasure::typedClassDef and TypeErasure:eraseParent val parents = defn.ObjectType :: AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.specialization).tpe // original trait, specialized to Foo[Int] :: inheritedParents // parents of the original trait in the form Foo[Int] (later specialized to Foo$sp$Int) + // Create new trait val traitSymbol = newNormalizedClassSymbol( specialization.traitSymbol.owner.enclosingPackageClass, // For specialized traits defined inside objects/classes etc, pre-Flatten the $sp$ and $impl$ def trees (i.e. // make them live in the enclosing package with the flattened name). We do this because it's easier than @@ -132,7 +133,8 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: // TODO: What happens if the creator of the specialized inline trait provides a self type? traitOrClassSymbol.info = ClassInfo(traitOrClassSymbol.owner.thisType, traitOrClassSymbol, traitOrClassSymbol.info.parents.map(freshTypeVarMap(_)), traitOrClassSymbol.info.decls) - + + // Order is depended on in Erasure::typedClassDef and TypeErasure:eraseParent private def generateImplementationClassParents(specialization: Specialization, interfaceSymbol: ClassSymbol)(using Context) = val objectParent = defn.ObjectType val traitSpParent = interfaceSymbol.typeRef.appliedTo(specialization.unspecializedTypeParams) // Set using old unspecializedTypeParams and replace after. @@ -364,16 +366,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: /* We need to do the parent removal after inlining into the $impl$ classes otherwise we break overriding/interface implementation rules during the inlining. The $impl$ inlining can also happen in the recursive calls, and so we need to do this right at the end (after the recursive calls): */ - val generatedTraitStats3 = - generatedTraitStats2.tapEach: stat => - if stat.symbol.isSpecializedTraitInterface then // We could have $impl$ classes from recursive calls as well. - stat.updateParents { parents => (parents: @unchecked) match - case obj :: Specialization(originalSpec) :: parents if specializations4.getInterfaceSymbol(originalSpec).get == stat.symbol.asClass => - obj :: parents - case obj :: parents => obj :: parents // We already removed the relevant parent. - } - .map(stat => if stat.symbol.isSpecializedTraitInterface then refreshClassDef(stat) else stat) - (generatedTraitStats3, generatedClassStats2, specializations4) + (generatedTraitStats2, generatedClassStats2, specializations4) val statsFinal = generatedTraitStatsFinal ++ generatedClassStatsFinal ++ diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index 5e975d4ca07f..973444e5f95c 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -1039,9 +1039,37 @@ object Erasure { override def typedClassDef(cdef: untpd.TypeDef, cls: ClassSymbol)(using Context): Tree = // drop Foo[Int] leading to duplicate Foo$sp$Int val TypeDef(_, implInit: Template) = cdef: @unchecked - val cdef1 = cpy.TypeDef(cdef.asInstanceOf[TypeDef])(rhs = cpy.Template(implInit.asInstanceOf[Template])(parents = implInit.asInstanceOf[Template].parents.filterNot( - p => Specialization.unapply(p.tpe).exists(s => ctx.specializedTraitState.specializedTraitCache.get.getInterfaceSymbol(s).nonEmpty) - ))) + + // Match corresponding class info erasure in TypeErasure::apply ClassInfo case + val cdef1 = + val oldParents = implInit.asInstanceOf[Template].parents + val superCtxNoSpec = disallowSpecializedCtx(using ctx.superCallContext) + val newParents = + if cls.isSpecializedTraitInterface then // {source: Bar, Foo both specialized traits} inline trait Bar$sp$Int extends Object, Bar, Foo$sp$Int + val (obj :: originalTrait :: inheritedParents) = oldParents : @unchecked + obj :: typedType(originalTrait)(using superCtxNoSpec) :: inheritedParents + else if cls.isSpecializedTraitImplementationClass then // {source: Bar, Foo both specialized traits} class Bar$impl$Int extends Object, Bar$sp$Int, Bar(10) + val (objectParent :: traitSpParent :: originalTraitSpecializedParent :: Nil) = oldParents : @unchecked + val newParent = originalTraitSpecializedParent match { + case _: untpd.Apply => typedExpr(originalTraitSpecializedParent)(using superCtxNoSpec) + case _ => typedType(originalTraitSpecializedParent)(using superCtxNoSpec) + } + objectParent :: traitSpParent :: newParent :: Nil + else + inContext(preErasureCtx) { + val extraSpTraits = oldParents.filter(p => p.symbol.isPrimaryConstructor && p.symbol.owner.isSpecializedTrait).map(p => p.tpe.resultType) + + // {source: class Bar extends Foo[Int](10) with Baz[Int](10)} + // class Bar extends Object, Foo(10), Bar(10), Foo$sp$Int, Bar$sp$Int + oldParents.map { tp => + if tp.symbol.isPrimaryConstructor && tp.symbol.owner.isSpecializedTrait then + typedExpr(tp)(using superCtxNoSpec) + else + tp + } ::: extraSpTraits.map(sym => TypeTree(sym)) + } + + cpy.TypeDef(cdef.asInstanceOf[TypeDef])(rhs = cpy.Template(implInit.asInstanceOf[Template])(parents = newParents)) val typedTree@TypeDef(name, impl @ Template(constr, _, self, _)) = super.typedClassDef(cdef1, cls): @unchecked // In the case where a trait extends a class, we need to strip any non trait class from the signature From 7903e8249a9fb66686cca9436784dfee7d68b826 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 25 May 2026 13:33:26 +0200 Subject: [PATCH 225/254] Add tests for inlining a specialized instance across compilation units --- .../A_1.scala | 9 +++++++++ .../B_2.scala | 4 ++++ .../A_1.scala | 6 ++++++ .../B_2.scala | 4 ++++ 4 files changed, 23 insertions(+) create mode 100644 tests/run/specialized-trait-inline-specialized-instance-hidden/A_1.scala create mode 100644 tests/run/specialized-trait-inline-specialized-instance-hidden/B_2.scala create mode 100644 tests/run/specialized-trait-inline-specialized-instance-with-specialization/A_1.scala create mode 100644 tests/run/specialized-trait-inline-specialized-instance-with-specialization/B_2.scala diff --git a/tests/run/specialized-trait-inline-specialized-instance-hidden/A_1.scala b/tests/run/specialized-trait-inline-specialized-instance-hidden/A_1.scala new file mode 100644 index 000000000000..f6ca2ab21d07 --- /dev/null +++ b/tests/run/specialized-trait-inline-specialized-instance-hidden/A_1.scala @@ -0,0 +1,9 @@ +//> using options -language:experimental.specializedTraits + +inline trait A[T: Specialized]: + def foo() = assert(Thread.currentThread.getStackTrace()(1).getClassName() == "A$impl$scala$Int") + +inline def bar = + val x = new A[Int]() {} + x.foo() + 5 diff --git a/tests/run/specialized-trait-inline-specialized-instance-hidden/B_2.scala b/tests/run/specialized-trait-inline-specialized-instance-hidden/B_2.scala new file mode 100644 index 000000000000..d57d993d3053 --- /dev/null +++ b/tests/run/specialized-trait-inline-specialized-instance-hidden/B_2.scala @@ -0,0 +1,4 @@ +//> using options -language:experimental.specializedTraits + +@main def Test = + assert(bar == 5) diff --git a/tests/run/specialized-trait-inline-specialized-instance-with-specialization/A_1.scala b/tests/run/specialized-trait-inline-specialized-instance-with-specialization/A_1.scala new file mode 100644 index 000000000000..4026e3488c96 --- /dev/null +++ b/tests/run/specialized-trait-inline-specialized-instance-with-specialization/A_1.scala @@ -0,0 +1,6 @@ +//> using options -language:experimental.specializedTraits + +inline trait A[T: Specialized]: + def foo() = assert(Thread.currentThread.getStackTrace()(1).getClassName() == "A$impl$scala$Int") + +inline def bar[T: Specialized] = new A[T]() {} diff --git a/tests/run/specialized-trait-inline-specialized-instance-with-specialization/B_2.scala b/tests/run/specialized-trait-inline-specialized-instance-with-specialization/B_2.scala new file mode 100644 index 000000000000..305743252757 --- /dev/null +++ b/tests/run/specialized-trait-inline-specialized-instance-with-specialization/B_2.scala @@ -0,0 +1,4 @@ +//> using options -language:experimental.specializedTraits + +@main def Test = + bar[Int].foo() From f6ac71a0f116626ff55520776ab83ca201e8ff2c Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 25 May 2026 18:20:13 +0200 Subject: [PATCH 226/254] Add explanatory comment --- compiler/src/dotty/tools/dotc/core/TypeErasure.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index ea3ec083485c..e4e588615b6a 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -783,7 +783,11 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else if semiEraseVCs && sym.isDerivedValueClass then eraseDerivedValueClass(tp) else if defn.isSyntheticFunctionClass(sym) then defn.functionTypeErasure(sym) else eraseNormalClassRef(tp) - case Specialization(spec) if ((ctx.phase == erasurePhase || ctx.erasedTypes) && spec.isSpecialized && ctx.property(DisallowSpecialized).isEmpty) => + case Specialization(spec) if ((ctx.phase == erasurePhase || ctx.erasedTypes) // At the beginning the $sp$ trait symbols are not present so up until + // erasure need to consider the signature of def foo(x: Foo[Int]): Int as + // foo(Foo):Int. Only at erasure do the symbol swap. This ensures + // the signatures don't change before erasure. + && spec.isSpecialized && ctx.property(DisallowSpecialized).isEmpty) => val specName = DesugarSpecializedTraits.newSpecializedTraitName(spec) // TODO: Maybe better as method on spec val interfaceSymbol = spec.traitSymbol.owner.enclosingPackageClass.info.decls.lookup(specName) assert(interfaceSymbol.exists && interfaceSymbol.isClass) From b1faf9a96b71870665bd3a555c81abc87e011ac1 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 25 May 2026 18:22:03 +0200 Subject: [PATCH 227/254] Add bridges for methods with specialized trait parameters; unlocks Vec[?] --- .../src/dotty/tools/dotc/transform/Bridges.scala | 3 ++- ...it-check-bridge-exists-hidden-by-inline.scala | 16 ++++++++++++++++ .../specialized-trait-check-bridge-exists.scala | 13 +++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/run/specialized-trait-check-bridge-exists-hidden-by-inline.scala create mode 100644 tests/run/specialized-trait-check-bridge-exists.scala diff --git a/compiler/src/dotty/tools/dotc/transform/Bridges.scala b/compiler/src/dotty/tools/dotc/transform/Bridges.scala index 41e674a6b337..62d33a5b590a 100644 --- a/compiler/src/dotty/tools/dotc/transform/Bridges.scala +++ b/compiler/src/dotty/tools/dotc/transform/Bridges.scala @@ -12,6 +12,7 @@ import ContextFunctionResults.{contextResultCount, contextFunctionResultTypeAfte import StdNames.nme import Constants.Constant import TypeErasure.transformInfo +import TypeErasure.disallowSpecializedCtx import Erasure.Boxing.adaptClosure /** A helper class for generating bridge methods in class `root`. */ @@ -19,7 +20,7 @@ class Bridges(root: ClassSymbol, thisPhase: DenotTransformer)(using Context) { import ast.tpd.* assert(ctx.phase == erasurePhase.next) - private val preErasureCtx = ctx.withPhase(erasurePhase) + private val preErasureCtx = disallowSpecializedCtx(using ctx.withPhase(erasurePhase.prev)) private lazy val elimErasedCtx = ctx.withPhase(elimErasedValueTypePhase.next) private class BridgesCursor(using Context) extends OverridingPairs.Cursor(root) { diff --git a/tests/run/specialized-trait-check-bridge-exists-hidden-by-inline.scala b/tests/run/specialized-trait-check-bridge-exists-hidden-by-inline.scala new file mode 100644 index 000000000000..249b41341139 --- /dev/null +++ b/tests/run/specialized-trait-check-bridge-exists-hidden-by-inline.scala @@ -0,0 +1,16 @@ +//> using options -language:experimental.specializedTraits +inline trait Trait[T: Specialized]: + def do_something() = println("Good morning") + +inline trait A[T: Specialized]: + def foo(x: Trait[T]): Unit + +inline trait B extends A[Int]: + override def foo(x: Trait[Int]): Unit = x.do_something() + +class C extends B // Should have a bridge for the specialized foo + +@main def Test = + val fooMethods = classOf[C].getDeclaredMethods.filter(_.getName == "foo") + assert(fooMethods.length == 2, s"expected 2 `foo` methods on Bar (real + bridge), found ${fooMethods.length}: ${fooMethods.mkString(", ")}") + diff --git a/tests/run/specialized-trait-check-bridge-exists.scala b/tests/run/specialized-trait-check-bridge-exists.scala new file mode 100644 index 000000000000..8142f1e69592 --- /dev/null +++ b/tests/run/specialized-trait-check-bridge-exists.scala @@ -0,0 +1,13 @@ +//> using options -language:experimental.specializedTraits + +inline trait Foo[T: Specialized]: + def foo(x: Foo[T]): Foo[T] + +class Bar extends Foo[Int]: + override def foo(x: Foo[Int]): Foo[Int] = x + +@main def Test = + val x = Bar() + val fooMethods = classOf[Bar].getDeclaredMethods.filter(_.getName == "foo") + assert(fooMethods.length == 2, s"expected 2 `foo` methods on Bar (real + bridge), found ${fooMethods.length}: ${fooMethods.mkString(", ")}") + From b0c46cc0c398592ea48acdf24de2bb401af63f0e Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 25 May 2026 18:22:23 +0200 Subject: [PATCH 228/254] Note a missing case --- tests/neg/specialized-trait-overload.scala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/neg/specialized-trait-overload.scala diff --git a/tests/neg/specialized-trait-overload.scala b/tests/neg/specialized-trait-overload.scala new file mode 100644 index 000000000000..32e973b5c6a9 --- /dev/null +++ b/tests/neg/specialized-trait-overload.scala @@ -0,0 +1,18 @@ +//> using options -language:experimental.specializedTraits + +// In principle we might be able to allow this because after erasure we will have different +// classes. The problem is we don't know the classes at the beginning of compilation +// so until erasure these overloads will be the same. It depends on to what extent the +// pre-erasure phases rely on "pre-computing" post-erasure signatures. For now we block +// it as it's not a requirement for specialized traits to work. + +inline trait Foo[T: Specialized]: + def foo(x: Foo[T]): Foo[T] + +class Bar: + def method1(x: Foo[Int]) = 10 + def method1(x: Foo[String]) = 10 // error: Conflicting definitions + +@main def Test = + val x = Bar() + From 80dfbdf42b4dea89da51c3215261c80884707185 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 25 May 2026 18:28:57 +0200 Subject: [PATCH 229/254] Do some docs updates --- docs/_docs/internals/specialized-traits.md | 48 ++++++++++++---------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/docs/_docs/internals/specialized-traits.md b/docs/_docs/internals/specialized-traits.md index b48c99967644..80002f323099 100644 --- a/docs/_docs/internals/specialized-traits.md +++ b/docs/_docs/internals/specialized-traits.md @@ -109,7 +109,9 @@ val myBar = Bar() [0] Note that of course an anonymous class instance such as `new Foo[Int] {}` where `Foo` extends some other trait `Bar` desugars in the compiler to `new Bar[Int] with Foo[Int] {}`, which means we can't distinguish these two cases. Therefore we begrudgingly allow `new Bar[Int] with Foo[Int] {}` although there is really no reason to use this in source code because it's exactly the same as writing `new Foo[Int] {}`. We disallow `new Foo[Int] with Bar[Int] {}` however. - + +Note: This demonstrates that 'class extends specialized trait' and 'object extends specialized trait' are allowed. However, 'trait extends specialized trait' is not. This is due to the restriction on extending inline traits with parameters by ordinary traits, as discussed in `inline-traits.md`. 'inline trait extends specialized trait' is allowed as not subject to this restriction. + @@ -200,7 +202,7 @@ inline trait Vec$sp$Int extends Vec[Int]: def scalarProduct(other: Vec$sp$Int): Int ``` -The unspecialized parent types (`Vec[Int]`) are then removed after inlining. This is necessary to avoid interface conflicts with the specialized + ## Specialized Traits in the Compiler We introduce a new phase `desugarSpecializedTraits` responsible for detecting specializations, generating the necessary `$sp$` and `$impl$` @@ -275,15 +277,16 @@ this problem arises not only with the `$sp$` traits, but also the `$impl$` class To resolve this problem without alternating between and looping the `specializeInlineTraits` and `desugarSpecializedTraits` phases in an inconvenient way, we opt to make: - `specializeInlineTraits` responsible for inlining inline traits written directly in user code. If a specialized trait creates an inline trait inlining opportunity which is not specialized, this is dealt with by specializeInlineTraits. Further if a user writes `class Bar extends Foo[Int]` where Foo is declared Specialized, `specializeInlineTraits` will do the inlining. - `desugarSpecializedTraits` responsible for finding specializations and generating the required `$sp$` traits and `$impl$` classes, inlining the parent specialized traits into these classes, and repeating until no more inlining can be performed and no more `$sp$` traits and `$impl$` classes are needed. This phase also performs replacement of e.g. `Vec[Int]` with `Vec$sp$Int` and `new Vec[Int]` with `new Vec$impl$Int`. -- `pruneInlineTraits` and `replaceInlinedTraitSymbols` responsible respectively for converting inline traits to pure interfaces, and for replacing members accessed on inline receivers with the corresponding inlined symbols. This is *whether the inline traits in question come from inline traits in source code or specialized trait expansion, in both cases.* (see the document on inline traits for a more detailed description of these phases). +- `pruneInlineTraits` responsible for converting inline traits to pure interfaces +- We also need to replace members accessed on inline receivers with the corresponding inlined symbols, and this is done in erasure. This is *whether the inline traits in question come from inline traits in source code or specialized trait expansion, in both cases.* (see the document on inline traits for a more detailed description of this operation and `pruneInlineTraits`). In particular this decision means that we run `specializeInlineTraits` before `desugarSpecializedTraits`, as otherwise we may duplicate the inlined bodies of the `$sp$` traits and `$impl$` classes, since we have already inlined them in `desugarSpecializedTraits`. -## Caching of Specialized Traits and Classes + To avoid redundant repeated code generation of the same traits and classes, specialized instance traits and classes are cached. The compiler will put their tasty and classfile artifacts in a special directory -on the class path. Each artifact will contain in an annotation a hash of the contents of the trait from which the instance was derived. Before creating a new specialized instance, the compiler will consult this directory to see whether an instance with the given name exists and whether its hash matches. In that case, the artifacts can be re-used. +on the class path. Each artifact will contain in an annotation a hash of the contents of the trait from which the instance was derived. Before creating a new specialized instance, the compiler will consult this directory to see whether an instance with the given name exists and whether its hash matches. In that case, the artifacts can be re-used. --> ## The `Specialized` Type Class @@ -316,39 +319,41 @@ inline trait Seq[T: Specialized](elems: Array[T]) extends Iterable[T]: def iterator: Iterator[T] = new ArrayIterator[T](elems) {} ``` -This generates the following instance traits (after inlining, conversion to pure interfaces and parent removal): +This generates the following instance traits (after inlining, conversion to pure interfaces and erasure): +// TODO: Check that this matches what is actually generated ```scala -inline trait Iterator$sp$Int: +inline trait Iterator$sp$Int extends Iterator: def hasNext: Boolean def next(): Int -inline trait ArrayIterator$sp$Int extends Iterator$sp$Int +inline trait ArrayIterator$sp$Int extends ArrayIterator, Iterator$sp$Int -inline trait Iterable$sp$Int: +inline trait Iterable$sp$Int extends Iterable: def iterator: Iterator$sp$Int def forall(f: Int => Unit): Unit -inline trait Seq$sp$Int extends Iterable$sp$Int: +inline trait Seq$sp$Int extends Seq, Iterable$sp$Int: def length: Int def apply(i: Int): Int ``` Note that these traits repeat the parent types of their corresponding inline traits (but with specialization added). For instance, `ArrayIterator$sp$Int` extends the specialized version of its parent `Iterator$sp$Int`, so the specialized trait may be used in contexts expecting: - The specialized trait `ArrayIterator$sp$Int` itself (i.e. `ArrayIterator[Int]` in source code) +- A generic `ArrayIterator` (i.e. `ArrayIterator[?]` in source code) - Specialized traits higher in the specialized hierarchy for example `Iterator$sp$Int`. The specialized implementation classes for `ArrayIterator` and `Seq` are as follows (after inlining; iff `new Seq[Int] {}` and `ArrayIterator[Int] {}` are to be found in the program): ```scala -class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int: +class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int, ArrayIterator(elems): private var current = 0 override def hasNext: Boolean = current < elems.length override def next(): Int = try elems(current) finally current += 1 -class Seq$impl$Int(elems: Array[Int]) extends Seq$sp$Int: +class Seq$impl$Int(elems: Array[Int]) extends Seq$sp$Int, Seq(elems): override def iterator: Iterator$sp$Int = new ArrayIterator$impl$Int(elems) override def forall(f: Int => Unit): Unit = @@ -358,17 +363,17 @@ class Seq$impl$Int(elems: Array[Int]) extends Seq$sp$Int: override def apply(i: Int): Int = elems(i) ``` -## Summary of restrictions on specialized traits - -| Behaviour | Is currently supported in... | -|--------------------------|-----------------------------------------------| -| Inheriting from specialized traits | In inline traits or anonymous class instances (for instance creation) only | -| Taking `Specialized` parameters| Only by inline traits | -| Use of `?` bounds | May not be used for Specialized parameters; however may be used for non-Specialized parameters in specialized traits. | +## Summary of implementation restrictions on specialized traits +These could be lifted with additional work. +| Behaviour | Limitation | Chance of fixing / limited by | +|--------------------------|-----------------------------------------------|---| +| Use of `?` bounds | May not be used for Specialized parameters; however may be used for non-Specialized parameters in specialized traits. | Fixable by adding bridge methods. We ban them because with if `class A extends I[Int]` implements method `def foo(x: Foo[Int])` which belongs to interface `I[T]` as `def foo(x: Foo[T])` then specializing A's `foo` means it no longer implements the interface correctly. Note that this can only occur when I is itself a specialized trait (otherwise the type signature `Foo[T]` is not valid in `I`) so in practice this reduces to `inline trait I$sp$Int extends I[Int]`. This means we can solve the problem by having `I$sp$Int` not extend `I[Int]` in the final generated code. This imposes that `I[?]` be banned as `I[Int]` in source (`I$sp$Int` after erasure) does not extend `I[?]` / `I` . Adding bridge methods seemed quite challenging because there are a large number of cases to deal with. | +| Defining specialized traits inside traits/classes/objects | May define specialized traits inside `object`s. May not define them inside `class`es or `trait`s. | Limited by the way we flatten the owners of generated `$impl$` classes and `$sp$` traits. We really need to build the `Foo$impl$` class directly next to Foo. For a single CU this would be possible if we walked the entire tree and found where these belong; for multiple CUs it is more complicated as the tree may not exist in the current CU. The case of path dependent specialized traits was deemed niche enough to not be high priority. | + ## Transportation of Specialized through generic code -It may surprise you to note that the following is valid scala. The `Numeric` constraint on `T` is only checked when +It may surprise you to note that the following is valid. The `Numeric` constraint on `T` is only checked when a concrete type is provided for `S` (and by extension `T`) when instantiating `T2`. ```scala inline trait T1[T: Numeric] @@ -655,3 +660,4 @@ This proposal +Maybe try rewriting as a spec of what we actually do rather than what we wanted to do and see if we get different results! From b99d58cb61aadf24c8224b4b3ee9743a75bbfbb4 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 25 May 2026 19:48:10 +0200 Subject: [PATCH 230/254] Move Specialized to own package to avoid conflict with specialized --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 10 +++++++--- library/src/scala/{ => specialize}/Specialized.scala | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) rename library/src/scala/{ => specialize}/Specialized.scala (93%) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 7b0e17f31b7d..005b05c8d17e 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -236,6 +236,7 @@ class Definitions { @tu lazy val ScalaCollectionImmutablePackageClass: ClassSymbol = requiredPackage("scala.collection.immutable").moduleClass.asClass @tu lazy val ScalaMathPackageClass: ClassSymbol = requiredPackage("scala.math").moduleClass.asClass @tu lazy val ScalaUtilPackageClass: ClassSymbol = requiredPackage("scala.util").moduleClass.asClass + @tu lazy val ScalaSpecializePackageVal: TermSymbol = requiredPackage("scala.specialize") // fundamental modules @tu lazy val SysPackage : Symbol = requiredModule("scala.sys.package") @@ -793,7 +794,7 @@ class Definitions { @tu lazy val StringAddClass : ClassSymbol = requiredClass("scala.runtime.StringAdd") @tu lazy val StringAdd_+ : Symbol = StringAddClass.requiredMethod(nme.raw.PLUS) - @tu lazy val SpecializedClass : ClassSymbol = requiredClass("scala.Specialized") + @tu lazy val SpecializedClass : ClassSymbol = requiredClass("scala.specialize.Specialized") @tu lazy val SpecializedModule: Symbol = SpecializedClass.companionModule @tu lazy val SpecializedModule_apply: Symbol = SpecializedModule.requiredMethod(nme.apply) @@ -1738,6 +1739,9 @@ class Definitions { private val PredefImportFns: RootRef = RootRef(() => ScalaPredefModule.termRef, isPredef=true) + private val SpecializeImportFns: RootRef = // TODO: Find a solution to importing in scala package without conflict with original specialized. + RootRef(() => ScalaSpecializePackageVal.termRef) + @tu private lazy val YimportsImportFns: List[RootRef] = ctx.settings.Yimports.value.map { name => val denot = getModuleIfDefined(name).suchThat(_.is(Module)) `orElse` @@ -1753,8 +1757,8 @@ class Definitions { @tu private lazy val ScalaRootImportFns: List[RootRef] = if !ctx.settings.Yimports.isDefault then YimportsImportFns else if ctx.settings.YnoImports.value then Nil - else if ctx.settings.YnoPredef.value then ScalaImportFns - else ScalaImportFns :+ PredefImportFns + else if ctx.settings.YnoPredef.value then ScalaImportFns :+ SpecializeImportFns + else ScalaImportFns :+ SpecializeImportFns :+ PredefImportFns @tu private lazy val JavaRootImportTypes: List[TermRef] = JavaRootImportFns.map(_.refFn()) @tu private lazy val ScalaRootImportTypes: List[TermRef] = ScalaRootImportFns.map(_.refFn()) diff --git a/library/src/scala/Specialized.scala b/library/src/scala/specialize/Specialized.scala similarity index 93% rename from library/src/scala/Specialized.scala rename to library/src/scala/specialize/Specialized.scala index 27c67aa37321..a2f70cc43a1d 100644 --- a/library/src/scala/Specialized.scala +++ b/library/src/scala/specialize/Specialized.scala @@ -1,4 +1,4 @@ -package scala +package scala.specialize import language.experimental.erasedDefinitions import scala.annotation.nowarn From 75c2a36c15e9e4d7d67666a77e18aad4bed9f82b Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 26 May 2026 12:23:15 +0200 Subject: [PATCH 231/254] Fix inline-trait-self-type-problems to use inline traits --- tests/neg/inline-trait-self-type-problems.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/neg/inline-trait-self-type-problems.scala b/tests/neg/inline-trait-self-type-problems.scala index 1e4af2a8d0ef..317620f27fbd 100644 --- a/tests/neg/inline-trait-self-type-problems.scala +++ b/tests/neg/inline-trait-self-type-problems.scala @@ -3,21 +3,21 @@ trait T2 trait T3 class Test -trait A[T]: +inline trait A[T]: this: T1 => -trait D extends A[Int] // error: self type of D does not conform to that of A -trait E extends D +inline trait D extends A[Int] // error: self type of D does not conform to that of A +inline trait E extends D -trait B[T]: +inline trait B[T]: this: T2 & T1 => -trait F extends A[Int], B[Int] // error: self type of F does not conform to that of A +inline trait F extends A[Int], B[Int] // error: self type of F does not conform to that of A -trait C[T]: +inline trait C[T]: this: T => -trait H extends C[Test]: // error self type of H does not conform to that of C +inline trait H extends C[Test]: // error self type of H does not conform to that of C this: T3 => class Cl2 extends Test with H with T3 From 2ec80bd69a3ecb184d24fdc7dcc4ad6ee717c2d0 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 26 May 2026 13:10:24 +0200 Subject: [PATCH 232/254] Support self types in specialized traits --- .../transform/DesugarSpecializedTraits.scala | 13 ++++++--- ...specialized-trait-self-type-problems.scala | 25 +++++++++++++++++ ...ait-partial-specialization-self-type.scala | 16 +++++++++++ tests/pos/specialized-trait-self-type.scala | 28 +++++++++++++++++++ 4 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 tests/neg/specialized-trait-self-type-problems.scala create mode 100644 tests/pos/specialized-trait-partial-specialization-self-type.scala create mode 100644 tests/pos/specialized-trait-self-type.scala diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index ac41673d6299..fe0f154fcb7a 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -83,7 +83,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: DesugarSpecializedTraits.newSpecializedTraitName(specialization), Flags.Synthetic | Flags.Trait | Flags.Inline, parents, - NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? + tm(specialization.traitSymbol.asClass.classInfo.selfType), specialization.traitSymbol.privateWithin, spanCoord(specialization.span), specialization.traitSymbol.compilationUnitInfo @@ -131,8 +131,13 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: val freshTypeVarMap = new TypeMap: def apply(t: Type) = tpMap.applyOrElse(t, mapOver) - // TODO: What happens if the creator of the specialized inline trait provides a self type? - traitOrClassSymbol.info = ClassInfo(traitOrClassSymbol.owner.thisType, traitOrClassSymbol, traitOrClassSymbol.info.parents.map(freshTypeVarMap(_)), traitOrClassSymbol.info.decls) + def mapSelfType(st: Type | Symbol): Type | Symbol = + if st.isInstanceOf[Symbol] then + st.asInstanceOf[Symbol].copy(info = freshTypeVarMap(st.asInstanceOf[Symbol].info)) + else + freshTypeVarMap(st.asInstanceOf[Type]) + + traitOrClassSymbol.info = ClassInfo(traitOrClassSymbol.owner.thisType, traitOrClassSymbol, traitOrClassSymbol.info.parents.map(freshTypeVarMap(_)), traitOrClassSymbol.info.decls, mapSelfType(traitOrClassSymbol.classInfo.selfInfo)) // Order is depended on in Erasure::typedClassDef and TypeErasure:eraseParent private def generateImplementationClassParents(specialization: Specialization, interfaceSymbol: ClassSymbol)(using Context) = @@ -150,7 +155,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: DesugarSpecializedTraits.newImplementationClassName(specialization), Flags.Synthetic, parents, - NoType, // TODO: What happens if the creator of the specialized inline trait provides a self type? + NoType, specialization.traitSymbol.privateWithin, spanCoord(specialization.span), specialization.traitSymbol.compilationUnitInfo diff --git a/tests/neg/specialized-trait-self-type-problems.scala b/tests/neg/specialized-trait-self-type-problems.scala new file mode 100644 index 000000000000..8f26613fa023 --- /dev/null +++ b/tests/neg/specialized-trait-self-type-problems.scala @@ -0,0 +1,25 @@ +//> using options -language:experimental.specializedTraits + +trait T1 +trait T2 +trait T3 +class Test + +inline trait A[T: Specialized]: + this: T1 => + +inline trait D extends A[Int] // error: self type of D does not conform to that of A // error: self type of A$sp$Int does not conform to that of A +inline trait E extends D + +inline trait B[T: Specialized]: + this: T2 & T1 => + +inline trait F extends A[Int], B[Int] // error: self type of F does not conform to that of A // error: self type of B$sp$Int does not conform to self type + +inline trait C[T: Specialized]: + this: T => + +inline trait H extends C[Test]: // error self type of H does not conform to that of C // error: self type of C$sp$Int does not conform to self type + this: T3 => + +class Cl2 extends Test with H with T3 diff --git a/tests/pos/specialized-trait-partial-specialization-self-type.scala b/tests/pos/specialized-trait-partial-specialization-self-type.scala new file mode 100644 index 000000000000..2c7ddeb66579 --- /dev/null +++ b/tests/pos/specialized-trait-partial-specialization-self-type.scala @@ -0,0 +1,16 @@ +//> using options -language:experimental.specializedTraits + +trait T1 +trait T2 +trait T3 +trait Test1 +trait Test2 + +inline trait A[T: Specialized, S: Specialized, Z]: + this: T1 & T & S => + +inline trait B[W: Specialized] extends A[W, Test2, Int]: + this: T1 & W & Test2 => + +class Cl2 extends B[Test1] with Test1 with Test2 with T1 + diff --git a/tests/pos/specialized-trait-self-type.scala b/tests/pos/specialized-trait-self-type.scala new file mode 100644 index 000000000000..4fc2105626ac --- /dev/null +++ b/tests/pos/specialized-trait-self-type.scala @@ -0,0 +1,28 @@ +//> using options -language:experimental.specializedTraits + +trait T1 +trait T2 +trait T3 +class Test + +inline trait A[T: Specialized]: + this: T1 => + +inline trait D extends A[Int]: + this: T1 => +inline trait E extends D: + this: T1 => + +inline trait B[T: Specialized]: + this: T2 & T1 => + +inline trait F extends A[Int], B[Int]: + this: T2 & T1 => + +inline trait C[T: Specialized]: + this: T => + +inline trait H extends C[Test]: + this: T3 & Test => + +class Cl2 extends Test with H with T3 From e9420dea5d04042f552151d37eabb7d517b95c7b Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 26 May 2026 13:10:33 +0200 Subject: [PATCH 233/254] Delete redundant old code --- .../transform/DesugarSpecializedTraits.scala | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index fe0f154fcb7a..d1ba29d97a28 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -325,28 +325,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: val generatedClassStats = specializations1.getNewImplementationSymbols.toList.map(buildImplementationClassTree) val specializations2 = specializations1.installNewInterfaceSymbols.installNewImplementationSymbols - - /* We have Vec$sp$Int extends Vec[Int] in order to do the inlining, but then remove this parent - afterwards to avoid interface implementation problems (see tests/run/specialized-trait-as-parameter.scala, - tests/run/specialized-trait-as-return-type.scala) */ - extension (classTree: Tree) - def updateParents(parentUpdater: List[Type] => List[Type]) = (classTree: @unchecked) match { - case td@TypeDef(name, t@Template(constr, preParentsOrDerived, self, preBody)) => - - val cls = td.symbol.asClass - val oldInfo = cls.classInfo - val newInfo = oldInfo.derivedClassInfo(declaredParents = parentUpdater(oldInfo.declaredParents)) - cls.info = newInfo - cls.copySymDenotation(info = newInfo).installAfter(DesugarSpecializedTraits.this) - } - - def refreshClassDef = (classTree: @unchecked) match { - case td@TypeDef(name, t@Template(constr, preParentsOrDerived, self, preBody)) => - ClassDef(td.symbol.asClass, constr, t.body) - } - - /* We need to inline recursively throughout generated specialized traits - see tests/run/specialized-trait-requires-inline-trait-inlining.scala */ - + val generatedTraitStats1 = generatedTraitStats.map { case tree: TypeDef => assert(tree.symbol.isInlineTrait) From f26cf2defa6a2ef3eaf2dd279767b9e06796c834 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 26 May 2026 13:26:56 +0200 Subject: [PATCH 234/254] Add some specialized trait macro tests --- .../Macro_1.scala | 10 ++++++++++ .../Test_2.scala | 8 ++++++++ .../Macro_1.scala | 16 ++++++++++++++++ .../Test_2.scala | 4 ++++ 4 files changed, 38 insertions(+) create mode 100644 tests/run/specialized-trait-body-macro-uses-type-param/Macro_1.scala create mode 100644 tests/run/specialized-trait-body-macro-uses-type-param/Test_2.scala create mode 100644 tests/run/specialized-trait-inline-specialization-macro/Macro_1.scala create mode 100644 tests/run/specialized-trait-inline-specialization-macro/Test_2.scala diff --git a/tests/run/specialized-trait-body-macro-uses-type-param/Macro_1.scala b/tests/run/specialized-trait-body-macro-uses-type-param/Macro_1.scala new file mode 100644 index 000000000000..593623d38800 --- /dev/null +++ b/tests/run/specialized-trait-body-macro-uses-type-param/Macro_1.scala @@ -0,0 +1,10 @@ +//> using options -language:experimental.specializedTraits +import scala.quoted.* + +inline def describe[T]: String = ${ describeImpl[T] } + +def describeImpl[T: Type](using Quotes): Expr[String] = + Expr(Type.show[T]) + +inline trait A[T: Specialized]: + val name: String = describe[T] diff --git a/tests/run/specialized-trait-body-macro-uses-type-param/Test_2.scala b/tests/run/specialized-trait-body-macro-uses-type-param/Test_2.scala new file mode 100644 index 000000000000..6d122f4d491e --- /dev/null +++ b/tests/run/specialized-trait-body-macro-uses-type-param/Test_2.scala @@ -0,0 +1,8 @@ +//> using options -language:experimental.specializedTraits + +class B extends A[Int] +class C extends A[String] + +@main def Test = + assert(B().name == "scala.Int", B().name) + assert(C().name == "scala.Predef.String", C().name) diff --git a/tests/run/specialized-trait-inline-specialization-macro/Macro_1.scala b/tests/run/specialized-trait-inline-specialization-macro/Macro_1.scala new file mode 100644 index 000000000000..3bcdbf6dd476 --- /dev/null +++ b/tests/run/specialized-trait-inline-specialization-macro/Macro_1.scala @@ -0,0 +1,16 @@ +//> using options -language:experimental.specializedTraits +import scala.quoted.* + +inline def foo(): Unit = + ${fooImpl} + +def fooImpl(using Quotes): Expr[Unit] = + '{ + val x = new A[Int]() {} + assert(x.foo(15) == 10) + assert(x.bar == "A$impl$scala$Int") + } + +inline trait A[T: Specialized]: + def foo(x: T) = 10 + def bar = Thread.currentThread.getStackTrace()(1).getClassName() diff --git a/tests/run/specialized-trait-inline-specialization-macro/Test_2.scala b/tests/run/specialized-trait-inline-specialization-macro/Test_2.scala new file mode 100644 index 000000000000..527d156fc648 --- /dev/null +++ b/tests/run/specialized-trait-inline-specialization-macro/Test_2.scala @@ -0,0 +1,4 @@ +//> using options -language:experimental.specializedTraits + +@main def Test = + foo() \ No newline at end of file From 72efee1ab937e7d54ea8160bde9b5419207c826b Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 26 May 2026 13:41:15 +0200 Subject: [PATCH 235/254] Add opaque type tests for specialized traits --- .../neg/specialized-trait-opaque-type-fail.scala | 16 ++++++++++++++++ tests/pos/specialized-trait-opaque-type.scala | 13 +++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 tests/neg/specialized-trait-opaque-type-fail.scala create mode 100644 tests/pos/specialized-trait-opaque-type.scala diff --git a/tests/neg/specialized-trait-opaque-type-fail.scala b/tests/neg/specialized-trait-opaque-type-fail.scala new file mode 100644 index 000000000000..68a4e67530a4 --- /dev/null +++ b/tests/neg/specialized-trait-opaque-type-fail.scala @@ -0,0 +1,16 @@ +//> using options -language:experimental.specializedTraits + +inline trait A[T: Specialized](val x: T): + opaque type Special = T + type Ordinary = T + opaque type Special2 = Int + + def foo1: Special = 10 // error: 10 does not conform to T + def bar1: Ordinary = 10 // error: 10 does not conform to T + def baz1: Special2 = 10 // This one is fine + +class B extends A[Int](100): + def foo2: Special = 10 // error: 10 does not conform to Special (same behaviour as ordinary traits) + def bar2: Ordinary = 10 // This one is fine + def baz2: Special2 = 10 // error: 10 does not conform to Special (same behaviour as ordinary traits) + \ No newline at end of file diff --git a/tests/pos/specialized-trait-opaque-type.scala b/tests/pos/specialized-trait-opaque-type.scala new file mode 100644 index 000000000000..d82ec5e1aebd --- /dev/null +++ b/tests/pos/specialized-trait-opaque-type.scala @@ -0,0 +1,13 @@ +//> using options -language:experimental.specializedTraits + +inline trait A[T](val x: T): + opaque type Special = T + + def getSpecial: Special = x + def eatSpecial(y: Special) = "Mmm, that was tasty!" + +class B extends A[Int](100) + +def foo = + val b = B() + println(b.eatSpecial(b.getSpecial)) From 44b4d74ee1fb8809365e9f623c5491f6eb0aedf8 Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 26 May 2026 16:19:14 +0200 Subject: [PATCH 236/254] Fix self type test error checking --- tests/neg/specialized-trait-self-type-problems.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/neg/specialized-trait-self-type-problems.scala b/tests/neg/specialized-trait-self-type-problems.scala index 8f26613fa023..60aa4708005f 100644 --- a/tests/neg/specialized-trait-self-type-problems.scala +++ b/tests/neg/specialized-trait-self-type-problems.scala @@ -8,18 +8,18 @@ class Test inline trait A[T: Specialized]: this: T1 => -inline trait D extends A[Int] // error: self type of D does not conform to that of A // error: self type of A$sp$Int does not conform to that of A +inline trait D extends A[Int] // error: self type of D does not conform to that of A inline trait E extends D inline trait B[T: Specialized]: this: T2 & T1 => -inline trait F extends A[Int], B[Int] // error: self type of F does not conform to that of A // error: self type of B$sp$Int does not conform to self type +inline trait F extends A[Int], B[Int] // error: self type of F does not conform to that of A inline trait C[T: Specialized]: this: T => -inline trait H extends C[Test]: // error self type of H does not conform to that of C // error: self type of C$sp$Int does not conform to self type +inline trait H extends C[Test]: // error self type of H does not conform to that of C this: T3 => class Cl2 extends Test with H with T3 From 74d0e742d8c9f9d42e11dfe4518d9db101612bbc Mon Sep 17 00:00:00 2001 From: Hamish Date: Tue, 26 May 2026 16:19:28 +0200 Subject: [PATCH 237/254] Fix caching issue with macros temporarily --- .../src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 8e56c0f61a7c..232ed5c0b5b1 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -1183,6 +1183,12 @@ class TreeUnpickler(reader: TastyReader, inContext(sourceChangeContext(Addr(0))(using ctx1)) { // avoids space leaks by not capturing the current context + // TODO: Sometimes the annotated source files are wrong when inlining macros + // into inline traits. This seems to fix it as we reload the correct source + // if not cached, but there ought to be a better way. + treeAtAddr.filterInPlace { (addr, _) => + addr.index < statsStart.index || addr.index >= end.index + } val fork = forkAt(statsStart) val stats = fork.readIndexedStats(localDummy, end) val inlinedMembers = (tparams ++ vparams ++ stats).filter(member => Inlines.isInlineableFromInlineTrait(cls, member)) From ced8228916801a3a1294f567b5e2f85323f8fdb3 Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 27 May 2026 18:43:04 +0200 Subject: [PATCH 238/254] No need to inline into specialized trait anonymous class instances; these will later be replaced by $impl$ classes. --- .../dotty/tools/dotc/inlines/Inlines.scala | 80 ++++++++++--------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 18ad8257fead..296f25095ca2 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -351,49 +351,53 @@ object Inlines: checkInlineTraitOverrides(cls.symbol.asClass) val clsOverriddenSyms = cls.symbol.info.decls.toList.flatMap(_.allOverriddenSymbols).toSet val ancestors = inlineTraitAncestors(cls) - val cycleFound = ancestors.exists { parent => - val parentSym = symbolFromParent(parent) - val errorPos = if cls.symbol.ownersIterator.contains(parentSym) then Some(cls.srcPos) // Trying to inline into the tree which defines parentSym (need to catch this separately - // as need to catch it before we inline the second time to avoid tripping an assertion) - else if ctx.inlineTraitState.inlineOrigins(cls.symbol).contains(parentSym) then - val userPos = tpd.enclosingInlineds.last.srcPos // Select the user code that caused this error so we get two errors if there are two problematic inlines, not one - Some(userPos) // Trying to inline into the inlined body of parentSym not in the defn tree - else None // Fine - errorPos.foreach(pos => - report.error(s"Inlining of inline traits looped. Tried to inline ${parentSym} into its own body.", pos) - ) - errorPos.nonEmpty - } + if cls.symbol.isAnonymousClass && ancestors.exists(tree => Specialization.unapply(tree.tpe).exists(_.isSpecialized)) then + cls // No need to inline into specialized trait anonymous class instances; these will later be replaced by $impl$ classes. + else + val cycleFound = ancestors.exists { parent => + val parentSym = symbolFromParent(parent) + val errorPos = if cls.symbol.ownersIterator.contains(parentSym) then Some(cls.srcPos) // Trying to inline into the tree which defines parentSym (need to catch this separately + // as need to catch it before we inline the second time to avoid tripping an assertion) + else if ctx.inlineTraitState.inlineOrigins(cls.symbol).contains(parentSym) then + val userPos = tpd.enclosingInlineds.last.srcPos // Select the user code that caused this error so we get two errors if there are two problematic inlines, not one + Some(userPos) // Trying to inline into the inlined body of parentSym not in the defn tree + else None // Fine + + errorPos.foreach(pos => + report.error(s"Inlining of inline traits looped. Tried to inline ${parentSym} into its own body.", pos) + ) + errorPos.nonEmpty + } - if cycleFound then cls - else { - val newDefs = inContext(ctx.withOwner(cls.symbol)) { - ancestors.foldLeft((List.empty[Tree], impl.body)){ - case ((inlineDefs, childDefs), parent) => - val parentTraitInliner = InlineParentTrait(parent) - // Inline body - val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) - // Need to put the new defs first because we process in linearization order to make overridees correct, - // but we want parent definitions to come first so that if child inline traits refer to values defined in a parent - // inline trait these are defined. - val inlinedDefs1 = parentTraitInliner.expandDefs(overriddenSymbols) ::: inlineDefs - cls.symbol.flags = updateFlagsFromInlinedParent(cls.symbol.flags, parent.symbol.flags) - - val childDefs1 = parentTraitInliner.adaptSuperCalls(childDefs) - (parentTraitInliner.adaptSuperCalls(inlinedDefs1), childDefs1) + if cycleFound then cls + else { + val newDefs = inContext(ctx.withOwner(cls.symbol)) { + ancestors.foldLeft((List.empty[Tree], impl.body)){ + case ((inlineDefs, childDefs), parent) => + val parentTraitInliner = InlineParentTrait(parent) + // Inline body + val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) + // Need to put the new defs first because we process in linearization order to make overridees correct, + // but we want parent definitions to come first so that if child inline traits refer to values defined in a parent + // inline trait these are defined. + val inlinedDefs1 = parentTraitInliner.expandDefs(overriddenSymbols) ::: inlineDefs + cls.symbol.flags = updateFlagsFromInlinedParent(cls.symbol.flags, parent.symbol.flags) + + val childDefs1 = parentTraitInliner.adaptSuperCalls(childDefs) + (parentTraitInliner.adaptSuperCalls(inlinedDefs1), childDefs1) + } } - } - val newbody = newDefs._1 ::: newDefs._2 - val paramAccessors = newbody.filter(_.symbol.is(ParamAccessor)) - - for pacc <- paramAccessors - otherstat <- newbody if !otherstat.symbol.is(ParamAccessor) && otherstat.denot.matches(pacc.denot.asSingleDenotation) - do report.error(s"Inlining of inline trait created name conflict on ${pacc.denot.name}. Constructor parameters of inline receivers may not collide with members of inline traits.", pacc.srcPos) + val newbody = newDefs._1 ::: newDefs._2 + val paramAccessors = newbody.filter(_.symbol.is(ParamAccessor)) + + for pacc <- paramAccessors + otherstat <- newbody if !otherstat.symbol.is(ParamAccessor) && otherstat.denot.matches(pacc.denot.asSingleDenotation) + do report.error(s"Inlining of inline trait created name conflict on ${pacc.denot.name}. Constructor parameters of inline receivers may not collide with members of inline traits.", pacc.srcPos) - val impl1 = cpy.Template(impl)(body = newbody) + val impl1 = cpy.Template(impl)(body = newbody) - cpy.TypeDef(cls)(rhs = impl1) + cpy.TypeDef(cls)(rhs = impl1) } case _ => cls From 47f60d64a91a7f3831ad01d091fb3e3ce16f096b Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 27 May 2026 18:45:44 +0200 Subject: [PATCH 239/254] Fix missing mapping for explicilty mentioned type parameters in inline trait trees --- compiler/src/dotty/tools/dotc/inlines/Inlines.scala | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 296f25095ca2..1a1ddf1474f1 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -920,6 +920,15 @@ object Inlines: case None => Select(this(qual), name) } } + case ident: Ident if ident.isType => + // A type parameter of the inline trait written as an identifier in source (e.g. the `S` + // in `new C[S] {}` if nested inside an inline trait) is not handled by the usual inlining machinery + // because S is not a method type parameter. We have our own way of handling this in types + // in the type map but need to also map the tree reference. + // See tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad.scala. + val mapped = inlinerTypeMap(ident.tpe) + if mapped ne ident.tpe then TypeTree(mapped).withSpan(ident.span) + else ident case tree => tree } From 6b519ac13a435410898a521738537ff7cbc1f5a0 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 28 May 2026 12:03:58 +0200 Subject: [PATCH 240/254] Bump benchmark version --- .../dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala index 11f77d0e7b3e..baf866c10e9a 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/SpecializedTraitsBenchmark.scala @@ -5,7 +5,7 @@ // as well as to kill the bloop server if you are republishing the same version as before. // scala-cli --power bloop exit -//> using scala 3.8.4-RC1-bin-SNAPSHOT-nonbootstrapped +//> using scala 3.9.0-RC1-bin-SNAPSHOT-nonbootstrapped //> using options -language:experimental.specializedTraits package dotty.tools.benchmarks From 27ad62b4bff9971c8eec2c38ccdbd1dfec7c64f4 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 28 May 2026 12:04:29 +0200 Subject: [PATCH 241/254] Fix rogue number --- docs/_docs/reference/error-codes/E232.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/reference/error-codes/E232.md b/docs/_docs/reference/error-codes/E232.md index 62e38853bd4f..da343c50c4a8 100644 --- a/docs/_docs/reference/error-codes/E232.md +++ b/docs/_docs/reference/error-codes/E232.md @@ -2,7 +2,7 @@ title: E228: Illegal Use of Specialized Error kind: Error --- -# E228: Illegal Use of Specialized Error +# E232: Illegal Use of Specialized Error This error occurs when Specialized is used outside of a context bound. From af6b40a7f70592daf2a9968b4ee7ac0652f12941 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 28 May 2026 12:14:09 +0200 Subject: [PATCH 242/254] Remove some old comments --- .../dotc/transform/DesugarSpecializedTraits.scala | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index d1ba29d97a28..28c0cda04ca6 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -678,27 +678,13 @@ end SpecializedTraitState // Cache / only generate once instead of multiple times. // Ideally standardise on either specialization or specializationMap -// TODO: Think carefully about use of primaryConstructor and the other appropriateConstructors call or whatever it was. - // TODO: Need to try with a bigger project with multiple packages later on to see if we get the behaviour that we are expecting to get in terms of the classes that we generate. // TODO: need to test with explicit evidence / our own custom type classes -// trait Vec$Sp[S] extends Vec[S, Int, Int, Int, Int] -// inline trait Two[S: Specialized] extends Vec$sp[S] -// Could potentially copy over the inline based on whether Two is inline or not? Needs some thought. - // In the case of foo[S](a: Vec[S, Int, Int, Int, Int]) I think we ideally do want this because we should be able to get speed gains by accessing the specialized members // TODO: Only specialize if there is some material increase in specialization - I think only if at least one new parameter gets fully specialized // Maybe it is better to not allow partial specializations -- we can think about that. // TODO: Don't synthesize specialized instances for random generic types probably - as Hamza said we want to be able to control the specialization - -// Concerns: -// - The superclass of `C` is a top class, or `C` itself is a top class. - -// If we can manage to get rid of the inheritance there that could be helpful in terms of avoiding multiple values -// BUT: generate a version which is with just inline traits that has this problem as well. -// These implementation classes are type correct as long as we inject the knowledge that a specialization trait -// like `Seq$sp$Int` is equal to its parameterized version `Seq[Int]` From b8de83f80b2dc0654b5b8579ea21306a5f541538 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 28 May 2026 12:17:09 +0200 Subject: [PATCH 243/254] Move impl class erasure to erasure --- .../transform/DesugarSpecializedTraits.scala | 23 +-------------- .../dotty/tools/dotc/transform/Erasure.scala | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 28c0cda04ca6..19abc1e9f573 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -224,18 +224,6 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: this replacement because it's easier to detect the anonymous classes earlier before they undergo too many transforms, and doing it at erasure would be strange given it's a tree transform and not a type transform. */ private def replaceImplementationClassesMap(specializations: SpecializedTraitCache)(using Context) = - val specializeTypeTree: Tree => Tree = tree => - tree match { - case Specialization(spec) => - ctx.specializedTraitState.specializedTraitCache.get.getInterfaceSymbol(spec).map: - specializedSymbol => - if spec.unspecializedTypeArgs.nonEmpty then - AppliedTypeTree(Ident(specializedSymbol.typeRef), spec.unspecializedTypeArgs).withSpan(tree.span) // TODO: Matching on a Specialization and then outputting ATT is weird - maybe have a method on specialization to convert to ATT .toAppliedTypeTree? - else - TypeTree(specializedSymbol.typeRef).withSpan(tree.span) - .getOrElse(tree) - case tree => tree - } def treeMap(tree: Tree): Tree = tree match { /* Replace new Foo[Int] {} with new Foo$impl$Int.asInstanceOf[Foo$sp$Int] @@ -263,16 +251,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: report.error("Anonymous classes acting as instances of Specialized traits may not have additional members; you can make a named object instead if you like.", anon.srcPos) anon.parentCalls match { - case (obj :: parentsOfSpecTrait) :+ (app@Apply(_, _)) if (obj.symbol.owner == ctx.definitions.ObjectClass) && (parentsOfSpecTrait.forall(x => spec.traitSymbol.asClass.parentSyms.exists(p => p == x.symbol.owner))) => - specializations.getImplementationSymbol(spec).map( specializedSymbol => - inContext(ctx.withSource(anon.typeTree.source)) { - Typed( - Select(New(ref(specializedSymbol)), anon.ctor) - .appliedToTypeTrees(spec.unspecializedTypeArgs) - .appliedToArgss(tpd.allArgss(app).tail.nestedMap(_.changeNonLocalOwners(anon.symbol.owner))) // Skip the type params which are not needed - , specializeTypeTree(anon.typeTree)) - }.withSpan(anon.typeTree.span) - ).getOrElse(tree) // We don't replace non-specialized anonymous class instantiations e.g. new Foo[T] where T is defined in the enclosing scope. + case (obj :: parentsOfSpecTrait) :+ (app@Apply(_, _)) if (obj.symbol.owner == ctx.definitions.ObjectClass) && (parentsOfSpecTrait.forall(x => spec.traitSymbol.asClass.parentSyms.exists(p => p == x.symbol.owner))) => tree case _ => report.error("Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like.", anon.srcPos) tree diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index 39bac5f931e5..218f9a166046 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -37,6 +37,7 @@ import core.Mode import util.Property import reporting.* import scala.annotation.tailrec +import dotty.tools.dotc.ast.tpd.* class Erasure extends Phase with DenotTransformer { @@ -809,6 +810,33 @@ object Erasure { } } + /* Erase anonymous instances of specialized traits to $impl$ classes */ + override def typedBlock(tree: untpd.Block, pt: Type)(using Context): Tree = tree.asInstanceOf[Block] match + case AnonymousSpecializationInstance(anon) => + inContext(preErasureCtx) { + Specialization.unapply(anon.typeTree.tpe, anon.typeTree.span).flatMap(spec => { + anon.parentCalls match { + case (obj :: parentsOfSpecTrait) :+ (app@Apply(_, _)) if spec.isSpecialized && (obj.symbol.owner == ctx.definitions.ObjectClass) && (parentsOfSpecTrait.forall(x => spec.traitSymbol.asClass.parentSyms.exists(p => p == x.symbol.owner))) => + val targetImplName = DesugarSpecializedTraits.newImplementationClassName(spec) + val implClass = spec.traitSymbol.enclosingPackageClass.info.decls.lookup(targetImplName) + assert(implClass.exists && implClass.isClass) + val erased = inContext(ctx.withSource(anon.typeTree.source)) { + Typed( + Select(New(ref(implClass)), anon.ctor) + .appliedToTypeTrees(spec.unspecializedTypeArgs) + .appliedToArgss(tpd.allArgss(app).tail.nestedMap(_.changeNonLocalOwners(anon.symbol.owner))) // Skip the type params which are not needed + , anon.typeTree) + }.withSpan(anon.typeTree.span) + Some(erased) + case _ => None + }}) + } match { + case Some(erased) => typedTyped(erased, anon.typeTree.tpe) + case None => super.typedBlock(tree, pt) + } + case _ => super.typedBlock(tree, pt) + + override def typedBind(tree: untpd.Bind, pt: Type)(using Context): Bind = atPhase(erasurePhase): checkBind(promote(tree)) From ff0e8f3d6c482538c3dee78571beaff2fcfa57a9 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 28 May 2026 12:25:49 +0200 Subject: [PATCH 244/254] Switch to specType --- .../transform/DesugarSpecializedTraits.scala | 43 +++++++++++------- ...ecialized-trait-erasure-impl-classes.scala | 36 +++++++++++++++ tests/run/specialized-trait-erasure.scala | 45 +++++++++++++++++++ ...al-class-extend-not-top-of-hierarchy.scala | 13 ++++++ 4 files changed, 122 insertions(+), 15 deletions(-) create mode 100644 tests/run/specialized-trait-erasure-impl-classes.scala create mode 100644 tests/run/specialized-trait-erasure.scala create mode 100644 tests/run/specialized-trait-manual-class-extend-not-top-of-hierarchy.scala diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 19abc1e9f573..fd0df5837159 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -46,6 +46,8 @@ import dotty.tools.dotc.core.DenotTransformers.IdentityDenotTransformer import dotty.tools.dotc.core.Names.TermName import dotty.tools.dotc.util.Spans.spanCoord import dotty.tools.dotc.util.Spans.NoSpan +import dotty.tools.dotc.transform.DesugarSpecializedTraits.specType +import dotty.tools.dotc.transform.DesugarSpecializedTraits.isTopClass class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: @@ -143,7 +145,7 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: private def generateImplementationClassParents(specialization: Specialization, interfaceSymbol: ClassSymbol)(using Context) = val objectParent = defn.ObjectType val traitSpParent = interfaceSymbol.typeRef.appliedTo(specialization.unspecializedTypeParams) // Set using old unspecializedTypeParams and replace after. - val originalTraitSpecializedParent = AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.mapUnspecializedArgs(specialization.unspecializedTypeParams.map(TypeTree(_)))).tpe + val originalTraitSpecializedParent = AppliedTypeTree(Ident(specialization.traitSymbol.typeRef), specialization.mapSpecializedUnspecializedArgs(tr => TypeTree(specType(tr.tpe)), specialization.unspecializedTypeParams.map(TypeTree(_)))).tpe (objectParent, traitSpParent, originalTraitSpecializedParent) private def newImplementationClass(specialization: Specialization, interfaceSymbol: ClassSymbol)(using Context) = @@ -445,7 +447,7 @@ object DesugarSpecializedTraits: // TODO: What happens with this name generation if we have Vec[Vec[T]] for example? We potentially don't have an Ident // TODO: Check what happens here when we have a case where the types being specialized into are user defined instead of primitives or type vars. private def generateName(specialization: Specialization, suffix: String)(using Context) = - val name = (specialization.traitSymbol.name ++ suffix).asTypeName ++ specialization.specializedTypeArgs.map(t => canonicalName(t.tpe)).mkString(str.SPECIALIZED_TRAIT_TYPE_SEP) + val name = (specialization.traitSymbol.name ++ suffix).asTypeName ++ specialization.specializedTypeArgs.map(t => canonicalName(specType(t.tpe))).mkString(str.SPECIALIZED_TRAIT_TYPE_SEP) if specialization.traitSymbol.owner.is(Flags.Package) then name else @@ -456,6 +458,21 @@ object DesugarSpecializedTraits: /*private[transform]*/ def newImplementationClassName(specialization: Specialization)(using Context): TypeName = generateName(specialization, str.SPECIALIZED_TRAIT_IMPL_SUFFIX) + + def isTopClass(s: Symbol)(using Context): Boolean = + (s eq defn.AnyClass) || (s eq defn.AnyValClass) || (s eq defn.ObjectClass) || (s eq defn.AnyRefAlias) + + def specType(tp: Type)(using Context): Type = + def isTopClassOrNothing(s: Symbol): Boolean = + (s eq defn.NothingClass) || isTopClass(s) + + def isSimpleClassType(s: Symbol): Boolean = + s.isClass && !s.is(Flags.Trait) && s.typeParams.isEmpty && s.isStatic + + tp.baseClasses.iterator.find(c => + isSimpleClassType(c) && (isTopClassOrNothing(c) || isTopClassOrNothing(c.asClass.superClass)) + ).map(_.typeRef).get + end DesugarSpecializedTraits /* @@ -540,29 +557,25 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree], val val specializedTypeArgs: List[Tree] = paramToArgList.filter((tParam, tArg) => specializedTypeParams.exists(_ =:= tParam)).map(_._2) // Type arguments provided to parameters that are marked with Specialized at their definition val unspecializedTypeArgs: List[Tree] = paramToArgList.filterNot((tParam, tArg) => specializedTypeParams.exists(_ =:= tParam)).map(_._2) // Type arguments provided to parameters that are not marked with Specialized at their definition - val specializedTypeParamsToTypeArgumentsMap: Map[Type, Tree] = paramToArgList.toMap.filter((k, v) => specializedTypeParams.exists(_ =:= k)) + val specializedTypeParamsToTypeArgumentsMap: Map[Type, Tree] = paramToArgList.toMap.filter((k, v) => specializedTypeParams.exists(_ =:= k)).view.mapValues(tr => TypeTree(specType(tr.tpe))).toMap // TODO: Maybe not efficient val specialization: List[Tree] = traitSymbol.typeParams.map(_.typeRef).map(specializedTypeParamsToTypeArgumentsMap.applyOrElse(_, TypeTree(_))) // TODO: Don't really like this name def constructorTypeParams: List[Type] = traitSymbol.primaryConstructor.rawParamss.head.map(_.typeRef) def unspecializedConstructorParams: List[Symbol] = traitSymbol.primaryConstructor.rawParamss.head.zip(traitSymbol.typeParams).filterNot((constrParam, typeParam) => specializedTypeParams.exists(_ =:= typeParam.typeRef)).map((constrParam, typeParam) => constrParam) def specializedConstructorParamToArgumentTypeMap: Map[Type, Type] = - traitSymbol.primaryConstructor.rawParamss.head.map(_.typeRef).zip(paramToArgList).filter((constrParam, paramArg) => specializedTypeParams.exists(_ =:= paramArg._1)).map((constrParam, paramArg) => (constrParam, paramArg._2.tpe)).toMap + traitSymbol.primaryConstructor.rawParamss.head.map(_.typeRef).zip(paramToArgList).filter((constrParam, paramArg) => specializedTypeParams.exists(_ =:= paramArg._1)).map((constrParam, paramArg) => (constrParam, specType(paramArg._2.tpe))).toMap val hasSpecializedParams: Boolean = specializedTypeParams.nonEmpty - def mapUnspecializedArgs(unspec: List[Tree]): List[Tree] = paramToArgList.foldLeft((List.empty[Tree], unspec))((resUnspec, paramArg) => ((resUnspec, paramArg): @unchecked) match { - case ((result, unspec), (param, arg)) if specializedTypeParams.exists(_ =:= param) => (arg :: result, unspec) + def mapSpecializedUnspecializedArgs(spec: Tree => Tree, unspec: List[Tree]): List[Tree] = paramToArgList.foldLeft((List.empty[Tree], unspec))((resUnspec, paramArg) => ((resUnspec, paramArg): @unchecked) match { + case ((result, unspec), (param, arg)) if specializedTypeParams.exists(_ =:= param) => (spec(arg) :: result, unspec) case ((result, head :: rest), (param, arg)) => (head :: result, rest) })._1.reverse - /* If inline trait Foo[T] has a method taking another Foo[T] there's no point specializing the reference - since the resulting sp$T$ would be the same as the starting trait. */ + /* If inline trait Foo[T: Specialized] has a method taking another Foo[T] there's no point specializing the reference + since the resulting sp$T$ would be the same as the starting trait. Also A[Object] specializes to A. */ def isSpecialized: Boolean = - hasSpecializedParams && typeArguments.exists(!_.tpe.existsPart(part => (part.typeSymbol.isTypeParam) || - (part.typeSymbol eq defn.AnyClass) || - (part.typeSymbol eq defn.ObjectClass) || - (part.typeSymbol eq defn.AnyValClass))) - + hasSpecializedParams && typeArguments.exists(tree => !isTopClass(specType(tree.tpe).classSymbol)) // Only works before erasure. def isFullySpecialized: Boolean = !specializedTypeArgs.exists(_.tpe.existsPart(part => (part.typeSymbol.isTypeParam))) @@ -573,10 +586,10 @@ class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree], val // We should really put that logic in the SpecializedTraitCache because it's at that point that we treat them as the same. override def equals(obj: Any): Boolean = obj.isInstanceOf[Specialization] && obj.asInstanceOf[Specialization].traitSymbol == traitSymbol - && specializedTypeArgs.zip(obj.asInstanceOf[Specialization].specializedTypeArgs).forall((a1, a2) => a1.tpe =:= a2.tpe) + && specializedTypeArgs.zip(obj.asInstanceOf[Specialization].specializedTypeArgs).forall((a1, a2) => specType(a1.tpe) == specType(a2.tpe)) override def hashCode(): Int = - (traitSymbol, specializedTypeArgs.map(_.tpe.widen.dealias.show)).hashCode() // TODO: Consider not using show for this for performance reasons (correctness also?) + (traitSymbol, specializedTypeArgs.map(tr => specType(tr.tpe))).hashCode() override def toString(): String = s"Specialization(${traitSymbol}, ${typeArguments}, ${span})" diff --git a/tests/run/specialized-trait-erasure-impl-classes.scala b/tests/run/specialized-trait-erasure-impl-classes.scala new file mode 100644 index 000000000000..ec56676e9f0a --- /dev/null +++ b/tests/run/specialized-trait-erasure-impl-classes.scala @@ -0,0 +1,36 @@ +//> using options -language:experimental.specializedTraits + +class Animal +class BigCat extends Animal +class Lion extends BigCat + +inline trait A[T: Specialized]: + def foo = Thread.currentThread.getStackTrace()(1).getClassName() + +abstract class methods: + // Specialization takes place + def bar1(a: A[Int]): Unit = assert(a.foo == "A$impl$scala$Int", a.foo) + def bar2(a: A[String]): Unit = assert(a.foo == "A$impl$java$lang$String", a.foo) + def bar3(a: A[Lion]): Unit = assert(a.foo == "A$impl$Animal", a.foo) + def bar4(a: A[BigCat]): Unit = assert(a.foo == "A$impl$Animal", a.foo) + def bar5(a: A[Animal]): Unit = assert(a.foo == "A$impl$Animal", a.foo) + + // SpecType is a top class -> no $impl$ class generated, just an anonymous class. + def bar6(a: A[List[Int]]): Unit = assert(!a.foo.contains("$impl$"), a.foo) + def bar7(a: A[IArray[Boolean]]): Unit = assert(!a.foo.contains("$impl$"), a.foo) + def bar8(a: A[Object]): Unit = assert(!a.foo.contains("$impl$"), a.foo) + def bar9(a: A[AnyVal]): Unit = assert(!a.foo.contains("$impl$"), a.foo) + def bar10(a: A[AnyRef]): Unit = assert(!a.foo.contains("$impl$"), a.foo) + +@main def Test = + val m = new methods {} + m.bar1(new A[Int]() {}) + m.bar2(new A[String]() {}) + m.bar3(new A[Lion]() {}) + m.bar4(new A[BigCat]() {}) + m.bar5(new A[Animal]() {}) + m.bar6(new A[List[Int]]() {}) + m.bar7(new A[IArray[Boolean]]() {}) + m.bar8(new A[Object]() {}) + m.bar9(new A[AnyVal]() {}) + m.bar10(new A[AnyRef]() {}) diff --git a/tests/run/specialized-trait-erasure.scala b/tests/run/specialized-trait-erasure.scala new file mode 100644 index 000000000000..d731b6c30fc1 --- /dev/null +++ b/tests/run/specialized-trait-erasure.scala @@ -0,0 +1,45 @@ +//> using options -language:experimental.specializedTraits + +class Animal +class BigCat extends Animal +class Lion extends BigCat + +inline trait A[T: Specialized]: + def foo = Thread.currentThread.getStackTrace()(1).getClassName() + +abstract class methods: + def bar1(a: A[Int]): Int // A[Int] -> A$sp$Int + def bar2(a: A[String]): String // A[String] -> A$sp$String + def bar3(a: A[Lion]): Lion // A[Lion] -> A$sp$Animal + def bar4(a: A[BigCat]): Lion // A[BigCat] -> A$sp$Animal + def bar5(a: A[Animal]): Lion // A[Animal] -> A$sp$Animal + + def bar6(a: A[List[Int]]): Lion // A[List[Int]] -> A + def bar7(a: A[IArray[Boolean]]): Lion // A[IArray[Bool]] -> A + def bar8(a: A[Object]): Lion // A[Object] -> A + def bar9(a: A[AnyVal]): Lion // A[AnyVal] -> A + def bar10(a: A[AnyRef]): Lion // A[AnyRef] -> A + +@main def Test = + val expectedParamErasure = Map( + "bar1" -> "A$sp$scala$Int", + "bar2" -> "A$sp$java$lang$String", + "bar3" -> "A$sp$Animal", + "bar4" -> "A$sp$Animal", + "bar5" -> "A$sp$Animal", + "bar6" -> "A", + "bar7" -> "A", + "bar8" -> "A", + "bar9" -> "A", + "bar10" -> "A", + ) + + val actualParamErasure = classOf[methods].getDeclaredMethods.iterator + .filter(_.getName.startsWith("bar")) + .map(m => m.getName -> m.getParameterTypes.head.getName) + .toMap + + for (name, expected) <- expectedParamErasure do + val actual = actualParamErasure.getOrElse( + name, sys.error(s"method $name not found on class methods")) + assert(actual == expected, s"$name: expected param type $expected, got $actual") diff --git a/tests/run/specialized-trait-manual-class-extend-not-top-of-hierarchy.scala b/tests/run/specialized-trait-manual-class-extend-not-top-of-hierarchy.scala new file mode 100644 index 000000000000..8ab8450dbdae --- /dev/null +++ b/tests/run/specialized-trait-manual-class-extend-not-top-of-hierarchy.scala @@ -0,0 +1,13 @@ +//> using options -language:experimental.specializedTraits + +class Animal +class Mammal extends Animal +class Binturong extends Mammal + +inline trait A[T: Specialized] + +class B extends A[Binturong] + +@main def Test = + val traits = classOf[B].getInterfaces() + assert(traits.exists(cl => cl.getName() == "A$sp$Animal")) From de2a0bba771bfc205e31615bbe7f839e6bb415fa Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 28 May 2026 12:33:23 +0200 Subject: [PATCH 245/254] Update comment --- .../dotty/tools/dotc/transform/DesugarSpecializedTraits.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index fd0df5837159..698a1e207786 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -496,7 +496,7 @@ object SpecializedTraitCache: type GenInterfaceSymbol = (Specialization, SpecializedTraitCache) => Context ?=> (ClassSymbol, SpecializedTraitCache) type GenImplementationSymbol = (Specialization, ClassSymbol) => Context ?=> ClassSymbol - +// TODO: We don't need to share this between phases anymore and maybe we don't even need it at all. Can also rename. class SpecializedTraitCache( private val newInterfaceSymbols: SpecializedTraitCache.SymbolMap = Map.empty, private val newImplementationSymbols: SpecializedTraitCache.SymbolMap = Map.empty, From a68af37cdcf2ea75df90b951495f2fd142bb607e Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 28 May 2026 12:38:51 +0200 Subject: [PATCH 246/254] Support variance with Specialized traits --- .../dotty/tools/dotc/core/TypeComparer.scala | 18 +++- .../tools/dotc/reporting/ErrorMessageID.scala | 1 + .../dotty/tools/dotc/reporting/messages.scala | 48 ++++++++++ .../transform/DesugarSpecializedTraits.scala | 7 +- .../src/dotty/tools/dotc/typer/Typer.scala | 8 +- docs/_docs/reference/error-codes/E233.md | 89 +++++++++++++++++++ ...pecialized-trait-contravariant-warns.scala | 5 ++ ...ecialized-trait-variance-not-allowed.scala | 42 +++++++++ .../specialized-trait-variance-nothing.scala | 12 +++ tests/run/specialized-trait-variance.scala | 39 ++++++++ 10 files changed, 262 insertions(+), 7 deletions(-) create mode 100644 docs/_docs/reference/error-codes/E233.md create mode 100644 tests/neg/specialized-trait-contravariant-warns.scala create mode 100644 tests/neg/specialized-trait-variance-not-allowed.scala create mode 100644 tests/pos/specialized-trait-variance-nothing.scala create mode 100644 tests/run/specialized-trait-variance.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index e8c05f7bc6c0..87af08f1cb94 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -26,7 +26,10 @@ import Capabilities.Capability import NameKinds.WildcardParamName import MatchTypes.isConcrete import reporting.Message.Note +import reporting.IllegalContravarianceInSpecializedTraitsNote import scala.util.boundary, boundary.break +import dotty.tools.dotc.transform.Specialization +import dotty.tools.dotc.transform.DesugarSpecializedTraits /** Provides methods to compare types. */ @@ -158,7 +161,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def testSubType(tp1: Type, tp2: Type): CompareResult = GADTused = false opaquesUsed = false - if !topLevelSubType(tp1, tp2) then CompareResult.Fail(Nil) + errorNotes = Nil + if !topLevelSubType(tp1, tp2) then CompareResult.Fail(errorNotes.map(_._2)) else if GADTused then CompareResult.OKwithGADTUsed else if opaquesUsed then CompareResult.OKwithOpaquesUsed // we cast on GADTused, so handles if both are used else CompareResult.OK @@ -1933,7 +1937,17 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling && defn.isByNameFunction(arg2.dealias) => isSubArg(arg1res, arg2.argInfos.head) case _ => - if v < 0 then isSubType(arg2, arg1) + if v < 0 then + val isValidSubtype = isSubType(arg2, arg1) + if tp1.classSymbol.isSpecializedTrait + && Specialization.traitParamIsSpecialized(tp1.classSymbol, tparam.paramRef.typeSymbol) + && isValidSubtype + && !(DesugarSpecializedTraits.specType(arg1) eq DesugarSpecializedTraits.specType(arg2)) + then // using contravariance in a way which specialized trait erasure cannot support + addErrorNote(IllegalContravarianceInSpecializedTraitsNote()) + false + else + isValidSubtype else if v > 0 then isSubType(arg1, arg2) else isSameType(arg2, arg1) diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index f4a42091568d..8fb2493cf3a6 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -248,6 +248,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case IllegalIdentifierID // errorNumber: 230 case ConcreteClassHasUnimplementedMethodsID // errorNumer: 231 case IllegalUseOfSpecializedID // errorNumber: 232 + case ContravarianceInSpecializedTraitsLimitationID // errorNumber: 233 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index df91896472bf..d232db1d5303 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -3952,3 +3952,51 @@ final class IllegalUseOfSpecialized(using Context) In this instance it was used in a way which is unsupported, such as trying to create a type synonym or a value with explicit type Specialized[X]. """ + +/** Shows up as a TypeError (in the notes field) if contravariance is attempted + * in a way which is incompatible with specialized traits. */ +final class IllegalContravarianceInSpecializedTraitsNote(using Context) extends Note: + def render(using Context): String = + i""" + This use of contravariance is incompatible with specialized traits. + + Specialized traits achieve a performance gain through a special erasure. + - Primitives are specialized: Foo[Int] erases to Foo$$sp$$Int + - Reference types are specialized to the highest non-top class: Foo[Lion] erases to Foo$$sp$$Animal + - Top classes are erased normally: Foo[Any] / Foo[AnyVal] / Foo[Object] / Foo[AnyRef] erase to Foo. + This means that contravariance patterns that cross these erasure categories will fail at + runtime due to a ClassCastException, so they are not permitted. + + Please see the docs for more information on how specialized traits are erased. + Suggested fixes: + - Make the type of the target site more general e.g. Foo[Object] instead of Foo[Animal]. + - Reconsider if you really need to use Object / Any / AnyRef / AnyVal in your code. + - Remove Specialized from the definition of the corresponding parameter. + """ + + override def covers(other: Note)(using Context): Boolean = + other.isInstanceOf[IllegalContravarianceInSpecializedTraitsNote] + +final class ContravarianceInSpecializedTraitsLimitation(using Context) + extends SyntaxMsg(ContravarianceInSpecializedTraitsLimitationID): + override protected def msg(using Context): String = + i"Type parameter is both Specialized and contravariant. This imposes additional typing restrictions." + override protected def explain(using Context): String = + i"""Specialized traits achieve a performance gain through a special erasure. + - Primitives are specialized: Foo[Int] erases to Foo$$sp$$Int + - Reference types are specialized to the highest non-top class: Foo[Lion] erases to Foo$$sp$$Animal + - Top classes are erased normally: Foo[Any] / Foo[AnyVal] / Foo[Object] / Foo[AnyRef] erase to Foo. + This means that contravariance patterns that cross these erasure categories will fail at + runtime due to a ClassCastException, so they are not permitted. + + For example, treating Foo[Any] as Foo[Animal] via contravariance is not allowed with Specialized. + + Please see the docs for more information on how specialized traits are erased. + + If you accept this limitation you can silence this warning with @nowarn. For example: + + @nowarn("id=E234") + inline trait Foo[-T: Specialized]: + + Otherwise, remove Specialized, or remove the contravariance. + """ diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index 698a1e207786..ecc46c68377d 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -48,6 +48,7 @@ import dotty.tools.dotc.util.Spans.spanCoord import dotty.tools.dotc.util.Spans.NoSpan import dotty.tools.dotc.transform.DesugarSpecializedTraits.specType import dotty.tools.dotc.transform.DesugarSpecializedTraits.isTopClass +import dotty.tools.dotc.reporting.ContravarianceInSpecializedTraitsLimitation class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: @@ -380,6 +381,9 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: // The approach we use for flattening makes this quite tricky: see e.g. tests/neg/specialized-trait-scoped-inside-object-deep-nesting.scala. // In theory can scan the tree to find where to put the generated traits instead, but this still doesn't work cross-CU, so for now we ban. report.error("Specialized traits may not be defined inside classes or traits (this would make them path-dependent which is not currently supported); they may be defined inside objects.", t.symbol.srcPos) + t.symbol.typeParams.foreach: par => + if par.paramVariance.is(Flags.Contravariant) && Specialization.classSpecializedTypeParams(t.symbol).exists(t => t.typeSymbol == par) then + report.warning(ContravarianceInSpecializedTraitsLimitation(), par.srcPos) specializations case Typed(Apply(Select(New(anon),ctor),List()), t: TypeTree) if anon.symbol.isAnonymousClass => (t.tpe, t.span) match { @@ -636,7 +640,8 @@ object Specialization: } def isSpecializedTrait(sym: Symbol)(using Context) = sym.isClass && sym.isAllOf(InlineTrait) && classSpecializedTypeParams(sym).nonEmpty - def isSpecializedMethod(sym: Symbol)(using Context) = sym.isAllOf(InlineMethod) && methodSpecializedTypeParams(sym).nonEmpty + def isSpecializedMethod(sym: Symbol)(using Context) = sym.isAllOf(InlineMethod) && methodSpecializedTypeParams(sym).nonEmpty + def traitParamIsSpecialized(traitSym: Symbol, tParam: Symbol)(using Context) = classSpecializedTypeParams(traitSym).exists(tp => tp.typeSymbol eq tParam) end Specialization class AnonymousSpecializationInstance( diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index cc5337fea990..0247e373bb46 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -4898,13 +4898,13 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer tree } else TypeComparer.testSubType(tree.tpe.widenExpr, pt) match - case CompareResult.Fail(_) => + case CompareResult.Fail(notes) => wtp match case wtp: MethodType => missingArgs(wtp) case _ => typr.println(i"adapt to subtype ${tree.tpe} !<:< $pt") //typr.println(TypeComparer.explained(tree.tpe <:< pt)) - adaptToSubType(wtp) + adaptToSubType(wtp, notes) case CompareResult.OKwithGADTUsed if pt.isValueType && !inContext(ctx.fresh.setGadtState(GadtState(GadtConstraint.empty))) { @@ -5005,7 +5005,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case tree: Closure => cpy.Closure(tree)(tpt = TypeTree(samParent)).withType(samParent) } - def adaptToSubType(wtp: Type): Tree = + def adaptToSubType(wtp: Type, cmpNotes: List[Message.Note] = Nil): Tree = // try converting a constant to the target type tree.tpe.widenTermRefExpr.normalized match case ConstantType(x) => @@ -5074,7 +5074,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else val tree1 = healAdapt(tree, pt) if tree1 ne tree then readapt(tree1) - else err.typeMismatch(tree, pt, failure.notes) + else err.typeMismatch(tree, pt, cmpNotes ++ failure.notes) pt match case _: SelectionProto => diff --git a/docs/_docs/reference/error-codes/E233.md b/docs/_docs/reference/error-codes/E233.md new file mode 100644 index 000000000000..891e441c67ee --- /dev/null +++ b/docs/_docs/reference/error-codes/E233.md @@ -0,0 +1,89 @@ +--- +title: E233: Contravariance in Specialized Traits Limitation +kind: Warning +--- +# E233: Contravariance in Specialized Traits Limitation + +Emitted when a type parameter of an inline trait is defined both Specialized and contravariant, +to remind the user that this comes with a reduction in expressivity. + +Ordinary contravariance works because of erasure, but specialized traits have a special erasure which prevents +some contravariance patterns involving Object, Any, AnyVal and AnyRef. + +The following example illustrates the problem: +```scala +inline trait RecyclingBin[-T: Specialized]: + def recycle(x: T) = println(s"Recycling ${x}") + +def recycleAnInteger(bin: RecyclingBin[Int]) = + bin.recycle(100) + +recycleAnInteger(new RecyclingBin[Anyval]() {}) // RecyclingBin[AnyVal] can be interpreted as RecyclingBin[Int] due to contravariance +recycleAnInteger(new RecyclingBin[Any]() {}) // RecyclingBin[Any] can be interpreted as RecyclingBin[Int] due to contravariance + +// Yet, this erases to: + +def recycleAnInteger(bin: RecyclingBin$sp$Int) = + bin.recycle(100) + +recycleAnInteger(new RecyclingBin() {}.asInstanceOf[RecyclingBin$sp$Int]) // RecyclingBin cannot be cast to RecyclingBin$sp$Int +recycleAnInteger(new RecyclingBin() {}.asInstanceOf[RecyclingBin$sp$Int]) // RecyclingBin cannot be cast to RecyclingBin$sp$Int + +``` +- `RecyclingBin[Any]`, `RecyclingBin[AnyVal]` may not be passed to `RecyclingBin[Int]` whereas normally they would be able to be passed +- `RecyclingBin[Any]`, `RecyclingBin[Object / AnyRef]` may not be passed to RecyclingBin[Paper] whereas normally they would. + +So we impose an additional restriction on contravariance with specialized parameters: +- If `A[F1]` is to be interpreted as `A[F2]` under `A[-T: Specialized]`,we require that `SpecType(F1) = SpecType(F2)`. Given that we also require `F1 >:> F2`, and looking at the definition of SpecType this is roughly equivalent to saying F1 may not be any of the top classes `Any`, `AnyVal`, `AnyRef` unless F2 is also. + +If this restriction is not satisfied, warning E233 is emitted, along with a type error. + +In order that the creator of the contravariant specialized trait is aware of this restriction, the warning E233 is emitted at the point of definition. + +Covariance does not pose the same problem, because it corresponds to interpreting `A[F1]` as `A[F2]` where `F1 <:< F2`. Either`A[F1]` and `A[F2]` both erase to the same type (`A$sp$SpecType(F2)` or `A` if `F1` and `F2` are both top classes), or `A[F1]` erases to `A$sp$F1` and `A[F2]` erases to `A`. But `A$sp$F1` is a subtype of `A` by definition so the upcast will succeed (and upcasts are generally cheap compared to downcasts on the JVM so this is acceptable from a performance perspective). + +--- + +## Example + +```scala sc:fail +inline trait Foo[-T: Specialized] // warning: Type parameter is both Specialized and contravariant. +``` + +### Error + +```scala sc:nocompile +-- [E233] Syntax Warning: tests/run/specialized-trait-variance.scala:19:18 ----- +19 |inline trait Foo[-T: Specialized]: + | ^ + |Type parameter is both Specialized and contravariant. This imposes additional typing restrictions. + |---------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Specialized traits achieve a performance gain through a special erasure. + | - Primitives are specialized: Foo[Int] erases to Foo$sp$Int + | - Reference types are specialized to the highest non-top class: Foo[Lion] erases to Foo$sp$Animal + | - Top classes are erased normally: Foo[Any / AnyVal / Object / AnyRef] erases to Foo. + | This means that contravariance patterns that cross these erasure categories will fail at + | runtime due to a ClassCastException, so they are not permitted. For example, treating Foo[Any] or + | Foo[Object] as Foo[Animal] via contravariance is not allowed with Specialized. + | + | Please see the docs for more information on how specialized traits are erased. + | + | If you accept this limitation you can silence this warning with @nowarn. For example: + | + | @nowarn("id=E233") + | inline trait Foo[-T: Specialized]: + | + | Otherwise, remove Specialized, or remove the contravariance. + | + ---------------------------------------------------------------------------- +``` + +### Solution +Accept downsides and ignore warning as follows, or remove one of Specialized / contravariant. + +```scala sc:compile sc-opts:-Werror +@nowarn("id=E233") +inline trait Foo[-T: Specialized]: +``` diff --git a/tests/neg/specialized-trait-contravariant-warns.scala b/tests/neg/specialized-trait-contravariant-warns.scala new file mode 100644 index 000000000000..68e7f12f1856 --- /dev/null +++ b/tests/neg/specialized-trait-contravariant-warns.scala @@ -0,0 +1,5 @@ +//> using options -language:experimental.specializedTraits -Werror + +// nopos-error: (warning): Type parameter is both Specialized and contravariant. This imposes additional typing restrictions. + +inline trait Bin[-T: Specialized] diff --git a/tests/neg/specialized-trait-variance-not-allowed.scala b/tests/neg/specialized-trait-variance-not-allowed.scala new file mode 100644 index 000000000000..85a08250d999 --- /dev/null +++ b/tests/neg/specialized-trait-variance-not-allowed.scala @@ -0,0 +1,42 @@ +//> using options -language:experimental.specializedTraits + +trait Animal: + def makeNoise: String +class Lion extends Animal: + override def makeNoise = "ROAR!" +class Dog extends Animal: + override def makeNoise: String = "BARK!" + +trait Material +class Paper extends Material +class Newspaper extends Paper + +inline trait MyList[+T: Specialized](val xs: List[T]): + def map[S](f: T => S) = xs.map(f) +inline trait Bin[-T: Specialized]: + def throwAway(x: T) = println(s"Throwing away ${x}") + +def sound(dogs: MyList[Dog]) = + dogs.map(_.makeNoise) + +def throwAwayGeneralPaper(bin: Bin[Paper]) = + val a4 = Paper() + bin.throwAway(a4) + +def main = + /* These ones are not allowed normally */ + val myAnimals: MyList[Animal] = new MyList(List(Dog(), Dog(), Lion())) {} + sound(myAnimals) // error: MyList[Animal] can be interpreted as MyList[Dog] due to covariance + + val myNewspaperWasteBin = new Bin[Newspaper]() {} + throwAwayGeneralPaper(myNewspaperWasteBin) // error: Bin[Newspaper] cannot be interpreted as Bin[Paper] due to contravariance + + /* These ones are specifically banned for specialized traits: */ + val myObjectBin = new Bin[Object]() {} + throwAwayTheNewspaper(myObjectBin) // error: This use of contravariance is not compatible with specialized traits. + + val myAnyBin = new Bin[Any]() {} + throwAwayTheNewspaper(myAnyBin) // error: This use of contravariance is not compatible with specialized traits. + + throwAwayAnInteger(myAnyBin) // error: This use of contravariance is not compatible with specialized traits. + diff --git a/tests/pos/specialized-trait-variance-nothing.scala b/tests/pos/specialized-trait-variance-nothing.scala new file mode 100644 index 000000000000..65fafd4516f1 --- /dev/null +++ b/tests/pos/specialized-trait-variance-nothing.scala @@ -0,0 +1,12 @@ +//> using options -language:experimental.specializedTraits + +inline trait Box[+T: Specialized] + +def checkNothingInt(x: Box[Int]) = println("Good morning") +def checkNothingString(x: Box[String]) = println("Good afternoon") + +@main def Test = + val x = new Box[Nothing] {} + + checkNothingInt(x) + checkNothingString(x) diff --git a/tests/run/specialized-trait-variance.scala b/tests/run/specialized-trait-variance.scala new file mode 100644 index 000000000000..0c7b7889e00a --- /dev/null +++ b/tests/run/specialized-trait-variance.scala @@ -0,0 +1,39 @@ +//> using options -language:experimental.specializedTraits + +import scala.annotation.nowarn + +trait Animal: + def makeNoise: String +class Lion extends Animal: + override def makeNoise = "ROAR!" +class Dog extends Animal: + override def makeNoise: String = "BARK!" + +trait Material +class Paper extends Material +class Newspaper extends Paper + +inline trait MyList[+T: Specialized](val xs: List[T]): + def map[S](f: T => S) = xs.map(f) + +inline trait Bin[-T: Specialized]: + def throwAway(x: T) = println(s"Throwing away ${x}") + +def sound(animals: MyList[Animal]) = + animals.map(_.makeNoise) + +def throwAwayTheNewspaper(bin: Bin[Newspaper]) = + val newspaper = Newspaper() + bin.throwAway(newspaper) + +def throwAwayAnInteger(bin: Bin[Int]) = + val integer = 100 + bin.throwAway(integer) + +@main def Test = + val myDogs: MyList[Dog] = new MyList(List(Dog(), Dog(), Dog())) {} + sound(myDogs) // MyList[Dog] can be interpreted as MyList[Animal] due to covariance + + val myWastepaperBasket = new Bin[Paper]() {} + throwAwayTheNewspaper(myWastepaperBasket) // Bin[Paper] can be interpreted as Bin[Newspaper] due to contravariance + From 7c29fdf121df24ccf8ce5aa28caea882c117788f Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 28 May 2026 12:39:14 +0200 Subject: [PATCH 247/254] Fix inline trait symbol replacement --- .../src/dotty/tools/dotc/transform/Erasure.scala | 12 +++--------- tests/pos/inline-trait-override-overload.scala | 10 ++++++++++ 2 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 tests/pos/inline-trait-override-overload.scala diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index 218f9a166046..42f3c5b5ed47 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -751,15 +751,9 @@ object Erasure { // it needs to happen coordinated with erasure of Specialized traits, so that: // a) we see the erased A$sp$Int traits and can point at their members // b) we make the replacement before boxing in case A.foo is typed with T and B.foo specializes this to e.g. Int - // Otherwise we will end up with Int.unbox(A.foo) instead of directly B.foo - val specializedInterfaceType = qual1.tpe.widenDealias - val newSym = inContext(preErasureCtx) { - val desiredType = tree.asInstanceOf[Select].tpe.widen.dealias - val name = tree.name - specializedInterfaceType.classSymbol.info.findMember(name, specializedInterfaceType) - .matchingDenotation(specializedInterfaceType, desiredType, name).symbol - } - + // Otherwise we will end up with Int.unbox(A.foo) instead of directly B.foo which won't typecheck. + val specializedInterfaceSym = qual1.tpe.widenDealias.classSymbol.asClass + val newSym = inContext(preErasureCtx) { sym.overridingSymbol(specializedInterfaceSym) } qual1.select(newSym) case qual1 if !isJvmAccessible(qual1.tpe.typeSymbol) || !qual1.tpe.derivesFrom(sym.owner) => diff --git a/tests/pos/inline-trait-override-overload.scala b/tests/pos/inline-trait-override-overload.scala new file mode 100644 index 000000000000..644de0420212 --- /dev/null +++ b/tests/pos/inline-trait-override-overload.scala @@ -0,0 +1,10 @@ +inline trait Foo[T]: + def foo(x: T) = x + def foo(x: String) = "Test" + +class Bar extends Foo[Int] + +@main def main = + val x = Bar() + println(x.foo(10)) + println(x.foo("Hello world")) \ No newline at end of file From 552d1b4d73e1ec9e25c4c9266767042d8c1a9bc9 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 28 May 2026 12:39:23 +0200 Subject: [PATCH 248/254] Add two more tests --- tests/pos/specialized-trait-self-as-param.scala | 10 ++++++++++ ...pecialized-trait-multiple-parameter-packs.scala | 14 ++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/pos/specialized-trait-self-as-param.scala create mode 100644 tests/run/specialized-trait-multiple-parameter-packs.scala diff --git a/tests/pos/specialized-trait-self-as-param.scala b/tests/pos/specialized-trait-self-as-param.scala new file mode 100644 index 000000000000..2dce27ad5c53 --- /dev/null +++ b/tests/pos/specialized-trait-self-as-param.scala @@ -0,0 +1,10 @@ +//> using options -language:experimental.specializedTraits +inline trait Trait[T: Specialized](x: Trait[T] | Int): + def do_something(): Unit = + assert(Thread.currentThread.getStackTrace()(1).getClassName().contains("$impl$")) + if x.isInstanceOf[Trait[T]] then + x.asInstanceOf[Trait[T]].do_something() + +@main def Test = + val b = new Trait(new Trait(10) {}) {} + b.do_something() diff --git a/tests/run/specialized-trait-multiple-parameter-packs.scala b/tests/run/specialized-trait-multiple-parameter-packs.scala new file mode 100644 index 000000000000..eb7710b00601 --- /dev/null +++ b/tests/run/specialized-trait-multiple-parameter-packs.scala @@ -0,0 +1,14 @@ +//> using options -language:experimental.specializedTraits + +inline trait A[T: Specialized, S: Numeric](val x: T)(val y: T)(val z: S)(p: S): + val p2 = p + def getArgs = (x, y, z, p2) + +class B(w: Int)(e: String) extends A[String, Int](e)("Y")(100)(w) + +@main def Test = + val b = B(41)("Good Morning") + assert(b.getArgs == ("Good Morning", "Y", 100, 41)) + + val a = new A[Boolean, Long](true)(false)(1000000000)(-10) {} + assert(a.getArgs == (true, false, 1000000000, -10)) From bf0edf82670e1432e4bf2922901d878cb56ee50d Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 28 May 2026 12:39:59 +0200 Subject: [PATCH 249/254] Update docs --- docs/_docs/internals/specialized-traits.md | 272 ++++++++++++--------- 1 file changed, 153 insertions(+), 119 deletions(-) diff --git a/docs/_docs/internals/specialized-traits.md b/docs/_docs/internals/specialized-traits.md index 80002f323099..431331641536 100644 --- a/docs/_docs/internals/specialized-traits.md +++ b/docs/_docs/internals/specialized-traits.md @@ -31,6 +31,9 @@ class Bar extends Foo[Int](42): def f(b: Foo): Int = 37 + Int.unbox(b.foo#1()) // virtual call to foo#1 resolves to bridge method foo#3, which in turn calls actual method foo#2, adding boxing. ``` +// TODO: I think we can rewrite this to talk about boxing maybe. Give a bit more background + + Specialized traits seek to resolve this problem. We also want to avoid pro-actively generating all specializations (as in Scala 2 `@specialized`; code bloat), or monomorphising the whole program (as in C++ templates; this leads to code bloat and long compile times, and is a problematic choice for binary APIs.) @@ -74,68 +77,23 @@ The idea is that we want to specialize vectors on the type parameter `T` in orde - Avoid boxing in the API for values like the result of `scalarProduct` - Specialize on the concrete `Numeric` class instance for `T`, so that calls to `num`'s methods have static targets and can be inlined. -## Terminology and Restrictions +In practice this means we create specialized instance traits of the form `inline trait Vec$sp$Int(elems: Array[Int])` and replace uses of `Vec[Int]` with `Vec$sp$Int`. This replacement happens as part of erasure. How this process works in detail is described below. -A _specialized trait_ is an inline trait that has at least one `Specialized` context bound. +## Expansion of Specialized Traits -A specialized context bound (or its expansion to a context parameter) is only allowed for +A _specialized trait_ is an inline trait that has at least one `Specialized` context bound. A specialized context bound (or its expansion to a context parameter) is only allowed for type parameters of inline methods and inline traits. Regular methods or traits or classes cannot take `Specialized[T]` parameters. -## Creating Specialized Trait Instances -Creation of an object with specialized behaviour can occur in one of two ways: - - Instantiating a `class` or using an `object` which extends the specialized trait, specializing its type parameters. E.g.: -```scala -inline trait Foo[T: Specialized](x: T): - def foo: T = x - -class Bar extends Foo[Int](10): // Type parameter does of course not need to be specified explicitly here (it can be inferred from the value type of 10) - def myMethod = "Hello I am a method" - -// Both Baz and myBar will have specialized instances of foo. -object Baz extends Foo(12) -val myBar = Bar() -``` - - Instantiation of an anonymous class instance directly from the Specialized trait: `new A[Ts](ps1)...(psN) {}` where `A` is a specialized trait and the type parameters `Ts` and term parameters `ps1, ,,, psN` can also be absent. This has a special meaning, as it desugars to instantiating a _specialized instance class_ (see Expansion of Specialized Traits). This comes with the twin advantages that: - - there is no need for the boilerplate of manually defining an object/class to extend the specialized trait - - this specialized instance class is reused every time such an instance is created (there is no proliferation of anonymous classes). - - However, to facilitate this reuse, we must impose the following restrictions. Anonymous class instances acting as instances of Specialized traits: - - can extend only a single specialized trait [0], - - cannot mix in further classes or traits, and - - cannot contain member definitions. - - Should these restrictions be undesirable, the user can always create their own named `object` or `class` extending from a specialized trait (i.e. the first case for creation of a specialized object just above), which does not induce these restrictions. - -[0] Note that of course an anonymous class instance such as `new Foo[Int] {}` where `Foo` extends some other trait `Bar` desugars in the compiler to `new Bar[Int] with Foo[Int] {}`, which means we can't distinguish these two cases. Therefore we begrudgingly allow -`new Bar[Int] with Foo[Int] {}` although there is really no reason to use this in source code because it's exactly the same as writing `new Foo[Int] {}`. We disallow `new Foo[Int] with Bar[Int] {}` however. - -Note: This demonstrates that 'class extends specialized trait' and 'object extends specialized trait' are allowed. However, 'trait extends specialized trait' is not. This is due to the restriction on extending inline traits with parameters by ordinary traits, as discussed in `inline-traits.md`. 'inline trait extends specialized trait' is allowed as not subject to this restriction. - - - - - - -This enables us to implement the following expansion scheme. - - -## Expansion of Specialized Traits - - - - -A type instance of a specialized trait such as `Vec[Tp]` has a special erasure, which depends on the specializing supertype of `Tp`. - -**Definition**: A _simple class type_ is a reference to a static class that does not have type parameters. References to traits and references containing non-static prefixes or refinements are excluded. +**Definition**: A _simple class type_ is a reference to a static class that does not have type parameters. References to traits and references containing non-static prefixes or refinements are excluded. // TODO: This excludes List[_]. **Definition**: A _top class_ is one of `Any`, `AnyVal`, or `Object`. -**Definition**: The _specializing supertype_ `SpecType(Tp)` of a type `Tp` is the smallest simple class type `C` such that - - - `Tp` is a subtype of `C` - - The superclass of `C` is a top class, or `C` itself is a top class. +**Definition**: The _specializing supertype_ `SpecType(Tp)` of a type `Tp` is defined as follows: +- `SpecType(Nothing) = Nothing` + - `SpecType(Tp) =` the smallest (in the sense of smallest set of values that the type contains) simple class type `C` such that: + - `C` is a supertype of `Tp` + - The superclass of `C` is a top class, or `C` itself is a top class. The _erasure_ of `Vec[Tp]` where `SpecType(Tp) = C` is: @@ -147,8 +105,7 @@ If there is more than one specialized type parameter, the specialized instance t An anonymous class instance creation like `new Vec[T](elems) {}` expands to an instance creation `new Vec$impl$TN(elems)` of a new _specialized instance class_ -named `Vec$impl$TN`. Here, `Vec$sp$TN` is the erasure of `Vec[T]` and the class name derives from that trait name by replacing `$sp` with `$impl$`. - +named `Vec$impl$TN`. The class name derives from the trait name by replacing `$sp` with `$impl$`. The specialized instance traits are created on demand the first time they are mentioned in a type. For example, here is the definition of the specialized instance `Vec$sp$Int` for `Vec[Int]`: @@ -202,6 +159,78 @@ inline trait Vec$sp$Int extends Vec[Int]: def scalarProduct(other: Vec$sp$Int): Int ``` + +## Creating Specialized Trait Instances +Creation of an object with specialized behaviour can occur in one of two ways: + - Instantiating a `class` or using an `object` which extends the specialized trait, specializing its type parameters. E.g.: +```scala +inline trait Foo[T: Specialized](x: T): + def foo: T = x + +class Bar extends Foo[Int](10): // Type parameter does of course not need to be specified explicitly here (it can be inferred from the value type of 10) + def myMethod = "Hello I am a method" + +// Both Baz and myBar will have specialized instances of foo. +object Baz extends Foo(12) +val myBar = Bar() +``` + - Instantiation of an anonymous class instance directly from the Specialized trait: `new A[Ts](ps1)...(psN) {}` where `A` is a specialized trait and the type parameters `Ts` and term parameters `ps1, ,,, psN` can also be absent. This has a special meaning, as it desugars to instantiating a _specialized instance class_ (see Expansion of Specialized Traits). This comes with the twin advantages that: + - there is no need for the boilerplate of manually defining an object/class to extend the specialized trait + - this specialized instance class is reused every time such an instance is created (there is no proliferation of anonymous classes). + + However, to facilitate this reuse, we must impose the following restrictions. Anonymous class instances acting as instances of Specialized traits: + - can extend only a single specialized trait [0], + - cannot mix in further classes or traits, and + - cannot contain member definitions. + + Should these restrictions be undesirable, the user can always create their own named `object` or `class` extending from a specialized trait (i.e. the first case for creation of a specialized object just above), which does not induce these restrictions. + +[0] Note that of course an anonymous class instance such as `new Foo[Int] {}` where `Foo` extends some other trait `Bar` desugars in the compiler to `new Bar[Int] with Foo[Int] {}`, which means we can't distinguish these two cases. Therefore we begrudgingly allow +`new Bar[Int] with Foo[Int] {}` although there is really no reason to use this in source code because it's exactly the same as writing `new Foo[Int] {}`. We disallow `new Foo[Int] with Bar[Int] {}` however. + +Note: This demonstrates that 'class extends specialized trait' and 'object extends specialized trait' are allowed. However, 'trait extends specialized trait' is not. This is due to the restriction on extending inline traits with parameters by ordinary traits, as discussed in `inline-traits.md`. 'inline trait extends specialized trait' is allowed as it is not subject to this restriction. + +## Variance and Specialized Traits +Specialized traits may define variance parameters e.g.: +```scala +inline trait MyFunction1[-T1: Specialized, +R: Specialized] +``` + +This would run into a problem in the contravariance case: +```scala +inline trait RecyclingBin[-T: Specialized]: + def recycle(x: T) = println(s"Recycling ${x}") + +def recycleAnInteger(bin: RecyclingBin[Int]) = + bin.recycle(100) + +recycleAnInteger(new RecyclingBin[Anyval]() {}) // RecyclingBin[AnyVal] can be interpreted as RecyclingBin[Int] due to contravariance +recycleAnInteger(new RecyclingBin[Any]() {}) // RecyclingBin[Any] can be interpreted as RecyclingBin[Int] due to contravariance + +// Yet, this erases to: + +def recycleAnInteger(bin: RecyclingBin$sp$Int) = + bin.recycle(100) +recycleAnInteger(new RecyclingBin() {}.asInstanceOf[RecyclingBin$sp$Int]) // RecyclingBin cannot be cast to RecyclingBin$sp$Int +recycleAnInteger(new RecyclingBin() {}.asInstanceOf[RecyclingBin$sp$Int]) // RecyclingBin cannot be cast to RecyclingBin$sp$Int + +// This will fail at runtime. We would rather catch it at compile time. +``` +Therefore we have the following issues: +- `RecyclingBin[Any]`, `RecyclingBin[AnyVal]` may not be passed to `RecyclingBin[Int]` whereas normally they would be able to be passed +- `RecyclingBin[Any]`, `RecyclingBin[Object / AnyRef]` may not be passed to RecyclingBin[Paper] whereas normally they would. + +So we impose an additional restriction on contravariance with specialized parameters: +- If `A[F1]` is to be interpreted as `A[F2]` under `A[-T: Specialized]`,we require that `SpecType(F1) = SpecType(F2)`. Given that we also require `F1 >:> F2`, and looking at the definition of SpecType this is roughly equivalent to saying F1 may not be any of the top classes `Any`, `AnyVal`, `AnyRef` unless F2 is also. + +Covariance does not pose the same problem, because it corresponds to interpreting `A[F1]` as `A[F2]` where `F1 <:< F2`. Either`A[F1]` and `A[F2]` both erase to the same type (`A$sp$SpecType(F2)` or `A` if `F1` and `F2` are both top classes), or `A[F1]` erases to `A$sp$F1` and `A[F2]` erases to `A`. But `A$sp$F1` is a subtype of `A` by definition so the upcast will succeed (and upcasts are generally cheap compared to downcasts on the JVM so this is acceptable from a performance perspective). + + + + + + + -## Specialized Traits in the Compiler -We introduce a new phase `desugarSpecializedTraits` responsible for detecting specializations, generating the necessary `$sp$` and `$impl$` -classes for these specializations, and replacing references to e.g. `Vec[Int]` with `Vec$sp$Int` and `new Vec[Int] {}` with `new Vec$impl$Int`. - -Specialized traits rely on the semantics and implementation of inline traits, so it may seem logical that `desugarSpecializedTraits` would merely generate -the prototypes for the classes, and allow `specializeInlineTraits` to inline the bodies. We *do not* do this. Rather, the phase `desugarSpecializedTraits` directly performs inlining of the parent traits (`Vec[Int]` in the above example) into the -generated `$sp$` traits and `$impl$` classes, sharing the relevant code with the `specializeInlineTraits` phase through -the `Inlines.scala` file. - -This is done because while we want to keep the two phases separate and avoid coupling where possible (thus allowing e.g. specialized traits to be disabled -while maintaining inline traits), implementing specialized traits requires being able to alternate - between the inlining and specializing steps in some cases. Consider the following -example: - -```scala -inline trait D[R: Specialized] - -inline trait C[S: Specialized]: - def w(y: D[S]): Unit = println("w") - -inline trait A[T: Specialized]: - def x(y: C[T]): Unit = println("x") - -class B extends A[Char] -``` -If we run just the specialization part of the specialized trait processing (without the inlining yet), we get: - -```scala -inline trait A$sp$Char extends A[Char] -``` - -Inlining the body of `A` results in: -```scala -inline trait A$sp$Char extends A[Char]: - def x(y: C[Char]): Unit = println("x") -``` -Notice that this generates a reference to `C[Char]` which ought to be specialized, given that `S` is marked as `Specialized` in -the definition of `C`. Therefore we need to run the specialization process again. This time we will generate: -```scala -inline trait C$sp$Char extends C[Char] -``` -Inlining the body of `C` into this trait will create a reference to `D[Char]`, which also ought to be specialized. Thus it is clear that -it is possible to create arbitrarily long chains requiring alternating between specialized trait generation and inline trait inlining. We note that -this problem arises not only with the `$sp$` traits, but also the `$impl$` classes (see `specialized-trait-inlining-causes-implementation-required.scala`). - -To resolve this problem without alternating between and looping the `specializeInlineTraits` and `desugarSpecializedTraits` phases in an inconvenient way, we opt to make: -- `specializeInlineTraits` responsible for inlining inline traits written directly in user code. If a specialized trait creates an inline trait inlining opportunity which is not specialized, this is dealt with by specializeInlineTraits. Further if a user writes `class Bar extends Foo[Int]` where Foo is declared Specialized, `specializeInlineTraits` will do the inlining. -- `desugarSpecializedTraits` responsible for finding specializations and generating the required `$sp$` traits and `$impl$` classes, inlining the parent specialized traits into these classes, and repeating until no more inlining can be performed and no more `$sp$` traits and `$impl$` classes are needed. This phase also performs replacement of e.g. `Vec[Int]` with `Vec$sp$Int` and `new Vec[Int]` with `new Vec$impl$Int`. -- `pruneInlineTraits` responsible for converting inline traits to pure interfaces -- We also need to replace members accessed on inline receivers with the corresponding inlined symbols, and this is done in erasure. This is *whether the inline traits in question come from inline traits in source code or specialized trait expansion, in both cases.* (see the document on inline traits for a more detailed description of this operation and `pruneInlineTraits`). - -In particular this decision means that we run `specializeInlineTraits` before `desugarSpecializedTraits`, as otherwise we may duplicate the inlined bodies of the `$sp$` traits and `$impl$` classes, since we have already inlined them in `desugarSpecializedTraits`. - - -To avoid redundant repeated code generation of the same traits and classes, specialized instance traits and classes are cached. The compiler will put their tasty and classfile artifacts in a special directory -on the class path. Each artifact will contain in an annotation a hash of the contents of the trait from which the instance was derived. Before creating a new specialized instance, the compiler will consult this directory to see whether an instance with the given name exists and whether its hash matches. In that case, the artifacts can be re-used. --> - -## The `Specialized` Type Class - -The `Specialized` Type Class is erased at runtime. Instances -of `Specialized[T]` are created automatically for types that do not contain type variables. ## A Larger Case Study @@ -373,6 +339,9 @@ These could be lifted with additional work. ## Transportation of Specialized through generic code + +The `Specialized` Type Class is erased at runtime. Instances of `Specialized[T]` are created automatically for types that do not contain type variables. + It may surprise you to note that the following is valid. The `Numeric` constraint on `T` is only checked when a concrete type is provided for `S` (and by extension `T`) when instantiating `T2`. ```scala @@ -461,6 +430,71 @@ The definitions are stuck in `A$sp$S$Int$` because it is not inline. This means Side note: Because we make the generated traits inline, we modify the behaviour of inline traits relative to the original semantics from Timothée's thesis, such that inline traits extended by other inline traits are still inlined (instead of inlining only at the first ordinary class extending the family of inline traits). This is necessary so that `A$sp$S$Int` can be made inline and still contain the specialized declarations which we need when we use it as an interface. The original argument for only inlining at the bottom of the hierarchy was to reduce code generation, and that this was sufficient when we only have inline traits, however the additional code generation is only linear in the number of traits in the sequence as we do not inline multiple copies, and we consider this acceptable to implement `Specialized`. + +## Specialized Traits in the Compiler +We introduce a new phase `desugarSpecializedTraits` responsible for detecting specializations, generating the necessary `$sp$` and `$impl$` +classes for these specializations, and inlining into them. It also replaces `new Vec[Int] {}` with `new Vec$impl$Int`. + +The replacement of references to e.g. `Vec[Int]` with `Vec$sp$Int` is done at erasure, because we cannot change signatures before then, and doing it afterwards would not prevent the boxing that we seek to avoid. + +Specialized traits rely on the semantics and implementation of inline traits, so it may seem logical that `desugarSpecializedTraits` would merely generate +the prototypes for the classes, and allow `specializeInlineTraits` to inline the bodies. We *do not* do this. Rather, the phase `desugarSpecializedTraits` directly performs inlining of the parent traits (`Vec[Int]` in the above example) into the +generated `$sp$` traits and `$impl$` classes, sharing the relevant code with the `specializeInlineTraits` phase through +the `Inlines.scala` file. + +This is done because while we want to keep the two phases separate and avoid coupling where possible (thus allowing e.g. specialized traits to be disabled +while maintaining inline traits), implementing specialized traits requires being able to alternate + between the inlining and specializing steps in some cases. Consider the following +example: + +```scala +inline trait D[R: Specialized] + +inline trait C[S: Specialized]: + def w(y: D[S]): Unit = println("w") + +inline trait A[T: Specialized]: + def x(y: C[T]): Unit = println("x") + +class B extends A[Char] +``` +If we run just the specialization part of the specialized trait processing (without the inlining yet), we get: + +```scala +inline trait A$sp$Char extends A[Char] +``` + +Inlining the body of `A` results in: +```scala +inline trait A$sp$Char extends A[Char]: + def x(y: C[Char]): Unit = println("x") +``` +Notice that this generates a reference to `C[Char]` which ought to be specialized, given that `S` is marked as `Specialized` in +the definition of `C`. Therefore we need to run the specialization process again. This time we will generate: +```scala +inline trait C$sp$Char extends C[Char] +``` +Inlining the body of `C` into this trait will create a reference to `D[Char]`, which also ought to be specialized. Thus it is clear that +it is possible to create arbitrarily long chains requiring alternating between specialized trait generation and inline trait inlining. We note that +this problem arises not only with the `$sp$` traits, but also the `$impl$` classes (see `specialized-trait-inlining-causes-implementation-required.scala`). + +To resolve this problem without alternating between and looping the `specializeInlineTraits` and `desugarSpecializedTraits` phases in an inconvenient way, we opt to make: +- `specializeInlineTraits` responsible for inlining inline traits written directly in user code. If a specialized trait creates an inline trait inlining opportunity which is not specialized, this is dealt with by specializeInlineTraits. Further if a user writes `class Bar extends Foo[Int]` where Foo is declared Specialized, `specializeInlineTraits` will do the inlining. +- `desugarSpecializedTraits` responsible for finding specializations and generating the required `$sp$` traits and `$impl$` classes, inlining the parent specialized traits into these classes, and repeating until no more inlining can be performed and no more `$sp$` traits and `$impl$` classes are needed. This phase also performs replacement of e.g. `Vec[Int]` with `Vec$sp$Int` and `new Vec[Int]` with `new Vec$impl$Int`. +- `pruneInlineTraits` responsible for converting inline traits to pure interfaces +- We also need to replace members accessed on inline receivers with the corresponding inlined symbols, and this is done in erasure. This is *whether the inline traits in question come from inline traits in source code or specialized trait expansion, in both cases.* (see the document on inline traits for a more detailed description of this operation and `pruneInlineTraits`). + +In particular this decision means that we run `specializeInlineTraits` before `desugarSpecializedTraits`, as otherwise we may duplicate the inlined bodies of the `$sp$` traits and `$impl$` classes, since we have already inlined them in `desugarSpecializedTraits`. + + +To avoid redundant repeated code generation of the same traits and classes, specialized instance traits and classes are cached. The compiler will put their tasty and classfile artifacts in a special directory +on the class path. Each artifact will contain in an annotation a hash of the contents of the trait from which the instance was derived. Before creating a new specialized instance, the compiler will consult this directory to see whether an instance with the given name exists and whether its hash matches. In that case, the artifacts can be re-used. --> + + + + From 480a97e83c1d20333176f8295a99b43464b45872 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 28 May 2026 13:05:26 +0200 Subject: [PATCH 250/254] Fix indirect ancestors bug for specialized trait anonymous class instances --- .../dotty/tools/dotc/transform/DesugarSpecializedTraits.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala index ecc46c68377d..0f0ca715e7e1 100644 --- a/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala +++ b/compiler/src/dotty/tools/dotc/transform/DesugarSpecializedTraits.scala @@ -254,7 +254,8 @@ class DesugarSpecializedTraits extends MacroTransform, IdentityDenotTransformer: report.error("Anonymous classes acting as instances of Specialized traits may not have additional members; you can make a named object instead if you like.", anon.srcPos) anon.parentCalls match { - case (obj :: parentsOfSpecTrait) :+ (app@Apply(_, _)) if (obj.symbol.owner == ctx.definitions.ObjectClass) && (parentsOfSpecTrait.forall(x => spec.traitSymbol.asClass.parentSyms.exists(p => p == x.symbol.owner))) => tree + case (obj :: parentsOfSpecTrait) :+ (app@Apply(_, _)) if (obj.symbol.owner == ctx.definitions.ObjectClass) && (parentsOfSpecTrait.forall(x => spec.traitSymbol.asClass.baseClasses.exists(p => p == x.symbol.owner))) => + tree case _ => report.error("Anonymous classes acting as instances of Specialized traits may not mix in other traits; you can make a named object instead if you like.", anon.srcPos) tree From 9f1d21ecf9c0b2ca2b649e0e6a1681114375d3cb Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 28 May 2026 13:05:44 +0200 Subject: [PATCH 251/254] Fix typo and add todo to spec --- docs/_docs/internals/inline-traits.md | 2 +- tests/neg/inline-trait-supercall-into-trait.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_docs/internals/inline-traits.md b/docs/_docs/internals/inline-traits.md index 3371491c6363..a5a91578d055 100644 --- a/docs/_docs/internals/inline-traits.md +++ b/docs/_docs/internals/inline-traits.md @@ -265,7 +265,7 @@ this pattern if the inline trait has no parameters. Note that this restriction does not block `inline trait extends trait` or `inline trait extends class` which are allowed with one (other) restriction: - Inline traits may not contain `super` references to classes or non-inline traits. This is because `super` references in scala may only reference - direct parents, and, after inlining, those references that were to direct parents in the original inline traits would now have to point to ancestor + direct parents, and, after inlining, those references that were to direct parents in the original inline traits would now have to point to ancestor // TODO: I think this ought to be fine actually because we can just call into the inline trait classes which are further than 1 hop away. ## Benefits of inline traits diff --git a/tests/neg/inline-trait-supercall-into-trait.scala b/tests/neg/inline-trait-supercall-into-trait.scala index faf19faf43ba..1c71df5cc554 100644 --- a/tests/neg/inline-trait-supercall-into-trait.scala +++ b/tests/neg/inline-trait-supercall-into-trait.scala @@ -2,6 +2,6 @@ trait A: def foo = "A" inline trait B extends A: - override def foo = super.foo // error: Inline trait smay not contain superclass references to classes or non-inline traits + override def foo = super.foo // error: Inline traits may not contain superclass references to classes or non-inline traits class C1 extends B From 9eeb3bb9fcda0ad80b08e5b4cd90c7e9421d45ec Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 28 May 2026 13:06:13 +0200 Subject: [PATCH 252/254] Add specialized trait super call tests --- ...lized-trait-supercall-non-overridden.scala | 6 +++ tests/run/specialized-trait-super-chain.scala | 46 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 tests/pos/specialized-trait-supercall-non-overridden.scala create mode 100644 tests/run/specialized-trait-super-chain.scala diff --git a/tests/pos/specialized-trait-supercall-non-overridden.scala b/tests/pos/specialized-trait-supercall-non-overridden.scala new file mode 100644 index 000000000000..d4f8a9c46c5c --- /dev/null +++ b/tests/pos/specialized-trait-supercall-non-overridden.scala @@ -0,0 +1,6 @@ +//> using options -language:experimental.specializedTraits +inline trait A[T: Specialized]: + def foo = 10 + +inline trait B extends A[Int]: + def bar = super.foo diff --git a/tests/run/specialized-trait-super-chain.scala b/tests/run/specialized-trait-super-chain.scala new file mode 100644 index 000000000000..73bd635f83e9 --- /dev/null +++ b/tests/run/specialized-trait-super-chain.scala @@ -0,0 +1,46 @@ +//> using options -language:experimental.specializedTraits +var hops = 0 + +inline trait A[T: Specialized]: + def foo() = + hops += 1 + "A" + +inline trait B[T: Specialized] extends A[T]: + override def foo() = + hops += 1 + super.foo() + +inline trait C[T: Specialized] extends B[T]: + override def foo() = + hops += 1 + "B" + +inline trait D[T: Specialized] extends C[T]: + override def foo() = + hops += 1 + super.foo() + +inline trait E[T: Specialized] extends D[T]: + override def foo() = + hops += 1 + super.foo() + +inline trait F[T: Specialized] extends E[T]: + override def foo() = + hops += 1 + super.foo() + +class C1 extends F[Int] + +@main def Test = + hops = 0 + val cl = C1() + assert(cl.foo() == "B") + assert(hops == 4) + + hops = 0 + val cl2 = new F[String]() {} + assert(cl.foo() == "B") + assert(hops == 4) + From 1326205331f5aa615579d2bb58c6798766d09083 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 28 May 2026 13:06:20 +0200 Subject: [PATCH 253/254] Test named parameter passing --- .../pos/specialized-trait-parameter-passing-as-named.scala | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tests/pos/specialized-trait-parameter-passing-as-named.scala diff --git a/tests/pos/specialized-trait-parameter-passing-as-named.scala b/tests/pos/specialized-trait-parameter-passing-as-named.scala new file mode 100644 index 000000000000..ac9cd596069b --- /dev/null +++ b/tests/pos/specialized-trait-parameter-passing-as-named.scala @@ -0,0 +1,6 @@ +//> using options -language:experimental.specializedTraits +inline trait A[T: Specialized](parameter: Int): + val x = parameter + +def main = + val y = new A[Int](parameter = 1000) {} From 7b0edea3b9a5eb4b9be4fde2c387cb4e2c03beaf Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 28 May 2026 13:26:19 +0200 Subject: [PATCH 254/254] Add gate test for Specialized inline methods on specialized traits --- .../specialized-trait-inline-method-missing-specialized.scala | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tests/neg/specialized-trait-inline-method-missing-specialized.scala diff --git a/tests/neg/specialized-trait-inline-method-missing-specialized.scala b/tests/neg/specialized-trait-inline-method-missing-specialized.scala new file mode 100644 index 000000000000..0b3f9fffb882 --- /dev/null +++ b/tests/neg/specialized-trait-inline-method-missing-specialized.scala @@ -0,0 +1,2 @@ +inline trait Vec[T: Specialized](val xs: List[T]): + def method[S: Specialized](other: Vec[S]): Vec[(T, S)] // error: Only inline traits and inline functions may take specialized type parameters