From f5ab5189517f080c4efefb87bd9ce4661e45f0ae Mon Sep 17 00:00:00 2001 From: Solal Pirelli Date: Tue, 4 Aug 2026 13:15:27 +0200 Subject: [PATCH 1/3] Optimizer misc improvements --- .../backend/jvm/opt/ClosureOptimizer.scala | 5 ++- .../tools/backend/jvm/opt/CopyProp.scala | 4 +-- .../backend/jvm/opt/OptimizerUtils.scala | 31 +++++++------------ .../jvm/OptimizationBytecodeTests.scala | 21 ++++++++++++- 4 files changed, 37 insertions(+), 24 deletions(-) diff --git a/compiler/src/dotty/tools/backend/jvm/opt/ClosureOptimizer.scala b/compiler/src/dotty/tools/backend/jvm/opt/ClosureOptimizer.scala index 96204fe7a998..01cb30da08bd 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/ClosureOptimizer.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/ClosureOptimizer.scala @@ -240,7 +240,10 @@ class ClosureOptimizer(optimizerUtils: OptimizerUtils, receiverProducers.size == 1 && receiverProducers.head == indy } - def isSpecializedVersion(specName: String, nonSpecName: String) = specName.startsWith(nonSpecName) && specializationSuffix.pattern.matcher(specName.substring(nonSpecName.length)).matches + def isSpecializedVersion(specName: String, nonSpecName: String) = + specName.startsWith(nonSpecName) + && ((specName == "applyVoid" && nonSpecName == "apply" && invocation.owner.startsWith("scala/Function")) + || specializationSuffix.pattern.matcher(specName.substring(nonSpecName.length)).matches) def sameOrSpecializedType(specTp: Type, nonSpecTp: Type) = { specTp == nonSpecTp || { diff --git a/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala b/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala index 1cc721f94bf7..1dccc81e0be3 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala @@ -28,8 +28,8 @@ import org.objectweb.asm class CopyProp(optimizerUtils: OptimizerUtils, callGraph: CallGraph, inliner: Inliner, ts: OptimizerKnownBTypes, settings: OptimizerSettings) { - private val modulesAllowSkipInitialization = - if settings.optAllowSkipCoreModuleInit then optimizerUtils.modulesAllowSkipInitialization else Set.empty + private val modulesAllowSkipInitialization: InternalName => Boolean = + if settings.optAllowSkipCoreModuleInit then n => n.startsWith("scala/") else Set.empty /** * For every `xLOAD n`, find all local variable slots that are aliases of `n` using an diff --git a/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala b/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala index 67b56164bf0b..64b31b5a779b 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala @@ -17,6 +17,7 @@ import org.objectweb.asm.{Handle, Opcodes, Type} * This component hosts tools and utilities used in the optimizer that require access to an `OptimizerKnownBTypes` instance. */ class OptimizerUtils(val ts: OptimizerKnownBTypes) { + private val ScalaPackage = "scala/package$" private val indyLambdaImplMethods: ConcurrentHashMap[InternalName, mutable.Map[MethodNode, mutable.Map[InvokeDynamicInsnNode, asm.Handle]]] = new ConcurrentHashMap @@ -135,9 +136,9 @@ class OptimizerUtils(val ts: OptimizerKnownBTypes) { def runtimeRefClassBoxedType(refClass: InternalName): asm.Type = asm.Type.getArgumentTypes(ts.srRefCreateMethods(refClass).methodType.descriptor)(0) - def isSideEffectFreeConstructorCall(insn: MethodInsnNode): Boolean = { - insn.name == BCodeUtils.INSTANCE_CONSTRUCTOR_NAME && sideEffectFreeConstructors((insn.owner, insn.desc)) - } + def isSideEffectFreeConstructorCall(insn: MethodInsnNode): Boolean = + insn.owner == ScalaPackage || + (insn.name == BCodeUtils.INSTANCE_CONSTRUCTOR_NAME && sideEffectFreeConstructors((insn.owner, insn.desc))) def isNewForSideEffectFreeConstructor(insn: AbstractInsnNode): Boolean = { insn.getOpcode == Opcodes.NEW && { @@ -156,7 +157,7 @@ class OptimizerUtils(val ts: OptimizerKnownBTypes) { // methods that are known to return a non-null result def isNonNullMethodInvocation(mi: MethodInsnNode): Boolean = { isJavaBox(mi) || isScalaBox(mi) || isPredefAutoBox(mi) || isRefCreate(mi) || isRefZero(mi) || AnalysisUtils.isClassTagApply(mi) || - isTupleApply(mi) + isTupleApply(mi) || mi.owner == ScalaPackage } // unused objects created by these constructors are eliminated by pushPop @@ -170,19 +171,6 @@ class OptimizerUtils(val ts: OptimizerKnownBTypes) { (ts.StringRef.internalName, MethodBType(List(ts.StringRef), UNIT).descriptor), (ts.StringRef.internalName, MethodBType(List(ArrayBType(CHAR)), UNIT).descriptor)) - lazy val modulesAllowSkipInitialization: Set[InternalName] = - Set( - "scala/Predef$", - "scala/runtime/ScalaRunTime$", - "scala/runtime/Scala3RunTime$", - "scala/reflect/ClassTag$", - "scala/reflect/ManifestFactory$", - "scala/Array$", - "scala/collection/ArrayOps$", - "scala/collection/StringOps$", - "scala/TupleXXL$" - ) ++ (1 to Definitions.MaxTupleArity).map(n => s"scala/Tuple$n$$") ++ AnalysisUtils.primitiveTypes.keysIterator - private lazy val classesOfSideEffectFreeConstructors: Set[String] = sideEffectFreeConstructors.map(_._1) @@ -229,8 +217,10 @@ class OptimizerUtils(val ts: OptimizerKnownBTypes) { val t = i.getType if (t == AbstractInsnNode.METHOD_INSN) { val mi = i.asInstanceOf[MethodInsnNode] - // invokespecial has, well, special semantics that depend on the class it's being invoked in, see, e.g., https://stackoverflow.com/a/8950564 - if (!allowPrivateCalls && i.getOpcode == Opcodes.INVOKESPECIAL && mi.name != BCodeUtils.INSTANCE_CONSTRUCTOR_NAME) { + if (mi.owner == ScalaPackage) { + // Ignore calls that, e.g., load the Range module -- that's still part of a forwarder + } else if (!allowPrivateCalls && i.getOpcode == Opcodes.INVOKESPECIAL && mi.name != BCodeUtils.INSTANCE_CONSTRUCTOR_NAME) { + // invokespecial has, well, special semantics that depend on the class it's being invoked in, see, e.g., https://stackoverflow.com/a/8950564 numCallsOrNew = 2 // stop here: don't inline forwarders with a private or super call } else { if (isScalaBox(mi) || isScalaUnbox(mi) || isPredefAutoBox(mi) || isPredefAutoUnbox(mi) || isJavaBox(mi) || isJavaUnbox(mi)) @@ -242,8 +232,9 @@ class OptimizerUtils(val ts: OptimizerKnownBTypes) { } } else if (nonForwarderInstructionTypes(t)) { if (i.getOpcode == Opcodes.GETSTATIC) { - if (!allowPrivateCalls && owner == i.asInstanceOf[FieldInsnNode].owner) + if (!allowPrivateCalls && owner == i.asInstanceOf[FieldInsnNode].owner) { numCallsOrNew = 2 // stop here: not forwarder or trivial + } } else { numCallsOrNew = 2 // stop here: not forwarder or trivial } diff --git a/compiler/test/dotty/tools/backend/jvm/OptimizationBytecodeTests.scala b/compiler/test/dotty/tools/backend/jvm/OptimizationBytecodeTests.scala index 7b2145a7e5bf..a40083f0bd48 100644 --- a/compiler/test/dotty/tools/backend/jvm/OptimizationBytecodeTests.scala +++ b/compiler/test/dotty/tools/backend/jvm/OptimizationBytecodeTests.scala @@ -78,7 +78,7 @@ class OptimizationBytecodeTests extends DottyBytecodeTest { val instructions = instructionsFromMethod(meth) for instr <- instructions do instr match { case AsmConverters.Invoke(_, owner, name, _, _) => assert(allowedCalls(owner, name), s"Found invoke to $owner.$name in:\n${instructions.mkString("\n")}") - case AsmConverters.InvokeDynamic(_, name, _, _, _) => assert(false, s"Found dynamic invoke to $name in:\n${instructions.mkString("\n")}") + case AsmConverters.InvokeDynamic(_, _, _, bsm, _) => assert(allowedCalls(bsm.owner, bsm.name), s"Found dynamic invoke to ${bsm.owner}.${bsm.name} in:\n${instructions.mkString("\n")}") case _ => () } } @@ -720,6 +720,25 @@ class OptimizationBytecodeTests extends DottyBytecodeTest { ) + @Test def inlineIntTo = + assertCalls(Calls.noneToClasses("Test"), + "extension (n: Int) { def myTo(m: Int): Range.Inclusive = Range.inclusive(n, m) }; (0 myTo 49)", + returnType = "Range.Inclusive" + ) + + // requires understanding `applyVoid` as a specialization on Function* + @Test def inlineRangeForeachArrayForeach = + assertCalls(Calls.noneToClasses("java/lang/invoke/LambdaMetafactory"), + """ + |final class C() { @noinline def test(): Boolean = false } + |val r = Range.inclusive(0, 49) + |val a = Array.fill(50)(new C()) + |var x = 1 + |r.foreach { n => a.foreach { c => if (c.test()) { x += 1 } } } + |x""".stripMargin + ) + + @Test def inlineSingleAbstractMethod = assertEquivalence( "if i eq null then throw null; 42", From f3cecbb9e04c05079faa4fe35d440d2652fd4102 Mon Sep 17 00:00:00 2001 From: Solal Pirelli Date: Fri, 14 Aug 2026 12:56:29 +0200 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Lukas Rytz --- compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala b/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala index 64b31b5a779b..14270fb42810 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala @@ -17,7 +17,7 @@ import org.objectweb.asm.{Handle, Opcodes, Type} * This component hosts tools and utilities used in the optimizer that require access to an `OptimizerKnownBTypes` instance. */ class OptimizerUtils(val ts: OptimizerKnownBTypes) { - private val ScalaPackage = "scala/package$" + private val ScalaPackageObject = "scala/package$" private val indyLambdaImplMethods: ConcurrentHashMap[InternalName, mutable.Map[MethodNode, mutable.Map[InvokeDynamicInsnNode, asm.Handle]]] = new ConcurrentHashMap From 361363490bdc4b5f55ea85ad48054c8d4aab95e8 Mon Sep 17 00:00:00 2001 From: Solal Pirelli Date: Fri, 14 Aug 2026 13:00:50 +0200 Subject: [PATCH 3/3] Some renames --- .../src/dotty/tools/backend/jvm/opt/CopyProp.scala | 10 +++++----- .../dotty/tools/backend/jvm/opt/OptimizerUtils.scala | 11 ++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala b/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala index 1dccc81e0be3..4edab8e879ce 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala @@ -309,8 +309,8 @@ class CopyProp(optimizerUtils: OptimizerUtils, callGraph: CallGraph, inliner: In // A queue of instructions producing a value that has to be eliminated. If possible, the // instruction (and its inputs) will be removed, otherwise a POP is inserted after val queue = mutable.Queue.empty[ProducedValue] - // Contains constructor invocations for values that can be eliminated if unused. - val sideEffectFreeConstructorCalls = mutable.ArrayBuffer.empty[MethodInsnNode] + // Contains the creation of values that can be eliminated if unused. + val sideEffectFreeCreations = mutable.ArrayBuffer.empty[MethodInsnNode] // instructions to remove (we don't change the bytecode while analyzing it. this allows // running the ProdConsAnalyzer only once.) @@ -398,7 +398,7 @@ class CopyProp(optimizerUtils: OptimizerUtils, callGraph: CallGraph, inliner: In case INVOKESPECIAL => val mi = insn.asInstanceOf[MethodInsnNode] - if (optimizerUtils.isSideEffectFreeConstructorCall(mi)) sideEffectFreeConstructorCalls += mi + if (optimizerUtils.isSideEffectFreeConstructorOrFactoryCall(mi)) sideEffectFreeCreations += mi case _ => } @@ -545,11 +545,11 @@ class CopyProp(optimizerUtils: OptimizerUtils, callGraph: CallGraph, inliner: In def removeConstructorCall(mi: MethodInsnNode): Unit = { toRemove += mi callGraph.removeCallsite(mi, method) - sideEffectFreeConstructorCalls -= mi + sideEffectFreeCreations -= mi changed = true } - for (mi <- sideEffectFreeConstructorCalls.toList) { // toList to allow removing elements while traversing + for (mi <- sideEffectFreeCreations.toList) { // toList to allow removing elements while traversing val frame = prodCons.frameAt(mi) val stackTop = frame.stackTop val numArgs = Type.getArgumentTypes(mi.desc).length diff --git a/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala b/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala index 14270fb42810..e45fa42d6a1a 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala @@ -17,6 +17,7 @@ import org.objectweb.asm.{Handle, Opcodes, Type} * This component hosts tools and utilities used in the optimizer that require access to an `OptimizerKnownBTypes` instance. */ class OptimizerUtils(val ts: OptimizerKnownBTypes) { + // Contains methods that get instances of various standard library modules, such as `Either$`, `Range$`, etc. private val ScalaPackageObject = "scala/package$" private val indyLambdaImplMethods: ConcurrentHashMap[InternalName, mutable.Map[MethodNode, mutable.Map[InvokeDynamicInsnNode, asm.Handle]]] = @@ -136,8 +137,8 @@ class OptimizerUtils(val ts: OptimizerKnownBTypes) { def runtimeRefClassBoxedType(refClass: InternalName): asm.Type = asm.Type.getArgumentTypes(ts.srRefCreateMethods(refClass).methodType.descriptor)(0) - def isSideEffectFreeConstructorCall(insn: MethodInsnNode): Boolean = - insn.owner == ScalaPackage || + def isSideEffectFreeConstructorOrFactoryCall(insn: MethodInsnNode): Boolean = + insn.owner == ScalaPackageObject || (insn.name == BCodeUtils.INSTANCE_CONSTRUCTOR_NAME && sideEffectFreeConstructors((insn.owner, insn.desc))) def isNewForSideEffectFreeConstructor(insn: AbstractInsnNode): Boolean = { @@ -150,14 +151,14 @@ class OptimizerUtils(val ts: OptimizerKnownBTypes) { def isSideEffectFreeCall(mi: MethodInsnNode): Boolean = { isScalaBox(mi) || // not Scala unbox, it may CCE isJavaBox(mi) || // not Java unbox, it may NPE - isSideEffectFreeConstructorCall(mi) || + isSideEffectFreeConstructorOrFactoryCall(mi) || AnalysisUtils.isClassTagApply(mi) } // methods that are known to return a non-null result def isNonNullMethodInvocation(mi: MethodInsnNode): Boolean = { isJavaBox(mi) || isScalaBox(mi) || isPredefAutoBox(mi) || isRefCreate(mi) || isRefZero(mi) || AnalysisUtils.isClassTagApply(mi) || - isTupleApply(mi) || mi.owner == ScalaPackage + isTupleApply(mi) || mi.owner == ScalaPackageObject } // unused objects created by these constructors are eliminated by pushPop @@ -217,7 +218,7 @@ class OptimizerUtils(val ts: OptimizerKnownBTypes) { val t = i.getType if (t == AbstractInsnNode.METHOD_INSN) { val mi = i.asInstanceOf[MethodInsnNode] - if (mi.owner == ScalaPackage) { + if (mi.owner == ScalaPackageObject) { // Ignore calls that, e.g., load the Range module -- that's still part of a forwarder } else if (!allowPrivateCalls && i.getOpcode == Opcodes.INVOKESPECIAL && mi.name != BCodeUtils.INSTANCE_CONSTRUCTOR_NAME) { // invokespecial has, well, special semantics that depend on the class it's being invoked in, see, e.g., https://stackoverflow.com/a/8950564