Skip to content

Inline Traits & Specialized Traits - #26156

Open
starswap wants to merge 2 commits into
scala:mainfrom
starswap:specialized-inline-traits
Open

Inline Traits & Specialized Traits#26156
starswap wants to merge 2 commits into
scala:mainfrom
starswap:specialized-inline-traits

Conversation

@starswap

@starswap starswap commented May 25, 2026

Copy link
Copy Markdown
Contributor

Final year master's project at EPFL. Introduces specialization for Scala 3 as an experimental feature, via:

The latest specification for each of these is included in docs accompanying the PR. To see a demo of the generated code and speed gain offered, try the benchmarks in tools/benchmarks/specializedtraits

How much have you relied on LLM-based tools in this contribution?

Moderately, for debugging and generating some of the benchmark examples.

How was the solution tested?

New automated tests


// Because opaque types can appear in inline traits and these are only allowed to be completed once (otherwise cyclic reference error)
// we need to force the body stats now if we have an inline trait so that we don't complete them twice, once in the LazyBodyAnnot and once
// in the main code.

@starswap starswap Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Check: Is this ok? I'm not sure if we can do this because maybe it is actually necessary to read the contents of the body annotation in the context of the body annotation. It seems to pass tests but I'm not sure if it's ok. Also this code was originally copied from the inline methods case when inline traits got built so it is possible not all of it is needed for inline traits.

@jmesyou
jmesyou force-pushed the specialized-inline-traits branch 4 times, most recently from 2470ef3 to 3c9b4c5 Compare July 8, 2026 04:35
@jmesyou
jmesyou force-pushed the specialized-inline-traits branch 2 times, most recently from 48ea7b5 to 193e164 Compare July 9, 2026 14:22
@bracevac
bracevac marked this pull request as ready for review July 9, 2026 14:28
@Gedochao
Gedochao requested a review from jchyb July 9, 2026 14:30

@jchyb jchyb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is incredible work, with a staggering amount of corner cases considered and tested (and well documented!). Thank you so much! Honestly, this is mostly mergeable already, but I did come up with some questions/concerns.

This is a huge amount of changes, and I was only assigned this last week (although I was familiar with the earlier proposals for specializations in Scala 3). All of this is to say, I might not have all of the context behind the project here, so please don't hesitate to correct me. I also still saw some leftover TODOs, and I tried to reply to most of them (generally, I think it's best if we keep those to the minimum when we merge).

I should mention also that for now I only skimmed over the TypeErasure.scala and the Inlines.scala parts, so I might have more suggestions for those later on.

One more NIT is that the code here, especially with the comments, can be very wide, which can make it hard to read depending on the device. Currently in the repo we don't have a standardized formatting unfortunately, but there were plans at some point to have scalafmt enabled. So for especially wide code like

case Specialization(spec) if spec.isFullySpecializedToTopClassesOrNothing => specializations.addErasedImplementation(spec) // We never inline into anonymous class instances (avoids cycles in inline trait inlining), so 
                                                                                                                           // all anonymous class instances must have a non-anonymous class final representation as an $impl$ class.

I think it's best to be proactive and uninline the comment:

// We never inline into anonymous class instances (avoids cycles in inline trait inlining), so
// all anonymous class instances must have a non-anonymous class final representation as an $impl$ class.
case Specialization(spec) if spec.isFullySpecializedToTopClassesOrNothing => specializations.addErasedImplementation(spec)

since I don't really know if scalafmt would be able to work with this (it might do something way uglier!). Most of the code is fine of course, I'm only speaking here mostly about a few examples, most of them in DesugarSpecialisedTraits

(Also sorry about the late-night notification! I don't trust GitHub nor my laptop to keep my comments saved until the morning...)

Comment on lines +74 to +75
if tree.symbol.isInlineTrait then
inlineInlineTraitsIfNew(Inlines.checkAndTransformInlineTrait(tree))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems like an repeat of the first case case tree: TypeDef if tree.symbol.isInlineTrait (unreachable code)

Comment on lines +246 to +253
// TODO: Not sure why we needed this function - it doesn't seem to make any difference to the test results
def flattenTree(inlinedTree: Tree): Tree = inlinedTree match {
case it@Inlined(call, bindings, expansion) =>
val callTrace = Inlines.inlineCallTrace(tree.symbol, inlinedTree.sourcePos)(using ctx.withSource(inlinedTree.source))
cpy.Inlined(it)(callTrace, bindings, expansion)(using inlineContext(it))
case Block(stats, expr) => Block(stats, flattenTree(expr))
case t => t // if inlining failed due to max inlines reached
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The nested Inlined nodes can be generally useful for reporting/checks etc., so if there is no need for it, maybe this could be removed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It indeed did not make a single difference to any of the testcases so I removed it

// specializeInlineTraits is responsible for inlining into D because it's not $sp$ or $impl$
// Because this code is synthetic we won't run the phase on this code as part of the usual
// megaphase transform so we need to do it manually.
transformFollowing(inlined)(using ctx.fresh.setInlineTraitState(ctx.inlineTraitState.copyInPhase(InlineTraitState.InlineContext.InlineTraits)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This might be too much to change at this point (I don't know the timeline of the project), but much of the comments here are concerned about the MegaPhase and MiniPhase structure. Considering we only have List(DesugarSpecialisedTraits, SpecialiseInlineTraits) in the MegaPhase here, and both of those operate on the same trees, and are closely coupled (requiring a transformFollowing call the next phase here), I feel like it would be the simplest to just have those as one phase, avoiding the issues/concerns (reading the docs, it seems like those started as amore legitimate latter transform phases and then were moved as problems were discovered, so I understand how we arrived at this point).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I did do some experimenting with this. However, I decided to specifically leave this these two as separate to try and distinguish the work between inline traits and specialized traits. DesugarSpecialisedTraits is specifically related to the generation of specialized implementations bounded by Specialized, while SpecializeInlineTraits is related both to inline and specialized traits. Could we leave this as follow on change as it might be too complicated of a refactor? i.e. group DesugarSpecialisedTraits and SpecialiseInlineTraits as a single pass.

isInlineable(tree.symbol)
&& !tree.tpe.widenTermRefExpr.isInstanceOf[MethodOrPoly]
&& StagingLevel.level == 0
def needsInlining(tree: Tree)(using Context): Boolean =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I feel like we should split up the logic for inlining for methods and traits into different files (e.g. keep Inlines.scala for methods and add TraitInlines.scala etc.). Here we have a needsInlining check that covers both cases, but then we have to check what the actual tree is and delegate it to the correct inlining procedure anyway, which themselves are totally different. Not to mention the Inlines.scala is already too big and confusing to navigate through (if there is any reused logic between the 2 variants, I think it can stay here)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm going to give this a try. The current state where we check whether needs inlining is not very elegant. At first glance, it does not look like there's too much shared logic but there is going to be some minor duplication.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

After some experimenting, this turned out to be a bigger than expected change. Perhaps like the MegaPhase change, we could do it as a follow-on? I can create issues to track this items, the MegaPhases one and other remaining TODOs in this PR

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sure, that sounds good, although from the 2 things I would give some more priority to the Inlines.scala split, since many different contributors are bound to touch that file for things unrelated to inline traits (and it shouldn't be too difficult to split, I'd mainly want to keep InlineParentTraits, inlineParentInlineTraits and their utils out of Inlines.scala to keep things manageable).

Comment on lines +739 to +743
// TODO: This gets around the fact that inline traits doesn't define inlinedMethod
// correctly but maybe there is a better way. Ultimately inline traits are not
// inline methods!
val oldOwners = if (inlinedMethod.exists) then inlinedMethod :: Nil else Nil
val newOwners = if (inlinedMethod.exists) then ctx.owner :: Nil else Nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A little confused around the changes here - aren't all inlined traits inlined in DesugarSpecialisedTraits? Or is it just the specialized ones, to do it early?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

DesugarInlineTraits only generates the specialized class implementation for traits that have a type parameter with a Specialized bound, while the remaining passes do the lifting of actually inlining the defs from an inline trait into their respective sites.

Using this class as an example:

//> using options -language:experimental.specializedTraits

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)

Without the Specialized bound, x has the following tree after erasure:

val x: Foo =
{
       final class $anon() extends Object(), Foo(Int.box(42)) {
       private val Foo$$x: Int = 42
       override def foo(): Int = this.Foo$$x
       override def foo(): Object = Int.box(this.foo())
         
new Object with Foo {...}():Foo
}

whereas with the bound, there is specialized implementation generated with inline definitions:

  class Foo$impl$scala$Int(Foo$impl$scala$Int$$x: Int) extends Object(),
    Foo$$sp$scala$Int(), Foo(Int.box(this.Foo$impl$scala$Int$$x)), 
    Foo$$sp$scala$Int {
    private val Foo$$x: Int = this.Foo$impl$scala$Int$$x
    override def foo(): Int = this.Foo$$x
    private val Foo$impl$scala$Int$$x: Int
    override def foo(): Object = Int.box(this.foo())
  }

val x: Foo$$sp$scala$Int = new Foo$impl$scala$Int(42):Foo$$sp$scala$Int

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I somewhat misunderstood what the TODO comment was talking about and thought for a little bit we did some additional work in the Inlining phase. Sorry about that (and thank you for explaining).

Comment on lines +460 to +462
// TODO: Perhaps move this to be a method on Specialization (or maybe we consider it a property of the phase and not belonging to the specialization I don't know)
def newSpecializedTraitName(specialization: Specialization)(using Context): TypeName =
generateName(specialization, str.SPECIALIZED_TRAIT_SUFFIX)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agreeing with the comment, better to move those into Specialization

end SpecializedTraitCache

/* Represents an application traitSymbol[typeArguments] */
class Specialization(val traitSymbol: Symbol, val typeArguments: List[Tree], val span: Span)(using Context): // TODO: As mentioned in tm above, maybe we can get away with List[Type] and remove a lot of the needless .tpe and TypeTree calls

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the current way is fine

Comment on lines +603 to +607
// 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 =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If the Specialization of two applications results in the same trees being generated (and that seems to be the case), then I think we can call them equal without any concerns here - I think the TODO can be removed

(params, args) => params.map(_.typeRef.asInstanceOf[Type]).zip(args)
).flatten

// TODO: Can we share these with the original Specialization? General Specialization superclass and then Method and Trait specializations below that? It ended up seeming a bit annoying but maybe there's a way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we could put paramToArgList into the constructor of the trait that includes the val specializedTypeArgs and share it that way, but that does seem indeed annoying so the current repetition is ok in my opinion

Comment on lines +1073 to +1107
// drop Foo[Int] leading to duplicate Foo$sp$Int
val TypeDef(_, implInit: Template) = cdef: @unchecked

// 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 && !cls.isRawSpecializedTraitImplementationClass 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we split the additions here into another method (like handleClassDefSpecialisations)? this is pretty independent from the rest of the method and the comments assume we know the code below refers to specialization

@jmesyou
jmesyou force-pushed the specialized-inline-traits branch from 193e164 to e32d485 Compare July 16, 2026 00:44
@jmesyou

jmesyou commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Fixed compilation issues with error codes before addressing comments by @jchyb (:

@jmesyou
jmesyou force-pushed the specialized-inline-traits branch from 8294896 to ab507a7 Compare July 22, 2026 05:35
Comment on lines +25 to +26
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
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
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

I think the typo is causing the CI crashes here. But it also exposes a small issue with how ErrorTypes are handled, with the specType method crashing on them. ErrorTypes are types used by the compiler in places that error out, so that a phase like Typer is able to report multiple errors, not just one and they don't conform to the regular type hierarchy.

Besides the fix above, could you add an additional neg test with something like:

//> using options -experimental -language:experimental.inlineTraits

object Snippet {
  inline trait RecyclingBin[-T: Specialized]:
      def recycle(x: T) = println(s"Recycling ${x}")

  def recycleAnInteger(bin: RecyclingBin[Int]) = bin.recycle(100)
  def recycleAnInteger(bin: Int) = () // second overload forces overload resolution

  recycleAnInteger(new RecyclingBin[Anyval]() {}) // Anyval is a typo for AnyVal
}

and a fix for it (I think returning a defn.ObjectClass.typeRef for ErrorTypes would be fine)

@jmesyou jmesyou Jul 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, thanks! Let me fix that.

Edit: Fixed

@jmesyou
jmesyou force-pushed the specialized-inline-traits branch 4 times, most recently from 42bf71a to 500bb47 Compare July 28, 2026 07:04
@jmesyou
jmesyou force-pushed the specialized-inline-traits branch 2 times, most recently from bd3e767 to 3cb72a8 Compare July 29, 2026 04:17
@jmesyou
jmesyou force-pushed the specialized-inline-traits branch from 3cb72a8 to 4324ca7 Compare July 29, 2026 04:28
@jmesyou

jmesyou commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

@jchyb I think the change is ready for another look. With the exception of splitting the logic between inline methods and inline traits into separate files and the grouping of MegaPhase into a single pass. I did a pretty big syntactical refactor of DesugarSpecializedTraits, so hopefully it is an easier read now.

@jmesyou

This comment was marked as resolved.

Comment thread docs/_docs/reference/error-codes/E234.md Outdated
@jmesyou
jmesyou force-pushed the specialized-inline-traits branch 3 times, most recently from 57e2aab to e1f8afb Compare August 4, 2026 04:00
@jmesyou

jmesyou commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

New test failure with:

tests/run-macros/tasty-extractors-owners

Possibly unrelated? Fixed

@SolalPirelli

Copy link
Copy Markdown
Contributor

The PR changes the .check file, and the output matches the .check that's on main, so revert those changes?

@jmesyou
jmesyou force-pushed the specialized-inline-traits branch 2 times, most recently from 3d3c968 to 261b73d Compare August 4, 2026 13:22

@jchyb jchyb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Apologies for awkward review timing. The DesugarSpecialisedTraits are way easier to read now - thank you for adjusting that. I still have some suggestions for some of the TODOs - I fear if we don’t resolve them now they will continue to stay unresolved, especially those pointing to more minor concerns. There are a couple that point to bigger issues/concerns, which I feel like are actually okay too leave in here for the added context (or for the MiniPhase merger later), so I didn’t tag them. I think after this round we’ll be able to merge.

inline val LOCALDUMMY_PREFIX = "<local " // owner of local blocks
inline val ANON_CLASS = "$anon"
inline val ANON_FUN = "$anonfun"
inline val SPECIALIZED_TRAIT_SUFFIX = "$$sp" // TODO: Added an extra $ to avoid name conflict with scala 2 specialization when we do isSpecializedTraitInterfaceName but maybe ideally there's a better way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's remove the TODO

Suggested change
inline val SPECIALIZED_TRAIT_SUFFIX = "$$sp" // TODO: Added an extra $ to avoid name conflict with scala 2 specialization when we do isSpecializedTraitInterfaceName but maybe ideally there's a better way.
inline val SPECIALIZED_TRAIT_SUFFIX_SCALA_3 = "$$sp" // Added an extra $ to avoid name conflict with scala 2 specialization.

override def transformTypeDef(tree: TypeDef)(using Context): Tree =
// We need to inline recursively because inlining may create further opportunities for inlining.
// Notably this does limit the composition potential of this miniphase.
// TODO: We might be able to fix that with transformFollowing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Recursively inlining is fine, let's remove the TODO (Especially if we plan to merge this into a single phase anyway)

Suggested change
// TODO: We might be able to fix that with transformFollowing

Comment on lines +891 to +892
// TODO: If we do have a general specialization superclass that links MethodSpecialization and Trait Specialization
// then we can put this in the general superclass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's remove the TODO, current differentiation is fine

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 with RefChecks

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Some duplication is fine, if the if check below completely lines up with another in RefChecks, we could try to extract that one into a global scope and call RefChecks.hasConflictingMembers in both places, but if not, I think we can remove the TODO.

Comment on lines +445 to +447
// TODO: Decide if we would rather make the cache itself mutable and have the container variable as a val,
// rather than creating several new versions and having the container variable as a var.
specializedTraitCache = specializedTraitCache2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Leftover TODO from previous review round, var is fine, let's remove the TODO

Comment on lines +623 to +626
// TODO: Once we have actual caching depending on how it's implemented, maybe we don't want to call this the cache anymore
// Maybe you can implement caching to use this directly. Also, if we want to change it to make it mutable maybe we can
// simplify it a bit. I think we still need the multiple levels because we need to be able to do multiple iterations of
// specialization symbol generation and inlining if we have specialized traits that depend on other specialized traits.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think if/when caching is added, it will be more obvious what to do with this, but I think currently the name is fine.

Let's remove the TODO.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I experimented with this a bit, my intuition was that the cache should be mutable as part of the last review round but decided similarly that it will become more obvious which is the better approach when caching is actually implemented.

Comment on lines +741 to +742
// TODO: Maybe can improve the name of this field
val specialization: List[Type] =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe something simple but descriptive like:

Suggested change
// TODO: Maybe can improve the name of this field
val specialization: List[Type] =
// Type params mapped to specialised arguments or left alone (when Specialized isn't used)
val allTypeParamsRemapped: List[Type] =

(just a suggestion, I mostly want to remove the TODO and stick with something)

Comment on lines +809 to +811
// TODO: This gets around the fact that inline traits doesn't define inlinedMethod
// correctly but maybe there is a better way. Ultimately inline traits are not
// inline methods!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe:

Suggested change
// TODO: This gets around the fact that inline traits doesn't define inlinedMethod
// correctly but maybe there is a better way. Ultimately inline traits are not
// inline methods!
// This is reused through InlineTraitAncestors for inline traits, so inlinedMethod might not exist there

Comment on lines +1067 to +1068
// TODO: This inlines also some calls to inline defs that were made in the inline trait body, is that ok?
val rhs1 = Inlined(tpd.ref(parentSym).withSpan(parent.span), Nil, inlined(rhs)._2.withSpan(parent.span).cloneIn(parentSym.source)).withSpan(parent.span)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I suspect it could cause some issues, but can't say for sure. Ideally this would be Inlining-only (and Typer, for transparent inlines). I suspect that if something comes up its should be easy enough to change (so we can leave the TODO intact for now, I think)

Comment on lines +1744 to +1747
// TODO: Maybe we don't want to import by default; maybe we do but we would rather not create another special case ImportFns for the importing.
// Problem which required the special case: the new Specialized lives in scala.specialize. This is to avoid conflict with the Scala2 specialized annotation.
// Therefore it is not imported by default with everything else that lives in the scala package.
private val SpecializeImportFns: RootRef =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's stick to the current way and remove the TODO:

Suggested change
// TODO: Maybe we don't want to import by default; maybe we do but we would rather not create another special case ImportFns for the importing.
// Problem which required the special case: the new Specialized lives in scala.specialize. This is to avoid conflict with the Scala2 specialized annotation.
// Therefore it is not imported by default with everything else that lives in the scala package.
private val SpecializeImportFns: RootRef =
// The new Specialized lives in scala.specialize. This is to avoid conflict with the Scala2 specialized annotation.
// Therefore it is not imported by default with everything else that lives in the scala package. so we additionally import it here
private val SpecializeImportFns: RootRef =

@jmesyou
jmesyou force-pushed the specialized-inline-traits branch from 261b73d to 71e948c Compare August 12, 2026 06:30
@jmesyou

jmesyou commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

@jchyb Thanks for taking a look through the change again (: I have removed all the TODOs that you've listed in the latest push. I also reorganized and cleaned up inline-traits.md as this latest push. It previously had some internal notes appended. I have since relocated those notes into their appropriate sections. That leaves the remaining TODOs grouped into two issues that I will open:

  1. Phasing of trait inlining and specialized trait generation
  2. Splitting trait inlining into its own respective file TraitInlining.scala from Inlining.scala

Other than those, there are no remaining TODOs

@SolalPirelli
SolalPirelli requested a review from jchyb August 12, 2026 08:58
@jmesyou
jmesyou force-pushed the specialized-inline-traits branch from 71e948c to fb4defc Compare August 12, 2026 17:07
@jmesyou

jmesyou commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Resolved merge conflicts

edit: looks like the test failure is a test machine failure of some kind

@jchyb jchyb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just noticed some more TODO's with simple fixes (or, in the case of the test ones, maybe we don't need to worry about type inference quirks). Sorry about not catching them earlier. I promise after this we can merge.

Comment on lines +982 to +986
override protected def inlineCopier: tpd.TreeCopier = new TypedTreeCopier() {
// TODO: Timothée left this comment: "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)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tested locally and all tests pass, it seems that we don't need to retype here

Suggested change
override protected def inlineCopier: tpd.TreeCopier = new TypedTreeCopier() {
// TODO: Timothée left this comment: "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 inlineCopier: tpd.TreeCopier = new TypedTreeCopier()

Comment on lines +14 to +15
// TODO: Is this what we want? Should it be Foo?
// need to discuss.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// TODO: Is this what we want? Should it be Foo?
// need to discuss.

values.foldRight[List[T]](Nill())(:+:.apply)

@main def Test =
val xs: List[Double] = :+:(9.1, :+:(68.52, :+:(18.4, :+:(83.5, Nill[Double]())))) // TODO : Can we prevent the need for an explicit type here or at least make it clearer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
val xs: List[Double] = :+:(9.1, :+:(68.52, :+:(18.4, :+:(83.5, Nill[Double]())))) // TODO : Can we prevent the need for an explicit type here or at least make it clearer
val xs: List[Double] = :+:(9.1, :+:(68.52, :+:(18.4, :+:(83.5, Nill[Double]()))))

}
zip(this, other)

def foreach[S](f: T => Unit): Unit = (this: List[T]) match { // TODO: Can we avoid the need to cast this to List[T] here? Should it not already be of that type?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
def foreach[S](f: T => Unit): Unit = (this: List[T]) match { // TODO: Can we avoid the need to cast this to List[T] here? Should it not already be of that type?
def foreach[S](f: T => Unit): Unit = (this: List[T]) match {

@jmesyou
jmesyou force-pushed the specialized-inline-traits branch from fb4defc to d68b2b1 Compare August 13, 2026 14:23
timotheeandres and others added 2 commits August 13, 2026 11:02
Co-authored-by: Hamish Starling <hamishstarling@hotmail.co.uk>
Co-authored-by: James You <james.you@protonmail.com>
Co-authored-by: James You <james.you@protonmail.com>
@jmesyou
jmesyou force-pushed the specialized-inline-traits branch from d68b2b1 to 4b63cde Compare August 13, 2026 15:03
@jmesyou

jmesyou commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Updated + I found a few more and cleaned up those too. I probably should've done a thorough grep beforehand, sorry about that. I have compiled a list of the remaining TODOs, I think most have been covered to some extent as part of code review but all are worth keeping.

❯ git log -2 -p | grep "TODO"
+      // TODO: Show errors where they actually show up in the inline trait and not at the type of the user 
+    // TODO: Depending on how we ultimately organize the phasing
+  // TODO: I reckon to get the best miniphase style processing we can do everything except 
+  // TODO: Do we want to compress this more by adopting e.g. specializedTypeNames from scala 2? 
+          else if (tree.symbol.isTypeParam && tree.symbol.owner.isClass) tree.span // TODO is this the correct span
-            else if (tree.symbol.isTypeParam && tree.symbol.owner.isClass) tree.span // TODO is this the correct span? -- indentation change
+      // TODO: We need to stop inlining if there is a non-inline trait or class that sits between the inline trait and the current class.
+        // TODO: This inlines also some calls to inline defs that were made in the inline trait body, is that ok? 
+      // TODO: check that things are inlined properly

@jchyb

jchyb commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Thank you so much! The PR looks good now. There is a weird CI issue with that doesn't seem to be related to anything here: assertion failed: wrong source set for $this.self # -1 of class dotty.tools.dotc.ast.Trees$Select, set to compiler/src/dotty/tools/dotc/core/Contexts.scala but context had compiler/src/dotty/tools/dotc/core/TypeApplications.scala - I can reproduce it locally, and bringing back the custom inlineCopier doesn't fix it, so it has to be caused by something from the rebase. I'll try to investigate more when I have time

@jmesyou

jmesyou commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

I was able to reproduce the test failures locally on the main branch. Did a quick investigation and it looks like the problematic commit is a4e1b441306d129813fdb8c5739edac0bfe7b9cd. Prior commits have clean tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants