diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala b/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala index ffff526f22ac..b0be064bd475 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala @@ -32,7 +32,7 @@ import dotty.tools.dotc.util.SrcPos * @version 1.0 * */ -trait BCodeBodyBuilder(val primitives: ScalaPrimitives) extends BCodeSkelBuilder { +trait BCodeBodyBuilder(val primitives: ScalaPrimitives, val bTypes: KnownBTypes) extends BCodeSkelBuilder { /* * Functionality to build the body of ASM MethodNode, except for `synchronized` and `try` expressions. */ @@ -155,7 +155,7 @@ trait BCodeBodyBuilder(val primitives: ScalaPrimitives) extends BCodeSkelBuilder // binary operation case rarg :: Nil => val isShift = isShiftOp(code) - resKind = tpeTK(larg).maxType(if (isShift) INT else tpeTK(rarg), bTypes) + resKind = tpeTK(larg).maxType(if (isShift) INT else tpeTK(rarg), bTypes.ObjectRef) if (isShift || isBitwiseOp(code)) { assert(resKind.isIntegralType || (resKind == BOOL), @@ -522,7 +522,7 @@ trait BCodeBodyBuilder(val primitives: ScalaPrimitives) extends BCodeSkelBuilder val thrownType = expectedType // `throw null` is valid although scala.Null (as defined in src/library-aux) isn't a subtype of Throwable. // Similarly for scala.Nothing (again, as defined in src/library-aux). - assert(thrownType == bTypes.srNullRef || thrownType == bTypes.srNothingRef || thrownType.asClassBType.isSubtypeOf(bTypes.jlThrowableRef)) + assert(thrownType.isNull || thrownType.isNothing || thrownType.asClassBType.isSubtypeOf(bTypes.jlThrowableRef)) emit(asm.Opcodes.ATHROW) end genAdaptAndSendToDest @@ -1160,7 +1160,7 @@ trait BCodeBodyBuilder(val primitives: ScalaPrimitives) extends BCodeSkelBuilder * `varsInScope`, ending at the current program point. */ def emitLocalVarScopes(): Unit = - if (BackendUtils.emitVars) { + if (emitVars) { val end = currProgramPoint() for ((sym, start) <- varsInScope.nn.reverse) { emitLocalVarScope(sym, start, end) @@ -1169,7 +1169,7 @@ trait BCodeBodyBuilder(val primitives: ScalaPrimitives) extends BCodeSkelBuilder end emitLocalVarScopes def adapt(from: BType, to: BType)(using Context): Unit = { - if (from == bTypes.srNothingRef) { + if (from.isNothing) { /* There are two possibilities for from being Nothing: emitting a "throw e" expressions and * loading a (phantom) value of type Nothing. * @@ -1216,7 +1216,7 @@ trait BCodeBodyBuilder(val primitives: ScalaPrimitives) extends BCodeSkelBuilder */ if (lastInsn.getOpcode != asm.Opcodes.ATHROW) emit(asm.Opcodes.ATHROW) - } else if (from == bTypes.srNullRef) { + } else if (from.isNull) { /* After loading an expression of type `scala.runtime.Null$`, introduce POP; ACONST_NULL. * This is required to pass the verifier: in Scala's type system, Null conforms to any * reference type. In bytecode, the type Null is represented by scala.runtime.Null$, which @@ -1483,7 +1483,7 @@ trait BCodeBodyBuilder(val primitives: ScalaPrimitives) extends BCodeSkelBuilder val ownerBType = bTypeLoader.bTypeFromType(method.owner.info) if (isInterface && !method.is(JavaDefined)) { val staticDesc = MethodBType(ownerBType :: bmType.argumentTypes, bmType.returnType).descriptor - val staticName = BackendUtils.traitSuperAccessorName(method) + val staticName = SymbolUtils.traitSuperAccessorName(method) bc.invokestatic(receiverName, staticName, staticDesc, isInterface, pos) } else { if (isInterface) { @@ -1607,7 +1607,7 @@ trait BCodeBodyBuilder(val primitives: ScalaPrimitives) extends BCodeSkelBuilder genLoad(nonNullSide, bTypes.ObjectRef) genCZJUMP(success, failure, op, bTypes.ObjectRef, targetIfNoJump) } else { - val tk = tpeTK(l).maxType(tpeTK(r), bTypes) + val tk = tpeTK(l).maxType(tpeTK(r), bTypes.ObjectRef) genLoad(l, tk) stack.push(tk) genLoad(r, tk) diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala b/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala index 2135eadbad2d..88f423bb112b 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala @@ -2,6 +2,8 @@ package dotty.tools package backend package jvm +import dotty.tools.backend.jvm.SymbolUtils.symExtensions + import scala.tools.asm import scala.tools.asm.{AnnotationVisitor, ClassWriter, Opcodes} import scala.collection.mutable @@ -27,6 +29,7 @@ import dotty.tools.dotc.transform.Mixin import dotty.tools.dotc.report import tpd.* import dotty.tools.dotc.config.ScalaSettingsProperties +import dotty.tools.dotc.util.NoSourcePosition /* * Encapsulates functionality to convert Scala AST Trees into ASM ClassNodes. @@ -35,7 +38,7 @@ import dotty.tools.dotc.config.ScalaSettingsProperties * @version 1.0 * */ -trait BCodeHelpers(val bTypeLoader: BTypeLoader, val bTypes: WellKnownBTypes) extends BCodeIdiomatic { +trait BCodeHelpers(val bTypeLoader: BTypeLoader) extends BCodeIdiomatic { // OK to cache because it won't change across Contexts private var cachedClassfileVersion: Int | Null = null @@ -547,6 +550,16 @@ trait BCodeHelpers(val bTypeLoader: BTypeLoader, val bTypes: WellKnownBTypes) ex /* builder of mirror classes */ class JMirrorBuilder extends JCommonBuilder { + def genMirrorClassIfNeeded(moduleClass: Symbol)(using Context): asm.tree.ClassNode | Null = { + if !moduleClass.isTopLevelModuleClass then + null + else if moduleClass.companionClass == NoSymbol then + genMirrorClass(moduleClass) + else + report.log(s"No mirror class for module with linked class: ${moduleClass.fullName}", NoSourcePosition) + null + } + /* Generate a mirror class for a top-level module. A mirror class is a class * containing only static methods that forward to the corresponding method * on the MODULE instance of the given Scala object. It will only be @@ -555,7 +568,7 @@ trait BCodeHelpers(val bTypeLoader: BTypeLoader, val bTypes: WellKnownBTypes) ex * * must-single-thread */ - def genMirrorClass(moduleClass: Symbol)(using Context): asm.tree.ClassNode = { + private def genMirrorClass(moduleClass: Symbol)(using Context): asm.tree.ClassNode = { assert(moduleClass.is(ModuleClass)) assert(moduleClass.companionClass == NoSymbol, moduleClass) val bType = bTypeLoader.mirrorClassBTypeFromSymbol(moduleClass) @@ -570,11 +583,11 @@ trait BCodeHelpers(val bTypeLoader: BTypeLoader, val bTypes: WellKnownBTypes) ex bType.info.flags, mirrorName, null /* no java-generic-signature */, - bTypes.ObjectRef.internalName, + ClassBType.javaLangObjectInternalName, EMPTY_STRING_ARRAY ) - if (BackendUtils.emitSource) { + if (emitSource) { mirrorClass.visitSource("" + ctx.compilationUnit.source.file.name, null /* SourceDebugExtension */) } diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeIdiomatic.scala b/compiler/src/dotty/tools/backend/jvm/BCodeIdiomatic.scala index edb2d626b1fb..15e5e92c6bb2 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeIdiomatic.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeIdiomatic.scala @@ -2,11 +2,13 @@ package dotty.tools package backend package jvm +import dotty.tools.backend.jvm.opt.CallGraph import scala.tools.asm import scala.annotation.switch import scala.tools.asm.tree.MethodInsnNode import dotty.tools.dotc.ast.Positioned import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.util.NoSourcePosition /* * A high-level facade to the ASM API for bytecode generation. @@ -15,9 +17,17 @@ import dotty.tools.dotc.core.Contexts.Context * @version 1.0 * */ -trait BCodeIdiomatic { - - def recordCallsitePosition(m: MethodInsnNode, pos: Positioned | Null)(using Context): Unit +trait BCodeIdiomatic(callGraph: Option[CallGraph]) { + private val debugLevel = 3 // 0 -> no debug info; 1-> filename; 2-> lines; 3-> varnames + final val emitSource = debugLevel >= 1 + final val emitLines = debugLevel >= 2 + final val emitVars = debugLevel >= 3 + + private def recordCallsitePosition(m: MethodInsnNode, pos: Positioned | Null)(using Context): Unit = + callGraph.foreach(_.recordCallsitePosition(m, pos match { + case p: Positioned => p.sourcePos + case null => NoSourcePosition + })) val CLASS_CONSTRUCTOR_NAME = "" val INSTANCE_CONSTRUCTOR_NAME = "" @@ -172,12 +182,12 @@ trait BCodeIdiomatic { recipe: String, argTypes: Seq[asm.Type], constants: Seq[String], - ts: WellKnownBTypes + bTypes: KnownBTypes ): Unit = { jmethod.visitInvokeDynamicInsn( "makeConcatWithConstants", - asm.Type.getMethodDescriptor(ts.StringRef.toASMType, argTypes*), - ts.jliStringConcatFactoryMakeConcatWithConstantsHandle, + asm.Type.getMethodDescriptor(bTypes.StringRef.toASMType, argTypes*), + bTypes.jliStringConcatFactoryMakeConcatWithConstantsHandle, (recipe +: constants)* ) } diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala index 945da843a17f..4a6150015630 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala @@ -161,9 +161,9 @@ trait BCodeSkelBuilder extends BCodeHelpers { /* ---------------- helper utils for generating classes and fields ---------------- */ - def genPlainClass(cd0: TypeDef)(using Context): ClassNode1 = (cd0: @unchecked) match { + def genPlainClass(cd0: TypeDef, topLevel: Boolean = false)(using Context): ClassNode1 = (cd0: @unchecked) match { case TypeDef(_, impl: Template) => - assert(cnode == null, "GenBCode detected nested methods.") + assert(topLevel || cnode == null, "GenBCode detected nested methods.") claszSymbol = cd0.symbol isCZStaticModule = claszSymbol.isStaticModuleClass @@ -298,7 +298,7 @@ trait BCodeSkelBuilder extends BCodeHelpers { private def initJClass(jclass: asm.ClassVisitor)(using Context): Unit = { val ps = claszSymbol.info.parents - val superClass: String = if ps.isEmpty then bTypes.ObjectRef.internalName + val superClass: String = if ps.isEmpty then ClassBType.javaLangObjectInternalName else bTypeLoader.classBTypeFromSymbol(ps.head.typeSymbol).internalName // We need to emit not only directly implemented interfaces, but also any indirectly implemented ones that are the target of super calls. @@ -343,7 +343,7 @@ trait BCodeSkelBuilder extends BCodeHelpers { thisName, thisSignature, superClass, interfaceNames.toArray) - if (BackendUtils.emitSource) { + if (emitSource) { cnode.visitSource(ctx.compilationUnit.source.file.name, null /* SourceDebugExtension */) } @@ -619,7 +619,7 @@ trait BCodeSkelBuilder extends BCodeHelpers { case _ => a } - if (BackendUtils.emitLines && tree.span.exists && !tree.hasAttachment(SyntheticUnit)) { + if (emitLines && tree.span.exists && !tree.hasAttachment(SyntheticUnit)) { val nr = val sourcePos = tree.sourcePos ( @@ -759,7 +759,7 @@ trait BCodeSkelBuilder extends BCodeHelpers { */ private def makeStatifiedDefDef(dd: DefDef)(using Context): DefDef = val origSym = dd.symbol.asTerm - val newSym = BackendUtils.makeStatifiedDefSymbol(origSym, origSym.name) + val newSym = SymbolUtils.makeStatifiedDefSymbol(origSym, origSym.name) tpd.DefDef(newSym, { paramRefss => val selfParamRef :: regularParamRefs = paramRefss.head: @unchecked val enclosingClass = origSym.owner.asClass @@ -799,8 +799,8 @@ trait BCodeSkelBuilder extends BCodeHelpers { // but remember to change it there if you make changes here // !!! val origSym = dd.symbol.asTerm - val name = BackendUtils.traitSuperAccessorName(origSym).toTermName - val sym = BackendUtils.makeStatifiedDefSymbol(origSym, name) + val name = SymbolUtils.traitSuperAccessorName(origSym).toTermName + val sym = SymbolUtils.makeStatifiedDefSymbol(origSym, name) tpd.DefDef(sym, { paramss => val params = paramss.head tpd.Apply(params.head.select(origSym), params.tail) @@ -898,7 +898,7 @@ trait BCodeSkelBuilder extends BCodeHelpers { else genLoadTo(trimmedRhs, returnType, LoadDestination.Return) - if (BackendUtils.emitVars) { + if (emitVars) { // add entries to LocalVariableTable JVM attribute val onePastLastProgramPoint = currProgramPoint() val hasStaticBitSet = ((flags & asm.Opcodes.ACC_STATIC) != 0) diff --git a/compiler/src/dotty/tools/backend/jvm/BTypeLoader.scala b/compiler/src/dotty/tools/backend/jvm/BTypeLoader.scala index f06b2671fcf3..7a9e85e40c4d 100644 --- a/compiler/src/dotty/tools/backend/jvm/BTypeLoader.scala +++ b/compiler/src/dotty/tools/backend/jvm/BTypeLoader.scala @@ -19,29 +19,13 @@ import scala.annotation.tailrec import scala.tools.asm import scala.tools.asm.tree.ClassNode -final class BTypeLoader(primitives: ScalaPrimitives, inlineInfoLoader: () => Option[InlineInfoLoader]) { - // Concurrent map because stack map frames are computed when in the class writer, which - // might run on multiple classes concurrently. - private val classBTypeCache = new ConcurrentHashMap[InternalName, ClassBType] +final class BTypeLoader(primitives: ScalaPrimitives, cache: ClassBType.Cache, inlineInfoLoader: Option[InlineInfoLoader]) { /** Maps special symbols, including primitive types, to their corresponding BType. */ // It's OK to cache this because all Contexts that go through here share their defns. // No locking, it's OK if this map gets initialized twice (though a little inefficient). private var specialBTypes: Map[Symbol, BType] | Null = null - - /** See doc of ClassBType.apply. This is where to use that method from. */ - def classBType[T](internalName: InternalName)(init: ClassBType => Either[T, ClassInfo]): Either[T, ClassBType] = - ClassBType(internalName, classBTypeCache)(init) - - /** See doc of ClassBType.apply. This is where to use that method from. Version that cannot fail. */ - def classBType(internalName: InternalName)(init: ClassBType => ClassInfo): ClassBType = - ClassBType(internalName, classBTypeCache)(ct => Right(init(ct))).fold(_ => assert(false), identity) - - /** Obtain a previously constructed ClassBType for a given internal name, or None if no such ClassBType was constructed. */ - def previouslyConstructedClassBType(internalName: InternalName): Option[ClassBType] = - Option(classBTypeCache.get(internalName)) - def bTypeFromSymbol(sym: Symbol)(using Context): BType = { if specialBTypes eq null then specialBTypes = Map( @@ -76,16 +60,16 @@ final class BTypeLoader(primitives: ScalaPrimitives, inlineInfoLoader: () => Opt assert( classSym != defn.NothingClass && classSym != defn.NullClass, s"Cannot create ClassBType for special class symbol ${classSym.showFullName}") - assert(classSym != defn.ArrayClass || BackendUtils.compilingArray, classSym) - assert(!classSym.isPrimitiveValueClass || BackendUtils.compilingPrimitive, s"Found $classSym while compiling ${ctx.compilationUnit.source.file.name}") + assert(classSym != defn.ArrayClass || compilingArray, classSym) + assert(!classSym.isPrimitiveValueClass || compilingPrimitive, s"Found $classSym while compiling ${ctx.compilationUnit.source.file.name}") - classBType(classSym.javaBinaryName)(ct => createClassInfo(ct, classSym.asClass)) + cache(classSym.javaBinaryName)(ct => createClassInfo(ct, classSym.asClass)) } def mirrorClassBTypeFromSymbol(moduleClassSym: Symbol)(using Context): ClassBType = { assert(moduleClassSym.isTopLevelModuleClass, s"not a top-level module class: $moduleClassSym") val internalName = moduleClassSym.javaBinaryName.stripSuffix(StdNames.str.MODULE_SUFFIX) - classBType(internalName)(_ => + cache(internalName)(_ => ClassInfo( superClass = Some(classBTypeFromSymbol(defn.ObjectClass)), interfaces = Nil, @@ -126,27 +110,6 @@ final class BTypeLoader(primitives: ScalaPrimitives, inlineInfoLoader: () => Opt throw new AssertionError(s"an unexpected type representation reached the compiler backend while compiling ${ctx.compilationUnit}: $tp.") } - /** - * Visit the class node and collect all referenced nested classes. - */ - def collectNestedClasses(classNode: ClassNode): (Iterable[ClassBType], Iterable[ClassBType]) = { - val c = new NestedClassesCollector[ClassBType](nestedOnly = true) { - def declaredNestedClasses(internalName: InternalName): List[ClassBType] = - previouslyConstructedClassBType(internalName).get.info.nestedClasses - - def getClassIfNested(internalName: InternalName): Option[ClassBType] = { - val c = previouslyConstructedClassBType(internalName).get - Option.when(c.isNestedClass)(c) - } - - def raiseError(msg: String, sig: String, e: Option[Throwable]): Unit = { - // don't crash on invalid generic signatures - } - } - c.visit(classNode) - (c.declaredInnerClasses, c.referredInnerClasses) - } - private def createClassInfo(classBType: ClassBType, classSym: Symbol)(using Context): ClassInfo = { val superClassSym: Symbol = { val t = classSym.asClass.superClass @@ -227,7 +190,7 @@ final class BTypeLoader(primitives: ScalaPrimitives, inlineInfoLoader: () => Opt val nestedInfo = buildNestedInfo(classSym) - val inlineInfo = inlineInfoLoader() match { + val inlineInfo = inlineInfoLoader match { case Some(loader) => buildInlineInfo(loader, classSym.asClass, classBType.internalName) case None => InlineInfo.empty } @@ -354,7 +317,7 @@ final class BTypeLoader(primitives: ScalaPrimitives, inlineInfoLoader: () => Opt val staticForwarders = if classSym.is(Trait) then // !!! This logic duplicates PlainSkelBuilder::makeStaticForwarder, copy changes there !!! classSym.info.decls.filter(s => s.isTerm && !s.isPrivate && !s.isStaticMember && s.name != nme.TRAIT_CONSTRUCTOR).map(s => { - BackendUtils.makeStatifiedDefSymbol(s.asTerm, BackendUtils.traitSuperAccessorName(s).toTermName) + SymbolUtils.makeStatifiedDefSymbol(s.asTerm, SymbolUtils.traitSuperAccessorName(s).toTermName) }) else Nil classMethods ++ staticForwarders @@ -395,4 +358,21 @@ final class BTypeLoader(primitives: ScalaPrimitives, inlineInfoLoader: () => Opt @tailrec private def isOriginallyStaticOwner(sym: Symbol)(using Context): Boolean = sym.is(PackageClass) || sym.is(ModuleClass) && isOriginallyStaticOwner(sym.originalOwner.originalLexicallyEnclosingClass) + + private def compilingArray(using Context) = + ctx.compilationUnit.source.file.name == "Array.scala" + + private val primitiveCompilationUnits = Set( + "Unit.scala", + "Boolean.scala", + "Char.scala", + "Byte.scala", + "Short.scala", + "Int.scala", + "Float.scala", + "Long.scala", + "Double.scala" + ) + private def compilingPrimitive(using Context) = + primitiveCompilationUnits(ctx.compilationUnit.source.file.name) } \ No newline at end of file diff --git a/compiler/src/dotty/tools/backend/jvm/BTypes.scala b/compiler/src/dotty/tools/backend/jvm/BTypes.scala index 4ec8a6719606..0ddcf5b5afca 100644 --- a/compiler/src/dotty/tools/backend/jvm/BTypes.scala +++ b/compiler/src/dotty/tools/backend/jvm/BTypes.scala @@ -5,7 +5,9 @@ package jvm import java.util.concurrent.ConcurrentHashMap import scala.tools.asm import dotty.tools.backend.jvm.BTypes.InternalName +import dotty.tools.backend.jvm.ClassBType.runtimeNullInternalName import dotty.tools.backend.jvm.opt.OptimizerWarning +import dotty.tools.dotc.core.Definitions import scala.collection.SortedMap import scala.tools.asm.Opcodes @@ -63,6 +65,8 @@ sealed trait BType { final def isArray: Boolean = this.isInstanceOf[ArrayBType] final def isClass: Boolean = this.isInstanceOf[ClassBType] final def isMethod: Boolean = this.isInstanceOf[MethodBType] + def isNothing: Boolean + def isNull: Boolean final def isNonVoidPrimitiveType: Boolean = isPrimitive && this != UNIT @@ -86,7 +90,7 @@ sealed trait BType { case ArrayBType(component) => other match { case ClassBType(name) => - name == "java/lang/Object" || name == "java/lang/Cloneable" || name == "java/io/Serializable" + name == ClassBType.javaLangObjectInternalName || name == "java/lang/Cloneable" || name == "java/io/Serializable" case ArrayBType(otherComponent) => // Array[Short]().isInstanceOf[Array[Int]] is false // but Array[String]().isInstanceOf[Array[Object]] is true @@ -98,7 +102,7 @@ sealed trait BType { case classType: ClassBType => // Quick test for Object to make a common case fast other match { - case ClassBType("java/lang/Object") => true + case ClassBType(ClassBType.javaLangObjectInternalName) => true case otherClassType: ClassBType => classType.isSubtypeOf(otherClassType) case _ => false } @@ -122,17 +126,17 @@ sealed trait BType { * Compute the upper bound of two types. * Takes promotions of numeric primitives into account. */ - final def maxType(other: BType, ts: WellKnownBTypes): BType = this match { + final def maxType(other: BType, objectRef: BType): BType = this match { case pt: PrimitiveBType => pt.maxValueType(other) case _: ArrayBType | _: ClassBType => - if this == ts.srNothingRef then return other - if other == ts.srNothingRef then return this - if this == other then return this + if this.isNothing then return other + if other.isNothing then return this + if this == other then return this assert(other.isRef, s"Cannot compute maxType: $this, $other") // Approximate `lub`. The common type of two references is always ObjectReference. - ts.ObjectRef + objectRef case _: MethodBType => throw new IllegalArgumentException("Cannot take the max of a method type") } @@ -215,6 +219,8 @@ sealed trait BType { } sealed trait PrimitiveBType extends BType { + final def isNothing: Boolean = false + final def isNull: Boolean = false /** * The upper bound of two primitive types. The `other` type has to be either a primitive @@ -642,8 +648,10 @@ final case class MethodInlineInfo(effectivelyFinal: Boolean = false, /** * A ClassBType represents a class or interface type. + * Because created ClassBTypes need to be cached to ensure we have access to them by name while emitting bytecode (for LUBs), + * a ClassBType can only be created through ClassBType.Cache. */ -case class ClassBType private(internalName: String) extends RefBType { +final case class ClassBType private(internalName: String) extends RefBType { /** * Write-once variable allows initializing a cyclic graph of infos. This is required for * nested classes. Example: for the definition `class A { class B }` we have @@ -670,7 +678,7 @@ case class ClassBType private(internalName: String) extends RefBType { // best-effort verification. def ifInit(c: ClassBType)(p: ClassBType => Boolean): Boolean = c._info == null || p(c) - def isJLO(t: ClassBType) = t.internalName == "java/lang/Object" + def isJLO(t: ClassBType) = t.internalName == ClassBType.javaLangObjectInternalName assert(!ClassBType.isInternalPhantomType(internalName), s"Cannot create ClassBType for phantom type $this") assert( @@ -686,6 +694,10 @@ case class ClassBType private(internalName: String) extends RefBType { assert(info.nestedClasses.forall(c => ifInit(c)(_.isNestedClass)), info.nestedClasses) } + // See Definitions; we cannot depend on a Context here + def isNothing: Boolean = internalName == ClassBType.runtimeNothingInternalName + def isNull: Boolean = internalName == ClassBType.runtimeNullInternalName + /** * @return The class name without the package prefix */ @@ -740,7 +752,7 @@ case class ClassBType private(internalName: String) extends RefBType { def isSubtypeOf(other: ClassBType): Boolean = { if (this == other) return true if (isInterface) { - if (other.internalName == "java/lang/Object") return true // interfaces conform to Object + if (other.internalName == ClassBType.javaLangObjectInternalName) return true // interfaces conform to Object if (!other.isInterface) return false // this is an interface, the other is some class other than object. interfaces cannot extend classes, so the result is false. // else: this and other are both interfaces. continue to (*) } else { @@ -761,8 +773,8 @@ case class ClassBType private(internalName: String) extends RefBType { * http://comments.gmane.org/gmane.comp.java.vm.languages/2293 * https://issues.scala-lang.org/browse/SI-3872 */ - def jvmWiseLUB(other: ClassBType, ts: WellKnownBTypes): ClassBType = { - def isNotNullOrNothing(c: ClassBType) = c != ts.srNullRef && c != ts.srNothingRef + def jvmWiseLUB(other: ClassBType, objectRef: ClassBType): ClassBType = { + def isNotNullOrNothing(c: ClassBType) = !c.isNull && !c.isNothing assert(isNotNullOrNothing(this) && isNotNullOrNothing(other), s"jvmWiseLUB for null or nothing: $this - $other") val res: ClassBType = (this.isInterface, other.isInterface) match { @@ -770,26 +782,26 @@ case class ClassBType private(internalName: String) extends RefBType { // exercised by test/files/run/t4761.scala if (other.isSubtypeOf(this)) this else if (this.isSubtypeOf(other)) other - else ts.ObjectRef + else objectRef case (true, false) => - if (other.isSubtypeOf(this)) this else ts.ObjectRef + if (other.isSubtypeOf(this)) this else objectRef case (false, true) => - if (this.isSubtypeOf(other)) other else ts.ObjectRef + if (this.isSubtypeOf(other)) other else objectRef case _ => - firstCommonSuffix(superClassesChain, other.superClassesChain, ts) + firstCommonSuffix(superClassesChain, other.superClassesChain, objectRef) } assert(isNotNullOrNothing(res), s"jvmWiseLUB computed: $res") res } - private def firstCommonSuffix(as: List[ClassBType], bs: List[ClassBType], ts: WellKnownBTypes): ClassBType = { + private def firstCommonSuffix(as: List[ClassBType], bs: List[ClassBType], objectRef: ClassBType): ClassBType = { var chainA = as.tail var chainB = bs.tail - var fcs = ts.ObjectRef + var fcs = objectRef while (chainA.nonEmpty && chainB.nonEmpty && chainA.head == chainB.head) { fcs = chainA.head chainA = chainA.tail @@ -800,42 +812,9 @@ case class ClassBType private(internalName: String) extends RefBType { } object ClassBType { - - /** - * Retrieve the `ClassBType` for the class with the given internal name, creating the entry if it doesn't - * already exist in the cache - * - * @param internalName The name of the class - * @param cache The cache to use. If you're wondering what to pass here, you're in the wrong place and should not be directly calling this. - * @param init Function to initialize the info of this `BType`. During execution of this function, - * code _may_ reenter into `apply(internalName, ...)` and retrieve the initializing - * `ClassBType`. - * @tparam T The type of the error result. - * @return The `ClassBType` - */ - final def apply[T](internalName: InternalName, cache: ConcurrentHashMap[InternalName, ClassBType]) - (init: ClassBType => Either[T, ClassInfo]): Either[T, ClassBType] = { - val cached = cache.get(internalName) - if cached ne null then Right(cached) - else { - val newRes = new ClassBType(internalName) - // synchronized is required to ensure proper initialization of info. - // see comment on def info - newRes.synchronized { - cache.putIfAbsent(internalName, newRes) match { - case null => - // We first create and add the ClassBType to the hash map before computing its info. This - // allows initializing cyclic dependencies, see the comment on variable ClassBType._info. - init(newRes).map(ci => { - newRes.info = ci - newRes - }) - case old => - Right(old) - } - } - } - } + private val runtimeNothingInternalName: String = Definitions.RuntimeNothingName.replace('.', '/') + private val runtimeNullInternalName: String = Definitions.RuntimeNullName.replace('.', '/') + val javaLangObjectInternalName: String = "java/lang/Object" // Primitive classes have no super class. A ClassBType for those is only created when // they are actually being compiled (e.g., when compiling scala/Boolean.scala). @@ -855,9 +834,61 @@ object ClassBType { "scala/Null", "scala/Nothing" ) + + final class Cache { + // Concurrent map because stack map frames are computed when in the class writer, which + // might run on multiple classes concurrently. + private val cache = new ConcurrentHashMap[InternalName, ClassBType] + + /** + * Retrieve the `ClassBType` for the class with the given internal name, creating the entry if it doesn't + * already exist in the cache + * + * @param internalName The name of the class + * @param init Function to initialize the info of this `BType`. During execution of this function, + * code _may_ reenter into `apply(internalName, ...)` and retrieve the initializing + * `ClassBType`. + * + * @tparam T The type of the error result. + * @return The `ClassBType` + */ + def apply[T](internalName: InternalName)(init: ClassBType => Either[T, ClassInfo]): Either[T, ClassBType] = { + val cached = cache.get(internalName) + if cached ne null then Right(cached) + else { + val newRes = new ClassBType(internalName) + // synchronized is required to ensure proper initialization of info. + // see comment on def info + newRes.synchronized { + cache.putIfAbsent(internalName, newRes) match { + case null => + // We first create and add the ClassBType to the hash map before computing its info. This + // allows initializing cyclic dependencies, see the comment on variable ClassBType._info. + init(newRes).map(ci => { + newRes.info = ci + newRes + }) + case old => + Right(old) + } + } + } + } + + /** Version of apply that cannot fail. */ + def apply(internalName: InternalName)(init: ClassBType => ClassInfo): ClassBType = + apply(internalName)(ct => Right(init(ct))).fold(_ => assert(false), identity) + + /** Obtain a previously constructed ClassBType for a given internal name, or None if no such ClassBType was constructed. */ + def previouslyConstructedClassBType(internalName: InternalName): Option[ClassBType] = + Option(cache.get(internalName)) + } } -case class ArrayBType(componentType: BType) extends RefBType { +final case class ArrayBType(componentType: BType) extends RefBType { + def isNothing: Boolean = false + def isNull: Boolean = false + def dimension: Int = componentType match { case a: ArrayBType => 1 + a.dimension case _ => 1 @@ -869,7 +900,10 @@ case class ArrayBType(componentType: BType) extends RefBType { } } -case class MethodBType(argumentTypes: List[BType], returnType: BType) extends BType +final case class MethodBType(argumentTypes: List[BType], returnType: BType) extends BType { + def isNothing: Boolean = false + def isNull: Boolean = false +} object BTypes { /** diff --git a/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala b/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala deleted file mode 100644 index c9f5783d53e4..000000000000 --- a/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala +++ /dev/null @@ -1,640 +0,0 @@ -package dotty.tools -package backend.jvm - -import dotty.tools.backend.jvm.BTypes.InternalName -import dotty.tools.dotc.core.Contexts.{Context, ctx} -import dotty.tools.dotc.core.Definitions -import dotty.tools.dotc.core.Flags.{JavaStatic, Method} -import dotty.tools.dotc.core.Names.TermName -import dotty.tools.dotc.core.StdNames.nme -import dotty.tools.dotc.core.Symbols.{Symbol, TermSymbol} -import dotty.tools.dotc.core.Types.MethodType - -import java.util.concurrent.ConcurrentHashMap -import scala.annotation.switch -import scala.collection.{BitSet, mutable} -import scala.jdk.CollectionConverters.* -import scala.tools.asm -import scala.tools.asm.tree.* -import scala.tools.asm.{Handle, Opcodes, Type} - -/** - * This component hosts tools and utilities used in the backend that require access to a `CoreBTypes` - * instance. - */ -class BackendUtils(val ts: WellKnownBTypes) { - - /** - * Classes with indyLambda closure instantiations where the SAM type is serializable (e.g. Scala's - * FunctionN) need a `\$deserializeLambda\$` method. This map contains classes for which such a - * method has been generated. It is used during ordinary code generation, as well as during - * inlining: when inlining an indyLambda instruction into a class, we need to make sure the class - * has the method. - */ - private val indyLambdaImplMethods: ConcurrentHashMap[InternalName, mutable.Map[MethodNode, mutable.Map[InvokeDynamicInsnNode, asm.Handle]]] = - new ConcurrentHashMap - - def collectSerializableLambdas(classNode: ClassNode): Array[Handle] = { - val indyLambdaBodyMethods = new mutable.ArrayBuffer[Handle] - for (m <- classNode.methods.asScala) { - val iter = m.instructions.iterator - while (iter.hasNext) { - val insn = iter.next() - insn match { - case indy: InvokeDynamicInsnNode - if indy.bsm == ts.jliLambdaMetaFactoryAltMetafactoryHandle => - import java.lang.invoke.LambdaMetafactory.FLAG_SERIALIZABLE - val metafactoryFlags = indy.bsmArgs(3).asInstanceOf[Integer].toInt - val isSerializable = (metafactoryFlags & FLAG_SERIALIZABLE) != 0 - if isSerializable then - val implMethod = indy.bsmArgs(1).asInstanceOf[Handle] - indyLambdaBodyMethods += implMethod - case _ => - } - } - } - indyLambdaBodyMethods.toArray - } - - /* - * Add: - * - * private static Object $deserializeLambda$(SerializedLambda l) { - * try return indy[scala.runtime.LambdaDeserialize.bootstrap, targetMethodGroup$0](l) - * catch { - * case i: IllegalArgumentException => - * try return indy[scala.runtime.LambdaDeserialize.bootstrap, targetMethodGroup$1](l) - * catch { - * case i: IllegalArgumentException => - * ... - * return indy[scala.runtime.LambdaDeserialize.bootstrap, targetMethodGroup${NUM_GROUPS-1}](l) - * } - * } - * } - * - * We use invokedynamic here to enable caching within the deserializer without needing to - * host a static field in the enclosing class. This allows us to add this method to interfaces - * that define lambdas in default methods. - * - * SI-10232 we can't pass arbitrary number of method handles to the final varargs parameter of the bootstrap - * method due to a limitation in the JVM. Instead, we emit a separate invokedynamic bytecode for each group of target - * methods. - */ - def addLambdaDeserialize(classNode: ClassNode, implMethodsArray: Array[Handle]): Unit = { - import asm.Opcodes.* - - val cw = classNode - - // Make sure to reference the ClassBTypes of all types that are used in the code generated - // here (e.g. java/util/Map) are initialized. Initializing a ClassBType adds it to - // `classBTypeFromInternalNameMap`. When writing the classfile, the asm ClassWriter computes - // stack map frames and invokes the `getCommonSuperClass` method. This method expects all - // ClassBTypes mentioned in the source code to exist in the map. - - val mv = cw.visitMethod(ACC_PRIVATE + ACC_STATIC + ACC_SYNTHETIC, "$deserializeLambda$", serializedLamdaObjDesc, null, null) - def emitLambdaDeserializeIndy(targetMethods: Seq[Handle]): Unit = { - mv.visitVarInsn(ALOAD, 0) - mv.visitInvokeDynamicInsn("lambdaDeserialize", serializedLamdaObjDesc, ts.jliLambdaDeserializeBootstrapHandle, targetMethods*) - } - - val targetMethodGroupLimit = 255 - 1 - 3 // JVM limit. See MAX_MH_ARITY in CallSite.java - val groups: Array[Array[Handle]] = implMethodsArray.grouped(targetMethodGroupLimit).toArray - val numGroups = groups.length - - import scala.tools.asm.Label - val initialLabels = Array.fill(numGroups - 1)(new Label()) - val terminalLabel = new Label - def nextLabel(i: Int) = if (i == numGroups - 2) terminalLabel else initialLabels(i + 1) - - for ((label, i) <- initialLabels.iterator.zipWithIndex) { - mv.visitTryCatchBlock(label, nextLabel(i), nextLabel(i), ts.jlIllegalArgExceptionRef.internalName) - } - for ((label, i) <- initialLabels.iterator.zipWithIndex) { - mv.visitLabel(label) - emitLambdaDeserializeIndy(groups(i).toIndexedSeq) - mv.visitInsn(ARETURN) - } - mv.visitLabel(terminalLabel) - emitLambdaDeserializeIndy(groups(numGroups - 1).toIndexedSeq) - mv.visitInsn(ARETURN) - } - - private lazy val serializedLamdaObjDesc = { - MethodBType(ts.jliSerializedLambdaRef :: Nil, ts.ObjectRef).descriptor - } - - /* - * Populates the InnerClasses JVM attribute with `refedInnerClasses`. See also the doc on inner - * classes in BTypes.scala. - * - * `refedInnerClasses` may contain duplicates, need not contain the enclosing inner classes of - * each inner class it lists (those are looked up and included). - * - * This method serializes in the InnerClasses JVM attribute in an appropriate order, - * not necessarily that given by `refedInnerClasses`. - * - * can-multi-thread - */ - final def addInnerClasses(jclass: asm.ClassVisitor, declaredInnerClasses: Iterable[ClassBType], refedInnerClasses: Iterable[ClassBType]): Unit = { - // sorting ensures nested classes are listed after their enclosing class thus satisfying the Eclipse Java compiler - val allNestedClasses = new mutable.TreeSet[ClassBType]()(using Ordering.by(_.internalName)) - allNestedClasses ++= declaredInnerClasses - refedInnerClasses.foreach(allNestedClasses ++= _.enclosingNestedClassesChain) - for nestedClass <- allNestedClasses - do { - // Extract the innerClassEntry - we know it exists, enclosingNestedClassesChain only returns nested classes. - val Some(e) = nestedClass.innerClassAttributeEntry: @unchecked - jclass.visitInnerClass(e.name, e.outerName, e.innerName, e.flags) - } - } - - def onIndyLambdaImplMethodIfPresent[T](hostClass: InternalName)(action: mutable.Map[MethodNode, mutable.Map[InvokeDynamicInsnNode, asm.Handle]] => T): Option[T] = - indyLambdaImplMethods.get(hostClass) match { - case null => None - case methods => Some(methods.synchronized(action(methods))) - } - - def onIndyLambdaImplMethod[T](hostClass: InternalName)(action: mutable.Map[MethodNode, mutable.Map[InvokeDynamicInsnNode, asm.Handle]] => T): T = { - val methods = indyLambdaImplMethods.computeIfAbsent(hostClass, _ => mutable.Map.empty) - methods.synchronized(action(methods)) - } - - def addIndyLambdaImplMethod(hostClass: InternalName, method: MethodNode, indy: InvokeDynamicInsnNode, handle: asm.Handle): Unit = { - onIndyLambdaImplMethod(hostClass)(_.getOrElseUpdate(method, mutable.Map.empty)(indy) = handle) - } - - def removeIndyLambdaImplMethod(hostClass: InternalName, method: MethodNode, indy: InvokeDynamicInsnNode): Unit = { - onIndyLambdaImplMethodIfPresent(hostClass)(_.get(method).foreach(_.remove(indy))) - } - - /** - * The methods used as lambda bodies for IndyLambda instructions within `method` of `hostClass`. - */ - def indyLambdaBodyMethods(hostClass: InternalName, method: MethodNode): Map[InvokeDynamicInsnNode, Handle] = { - onIndyLambdaImplMethodIfPresent(hostClass)(ms => ms.getOrElse(method, Nil).toMap).getOrElse(Map.empty) - } - - def isPredefLoad(insn: AbstractInsnNode): Boolean = BackendUtils.isModuleLoad(insn, _ == ts.PredefRef.internalName) - - // ============================================================================================== - - val primitiveAsmTypeSortToBType: Map[Int, PrimitiveBType] = Map( - asm.Type.BOOLEAN -> BOOL, - asm.Type.BYTE -> BYTE, - asm.Type.CHAR -> CHAR, - asm.Type.SHORT -> SHORT, - asm.Type.INT -> INT, - asm.Type.LONG -> LONG, - asm.Type.FLOAT -> FLOAT, - asm.Type.DOUBLE -> DOUBLE - ) - - def isScalaBox(insn: MethodInsnNode): Boolean = - insn.owner == ts.srBoxesRuntimeRef.internalName && { - val args = asm.Type.getArgumentTypes(insn.desc) - args.length == 1 && (primitiveAsmTypeSortToBType.get(args(0).getSort) match - case Some(prim) => - val MethodNameAndType(name, tp) = ts.srBoxesRuntimeBoxToMethods(prim) - name == insn.name && tp.descriptor == insn.desc - case None => false) - } - - def getScalaBox(primitiveType: asm.Type): MethodInsnNode = { - val bType = primitiveAsmTypeSortToBType(primitiveType.getSort) - val MethodNameAndType(name, methodBType) = ts.srBoxesRuntimeBoxToMethods(bType) - new MethodInsnNode(Opcodes.INVOKESTATIC, ts.srBoxesRuntimeRef.internalName, name, methodBType.descriptor, /*itf =*/ false) - } - - def getScalaUnbox(primitiveType: asm.Type): MethodInsnNode = { - val bType = primitiveAsmTypeSortToBType(primitiveType.getSort) - val MethodNameAndType(name, methodBType) = ts.srBoxesRuntimeUnboxToMethods(bType) - new MethodInsnNode(Opcodes.INVOKESTATIC, ts.srBoxesRuntimeRef.internalName, name, methodBType.descriptor, /*itf =*/ false) - } - - def isScalaUnbox(insn: MethodInsnNode): Boolean = { - insn.owner == ts.srBoxesRuntimeRef.internalName && (primitiveAsmTypeSortToBType.get(asm.Type.getReturnType(insn.desc).getSort) match { - case Some(prim) => - val MethodNameAndType(name, tp) = ts.srBoxesRuntimeUnboxToMethods(prim) - name == insn.name && tp.descriptor == insn.desc - case _ => false - }) - } - - private def calleeInMap(insn: MethodInsnNode, map: Map[InternalName, MethodNameAndType]): Boolean = map.get(insn.owner) match { - case Some(MethodNameAndType(name, tp)) => insn.name == name && insn.desc == tp.descriptor - case _ => false - } - - def isJavaBox(insn: MethodInsnNode): Boolean = calleeInMap(insn, ts.javaBoxMethods) - def isJavaUnbox(insn: MethodInsnNode): Boolean = calleeInMap(insn, ts.javaUnboxMethods) - - def isPredefAutoBox(insn: MethodInsnNode): Boolean = { - insn.owner == ts.PredefRef.internalName && (ts.predefAutoBoxMethods.get(insn.name) match { - case Some(tp) => insn.desc == tp.descriptor - case _ => false - }) - } - - def isPredefAutoUnbox(insn: MethodInsnNode): Boolean = { - insn.owner == ts.PredefRef.internalName && (ts.predefAutoUnboxMethods.get(insn.name) match { - case Some(tp) => insn.desc == tp.descriptor - case _ => false - }) - } - - def getBoxedUnit: FieldInsnNode = - new FieldInsnNode(Opcodes.GETSTATIC, ts.srBoxedUnitRef.internalName, "UNIT", ts.srBoxedUnitRef.descriptor) - - def isBoxedUnit(insn: AbstractInsnNode): Boolean = { - insn.getOpcode == Opcodes.GETSTATIC && { - val fi = insn.asInstanceOf[FieldInsnNode] - fi.owner == ts.srBoxedUnitRef.internalName && fi.name == "UNIT" && fi.desc == ts.srBoxedUnitRef.descriptor - } - } - - def isRefCreate(insn: MethodInsnNode): Boolean = calleeInMap(insn, ts.srRefCreateMethods) - def isRefZero(insn: MethodInsnNode): Boolean = calleeInMap(insn, ts.srRefZeroMethods) - - def isPrimitiveBoxConstructor(insn: MethodInsnNode): Boolean = calleeInMap(insn, ts.primitiveBoxConstructors) - def isRuntimeRefConstructor(insn: MethodInsnNode): Boolean = calleeInMap(insn, ts.srRefConstructors) - def isTupleConstructor(insn: MethodInsnNode): Boolean = calleeInMap(insn, ts.tupleClassConstructors) - def isTupleApply(insn: MethodInsnNode): Boolean = insn.owner.startsWith("scala/Tuple") && insn.owner.endsWith("$") && insn.name == "apply" - - def runtimeRefClassBoxedType(refClass: InternalName): asm.Type = asm.Type.getArgumentTypes(ts.srRefCreateMethods(refClass).methodType.descriptor)(0) - - def isSideEffectFreeConstructorCall(insn: MethodInsnNode): Boolean = { - insn.name == GenBCode.INSTANCE_CONSTRUCTOR_NAME && sideEffectFreeConstructors((insn.owner, insn.desc)) - } - - def isNewForSideEffectFreeConstructor(insn: AbstractInsnNode): Boolean = { - insn.getOpcode == Opcodes.NEW && { - val ti = insn.asInstanceOf[TypeInsnNode] - classesOfSideEffectFreeConstructors.contains(ti.desc) - } - } - - def isSideEffectFreeCall(mi: MethodInsnNode): Boolean = { - isScalaBox(mi) || // not Scala unbox, it may CCE - isJavaBox(mi) || // not Java unbox, it may NPE - isSideEffectFreeConstructorCall(mi) || - BackendUtils.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) || BackendUtils.isClassTagApply(mi) || - isTupleApply(mi) - } - - // unused objects created by these constructors are eliminated by pushPop - private lazy val sideEffectFreeConstructors: Set[(String, String)] = - val ownerDesc = (p: (InternalName, MethodNameAndType)) => (p._1, p._2.methodType.descriptor) - ts.primitiveBoxConstructors.map(ownerDesc).toSet ++ - ts.srRefConstructors.map(ownerDesc) ++ - ts.tupleClassConstructors.map(ownerDesc) ++ Set( - (ts.ObjectRef.internalName, MethodBType(Nil, UNIT).descriptor), - (ts.StringRef.internalName, MethodBType(Nil, UNIT).descriptor), - (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$$") ++ BackendUtils.primitiveTypes.keysIterator - - private lazy val classesOfSideEffectFreeConstructors: Set[String] = - sideEffectFreeConstructors.map(_._1) - - private val nonForwarderInstructionTypes: BitSet = { - import AbstractInsnNode.* - BitSet(FIELD_INSN, INVOKE_DYNAMIC_INSN, JUMP_INSN, IINC_INSN, TABLESWITCH_INSN, LOOKUPSWITCH_INSN) - } - - /** - * Identify forwarders, aliases, anonfun\$adapted methods, bridges, trivial methods (x + y), etc - * Returns - * -1 : no match - * 1 : trivial (no method calls), but not field getters - * 2 : factory - * 3 : forwarder with boxing adaptation - * 4 : generic forwarder / alias - * - * TODO: should delay some checks to `canInline` (during inlining) - * problem is: here we don't have access to the callee / accessed field, so we can't check accessibility - * - INVOKESPECIAL is not the only way to call private methods, INVOKESTATIC is also possible - * - the body of the callee can change between here (we're in inliner heuristics) and the point - * when we actually inline it (code may have been inlined into the callee) - * - methods accessing a public field could be inlined. on the other hand, methods accessing a private - * static field should not be inlined. - */ - def looksLikeForwarderOrFactoryOrTrivial(method: MethodNode, owner: InternalName, allowPrivateCalls: Boolean): Int = { - val paramTypes = Type.getArgumentTypes(method.desc) - val numPrimitives = paramTypes.count(_.getSort < Type.ARRAY) + (if (Type.getReturnType(method.desc).getSort < Type.ARRAY) 1 else 0) - - val maxSize = - 3 + // forwardee call, return - paramTypes.length + // param load - numPrimitives * 2 + // box / unbox call, for example Predef.int2Integer - paramTypes.length + 2 // some slack: +1 for each parameter, receiver, return value. allow things like casts. - - if (method.instructions.iterator.asScala.count(_.getOpcode > 0) > maxSize) return -1 - - var numBoxConv = 0 - var numCallsOrNew = 0 - var callMi: MethodInsnNode | Null = null - val it = method.instructions.iterator - while (it.hasNext && numCallsOrNew < 2) { - val i = it.next() - 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 != GenBCode.INSTANCE_CONSTRUCTOR_NAME) { - 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)) - numBoxConv += 1 - else { - numCallsOrNew += 1 - callMi = mi - } - } - } else if (nonForwarderInstructionTypes(t)) { - if (i.getOpcode == Opcodes.GETSTATIC) { - if (!allowPrivateCalls && owner == i.asInstanceOf[FieldInsnNode].owner) - numCallsOrNew = 2 // stop here: not forwarder or trivial - } else { - numCallsOrNew = 2 // stop here: not forwarder or trivial - } - } - } - if (numCallsOrNew > 1 || numBoxConv > paramTypes.length + 1) -1 - else if (numCallsOrNew == 0) if (numBoxConv == 0) 1 else 3 - else if (callMi.nn.name == GenBCode.INSTANCE_CONSTRUCTOR_NAME) 2 // if numCallsOrNew > 0 then callMi is nonnull - else if (numBoxConv > 0) 3 - else 4 - } -} - -object BackendUtils { - - private def debugLevel = 3 // 0 -> no debug info; 1-> filename; 2-> lines; 3-> varnames - final val emitSource = debugLevel >= 1 - final val emitLines = debugLevel >= 2 - final val emitVars = debugLevel >= 3 - - private lazy val primitiveTypes: Map[String, asm.Type] = Map( - ("Unit", asm.Type.VOID_TYPE), - ("Boolean", asm.Type.BOOLEAN_TYPE), - ("Char", asm.Type.CHAR_TYPE), - ("Byte", asm.Type.BYTE_TYPE), - ("Short", asm.Type.SHORT_TYPE), - ("Int", asm.Type.INT_TYPE), - ("Float", asm.Type.FLOAT_TYPE), - ("Long", asm.Type.LONG_TYPE), - ("Double", asm.Type.DOUBLE_TYPE)) - - - /** - * A pseudo-flag indicating if a MethodNode's unreachable code has been eliminated. - * - * The ASM Analyzer class does not compute any frame information for unreachable instructions. - * Transformations that use an analyzer (including inlining) therefore require unreachable code - * to be eliminated. - * - * This flag allows running dead code elimination whenever an analyzer is used. If the method - * is already optimized, DCE can return early. - */ - private val ACC_DCE_DONE = 0x2000000 - def isDceDone(method: MethodNode): Boolean = (method.access & ACC_DCE_DONE) != 0 - def setDceDone(method: MethodNode): Unit = method.access |= ACC_DCE_DONE - def clearDceDone(method: MethodNode): Unit = method.access &= ~ACC_DCE_DONE - - private val LABEL_REACHABLE_STATUS = 0x1000000 - private def isLabelFlagSet(l: LabelNode1, f: Int): Boolean = (l.flags & f) != 0 - private def setLabelFlag(l: LabelNode1, f: Int): Unit = l.flags |= f - private def clearLabelFlag(l: LabelNode1, f: Int): Unit = l.flags &= ~f - def isLabelReachable(label: LabelNode) = isLabelFlagSet(label.asInstanceOf[LabelNode1], LABEL_REACHABLE_STATUS) - def setLabelReachable(label: LabelNode) = setLabelFlag(label.asInstanceOf[LabelNode1], LABEL_REACHABLE_STATUS) - def clearLabelReachable(label: LabelNode) = clearLabelFlag(label.asInstanceOf[LabelNode1], LABEL_REACHABLE_STATUS) - - // ============================================================================================== - - def isModuleLoad(insn: AbstractInsnNode, nameMatches: InternalName => Boolean): Boolean = insn match { - case fi: FieldInsnNode => - fi.getOpcode == Opcodes.GETSTATIC && - nameMatches(fi.owner) && - fi.name == "MODULE$" && - fi.desc.length == fi.owner.length + 2 && - fi.desc.regionMatches(1, fi.owner, 0, fi.owner.length) - case _ => false - } - - def isJavaLangStaticLoad(insn: AbstractInsnNode): Boolean = insn match { - case fi: FieldInsnNode => - fi.getOpcode == Opcodes.GETSTATIC && - fi.owner.startsWith("java/lang/") - case _ => false - } - - // ============================================================================================== - - def isArrayGetLength(mi: MethodInsnNode): Boolean = mi.owner == "java/lang/reflect/Array" && mi.name == "getLength" && mi.desc == "(Ljava/lang/Object;)I" - - // If argument i of the method is null-checked, the bit `i+1` of the result is 1 - def argumentsNullCheckedByCallee(mi: MethodInsnNode): Long = { - if (isArrayGetLength(mi)) 1 - else 0 - } - - // ============================================================================================== - - final case class LambdaMetaFactoryCall(indy: InvokeDynamicInsnNode, samMethodType: asm.Type, implMethod: Handle, instantiatedMethodType: asm.Type) - - object LambdaMetaFactoryCall { - private val lambdaMetaFactoryMetafactoryHandle = new Handle( - Opcodes.H_INVOKESTATIC, - "java/lang/invoke/LambdaMetafactory", - "metafactory", - "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", - /* itf = */ false) - - private val lambdaMetaFactoryAltMetafactoryHandle = new Handle( - Opcodes.H_INVOKESTATIC, - "java/lang/invoke/LambdaMetafactory", - "altMetafactory", - "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;", - /* itf = */ false) - - def unapply(insn: AbstractInsnNode): Option[(InvokeDynamicInsnNode, asm.Type, Handle, asm.Type, Array[asm.Type])] = insn match { - case indy: InvokeDynamicInsnNode if indy.bsm == lambdaMetaFactoryMetafactoryHandle || indy.bsm == lambdaMetaFactoryAltMetafactoryHandle => - indy.bsmArgs match { - case Array(samMethodType: asm.Type, implMethod: Handle, instantiatedMethodType: asm.Type, _*) => - // LambdaMetaFactory performs a number of automatic adaptations when invoking the lambda - // implementation method (casting, boxing, unboxing, and primitive widening, see Javadoc). - // - // The closure optimizer supports only one of those adaptations: it will cast arguments - // to the correct type when re-writing a closure call to the body method. Example: - // - // val fun: String => String = l => l - // val l = List("") - // fun(l.head) - // - // The samMethodType of Function1 is `(Object)Object`, while the instantiatedMethodType - // is `(String)String`. The return type of `List.head` is `Object`. - // - // The implMethod has the signature `C$anonfun(String)String`. - // - // At the closure callsite, we have an `INVOKEINTERFACE Function1.apply (Object)Object`, - // so the object returned by `List.head` can be directly passed into the call (no cast). - // - // The closure object will cast the object to String before passing it to the implMethod. - // - // When re-writing the closure callsite to the implMethod, we have to insert a cast. - // - // The check below ensures that - // (1) the implMethod type has the expected arguments (captured types plus argument types - // from instantiatedMethodType) - // (2) the receiver of the implMethod matches the first captured type, if any, otherwise - // the first parameter type of instantiatedMethodType - // (3) all parameters that are not the same in samMethodType and instantiatedMethodType - // are reference types, so that we can insert casts to perform the same adaptation - // that the closure object would. - - val isStatic = implMethod.getTag == Opcodes.H_INVOKESTATIC - val indyParamTypes = asm.Type.getArgumentTypes(indy.desc) - val instantiatedMethodArgTypes = instantiatedMethodType.getArgumentTypes - - val (receiverType, expectedImplMethodType) = - if (isStatic) { - val paramTypes = indyParamTypes ++ instantiatedMethodArgTypes - (None, asm.Type.getMethodType(instantiatedMethodType.getReturnType, paramTypes*)) - } else if (implMethod.getTag == Opcodes.H_NEWINVOKESPECIAL) { - (Some(instantiatedMethodType.getReturnType), asm.Type.getMethodType(asm.Type.VOID_TYPE, instantiatedMethodArgTypes*)) - } else { - if (indyParamTypes.nonEmpty) { - val paramTypes = indyParamTypes.tail ++ instantiatedMethodArgTypes - (Some(indyParamTypes(0)), asm.Type.getMethodType(instantiatedMethodType.getReturnType, paramTypes*)) - } else { - val paramTypes = instantiatedMethodArgTypes.tail - (Some(instantiatedMethodArgTypes(0)), asm.Type.getMethodType(instantiatedMethodType.getReturnType, paramTypes*)) - } - } - - val isIndyLambda = - asm.Type.getType(implMethod.getDesc).getArgumentTypes.sameElements(expectedImplMethodType.getArgumentTypes) // (1) - && receiverType.forall(rt => implMethod.getOwner == rt.getInternalName) // (2) - && samMethodType.getArgumentTypes.corresponds(instantiatedMethodArgTypes)((samArgType, instArgType) => - samArgType == instArgType || BCodeUtils.isReference(samArgType) && BCodeUtils.isReference(instArgType)) // (3) - - if (isIndyLambda) Some((indy, samMethodType, implMethod, instantiatedMethodType, indyParamTypes)) - else None - - case _ => None - } - case _ => None - } - } - - // ============================================================================================== - - def isRuntimeArrayLoadOrUpdate(insn: AbstractInsnNode): Boolean = insn.getOpcode == Opcodes.INVOKEVIRTUAL && { - val mi = insn.asInstanceOf[MethodInsnNode] - mi.owner == "scala/runtime/ScalaRunTime$" && { - mi.name == "array_apply" && mi.desc == "(Ljava/lang/Object;I)Ljava/lang/Object;" || - mi.name == "array_update" && mi.desc == "(Ljava/lang/Object;ILjava/lang/Object;)V" - } - } - - private val primitiveManifestApplies: Map[String, String] = primitiveTypes map { - case (k, _) => (k, s"()Lscala/reflect/ManifestFactory$$${k}Manifest;") - } - - private def isClassTagApply(mi: MethodInsnNode): Boolean = { - mi.owner == "scala/reflect/ClassTag$" && { - mi.name == "apply" && mi.desc == "(Ljava/lang/Class;)Lscala/reflect/ClassTag;" || - primitiveManifestApplies.get(mi.name).contains(mi.desc) - } - } - - // ============================================================================================== - - def isTraitSuperAccessor(method: MethodNode, owner: ClassBType): Boolean = { - owner.isInterface && - BCodeUtils.isSyntheticMethod(method) && - method.name.endsWith("$") && - BCodeUtils.isStaticMethod(method) && - BCodeUtils.findSingleCall(method, mi => mi.itf && mi.getOpcode == Opcodes.INVOKESPECIAL && mi.name + "$" == method.name).nonEmpty - } - - def isMixinForwarder(method: MethodNode, owner: ClassBType): Boolean = { - !owner.isInterface && - // isSyntheticMethod(method) && // mixin forwarders are not synthetic it seems - !BCodeUtils.isStaticMethod(method) && - BCodeUtils.findSingleCall(method, mi => mi.itf && mi.getOpcode == Opcodes.INVOKESTATIC && mi.name == method.name + "$").nonEmpty - } - - def isTraitSuperAccessorOrMixinForwarder(method: MethodNode, owner: ClassBType): Boolean = { - isTraitSuperAccessor(method, owner) || isMixinForwarder(method, owner) - } - - def traitSuperAccessorName(sym: Symbol)(using Context): String = { - val nameString = sym.javaSimpleName - if (sym.name == nme.TRAIT_CONSTRUCTOR) nameString - else nameString + "$" - } - - def makeStatifiedDefSymbol(origSym: TermSymbol, name: TermName)(using Context): TermSymbol = - val info = origSym.info match - case mt: MethodType => - MethodType(nme.SELF :: mt.paramNames, origSym.owner.typeRef :: mt.paramInfos, mt.resType) - origSym.copy( - name = name.toTermName, - flags = Method | JavaStatic, - info = info - ).asTerm - - // === - - def compilingArray(using Context) = - ctx.compilationUnit.source.file.name == "Array.scala" - - private val primitiveCompilationUnits = Set( - "Unit.scala", - "Boolean.scala", - "Char.scala", - "Byte.scala", - "Short.scala", - "Int.scala", - "Float.scala", - "Long.scala", - "Double.scala" - ) - - def compilingPrimitive(using Context) = - primitiveCompilationUnits(ctx.compilationUnit.source.file.name) - - // === - - def methodSignature(classInternalName: InternalName, name: String, desc: String) = { - classInternalName + "::" + name + desc - } - - def methodSignature(classInternalName: InternalName, method: MethodNode): String = { - methodSignature(classInternalName, method.name, method.desc) - } - - def siteString(owner: String, method: String): String = { - val c = owner.replace('/', '.').replaceAll("\\$+", ".").replaceAll("\\.$", "") - if (method.isEmpty) c - else s"$c.$method" - } -} diff --git a/compiler/src/dotty/tools/backend/jvm/CodeGen.scala b/compiler/src/dotty/tools/backend/jvm/CodeGen.scala index 8081f27ce035..39bdd9912497 100644 --- a/compiler/src/dotty/tools/backend/jvm/CodeGen.scala +++ b/compiler/src/dotty/tools/backend/jvm/CodeGen.scala @@ -5,7 +5,6 @@ import dotty.tools.dotc.ast.Trees.{PackageDef, ValDef} import dotty.tools.dotc.ast.tpd import scala.collection.mutable -import dotty.tools.dotc.{CompilationUnit, interfaces, report, util} import dotty.tools.dotc.sbt.ExtractDependencies import dotty.tools.dotc.core.* import Contexts.* @@ -18,46 +17,26 @@ import dotty.tools.dotc.core.tasty.TastyUnpickler import scala.tools.asm.tree.* import tpd.* import dotty.tools.io.AbstractFile -import dotty.tools.dotc.ast.Positioned -import dotty.tools.dotc.util.NoSourcePosition -import SymbolUtils.given -import dotty.tools.backend.ScalaPrimitives import dotty.tools.dotc.interfaces.CompilerCallback -import opt.CallGraph - -class CodeGen(val backendUtils: BackendUtils, val primitives: ScalaPrimitives, val frontendAccess: PostProcessorFrontendAccess, - val callGraph: CallGraph, val bTypeLoader: BTypeLoader, val bTypes: WellKnownBTypes, - val generatedClassHandler: GeneratedClassHandler) { - private class Impl extends BCodeHelpers(bTypeLoader, bTypes), BCodeBodyBuilder(primitives), BCodeSyncAndTry { - def recordCallsitePosition(m: MethodInsnNode, pos: Positioned | Null)(using Context): Unit = - callGraph.callsitePositions.get(m) = pos match { - case p: Positioned => p.sourcePos - case null => NoSourcePosition - } - } - private val impl = new Impl() +import dotty.tools.dotc.report +import dotty.tools.dotc.util.SourceFile - private lazy val mirrorCodeGen = impl.JMirrorBuilder() +class CodeGen(impl: BCodeSyncAndTry) { + private val mirrorBuilder = new impl.JMirrorBuilder() /** - * Generate ASM ClassNodes for classes found in the context's compilation unit. The resulting classes are - * passed to the `generatedClassHandler`. + * Generate ASM ClassNodes for classes found in the context's compilation unit. */ - def genUnit()(using ctx: Context): Unit = { + def genUnit()(using ctx: Context): GeneratedCompilationUnit = { val generatedClasses = mutable.ListBuffer.empty[GeneratedClass] val generatedTasty = mutable.ListBuffer.empty[GeneratedTasty] def genClassDef(cd: TypeDef): Unit = try val sym = cd.symbol - val sourceFile = ctx.compilationUnit.source.file - val mainClassNode = genClass(cd) - val mirrorClassNode = - if !sym.isTopLevelModuleClass then null - else if sym.companionClass == NoSymbol then mirrorCodeGen.genMirrorClass(sym) - else - report.log(s"No mirror class for module with linked class: ${sym.fullName}", NoSourcePosition) - null + // This builder cannot be shared as it includes per-class mutable state that is not reset + val mainClassNode = new impl.SyncAndTryBuilder().genPlainClass(cd, topLevel = true) + val mirrorClassNode = mirrorBuilder.genMirrorClassIfNeeded(sym) if sym.isClass then val tastyAttrNode = if (mirrorClassNode ne null) mirrorClassNode else mainClassNode @@ -106,18 +85,16 @@ class CodeGen(val backendUtils: BackendUtils, val primitives: ScalaPrimitives, v } genClassDefs(ctx.compilationUnit.tpdTree) - generatedClassHandler.process( - GeneratedCompilationUnit(ctx.compilationUnit.source.file, generatedClasses.toList, generatedTasty.toList) - ) + GeneratedCompilationUnit(ctx.compilationUnit.source.file, generatedClasses.toList, generatedTasty.toList) } // Creates a callback that will be evaluated in PostProcessor after creating a file - private def onFileCreated(cls: ClassNode, claszSymbol: Symbol, sourceFile: util.SourceFile)(using Context): AbstractFile => Unit = { + private def onFileCreated(cls: ClassNode, claszSymbol: Symbol, sourceFile: SourceFile)(using Context): AbstractFile => Unit = { val isLocal = atPhase(sbtExtractDependenciesPhase) { claszSymbol.isLocal } + val className = cls.name.replace('/', '.') clsFile => { - val className = cls.name.replace('/', '.') ctx.compilerCallback match case cb: CompilerCallback => cb.onClassGenerated(sourceFile, clsFile, className) case null => () @@ -133,10 +110,4 @@ class CodeGen(val backendUtils: BackendUtils, val primitives: ScalaPrimitives, v cb.generatedNonLocalClass(sourceFile, clsFile.jpath, className, fullClassName) } } - - private def genClass(cd: TypeDef)(using Context): ClassNode = { - val b = new impl.SyncAndTryBuilder - b.genPlainClass(cd) - } - } diff --git a/compiler/src/dotty/tools/backend/jvm/GenBCode.scala b/compiler/src/dotty/tools/backend/jvm/GenBCode.scala index 88b021bf9f1a..0b21466c52bf 100644 --- a/compiler/src/dotty/tools/backend/jvm/GenBCode.scala +++ b/compiler/src/dotty/tools/backend/jvm/GenBCode.scala @@ -7,11 +7,13 @@ import dotty.tools.dotc.core.* import dotty.tools.dotc.interfaces.CompilerCallback import Contexts.* import dotty.tools.backend.ScalaPrimitives -import dotty.tools.backend.jvm.opt.{BCodeRepository, BTypesFromClassfile, CallGraph} +import dotty.tools.backend.jvm.opt.{BCodeRepository, BTypesFromClassfile, CallGraph, IndyLambdaImplTracker, OptimizerKnownBTypes} import dotty.tools.dotc.core.Decorators.em import dotty.tools.io.* +import scala.annotation.stableNull import scala.collection.mutable +import scala.compiletime.uninitialized /** * GenBCode has 3 parts: @@ -26,121 +28,64 @@ import scala.collection.mutable * which is why we have abstractions to hide it such as OptimizerSettings. */ class GenBCode extends Phase { self => - override def phaseName: String = GenBCode.name - override def description: String = GenBCode.description - override def isRunnable(using Context): Boolean = super.isRunnable && !ctx.usedBestEffortTasty - - private var _backendUtils: BackendUtils | Null = null - def backendUtils(using Context): BackendUtils = { - if _backendUtils eq null then - _backendUtils = BackendUtils(wellKnownBTypes) - _backendUtils.nn - } - - private var _frontendAccess: PostProcessorFrontendAccess | Null = null - def frontendAccess(using Context): PostProcessorFrontendAccess = { - if _frontendAccess eq null then - _frontendAccess = PostProcessorFrontendAccess(ctx) - _frontendAccess.nn - } - - private var _primitives: ScalaPrimitives | Null = null - def primitives(using Context): ScalaPrimitives = { - if _primitives eq null then - _primitives = ScalaPrimitives() - _primitives.nn - } - - private var _byteCodeRepository: BCodeRepository | Null = null - def byteCodeRepository(using Context): BCodeRepository = { - if _byteCodeRepository eq null then - _byteCodeRepository = BCodeRepository(ctx.platform.classPath, backendUtils) - _byteCodeRepository.nn - } - - private var _bTypesFromClassfile: BTypesFromClassfile | Null = null - def bTypesFromClassfile(using Context): BTypesFromClassfile = { - if _bTypesFromClassfile eq null then - _bTypesFromClassfile = BTypesFromClassfile(byteCodeRepository, bTypeLoader) - _bTypesFromClassfile.nn - } - - private var _bTypeLoader: BTypeLoader | Null = null - def bTypeLoader(using Context): BTypeLoader = { - if _bTypeLoader eq null then - // lazy load to break the circular dependency - def inlineInfoLoader() = Option.when[InlineInfoLoader](ctx.settings.optInlineEnabled)(bTypesFromClassfile) - _bTypeLoader = BTypeLoader(primitives, inlineInfoLoader) - _bTypeLoader.nn - } - - private var _wellKnownBTypes: WellKnownBTypes | Null = null - def wellKnownBTypes(using Context): WellKnownBTypes = { - if _wellKnownBTypes eq null then - // lazy load to break the circular dependency - def inlineInfoLoader() = Option.when[InlineInfoLoader](ctx.settings.optInlineEnabled)(bTypesFromClassfile) - _wellKnownBTypes = WellKnownBTypes(frontendAccess, bTypeLoader)(using ctx) - _wellKnownBTypes.nn - } - - private var _callGraph: CallGraph | Null = null - def callGraph(using Context): CallGraph = { - if _callGraph eq null then - _callGraph = new CallGraph(frontendAccess, byteCodeRepository, bTypesFromClassfile) - _callGraph.nn - } - - private var _postProcessor: PostProcessor | Null = null - def postProcessor(using Context): PostProcessor = { - if _postProcessor eq null then - _postProcessor = new PostProcessor(frontendAccess, byteCodeRepository, bTypesFromClassfile, callGraph, backendUtils, bTypeLoader, wellKnownBTypes) - _postProcessor.nn - } - - private var _generatedClassHandler: GeneratedClassHandler | Null = null - def generatedClassHandler(using Context): GeneratedClassHandler = { - if _generatedClassHandler eq null then { - val handler = ctx.settings.YbackendParallelism.value match { - case 1 => GeneratedClassHandler.serial(postProcessor) - case maxThreads => - // The thread pool queue is limited in size. When it's full, the `CallerRunsPolicy` causes - // a new task to be executed on the main thread, which provides back-pressure. - // The queue size is large enough to ensure that running a task on the main thread does - // not take longer than to exhaust the queue for the backend workers. - val queueSize = ctx.settings.YbackendWorkerQueue.valueSetByUser.getOrElse(maxThreads * 2) - GeneratedClassHandler.parallel(postProcessor, maxThreads, queueSize, this, ctx.profiler) - } - _generatedClassHandler = - if ctx.settings.optInlineEnabled || ctx.settings.optClosureInvocations - then GeneratedClassHandler.withGlobalOptimizations(handler) - else handler + private var _initialized: Boolean = false + private var _codeGen: CodeGen = uninitialized + private var _postProcessor: PostProcessor = uninitialized + private var _generatedClassHandler: GeneratedClassHandler = uninitialized + + private def ensureInit()(using Context): Unit = + if _initialized then + return + def createClassHandler(postProcessor: PostProcessor) = ctx.settings.YbackendParallelism.value match { + case 1 => GeneratedClassHandler.serial(postProcessor) + case maxThreads => + // The thread pool queue is limited in size. When it's full, the `CallerRunsPolicy` causes + // a new task to be executed on the main thread, which provides back-pressure. + // The queue size is large enough to ensure that running a task on the main thread does + // not take longer than to exhaust the queue for the backend workers. + val queueSize = ctx.settings.YbackendWorkerQueue.valueSetByUser.getOrElse(maxThreads * 2) + GeneratedClassHandler.parallel(postProcessor, maxThreads, queueSize, this, ctx.profiler) } - _generatedClassHandler.nn - } - - private var _codeGen: CodeGen | Null = null - def codeGen(using Context): CodeGen = { - if _codeGen eq null then - _codeGen = new CodeGen(backendUtils, primitives, frontendAccess, callGraph, bTypeLoader, wellKnownBTypes, generatedClassHandler) - _codeGen.nn - } + val primitives = new ScalaPrimitives() + val classBTypeCache = new ClassBType.Cache() + if ctx.settings.optInlineEnabled || ctx.settings.optClosureInvocations then + val indyTracker = new IndyLambdaImplTracker() + val byteCodeRepository = new BCodeRepository(ctx.platform.classPath, indyTracker) + val bTypesFromClassfile = new BTypesFromClassfile(byteCodeRepository, classBTypeCache) + val bTypeLoader = new BTypeLoader(primitives, classBTypeCache, Some(bTypesFromClassfile)) + val knownBTypes = new OptimizerKnownBTypes(bTypeLoader) + val callGraph = new CallGraph(byteCodeRepository, bTypesFromClassfile) + _postProcessor = new PostProcessorWithOptimizations(classBTypeCache, byteCodeRepository, bTypesFromClassfile, callGraph, indyTracker, knownBTypes) + _generatedClassHandler = GeneratedClassHandler.withGlobalOptimizations(createClassHandler(_postProcessor)) + object impl extends BCodeIdiomatic(Some(callGraph)), BCodeHelpers(bTypeLoader), BCodeBodyBuilder(primitives, knownBTypes), BCodeSyncAndTry + _codeGen = new CodeGen(impl) + else + val bTypeLoader = new BTypeLoader(primitives, classBTypeCache, None) + val knownBTypes = new KnownBTypes(bTypeLoader) + _postProcessor = new PostProcessor(classBTypeCache, knownBTypes) + _generatedClassHandler = createClassHandler(_postProcessor) + object impl extends BCodeIdiomatic(None), BCodeHelpers(bTypeLoader), BCodeBodyBuilder(primitives, knownBTypes), BCodeSyncAndTry + _codeGen = new CodeGen(impl) + _initialized = true protected def run(using Context): Unit = - codeGen.genUnit() + ensureInit() + _generatedClassHandler.process(_codeGen.genUnit()) ctx.compilerCallback match case cb: CompilerCallback => cb.onSourceCompiled(ctx.source) case null => () - override def runOn(units: List[CompilationUnit])(using ctx:Context): List[CompilationUnit] = { + override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = { try val result = super.runOn(units) - for (exn, f) <- generatedClassHandler.complete() do - report.error(em"unable to write $f $exn") - exn.printStackTrace() + if _initialized then + for (exn, f) <- _generatedClassHandler.complete() do + report.error(em"unable to write $f $exn") + exn.printStackTrace() result finally ctx.settings.outputDir.value match @@ -151,11 +96,9 @@ class GenBCode extends Phase { self => report.error("Cannot suspend and output to a jar at the same time. See suspension with -Xprint-suspension.") jar.close() case _ => () - // created lazily, clean them up only if they were initialized - if _postProcessor ne null then - postProcessor.close() - if _generatedClassHandler ne null then - generatedClassHandler.close() + if _initialized then + _postProcessor.close() + _generatedClassHandler.close() } } @@ -165,5 +108,4 @@ object GenBCode { val CLASS_CONSTRUCTOR_NAME = "" val INSTANCE_CONSTRUCTOR_NAME = "" - } diff --git a/compiler/src/dotty/tools/backend/jvm/KnownBTypes.scala b/compiler/src/dotty/tools/backend/jvm/KnownBTypes.scala new file mode 100644 index 000000000000..97807c6d4000 --- /dev/null +++ b/compiler/src/dotty/tools/backend/jvm/KnownBTypes.scala @@ -0,0 +1,158 @@ +package dotty.tools.backend.jvm + +import dotty.tools.dotc.core.Symbols.* +import dotty.tools.dotc.transform.Erasure + +import scala.tools.asm.{Handle, Opcodes} +import dotty.tools.dotc.core.Symbols +import BTypes.* +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.StdNames.* +import dotty.tools.backend.ScalaPrimitivesOps.* + +import scala.annotation.constructorOnly + +case class MethodNameAndType(name: String, methodType: MethodBType) + +class KnownBTypes(loader: BTypeLoader)(using @constructorOnly initctx: Context) { + val ObjectRef: ClassBType = loader.classBTypeFromSymbol(defn.ObjectClass) + val StringRef: ClassBType = loader.classBTypeFromSymbol(defn.StringClass) + val srNullRef: ClassBType = loader.classBTypeFromSymbol(defn.RuntimeNullClass) + val jlClassRef: ClassBType = loader.classBTypeFromSymbol(defn.ClassClass) + + val jlThrowableRef: ClassBType = loader.classBTypeFromSymbol(defn.ThrowableClass) + val jlClassCastExceptionRef: ClassBType = loader.classBTypeFromSymbol(defn.ClassCastExceptionClass) + val jlIllegalArgExceptionRef: ClassBType = loader.classBTypeFromSymbol(defn.IllegalArgumentExceptionClass) + + val srBoxesRuntimeRef: ClassBType = loader.classBTypeFromSymbol(requiredClass[scala.runtime.BoxesRunTime]) + + val jliSerializedLambdaRef: ClassBType = loader.classBTypeFromSymbol(requiredClass[java.lang.invoke.SerializedLambda]) + + protected val jliLambdaMetafactoryRef: ClassBType = loader.classBTypeFromSymbol(requiredClass[java.lang.invoke.LambdaMetafactory]) + protected val jliStringConcatFactoryRef: ClassBType = loader.classBTypeFromSymbol(requiredClass[java.lang.invoke.StringConcatFactory]) + protected val jliMethodHandlesLookupRef: ClassBType = loader.classBTypeFromSymbol(defn.MethodHandlesLookupClass) + protected val jliMethodTypeRef: ClassBType = loader.classBTypeFromSymbol(requiredClass[java.lang.invoke.MethodType]) + protected val jliMethodHandleRef: ClassBType = loader.classBTypeFromSymbol(defn.MethodHandleClass) + protected val jliCallSiteRef: ClassBType = loader.classBTypeFromSymbol(requiredClass[java.lang.invoke.CallSite]) + protected val srLambdaDeserialize: ClassBType = loader.classBTypeFromSymbol(requiredClass[scala.runtime.LambdaDeserialize]) + val jliLambdaDeserializeBootstrapHandle: Handle = new Handle( + Opcodes.H_INVOKESTATIC, + srLambdaDeserialize.internalName, + "bootstrap", + MethodBType( + List(jliMethodHandlesLookupRef, StringRef, jliMethodTypeRef, ArrayBType(jliMethodHandleRef)), + jliCallSiteRef + ).descriptor, + /* itf = */ false + ) + val jliLambdaMetaFactoryMetafactoryHandle: Handle = new Handle( + Opcodes.H_INVOKESTATIC, + jliLambdaMetafactoryRef.internalName, + "metafactory", + MethodBType( + List(jliMethodHandlesLookupRef, StringRef, jliMethodTypeRef, jliMethodTypeRef, jliMethodHandleRef, jliMethodTypeRef), + jliCallSiteRef + ).descriptor, + /* itf = */ false + ) + val jliLambdaMetaFactoryAltMetafactoryHandle: Handle = new Handle( + Opcodes.H_INVOKESTATIC, + jliLambdaMetafactoryRef.internalName, + "altMetafactory", + MethodBType( + List(jliMethodHandlesLookupRef, StringRef, jliMethodTypeRef, ArrayBType(ObjectRef)), + jliCallSiteRef + ).descriptor, + /* itf = */ false + ) + val jliStringConcatFactoryMakeConcatWithConstantsHandle: Handle = new Handle( + Opcodes.H_INVOKESTATIC, + jliStringConcatFactoryRef.internalName, + "makeConcatWithConstants", + MethodBType( + List(jliMethodHandlesLookupRef, StringRef, jliMethodTypeRef, StringRef, ArrayBType(ObjectRef)), + jliCallSiteRef + ).descriptor, + /* itf = */ false + ) + + /** + * Map from primitive types to their boxed class type. Useful when pushing class literals onto the + * operand stack (ldc instruction taking a class literal), see genConstant. + */ + val boxedClassOfPrimitive: Map[BType, ClassBType] = Map( + UNIT -> loader.classBTypeFromSymbol(requiredClass[java.lang.Void]), + BOOL -> loader.classBTypeFromSymbol(requiredClass[java.lang.Boolean]), + BYTE -> loader.classBTypeFromSymbol(requiredClass[java.lang.Byte]), + SHORT -> loader.classBTypeFromSymbol(requiredClass[java.lang.Short]), + CHAR -> loader.classBTypeFromSymbol(requiredClass[java.lang.Character]), + INT -> loader.classBTypeFromSymbol(requiredClass[java.lang.Integer]), + LONG -> loader.classBTypeFromSymbol(requiredClass[java.lang.Long]), + FLOAT -> loader.classBTypeFromSymbol(requiredClass[java.lang.Float]), + DOUBLE -> loader.classBTypeFromSymbol(requiredClass[java.lang.Double]) + ) + + /** + * Maps the method symbol for a box method to the boxed type of the result. For example, the + * method symbol for `Byte.box()` is mapped to the ClassBType `java/lang/Byte`. + */ + val boxResultType: Map[Symbol, ClassBType] = Map( + Erasure.Boxing.boxMethod(defn.UnitClass) -> loader.classBTypeFromSymbol(requiredClass[java.lang.Void]), + Erasure.Boxing.boxMethod(defn.BooleanClass) -> loader.classBTypeFromSymbol(requiredClass[java.lang.Boolean]), + Erasure.Boxing.boxMethod(defn.ByteClass) -> loader.classBTypeFromSymbol(requiredClass[java.lang.Byte]), + Erasure.Boxing.boxMethod(defn.ShortClass) -> loader.classBTypeFromSymbol(requiredClass[java.lang.Short]), + Erasure.Boxing.boxMethod(defn.CharClass) -> loader.classBTypeFromSymbol(requiredClass[java.lang.Character]), + Erasure.Boxing.boxMethod(defn.IntClass) -> loader.classBTypeFromSymbol(requiredClass[java.lang.Integer]), + Erasure.Boxing.boxMethod(defn.LongClass) -> loader.classBTypeFromSymbol(requiredClass[java.lang.Long]), + Erasure.Boxing.boxMethod(defn.FloatClass) -> loader.classBTypeFromSymbol(requiredClass[java.lang.Float]), + Erasure.Boxing.boxMethod(defn.DoubleClass) -> loader.classBTypeFromSymbol(requiredClass[java.lang.Double]) + ) + /** + * Maps the method symbol for an unbox method to the primitive type of the result. + * For example, the method symbol for `Byte.unbox()` is mapped to the PrimitiveBType BYTE. + */ + val unboxResultType: Map[Symbol, BType] = Map( + Erasure.Boxing.unboxMethod(defn.UnitClass) -> UNIT, + Erasure.Boxing.unboxMethod(defn.BooleanClass) -> BOOL, + Erasure.Boxing.unboxMethod(defn.ByteClass) -> BYTE, + Erasure.Boxing.unboxMethod(defn.ShortClass) -> SHORT, + Erasure.Boxing.unboxMethod(defn.CharClass) -> CHAR, + Erasure.Boxing.unboxMethod(defn.IntClass) -> INT, + Erasure.Boxing.unboxMethod(defn.LongClass) -> LONG, + Erasure.Boxing.unboxMethod(defn.FloatClass) -> FLOAT, + Erasure.Boxing.unboxMethod(defn.DoubleClass) -> DOUBLE + ) + + val asmBoxTo: Map[BType, MethodNameAndType] = Map( + BOOL -> MethodNameAndType("boxToBoolean", MethodBType(List(BOOL), boxedClassOfPrimitive(BOOL))), + BYTE -> MethodNameAndType("boxToByte", MethodBType(List(BYTE), boxedClassOfPrimitive(BYTE))), + CHAR -> MethodNameAndType("boxToCharacter", MethodBType(List(CHAR), boxedClassOfPrimitive(CHAR))), + SHORT -> MethodNameAndType("boxToShort", MethodBType(List(SHORT), boxedClassOfPrimitive(SHORT))), + INT -> MethodNameAndType("boxToInteger", MethodBType(List(INT), boxedClassOfPrimitive(INT))), + LONG -> MethodNameAndType("boxToLong", MethodBType(List(LONG), boxedClassOfPrimitive(LONG))), + FLOAT -> MethodNameAndType("boxToFloat", MethodBType(List(FLOAT), boxedClassOfPrimitive(FLOAT))), + DOUBLE -> MethodNameAndType("boxToDouble", MethodBType(List(DOUBLE), boxedClassOfPrimitive(DOUBLE))) + ) + val asmUnboxTo: Map[BType, MethodNameAndType] = Map( + BOOL -> MethodNameAndType("unboxToBoolean", MethodBType(List(ObjectRef), BOOL)), + BYTE -> MethodNameAndType("unboxToByte", MethodBType(List(ObjectRef), BYTE)), + CHAR -> MethodNameAndType("unboxToChar", MethodBType(List(ObjectRef), CHAR)), + SHORT -> MethodNameAndType("unboxToShort", MethodBType(List(ObjectRef), SHORT)), + INT -> MethodNameAndType("unboxToInt", MethodBType(List(ObjectRef), INT)), + LONG -> MethodNameAndType("unboxToLong", MethodBType(List(ObjectRef), LONG)), + FLOAT -> MethodNameAndType("unboxToFloat", MethodBType(List(ObjectRef), FLOAT)), + DOUBLE -> MethodNameAndType("unboxToDouble", MethodBType(List(ObjectRef), DOUBLE)) + ) + + val typeOfArrayOp: Map[Int, BType] = Map( + ZARRAY_LENGTH -> BOOL, ZARRAY_GET -> BOOL, ZARRAY_SET -> BOOL, + BARRAY_LENGTH -> BYTE, BARRAY_GET -> BYTE, BARRAY_SET -> BYTE, + SARRAY_LENGTH -> SHORT, SARRAY_GET -> SHORT, SARRAY_SET -> SHORT, + CARRAY_LENGTH -> CHAR, CARRAY_GET -> CHAR, CARRAY_SET -> CHAR, + IARRAY_LENGTH -> INT, IARRAY_GET -> INT, IARRAY_SET -> INT, + LARRAY_LENGTH -> LONG, LARRAY_GET -> LONG, LARRAY_SET -> LONG, + FARRAY_LENGTH -> FLOAT, FARRAY_GET -> FLOAT, FARRAY_SET -> FLOAT, + DARRAY_LENGTH -> DOUBLE, DARRAY_GET -> DOUBLE, DARRAY_SET -> DOUBLE, + OARRAY_LENGTH -> ObjectRef, OARRAY_GET -> ObjectRef, OARRAY_SET -> ObjectRef + ) +} diff --git a/compiler/src/dotty/tools/backend/jvm/PostProcessor.scala b/compiler/src/dotty/tools/backend/jvm/PostProcessor.scala index dcaa2f384c96..427d04a0c28e 100644 --- a/compiler/src/dotty/tools/backend/jvm/PostProcessor.scala +++ b/compiler/src/dotty/tools/backend/jvm/PostProcessor.scala @@ -1,5 +1,7 @@ package dotty.tools.backend.jvm +import dotty.tools.backend.jvm.BTypes.InternalName + import java.util.concurrent.ConcurrentHashMap import dotty.tools.dotc.util.SourcePosition import dotty.tools.io.AbstractFile @@ -7,32 +9,27 @@ import dotty.tools.io.FileWriters import dotty.tools.dotc.core.Contexts.* import dotty.tools.dotc.core.Decorators.em -import scala.tools.asm.ClassWriter -import scala.tools.asm.tree.ClassNode +import scala.tools.asm.{ClassWriter, Handle} +import scala.tools.asm.tree.{ClassNode, InvokeDynamicInsnNode} import dotty.tools.backend.jvm.opt.* +import dotty.tools.dotc.core.Symbols.defn import dotty.tools.dotc.report import dotty.tools.io.PlainFile.toPlainFile import java.nio.file.{Files, Paths} +import scala.jdk.CollectionConverters.* +import scala.collection.mutable import scala.tools.asm import scala.util.chaining.scalaUtilChainingOps /** * Implements late stages of the backend, i.e., * optimizations, post-processing and classfile serialization and writing. + * + * This base class doesn't do optimizations, use the subclass for that if they're enabled. */ -class PostProcessor(frontendAccess: PostProcessorFrontendAccess, - byteCodeRepository: BCodeRepository, bTypesFromClassfile: BTypesFromClassfile, - callGraph: CallGraph, backendUtils: BackendUtils, - bTypeLoader: BTypeLoader, bTypes: WellKnownBTypes)(using Context) { - - private val optSettings = new OptimizerSettings() - private val closureOptimizer = new ClosureOptimizer(frontendAccess, backendUtils, byteCodeRepository, callGraph, bTypes, bTypesFromClassfile, optSettings) - private val heuristics = new InlinerHeuristics(frontendAccess, backendUtils, byteCodeRepository, callGraph, bTypes, optSettings) - private val inliner = new Inliner(frontendAccess, backendUtils, callGraph, bTypeLoader, bTypesFromClassfile, byteCodeRepository, heuristics, closureOptimizer, optSettings) - private val localOpt = new LocalOpt(backendUtils, callGraph, inliner, bTypes, bTypesFromClassfile, optSettings) +class PostProcessor(classBTypeCache: ClassBType.Cache, bTypes: KnownBTypes)(using Context) { - given FileWriters.ReadOnlyContext = FileWriters.ReadOnlyContext.eager private val classfileWriter: FileWriters.ClassfileWriter = { val dumpClassesPath = ctx.settings.Xdumpclasses.valueSetByUser @@ -46,14 +43,14 @@ class PostProcessor(frontendAccess: PostProcessorFrontendAccess, private type ClassnamePosition = (String, SourcePosition) private val caseInsensitively = new ConcurrentHashMap[String, ClassnamePosition] - def sendToDisk(clazz: GeneratedClass): Unit = { + final def sendToDisk(clazz: GeneratedClass): Unit = { val classNode = clazz.classNode val internalName = classNode.name.nn val bytes = try if !clazz.isArtifact then - localOpt.methodOptimizations(classNode) - setSerializableLambdas(classNode) + runLocalOptimizations(classNode) + addLambdaDeserialize(classNode) warnCaseInsensitiveOverwrite(clazz) setInnerClasses(classNode) serializeClass(classNode) @@ -69,31 +66,19 @@ class PostProcessor(frontendAccess: PostProcessorFrontendAccess, clazz.onFileCreated(clsFile) } - def sendToDisk(tasty: GeneratedTasty): Unit = { + final def sendToDisk(tasty: GeneratedTasty): Unit = { val GeneratedTasty(classNode, tastyGenerator) = tasty val internalName = classNode.name.nn classfileWriter.writeTasty(classNode.name.nn, tastyGenerator()) } - def runGlobalOptimizations(generatedUnits: Iterable[GeneratedCompilationUnit]): Unit = { - // add classes to the bytecode repo before building the call graph: the latter needs to - // look up classes and methods in the code repo. - for u <- generatedUnits - c <- u.classes - do - byteCodeRepository.add(c.classNode, Some(u.sourceFile.canonicalPath)) - for u <- generatedUnits - c <- u.classes - if !c.isArtifact // skip call graph for mirror / bean: we don't inline into them, and they are not referenced from other classes - do - callGraph.addClass(c.classNode) - if ctx.settings.optInlineEnabled then - inliner.runInlinerAndClosureOptimizer() - else if ctx.settings.optClosureInvocations then - closureOptimizer.rewriteClosureApplyInvocations(None, scala.collection.mutable.Map.empty) - } + def runGlobalOptimizations(generatedUnits: Iterable[GeneratedCompilationUnit]): Unit = + () // no optimizations by default - def close(): Unit = + protected def runLocalOptimizations(classNode: ClassNode): Unit = + () // no optimizations by default + + final def close(): Unit = classfileWriter.close() private def warnCaseInsensitiveOverwrite(clazz: GeneratedClass): Unit = { @@ -121,17 +106,31 @@ class PostProcessor(frontendAccess: PostProcessorFrontendAccess, } } - private def setSerializableLambdas(classNode: ClassNode): Unit = { - import backendUtils.{collectSerializableLambdas, addLambdaDeserialize} - val serializableLambdas = collectSerializableLambdas(classNode) - if serializableLambdas.nonEmpty then - addLambdaDeserialize(classNode, serializableLambdas) - } - private def setInnerClasses(classNode: ClassNode): Unit = { classNode.innerClasses.nn.clear() - val (declared, referred) = bTypeLoader.collectNestedClasses(classNode) - backendUtils.addInnerClasses(classNode, declared, referred) + val (declared, referred) = collectNestedClasses(classNode) + addInnerClasses(classNode, declared, referred) + } + + /** + * Visit the class node and collect all referenced nested classes. + */ + private def collectNestedClasses(classNode: ClassNode): (Iterable[ClassBType], Iterable[ClassBType]) = { + val c = new NestedClassesCollector[ClassBType](nestedOnly = true) { + def declaredNestedClasses(internalName: InternalName): List[ClassBType] = + classBTypeCache.previouslyConstructedClassBType(internalName).get.info.nestedClasses + + def getClassIfNested(internalName: InternalName): Option[ClassBType] = { + val c = classBTypeCache.previouslyConstructedClassBType(internalName).get + Option.when(c.isNestedClass)(c) + } + + def raiseError(msg: String, sig: String, e: Option[Throwable]): Unit = { + // don't crash on invalid generic signatures + } + } + c.visit(classNode) + (c.declaredInnerClasses, c.referredInnerClasses) } private def serializeClass(classNode: ClassNode): Array[Byte] = { @@ -140,6 +139,120 @@ class PostProcessor(frontendAccess: PostProcessorFrontendAccess, cw.toByteArray.nn } + private def collectSerializableLambdas(classNode: ClassNode): Array[Handle] = { + val indyLambdaBodyMethods = new mutable.ArrayBuffer[Handle] + for (m <- classNode.methods.asScala) { + val iter = m.instructions.iterator + while (iter.hasNext) { + val insn = iter.next() + insn match { + case indy: InvokeDynamicInsnNode + if indy.bsm == bTypes.jliLambdaMetaFactoryAltMetafactoryHandle => + import java.lang.invoke.LambdaMetafactory.FLAG_SERIALIZABLE + val metafactoryFlags = indy.bsmArgs(3).asInstanceOf[Integer].toInt + val isSerializable = (metafactoryFlags & FLAG_SERIALIZABLE) != 0 + if isSerializable then + val implMethod = indy.bsmArgs(1).asInstanceOf[Handle] + indyLambdaBodyMethods += implMethod + case _ => + } + } + } + indyLambdaBodyMethods.toArray + } + + private val serializedLamdaObjDesc = MethodBType(bTypes.jliSerializedLambdaRef :: Nil, bTypes.ObjectRef).descriptor + /* + * Add: + * + * private static Object $deserializeLambda$(SerializedLambda l) { + * try return indy[scala.runtime.LambdaDeserialize.bootstrap, targetMethodGroup$0](l) + * catch { + * case i: IllegalArgumentException => + * try return indy[scala.runtime.LambdaDeserialize.bootstrap, targetMethodGroup$1](l) + * catch { + * case i: IllegalArgumentException => + * ... + * return indy[scala.runtime.LambdaDeserialize.bootstrap, targetMethodGroup${NUM_GROUPS-1}](l) + * } + * } + * } + * + * We use invokedynamic here to enable caching within the deserializer without needing to + * host a static field in the enclosing class. This allows us to add this method to interfaces + * that define lambdas in default methods. + * + * SI-10232 we can't pass arbitrary number of method handles to the final varargs parameter of the bootstrap + * method due to a limitation in the JVM. Instead, we emit a separate invokedynamic bytecode for each group of target + * methods. + */ + private def addLambdaDeserialize(classNode: ClassNode): Unit = { + val implMethodsArray = collectSerializableLambdas(classNode) + if implMethodsArray.isEmpty then + return + + import asm.Opcodes.* + val cw = classNode + // Make sure to reference the ClassBTypes of all types that are used in the code generated + // here (e.g. java/util/Map) are initialized. Initializing a ClassBType adds it to + // `classBTypeFromInternalNameMap`. When writing the classfile, the asm ClassWriter computes + // stack map frames and invokes the `getCommonSuperClass` method. This method expects all + // ClassBTypes mentioned in the source code to exist in the map. + + val mv = cw.visitMethod(ACC_PRIVATE + ACC_STATIC + ACC_SYNTHETIC, "$deserializeLambda$", serializedLamdaObjDesc, null, null) + def emitLambdaDeserializeIndy(targetMethods: Seq[Handle]): Unit = { + mv.visitVarInsn(ALOAD, 0) + mv.visitInvokeDynamicInsn("lambdaDeserialize", serializedLamdaObjDesc, bTypes.jliLambdaDeserializeBootstrapHandle, targetMethods*) + } + + val targetMethodGroupLimit = 255 - 1 - 3 // JVM limit. See MAX_MH_ARITY in CallSite.java + val groups: Array[Array[Handle]] = implMethodsArray.grouped(targetMethodGroupLimit).toArray + val numGroups = groups.length + + import scala.tools.asm.Label + val initialLabels = Array.fill(numGroups - 1)(new Label()) + val terminalLabel = new Label + def nextLabel(i: Int) = if (i == numGroups - 2) terminalLabel else initialLabels(i + 1) + + for ((label, i) <- initialLabels.iterator.zipWithIndex) { + mv.visitTryCatchBlock(label, nextLabel(i), nextLabel(i), bTypes.jlIllegalArgExceptionRef.internalName) + } + for ((label, i) <- initialLabels.iterator.zipWithIndex) { + mv.visitLabel(label) + emitLambdaDeserializeIndy(groups(i).toIndexedSeq) + mv.visitInsn(ARETURN) + } + mv.visitLabel(terminalLabel) + emitLambdaDeserializeIndy(groups(numGroups - 1).toIndexedSeq) + mv.visitInsn(ARETURN) + } + + /* + * Populates the InnerClasses JVM attribute with `refedInnerClasses`. See also the doc on inner + * classes in BTypes.scala. + * + * `refedInnerClasses` may contain duplicates, need not contain the enclosing inner classes of + * each inner class it lists (those are looked up and included). + * + * This method serializes in the InnerClasses JVM attribute in an appropriate order, + * not necessarily that given by `refedInnerClasses`. + * + * can-multi-thread + */ + private def addInnerClasses(jclass: asm.ClassVisitor, declaredInnerClasses: Iterable[ClassBType], refedInnerClasses: Iterable[ClassBType]): Unit = { + // sorting ensures nested classes are listed after their enclosing class thus satisfying the Eclipse Java compiler + val allNestedClasses = new mutable.TreeSet[ClassBType]()(using Ordering.by(_.internalName)) + allNestedClasses ++= declaredInnerClasses + refedInnerClasses.foreach(allNestedClasses ++= _.enclosingNestedClassesChain) + for nestedClass <- allNestedClasses + do { + // Extract the innerClassEntry - we know it exists, enclosingNestedClassesChain only returns nested classes. + val Some(e) = nestedClass.innerClassAttributeEntry: @unchecked + jclass.visitInnerClass(e.name, e.outerName, e.innerName, e.flags) + } + } + + // ----------------------------------------------------------------------------------------- // finding the least upper bound in agreement with the bytecode verifier (given two internal names handed by ASM) // Background: @@ -153,16 +266,15 @@ class PostProcessor(frontendAccess: PostProcessorFrontendAccess, * It's what ASM needs to know in order to compute stack map frames, http://asm.ow2.org/doc/developer-guide.html#controlflow */ private final class ClassWriterWithBTypeLub(flags: Int) extends ClassWriter(flags) { - /** * This method is used by asm when computing stack map frames. */ override def getCommonSuperClass(inameA: String, inameB: String): String = { // All types that appear in a class node need to have their ClassBType cached, // i.e., have been loaded either from symbols or from class files. - val a = bTypeLoader.previouslyConstructedClassBType(inameA).get - val b = bTypeLoader.previouslyConstructedClassBType(inameB).get - val lub = a.jvmWiseLUB(b, bTypes) + val a = classBTypeCache.previouslyConstructedClassBType(inameA).get + val b = classBTypeCache.previouslyConstructedClassBType(inameB).get + val lub = a.jvmWiseLUB(b, bTypes.ObjectRef) val lubName = lub.internalName assert(lubName != "scala/Any") lubName // ASM caches the answer during the lifetime of a ClassWriter. We outlive that. Not sure whether caching on our side would improve things. @@ -170,6 +282,35 @@ class PostProcessor(frontendAccess: PostProcessorFrontendAccess, } } +final class PostProcessorWithOptimizations(classBTypeCache: ClassBType.Cache, byteCodeRepository: BCodeRepository, bTypesFromClassfile: BTypesFromClassfile, + callGraph: CallGraph, indyTracker: IndyLambdaImplTracker, + bTypes: OptimizerKnownBTypes)(using Context) extends PostProcessor(classBTypeCache, bTypes) { + private val optSettings = new OptimizerSettings() + private val optimizerUtils = new OptimizerUtils(bTypes) + private val closureOptimizer = new ClosureOptimizer(optimizerUtils, indyTracker, byteCodeRepository, callGraph, bTypes, bTypesFromClassfile, optSettings) + private val heuristics = new InlinerHeuristics(optimizerUtils, byteCodeRepository, callGraph, bTypes, optSettings) + private val inliner = new Inliner(indyTracker, callGraph, classBTypeCache, bTypesFromClassfile, byteCodeRepository, heuristics, closureOptimizer, optSettings) + private val localOpt = new LocalOpt(optimizerUtils, indyTracker, callGraph, inliner, bTypes, bTypesFromClassfile, optSettings) + + override def runGlobalOptimizations(generatedUnits: Iterable[GeneratedCompilationUnit]): Unit = { + // add classes to the bytecode repo before building the call graph: the latter needs to + // look up classes and methods in the code repo. + for u <- generatedUnits + c <- u.classes + do + byteCodeRepository.add(c.classNode, Some(u.sourceFile.canonicalPath)) + for u <- generatedUnits + c <- u.classes + if !c.isArtifact // skip call graph for mirror / bean: we don't inline into them, and they are not referenced from other classes + do + callGraph.addClass(c.classNode) + inliner.runInlinerAndClosureOptimizer(i => report.optimizerWarning(i.msg, i.site, i.pos)) + } + + protected override def runLocalOptimizations(classNode: ClassNode): Unit = + localOpt.methodOptimizations(classNode) +} + /** * The result of code generation. [[isArtifact]] is `true` for mirror. */ diff --git a/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala b/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala deleted file mode 100644 index c9dce10f98f4..000000000000 --- a/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala +++ /dev/null @@ -1,48 +0,0 @@ -package dotty.tools -package backend.jvm - -import dotty.tools.dotc.core.Contexts.Context -import dotty.tools.dotc.report -import dotty.tools.dotc.reporting.Message -import dotty.tools.dotc.util.SrcPos - -import scala.compiletime.uninitialized - -/** - * Abstracts the frontend data structures, specially the Context, that need to be accessed in a single-threaded manner. - */ -final class PostProcessorFrontendAccess(val ctx: Context) { - import PostProcessorFrontendAccess.* - - def optimizerWarning(msg: Context ?=> Message, site: String, pos: SrcPos): Unit = - report.optimizerWarning(msg(using ctx), site, pos)(using ctx) - - private val frontendLock: AnyRef = new Object() - - private[PostProcessorFrontendAccess] def frontendSynch[T](x: => T): T = frontendLock.synchronized(x) - - def perRunLazy[T](init: => T): Lazy[T] = new SynchronizedLazy(this, init) -} - -object PostProcessorFrontendAccess { - abstract class Lazy[T] { - def get: T - } - - /** A container for value with lazy initialization synchronized on compiler frontend - * Used for sharing variables requiring a Context for initialization, between different threads - * Similar to Scala 2 BTypes.LazyVar, but without re-initialization of BTypes.LazyWithLock. These were not moved to PostProcessorFrontendAccess only due to problematic architectural decisions. - */ - private class SynchronizedLazy[T](frontendAccess: PostProcessorFrontendAccess, init: => T) extends Lazy[T] { - @volatile private var isInit: Boolean = false - private var v: T = uninitialized - - override def get: T = - if isInit then v - else frontendAccess.frontendSynch { - if !isInit then v = init - isInit = true - v - } - } -} diff --git a/compiler/src/dotty/tools/backend/jvm/SymbolUtils.scala b/compiler/src/dotty/tools/backend/jvm/SymbolUtils.scala index bc1deec550d5..a4b8b00ea1d0 100644 --- a/compiler/src/dotty/tools/backend/jvm/SymbolUtils.scala +++ b/compiler/src/dotty/tools/backend/jvm/SymbolUtils.scala @@ -1,14 +1,31 @@ package dotty.tools.backend.jvm import dotty.tools.dotc.core.Flags.* - import dotty.tools.dotc.core.* import Contexts.* import Symbols.* import Phases.* import NameKinds.{LazyBitMapName, LazyLocalName} +import dotty.tools.dotc.core.Names.TermName +import dotty.tools.dotc.core.StdNames.nme +import dotty.tools.dotc.core.Types.MethodType object SymbolUtils: + def traitSuperAccessorName(sym: Symbol)(using Context): String = + val nameString = sym.javaSimpleName + if (sym.name == nme.TRAIT_CONSTRUCTOR) nameString + else nameString + "$" + + def makeStatifiedDefSymbol(origSym: TermSymbol, name: TermName)(using Context): TermSymbol = + val info = origSym.info match + case mt: MethodType => + MethodType(nme.SELF :: mt.paramNames, origSym.owner.typeRef :: mt.paramInfos, mt.resType) + origSym.copy( + name = name.toTermName, + flags = Method | JavaStatic, + info = info + ).asTerm + given symExtensions: AnyRef with extension (sym: Symbol) /** Fields of static modules will be static at backend diff --git a/compiler/src/dotty/tools/backend/jvm/WellKnownBTypes.scala b/compiler/src/dotty/tools/backend/jvm/WellKnownBTypes.scala deleted file mode 100644 index 1059f29ce150..000000000000 --- a/compiler/src/dotty/tools/backend/jvm/WellKnownBTypes.scala +++ /dev/null @@ -1,348 +0,0 @@ -package dotty.tools.backend.jvm - -import dotty.tools.dotc.core.Symbols.* -import dotty.tools.dotc.transform.Erasure - -import scala.tools.asm.{Handle, Opcodes} -import dotty.tools.dotc.core.Symbols -import BTypes.* -import dotty.tools.dotc.core.Contexts.Context -import dotty.tools.dotc.core.StdNames.* -import PostProcessorFrontendAccess.Lazy - - -case class MethodNameAndType(name: String, methodType: MethodBType) - -final class WellKnownBTypes(ppa: PostProcessorFrontendAccess, ts: BTypeLoader)(using Context) { - - def ObjectRef: ClassBType = _ObjectRef.get - private lazy val _ObjectRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(defn.ObjectClass)) - - def srNothingRef: ClassBType = _srNothingRef.get - private lazy val _srNothingRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(defn.RuntimeNothingClass)) - - def srNullRef: ClassBType = _srNullRef.get - private lazy val _srNullRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(defn.RuntimeNullClass)) - - /** - * Map from primitive types to their boxed class type. Useful when pushing class literals onto the - * operand stack (ldc instruction taking a class literal), see genConstant. - */ - def boxedClassOfPrimitive: Map[BType, ClassBType] = _boxedClassOfPrimitive.get - private lazy val _boxedClassOfPrimitive: Lazy[Map[BType, ClassBType]] = ppa.perRunLazy(Map( - UNIT -> ts.classBTypeFromSymbol(requiredClass[java.lang.Void]), - BOOL -> ts.classBTypeFromSymbol(requiredClass[java.lang.Boolean]), - BYTE -> ts.classBTypeFromSymbol(requiredClass[java.lang.Byte]), - SHORT -> ts.classBTypeFromSymbol(requiredClass[java.lang.Short]), - CHAR -> ts.classBTypeFromSymbol(requiredClass[java.lang.Character]), - INT -> ts.classBTypeFromSymbol(requiredClass[java.lang.Integer]), - LONG -> ts.classBTypeFromSymbol(requiredClass[java.lang.Long]), - FLOAT -> ts.classBTypeFromSymbol(requiredClass[java.lang.Float]), - DOUBLE -> ts.classBTypeFromSymbol(requiredClass[java.lang.Double]) - )) - - lazy val boxedClasses: Set[ClassBType] = boxedClassOfPrimitive.values.toSet - - /** - * Maps the method symbol for a box method to the boxed type of the result. For example, the - * method symbol for `Byte.box()` is mapped to the ClassBType `java/lang/Byte`. - */ - def boxResultType: Map[Symbol, ClassBType] = _boxResultType.get - private lazy val _boxResultType: Lazy[Map[Symbol, ClassBType]] = ppa.perRunLazy{ - val boxMethods = defn.ScalaValueClasses().map{x => - (x, Erasure.Boxing.boxMethod(x.asClass)) - }.toMap - for ((valueClassSym, boxMethodSym) <- boxMethods) - yield boxMethodSym -> boxedClassOfPrimitive(ts.bTypeFromSymbol(valueClassSym)) - } - - /** - * Maps the method symbol for an unbox method to the primitive type of the result. - * For example, the method symbol for `Byte.unbox()` is mapped to the PrimitiveBType BYTE. */ - def unboxResultType: Map[Symbol, BType] = _unboxResultType.get - private lazy val _unboxResultType = ppa.perRunLazy[Map[Symbol, BType]]{ - val unboxMethods: Map[Symbol, Symbol] = - defn.ScalaValueClasses().map(x => (x, Erasure.Boxing.unboxMethod(x.asClass))).toMap - for ((valueClassSym, unboxMethodSym) <- unboxMethods) - yield unboxMethodSym -> ts.bTypeFromSymbol(valueClassSym) - } - - def srBoxedUnitRef: ClassBType = _srBoxedUnitRef.get - private lazy val _srBoxedUnitRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(requiredClass[scala.runtime.BoxedUnit])) - - def StringRef: ClassBType = _StringRef.get - private lazy val _StringRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(defn.StringClass)) - - def PredefRef: ClassBType = _PredefRef.get - private lazy val _PredefRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(defn.ScalaPredefModuleClass)) - - def jlClassRef: ClassBType = _jlClassRef.get - private lazy val _jlClassRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(requiredClass[java.lang.Class[?]])) - - def jlThrowableRef: ClassBType = _jlThrowableRef.get - private lazy val _jlThrowableRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(defn.ThrowableClass)) - - def jlCloneableRef: ClassBType = _jlCloneableRef.get - private lazy val _jlCloneableRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(defn.JavaCloneableClass)) - - def jiSerializableRef: ClassBType = _jiSerializableRef.get - private lazy val _jiSerializableRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(requiredClass[java.io.Serializable])) - - def jlClassCastExceptionRef: ClassBType = _jlClassCastExceptionRef.get - private lazy val _jlClassCastExceptionRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(requiredClass[java.lang.ClassCastException])) - - def jlIllegalArgExceptionRef: ClassBType = _jlIllegalArgExceptionRef.get - private lazy val _jlIllegalArgExceptionRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(requiredClass[java.lang.IllegalArgumentException])) - - def jliSerializedLambdaRef: ClassBType = _jliSerializedLambdaRef.get - private lazy val _jliSerializedLambdaRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(requiredClass[java.lang.invoke.SerializedLambda])) - - def srBoxesRuntimeRef: ClassBType = _srBoxesRuntimeRef.get - private lazy val _srBoxesRuntimeRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(requiredClass[scala.runtime.BoxesRunTime])) - - private def jliCallSiteRef: ClassBType = _jliCallSiteRef.get - private lazy val _jliCallSiteRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(requiredClass[java.lang.invoke.CallSite])) - - private def jliLambdaMetafactoryRef: ClassBType = _jliLambdaMetafactoryRef.get - private lazy val _jliLambdaMetafactoryRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(requiredClass[java.lang.invoke.LambdaMetafactory])) - - private def jliMethodHandleRef: ClassBType = _jliMethodHandleRef.get - private lazy val _jliMethodHandleRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(defn.MethodHandleClass)) - - private def jliMethodHandlesLookupRef: ClassBType = _jliMethodHandlesLookupRef.get - private lazy val _jliMethodHandlesLookupRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(defn.MethodHandlesLookupClass)) - - private def jliMethodTypeRef: ClassBType = _jliMethodTypeRef.get - private lazy val _jliMethodTypeRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(requiredClass[java.lang.invoke.MethodType])) - - private def jliStringConcatFactoryRef: ClassBType = _jliStringConcatFactoryRef.get - private lazy val _jliStringConcatFactoryRef: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(requiredClass[java.lang.invoke.StringConcatFactory])) - - private def srLambdaDeserialize: ClassBType = _srLambdaDeserialize.get - private lazy val _srLambdaDeserialize: Lazy[ClassBType] = ppa.perRunLazy(ts.classBTypeFromSymbol(requiredClass[scala.runtime.LambdaDeserialize])) - - - def jliLambdaMetaFactoryMetafactoryHandle: Handle = _jliLambdaMetaFactoryMetafactoryHandle.get - private lazy val _jliLambdaMetaFactoryMetafactoryHandle: Lazy[Handle] = ppa.perRunLazy{new Handle( - Opcodes.H_INVOKESTATIC, - jliLambdaMetafactoryRef.internalName, - "metafactory", - MethodBType( - List(jliMethodHandlesLookupRef, StringRef, jliMethodTypeRef, jliMethodTypeRef, jliMethodHandleRef, jliMethodTypeRef), - jliCallSiteRef - ).descriptor, - /* itf = */ false)} - - def jliLambdaMetaFactoryAltMetafactoryHandle: Handle = _jliLambdaMetaFactoryAltMetafactoryHandle.get - private lazy val _jliLambdaMetaFactoryAltMetafactoryHandle: Lazy[Handle] = ppa.perRunLazy{ new Handle( - Opcodes.H_INVOKESTATIC, - jliLambdaMetafactoryRef.internalName, - "altMetafactory", - MethodBType( - List(jliMethodHandlesLookupRef, StringRef, jliMethodTypeRef, ArrayBType(ObjectRef)), - jliCallSiteRef - ).descriptor, - /* itf = */ false)} - - def jliLambdaDeserializeBootstrapHandle: Handle = _jliLambdaDeserializeBootstrapHandle.get - private lazy val _jliLambdaDeserializeBootstrapHandle: Lazy[Handle] = ppa.perRunLazy{ new Handle( - Opcodes.H_INVOKESTATIC, - srLambdaDeserialize.internalName, - "bootstrap", - MethodBType( - List(jliMethodHandlesLookupRef, StringRef, jliMethodTypeRef, ArrayBType(jliMethodHandleRef)), - jliCallSiteRef - ).descriptor, - /* itf = */ false)} - - def jliStringConcatFactoryMakeConcatWithConstantsHandle: Handle = _jliStringConcatFactoryMakeConcatWithConstantsHandle.get - private lazy val _jliStringConcatFactoryMakeConcatWithConstantsHandle: Lazy[Handle] = ppa.perRunLazy{ new Handle( - Opcodes.H_INVOKESTATIC, - jliStringConcatFactoryRef.internalName, - "makeConcatWithConstants", - MethodBType( - List(jliMethodHandlesLookupRef, StringRef, jliMethodTypeRef, StringRef, ArrayBType(ObjectRef)), - jliCallSiteRef - ).descriptor, - /* itf = */ false)} - - /** - * Methods in scala.runtime.BoxesRuntime - * No need to wrap in Lazy to synchronize access, symbols won't change - */ - lazy val asmBoxTo : Map[BType, MethodNameAndType] = Map( - BOOL -> MethodNameAndType("boxToBoolean", MethodBType(List(BOOL), boxedClassOfPrimitive(BOOL))), - BYTE -> MethodNameAndType("boxToByte", MethodBType(List(BYTE), boxedClassOfPrimitive(BYTE))), - CHAR -> MethodNameAndType("boxToCharacter", MethodBType(List(CHAR), boxedClassOfPrimitive(CHAR))), - SHORT -> MethodNameAndType("boxToShort", MethodBType(List(SHORT), boxedClassOfPrimitive(SHORT))), - INT -> MethodNameAndType("boxToInteger", MethodBType(List(INT), boxedClassOfPrimitive(INT))), - LONG -> MethodNameAndType("boxToLong", MethodBType(List(LONG), boxedClassOfPrimitive(LONG))), - FLOAT -> MethodNameAndType("boxToFloat", MethodBType(List(FLOAT), boxedClassOfPrimitive(FLOAT))), - DOUBLE -> MethodNameAndType("boxToDouble", MethodBType(List(DOUBLE), boxedClassOfPrimitive(DOUBLE))) - ) - - lazy val asmUnboxTo: Map[BType, MethodNameAndType] = Map( - BOOL -> MethodNameAndType("unboxToBoolean", MethodBType(List(ObjectRef), BOOL)), - BYTE -> MethodNameAndType("unboxToByte", MethodBType(List(ObjectRef), BYTE)), - CHAR -> MethodNameAndType("unboxToChar", MethodBType(List(ObjectRef), CHAR)), - SHORT -> MethodNameAndType("unboxToShort", MethodBType(List(ObjectRef), SHORT)), - INT -> MethodNameAndType("unboxToInt", MethodBType(List(ObjectRef), INT)), - LONG -> MethodNameAndType("unboxToLong", MethodBType(List(ObjectRef), LONG)), - FLOAT -> MethodNameAndType("unboxToFloat", MethodBType(List(ObjectRef), FLOAT)), - DOUBLE -> MethodNameAndType("unboxToDouble", MethodBType(List(ObjectRef), DOUBLE)) - ) - - lazy val typeOfArrayOp: Map[Int, BType] = { - import dotty.tools.backend.ScalaPrimitivesOps.* - Map( - (List(ZARRAY_LENGTH, ZARRAY_GET, ZARRAY_SET) map (_ -> BOOL)) ++ - (List(BARRAY_LENGTH, BARRAY_GET, BARRAY_SET) map (_ -> BYTE)) ++ - (List(SARRAY_LENGTH, SARRAY_GET, SARRAY_SET) map (_ -> SHORT)) ++ - (List(CARRAY_LENGTH, CARRAY_GET, CARRAY_SET) map (_ -> CHAR)) ++ - (List(IARRAY_LENGTH, IARRAY_GET, IARRAY_SET) map (_ -> INT)) ++ - (List(LARRAY_LENGTH, LARRAY_GET, LARRAY_SET) map (_ -> LONG)) ++ - (List(FARRAY_LENGTH, FARRAY_GET, FARRAY_SET) map (_ -> FLOAT)) ++ - (List(DARRAY_LENGTH, DARRAY_GET, DARRAY_SET) map (_ -> DOUBLE)) ++ - (List(OARRAY_LENGTH, OARRAY_GET, OARRAY_SET) map (_ -> ObjectRef)) * - ) - } - - // java/lang/Boolean -> MethodNameAndType(valueOf,(Z)Ljava/lang/Boolean;) - def javaBoxMethods: Map[InternalName, MethodNameAndType] = _javaBoxMethods.get - private lazy val _javaBoxMethods: Lazy[Map[InternalName, MethodNameAndType]] = ppa.perRunLazy { - Map.from(defn.ScalaValueClassesNoUnit().map(primitive => { - val boxed = defn.boxedClass(primitive) - val unboxed = ts.bTypeFromSymbol(primitive) - val method = MethodNameAndType("valueOf", MethodBType(List(unboxed), boxedClassOfPrimitive(unboxed))) - (ts.classBTypeFromSymbol(boxed).internalName, method) - })) - } - - // java/lang/Boolean -> MethodNameAndType(booleanValue,()Z) - def javaUnboxMethods: Map[InternalName, MethodNameAndType] = _javaUnboxMethods.get - private lazy val _javaUnboxMethods: Lazy[Map[InternalName, MethodNameAndType]] = ppa.perRunLazy { - Map.from(defn.ScalaValueClassesNoUnit().map(primitive => { - val boxed = defn.boxedClass(primitive) - val name = primitive.name.toString.toLowerCase + "Value" - (ts.classBTypeFromSymbol(boxed).internalName, MethodNameAndType(name, MethodBType(Nil, ts.bTypeFromSymbol(primitive)))) - })) - } - - private def predefBoxingMethods(isBox: Boolean, getName: (String, String) => String): Map[String, MethodBType] = - Map.from(defn.ScalaValueClassesNoUnit().map(primitive => { - val unboxed = ts.bTypeFromSymbol(primitive) - val boxed = boxedClassOfPrimitive(unboxed) - val name = getName(primitive.name.toString, defn.boxedClass(primitive).name.toString) - (name, MethodBType(List(if isBox then unboxed else boxed), if isBox then boxed else unboxed)) - })) - - // boolean2Boolean -> (Z)Ljava/lang/Boolean; - def predefAutoBoxMethods: Map[String, MethodBType] = _predefAutoBoxMethods.get - private lazy val _predefAutoBoxMethods: Lazy[Map[String, MethodBType]] = ppa.perRunLazy(predefBoxingMethods(true, (primitive, boxed) => primitive.toLowerCase + "2" + boxed)) - - // Boolean2boolean -> (Ljava/lang/Boolean;)Z - def predefAutoUnboxMethods: Map[String, MethodBType] = _predefAutoUnboxMethods.get - private lazy val _predefAutoUnboxMethods: Lazy[Map[String, MethodBType]] = ppa.perRunLazy(predefBoxingMethods(false, (primitive, boxed) => boxed + "2" + primitive.toLowerCase)) - - // scala/runtime/BooleanRef -> MethodNameAndType(create,(Z)Lscala/runtime/BooleanRef;) - def srRefCreateMethods: Map[InternalName, MethodNameAndType] = _srRefCreateMethods.get - private lazy val _srRefCreateMethods: Lazy[Map[InternalName, MethodNameAndType]] = ppa.perRunLazy { - Map.from(defn.ScalaValueClassesNoUnit().union(Set(defn.ObjectClass)).flatMap(primitive => { - val boxed = if primitive == defn.ObjectClass then primitive else defn.boxedClass(primitive) - val unboxed = if primitive == defn.ObjectClass then ObjectRef else ts.bTypeFromSymbol(primitive) - val refClass = Symbols.requiredClass("scala.runtime." + primitive.name.toString + "Ref") - val volatileRefClass = Symbols.requiredClass("scala.runtime.Volatile" + primitive.name.toString + "Ref") - List( - (ts.classBTypeFromSymbol(refClass).internalName, MethodNameAndType(nme.create.toString, MethodBType(List(unboxed), ts.bTypeFromSymbol(refClass)))), - (ts.classBTypeFromSymbol(volatileRefClass).internalName, MethodNameAndType(nme.create.toString, MethodBType(List(unboxed), ts.bTypeFromSymbol(volatileRefClass)))) - ) - })) - } - - // scala/runtime/BooleanRef -> MethodNameAndType(zero,()Lscala/runtime/BooleanRef;) - def srRefZeroMethods: Map[InternalName, MethodNameAndType] = _srRefZeroMethods.get - private lazy val _srRefZeroMethods: Lazy[Map[InternalName, MethodNameAndType]] = ppa.perRunLazy { - Map.from(defn.ScalaValueClassesNoUnit().union(Set(defn.ObjectClass)).flatMap(primitive => { - val boxed = if primitive == defn.ObjectClass then primitive else defn.boxedClass(primitive) - val refClass = Symbols.requiredClass("scala.runtime." + primitive.name.toString + "Ref") - val volatileRefClass = Symbols.requiredClass("scala.runtime.Volatile" + primitive.name.toString + "Ref") - List( - (ts.classBTypeFromSymbol(refClass).internalName, MethodNameAndType(nme.zero.toString, MethodBType(List(), ts.bTypeFromSymbol(refClass)))), - (ts.classBTypeFromSymbol(volatileRefClass).internalName, MethodNameAndType(nme.zero.toString, MethodBType(List(), ts.bTypeFromSymbol(volatileRefClass)))) - ) - })) - } - - // java/lang/Boolean -> MethodNameAndType(,(Z)V) - def primitiveBoxConstructors: Map[InternalName, MethodNameAndType] = _primitiveBoxConstructors.get - private lazy val _primitiveBoxConstructors: Lazy[Map[InternalName, MethodNameAndType]] = ppa.perRunLazy { - Map.from(defn.ScalaValueClassesNoUnit().map(primitive => { - val boxed = defn.boxedClass(primitive) - val unboxed = ts.bTypeFromSymbol(primitive) - (ts.classBTypeFromSymbol(boxed).internalName, MethodNameAndType(nme.CONSTRUCTOR.toString, MethodBType(List(unboxed), UNIT))) - })) - } - - // Z -> MethodNameAndType(boxToBoolean,(Z)Ljava/lang/Boolean;) - def srBoxesRuntimeBoxToMethods: Map[BType, MethodNameAndType] = _srBoxesRuntimeBoxToMethods.get - private lazy val _srBoxesRuntimeBoxToMethods: Lazy[Map[BType, MethodNameAndType]] = ppa.perRunLazy { - Map.from(defn.ScalaValueClassesNoUnit().map(primitive => { - val bType = ts.bTypeFromSymbol(primitive) - val boxed = boxedClassOfPrimitive(bType) - val name = "boxTo" + defn.boxedClass(primitive).name.toString - (bType, MethodNameAndType(name, MethodBType(List(bType), boxed))) - })) - } - - // Z -> MethodNameAndType(unboxToBoolean,(Ljava/lang/Object;)Z) - def srBoxesRuntimeUnboxToMethods: Map[BType, MethodNameAndType] = _srBoxesRuntimeUnboxToMethods.get - private lazy val _srBoxesRuntimeUnboxToMethods: Lazy[Map[BType, MethodNameAndType]] = ppa.perRunLazy { - Map.from(defn.ScalaValueClassesNoUnit().map(primitive => { - val bType = ts.bTypeFromSymbol(primitive) - val name = "unboxTo" + primitive.name.toString - (bType, MethodNameAndType(name, MethodBType(List(ObjectRef), bType))) - })) - } - - // scala/runtime/BooleanRef -> MethodNameAndType(,(Z)V) - def srRefConstructors: Map[InternalName, MethodNameAndType] = _srRefConstructors.get - private lazy val _srRefConstructors: Lazy[Map[InternalName, MethodNameAndType]] = ppa.perRunLazy { - Map.from(defn.ScalaValueClassesNoUnit().union(Set(defn.ObjectClass)).flatMap(primitive => { - val boxed = if primitive == defn.ObjectClass then primitive else defn.boxedClass(primitive) - val unboxed = if primitive == defn.ObjectClass then ObjectRef else ts.bTypeFromSymbol(primitive) - val refClass = Symbols.requiredClass("scala.runtime." + primitive.name.toString + "Ref") - val volatileRefClass = Symbols.requiredClass("scala.runtime.Volatile" + primitive.name.toString + "Ref") - List( - (ts.classBTypeFromSymbol(refClass).internalName, MethodNameAndType(nme.CONSTRUCTOR.toString, MethodBType(List(unboxed), UNIT))), - (ts.classBTypeFromSymbol(volatileRefClass).internalName, MethodNameAndType(nme.CONSTRUCTOR.toString, MethodBType(List(unboxed), UNIT))) - ) - })) - } - - // scala/Tuple3 -> MethodNameAndType(,(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V) - // scala/Tuple2$mcZC$sp -> MethodNameAndType(,(ZC)V) - // ... this was easy in scala2, but now we don't specialize them so we have to know each name - // tuple1 is specialized for D, I, J - // tuple2 is specialized for C, D, I, J, Z in each parameter - def tupleClassConstructors: Map[InternalName, MethodNameAndType] = _tupleClassConstructors.get - private lazy val _tupleClassConstructors: Lazy[Map[InternalName, MethodNameAndType]] = ppa.perRunLazy { - val spec1 = List(defn.DoubleClass, defn.IntClass, defn.LongClass) - val spec2 = List(defn.CharClass, defn.DoubleClass, defn.IntClass, defn.LongClass, defn.BooleanClass) - Map.from( - Iterator.concat( - (1 to 22).map { n => - ("scala/Tuple" + n, MethodNameAndType(nme.CONSTRUCTOR.toString, MethodBType(List.fill(n)(ObjectRef), UNIT))) - }, - spec1.map { sp1 => - val prim = ts.bTypeFromSymbol(sp1) - ("scala/Tuple1$mc" + prim.descriptor + "$sp", MethodNameAndType(nme.CONSTRUCTOR.toString, MethodBType(List(), UNIT))) - }, - for sp2a <- spec2; sp2b <- spec2 yield { - val primA = ts.bTypeFromSymbol(sp2a) - val primB = ts.bTypeFromSymbol(sp2b) - ("scala/Tuple2$mc" + primA.descriptor + primB.descriptor + "$sp", MethodNameAndType(nme.CONSTRUCTOR.toString, MethodBType(List(primA, primB), UNIT))) - } - ) - ) - } -} diff --git a/compiler/src/dotty/tools/backend/jvm/analysis/AnalysisUtils.scala b/compiler/src/dotty/tools/backend/jvm/analysis/AnalysisUtils.scala new file mode 100644 index 000000000000..8d0d2cdf2381 --- /dev/null +++ b/compiler/src/dotty/tools/backend/jvm/analysis/AnalysisUtils.scala @@ -0,0 +1,181 @@ +package dotty.tools.backend.jvm.analysis + +import dotty.tools.backend.jvm.BTypes.InternalName +import dotty.tools.backend.jvm.{BCodeUtils, ClassBType} + +import scala.tools.asm +import scala.tools.asm.{Handle, Opcodes} +import scala.tools.asm.tree.{AbstractInsnNode, FieldInsnNode, InvokeDynamicInsnNode, MethodInsnNode, MethodNode} + +object AnalysisUtils { + + val primitiveTypes: Map[String, asm.Type] = Map( + ("Unit", asm.Type.VOID_TYPE), + ("Boolean", asm.Type.BOOLEAN_TYPE), + ("Char", asm.Type.CHAR_TYPE), + ("Byte", asm.Type.BYTE_TYPE), + ("Short", asm.Type.SHORT_TYPE), + ("Int", asm.Type.INT_TYPE), + ("Float", asm.Type.FLOAT_TYPE), + ("Long", asm.Type.LONG_TYPE), + ("Double", asm.Type.DOUBLE_TYPE)) + + def isModuleLoad(insn: AbstractInsnNode, nameMatches: InternalName => Boolean): Boolean = insn match { + case fi: FieldInsnNode => + fi.getOpcode == Opcodes.GETSTATIC && + nameMatches(fi.owner) && + fi.name == "MODULE$" && + fi.desc.length == fi.owner.length + 2 && + fi.desc.regionMatches(1, fi.owner, 0, fi.owner.length) + case _ => false + } + + def isJavaLangStaticLoad(insn: AbstractInsnNode): Boolean = insn match { + case fi: FieldInsnNode => + fi.getOpcode == Opcodes.GETSTATIC && + fi.owner.startsWith("java/lang/") + case _ => false + } + + // ============================================================================================== + + def isArrayGetLength(mi: MethodInsnNode): Boolean = mi.owner == "java/lang/reflect/Array" && mi.name == "getLength" && mi.desc == "(Ljava/lang/Object;)I" + + // If argument i of the method is null-checked, the bit `i+1` of the result is 1 + def argumentsNullCheckedByCallee(mi: MethodInsnNode): Long = { + if (isArrayGetLength(mi)) 1 + else 0 + } + + // ============================================================================================== + + final case class LambdaMetaFactoryCall(indy: InvokeDynamicInsnNode, samMethodType: asm.Type, implMethod: Handle, instantiatedMethodType: asm.Type) + + object LambdaMetaFactoryCall { + private val lambdaMetaFactoryMetafactoryHandle = new Handle( + Opcodes.H_INVOKESTATIC, + "java/lang/invoke/LambdaMetafactory", + "metafactory", + "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", + /* itf = */ false) + + private val lambdaMetaFactoryAltMetafactoryHandle = new Handle( + Opcodes.H_INVOKESTATIC, + "java/lang/invoke/LambdaMetafactory", + "altMetafactory", + "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;", + /* itf = */ false) + + def unapply(insn: AbstractInsnNode): Option[(InvokeDynamicInsnNode, asm.Type, Handle, asm.Type, Array[asm.Type])] = insn match { + case indy: InvokeDynamicInsnNode if indy.bsm == lambdaMetaFactoryMetafactoryHandle || indy.bsm == lambdaMetaFactoryAltMetafactoryHandle => + indy.bsmArgs match { + case Array(samMethodType: asm.Type, implMethod: Handle, instantiatedMethodType: asm.Type, _*) => + // LambdaMetaFactory performs a number of automatic adaptations when invoking the lambda + // implementation method (casting, boxing, unboxing, and primitive widening, see Javadoc). + // + // The closure optimizer supports only one of those adaptations: it will cast arguments + // to the correct type when re-writing a closure call to the body method. Example: + // + // val fun: String => String = l => l + // val l = List("") + // fun(l.head) + // + // The samMethodType of Function1 is `(Object)Object`, while the instantiatedMethodType + // is `(String)String`. The return type of `List.head` is `Object`. + // + // The implMethod has the signature `C$anonfun(String)String`. + // + // At the closure callsite, we have an `INVOKEINTERFACE Function1.apply (Object)Object`, + // so the object returned by `List.head` can be directly passed into the call (no cast). + // + // The closure object will cast the object to String before passing it to the implMethod. + // + // When re-writing the closure callsite to the implMethod, we have to insert a cast. + // + // The check below ensures that + // (1) the implMethod type has the expected arguments (captured types plus argument types + // from instantiatedMethodType) + // (2) the receiver of the implMethod matches the first captured type, if any, otherwise + // the first parameter type of instantiatedMethodType + // (3) all parameters that are not the same in samMethodType and instantiatedMethodType + // are reference types, so that we can insert casts to perform the same adaptation + // that the closure object would. + + val isStatic = implMethod.getTag == Opcodes.H_INVOKESTATIC + val indyParamTypes = asm.Type.getArgumentTypes(indy.desc) + val instantiatedMethodArgTypes = instantiatedMethodType.getArgumentTypes + + val (receiverType, expectedImplMethodType) = + if (isStatic) { + val paramTypes = indyParamTypes ++ instantiatedMethodArgTypes + (None, asm.Type.getMethodType(instantiatedMethodType.getReturnType, paramTypes*)) + } else if (implMethod.getTag == Opcodes.H_NEWINVOKESPECIAL) { + (Some(instantiatedMethodType.getReturnType), asm.Type.getMethodType(asm.Type.VOID_TYPE, instantiatedMethodArgTypes*)) + } else { + if (indyParamTypes.nonEmpty) { + val paramTypes = indyParamTypes.tail ++ instantiatedMethodArgTypes + (Some(indyParamTypes(0)), asm.Type.getMethodType(instantiatedMethodType.getReturnType, paramTypes*)) + } else { + val paramTypes = instantiatedMethodArgTypes.tail + (Some(instantiatedMethodArgTypes(0)), asm.Type.getMethodType(instantiatedMethodType.getReturnType, paramTypes*)) + } + } + + val isIndyLambda = + asm.Type.getType(implMethod.getDesc).getArgumentTypes.sameElements(expectedImplMethodType.getArgumentTypes) // (1) + && receiverType.forall(rt => implMethod.getOwner == rt.getInternalName) // (2) + && samMethodType.getArgumentTypes.corresponds(instantiatedMethodArgTypes)((samArgType, instArgType) => + samArgType == instArgType || BCodeUtils.isReference(samArgType) && BCodeUtils.isReference(instArgType)) // (3) + + if (isIndyLambda) Some((indy, samMethodType, implMethod, instantiatedMethodType, indyParamTypes)) + else None + + case _ => None + } + case _ => None + } + } + + // ============================================================================================== + + def isRuntimeArrayLoadOrUpdate(insn: AbstractInsnNode): Boolean = insn.getOpcode == Opcodes.INVOKEVIRTUAL && { + val mi = insn.asInstanceOf[MethodInsnNode] + mi.owner == "scala/runtime/ScalaRunTime$" && { + mi.name == "array_apply" && mi.desc == "(Ljava/lang/Object;I)Ljava/lang/Object;" || + mi.name == "array_update" && mi.desc == "(Ljava/lang/Object;ILjava/lang/Object;)V" + } + } + + private val primitiveManifestApplies: Map[String, String] = primitiveTypes map { + case (k, _) => (k, s"()Lscala/reflect/ManifestFactory$$${k}Manifest;") + } + + def isClassTagApply(mi: MethodInsnNode): Boolean = { + mi.owner == "scala/reflect/ClassTag$" && { + mi.name == "apply" && mi.desc == "(Ljava/lang/Class;)Lscala/reflect/ClassTag;" || + primitiveManifestApplies.get(mi.name).contains(mi.desc) + } + } + + // ============================================================================================== + + def isTraitSuperAccessor(method: MethodNode, owner: ClassBType): Boolean = { + owner.isInterface && + BCodeUtils.isSyntheticMethod(method) && + method.name.endsWith("$") && + BCodeUtils.isStaticMethod(method) && + BCodeUtils.findSingleCall(method, mi => mi.itf && mi.getOpcode == Opcodes.INVOKESPECIAL && mi.name + "$" == method.name).nonEmpty + } + + def isMixinForwarder(method: MethodNode, owner: ClassBType): Boolean = { + !owner.isInterface && + // isSyntheticMethod(method) && // mixin forwarders are not synthetic it seems + !BCodeUtils.isStaticMethod(method) && + BCodeUtils.findSingleCall(method, mi => mi.itf && mi.getOpcode == Opcodes.INVOKESTATIC && mi.name == method.name + "$").nonEmpty + } + + def isTraitSuperAccessorOrMixinForwarder(method: MethodNode, owner: ClassBType): Boolean = { + isTraitSuperAccessor(method, owner) || isMixinForwarder(method, owner) + } + +} diff --git a/compiler/src/dotty/tools/backend/jvm/analysis/NullnessAnalyzer.scala b/compiler/src/dotty/tools/backend/jvm/analysis/NullnessAnalyzer.scala index ec9d434befbc..3e6a39913032 100644 --- a/compiler/src/dotty/tools/backend/jvm/analysis/NullnessAnalyzer.scala +++ b/compiler/src/dotty/tools/backend/jvm/analysis/NullnessAnalyzer.scala @@ -15,13 +15,11 @@ package backend.jvm package analysis import java.util - import scala.annotation.switch import scala.tools.asm.tree.analysis.* import scala.tools.asm.tree.* import scala.tools.asm.{Opcodes, Type} import dotty.tools.backend.jvm.BCodeUtils.FrameExtensions -import dotty.tools.backend.jvm.BackendUtils.isModuleLoad /** * See the package object `analysis` for details on the ASM analysis framework. @@ -108,7 +106,7 @@ final class NullnessInterpreter(knownNonNullInvocation: MethodInsnNode => Boolea case Opcodes.GETSTATIC => val fi = insn.asInstanceOf[FieldInsnNode] - if (modulesNonNull && isModuleLoad(fi, _ == fi.owner)) NotNullValue + if (modulesNonNull && AnalysisUtils.isModuleLoad(fi, _ == fi.owner)) NotNullValue else NullnessValue.unknown(insn) // for Opcodes.NEW, we use Unknown. The value will become NotNull after the constructor call. @@ -216,7 +214,7 @@ class NullnessFrame(nLocals: Int, nStack: Int) extends AliasingFrame[NullnessVal aliasesOf(this.stackTop - numArgs) case INVOKESTATIC => - var nullChecked = BackendUtils.argumentsNullCheckedByCallee(insn.asInstanceOf[MethodInsnNode]) + var nullChecked = AnalysisUtils.argumentsNullCheckedByCallee(insn.asInstanceOf[MethodInsnNode]) var i = 0 var res: AliasSet | Null = null while (nullChecked > 0) { diff --git a/compiler/src/dotty/tools/backend/jvm/analysis/TypeFlowAnalyzer.scala b/compiler/src/dotty/tools/backend/jvm/analysis/TypeFlowAnalyzer.scala index 590fa8a7a78f..fe3d4ac6ea8b 100644 --- a/compiler/src/dotty/tools/backend/jvm/analysis/TypeFlowAnalyzer.scala +++ b/compiler/src/dotty/tools/backend/jvm/analysis/TypeFlowAnalyzer.scala @@ -19,7 +19,6 @@ import scala.tools.asm.{Opcodes, Type} import scala.tools.asm.tree.{AbstractInsnNode, InsnNode, MethodNode} import scala.tools.asm.tree.analysis.{Analyzer, BasicInterpreter, BasicValue} import dotty.tools.backend.jvm.BCodeUtils.FrameExtensions -import dotty.tools.backend.jvm.BackendUtils.LambdaMetaFactoryCall abstract class TypeFlowInterpreter extends BasicInterpreter(scala.tools.asm.Opcodes.ASM7) { import TypeFlowInterpreter.* @@ -47,7 +46,7 @@ abstract class TypeFlowInterpreter extends BasicInterpreter(scala.tools.asm.Opco val v = super.naryOperation(insn, values) insn.getOpcode match { case Opcodes.INVOKEDYNAMIC => insn match { - case LambdaMetaFactoryCall(_, _, _, _, _) => new LMFValue(v.getType) + case AnalysisUtils.LambdaMetaFactoryCall(_, _, _, _, _) => new LMFValue(v.getType) case _ => v } case _ => v @@ -69,7 +68,7 @@ object TypeFlowInterpreter { // Marker trait for BasicValue subclasses that add a special meaning on top of the value's `getType`. trait SpecialValue - private val obj = Type.getObjectType("java/lang/Object") + private val obj = Type.getObjectType(ClassBType.javaLangObjectInternalName) // A BasicValue with equality that knows about special versions class SpecialAwareBasicValue(tpe: Type | Null) extends BasicValue(tpe) { diff --git a/compiler/src/dotty/tools/backend/jvm/opt/BCodeRepository.scala b/compiler/src/dotty/tools/backend/jvm/opt/BCodeRepository.scala index 59a4b453e10b..641bc1a9de3a 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/BCodeRepository.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/BCodeRepository.scala @@ -14,9 +14,9 @@ package dotty.tools.backend.jvm.opt import dotty.tools.backend.jvm.BCodeUtils.* import dotty.tools.backend.jvm.BTypes.InternalName -import dotty.tools.backend.jvm.BackendUtils.LambdaMetaFactoryCall import dotty.tools.backend.jvm.opt.* -import dotty.tools.backend.jvm.{BackendUtils, ClassNode1} +import dotty.tools.backend.jvm.ClassNode1 +import dotty.tools.backend.jvm.analysis.AnalysisUtils.LambdaMetaFactoryCall import dotty.tools.dotc.classpath.{AggregateClassPath, CtSymClassPath, JrtClassPath} import dotty.tools.io import dotty.tools.io.ClassPath @@ -31,7 +31,7 @@ import scala.tools.asm.{Attribute, ClassReader, Type} * The BCodeRepository provides utilities to read the bytecode of classfiles from the compilation * classpath. Parsed classes are cached in the `classes` map. */ -class BCodeRepository(classPath: ClassPath, backendUtils: BackendUtils) { +class BCodeRepository(classPath: ClassPath, indyTracker: IndyLambdaImplTracker) { type ClassAndModuleNodes = (ClassNode, Option[ModuleNode]) @@ -273,7 +273,7 @@ class BCodeRepository(classPath: ClassPath, backendUtils: BackendUtils) { iter.remove() case AbstractInsnNode.INVOKE_DYNAMIC_INSN => insn match { case LambdaMetaFactoryCall(indy, _, implMethod, _, _) => - backendUtils.addIndyLambdaImplMethod(classNode.name, m, indy, implMethod) + indyTracker.add(classNode.name, m, indy, implMethod) case _ => } case _ => diff --git a/compiler/src/dotty/tools/backend/jvm/opt/BTypesFromClassfile.scala b/compiler/src/dotty/tools/backend/jvm/opt/BTypesFromClassfile.scala index 12f2046ec9ee..4e73b2def4b7 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/BTypesFromClassfile.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/BTypesFromClassfile.scala @@ -23,7 +23,7 @@ import scala.jdk.CollectionConverters.* import scala.tools.asm.Opcodes import scala.tools.asm.tree.{ClassNode, InnerClassNode, ModuleNode} -class BTypesFromClassfile(byteCodeRepository: BCodeRepository, bTypeLoader: BTypeLoader) extends InlineInfoLoader { +class BTypesFromClassfile(byteCodeRepository: BCodeRepository, cache: ClassBType.Cache) extends InlineInfoLoader { /** * Obtain the BType for a type descriptor or internal name. For class descriptors, the ClassBType @@ -62,7 +62,7 @@ class BTypesFromClassfile(byteCodeRepository: BCodeRepository, bTypeLoader: BTyp * be found in the `byteCodeRepository`, the `info` of the resulting ClassBType is undefined. */ def classBTypeFromParsedClassfile(internalName: InternalName): Either[OptimizerWarning, ClassBType] = { - bTypeLoader.classBType(internalName) { _ => + cache(internalName) { _ => byteCodeRepository.classNode(internalName) match { case Left(msg) => Left(NoClassBTypeInfo(msg)) case Right(c, m) => computeClassInfoFromClassNode(c, m) @@ -74,7 +74,7 @@ class BTypesFromClassfile(byteCodeRepository: BCodeRepository, bTypeLoader: BTyp * Construct the [[BTypes.ClassBType]] for a parsed classfile. */ def classBTypeFromClassNode(classNode: ClassNode, moduleNode: Option[ModuleNode]): Either[OptimizerWarning, ClassBType] = { - bTypeLoader.classBType(classNode.name) { _ => + cache(classNode.name) { _ => computeClassInfoFromClassNode(classNode, moduleNode) } } @@ -82,7 +82,7 @@ class BTypesFromClassfile(byteCodeRepository: BCodeRepository, bTypeLoader: BTyp private def computeClassInfoFromClassNode(classNode: ClassNode, moduleNode: Option[ModuleNode]): Either[OptimizerWarning, ClassInfo] = { val superClass = classNode.superName match { case null => - assert(classNode.name == "java/lang/Object", s"class with missing super type: ${classNode.name}") + assert(classNode.name == ClassBType.javaLangObjectInternalName, s"class with missing super type: ${classNode.name}") Right(None) case superName => classBTypeFromParsedClassfile(superName).map(Some.apply) diff --git a/compiler/src/dotty/tools/backend/jvm/opt/BoxUnbox.scala b/compiler/src/dotty/tools/backend/jvm/opt/BoxUnbox.scala index 35dd1dbb79eb..e90a37438725 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/BoxUnbox.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/BoxUnbox.scala @@ -26,7 +26,7 @@ import dotty.tools.backend.jvm.analysis.{AsmAnalyzer, ProdConsAnalyzer} import dotty.tools.backend.jvm.BCodeUtils.* import dotty.tools.dotc.core.StdNames.nme -final class BoxUnbox(backendUtils: BackendUtils, callGraph: CallGraph, ts: WellKnownBTypes) { +final class BoxUnbox(optimizerUtils: OptimizerUtils, callGraph: CallGraph, ts: OptimizerKnownBTypes) { /** * Eliminate box-unbox pairs within `method`. Such appear commonly after closure elimination: @@ -416,7 +416,7 @@ final class BoxUnbox(backendUtils: BackendUtils, callGraph: CallGraph, ts: WellK } // We don't need to worry about CallGraph.closureInstantiations and - // BackendUtils.indyLambdaImplMethods, the removed instructions are not IndyLambdas + // OptimizerUtils.indyLambdaImplMethods, the removed instructions are not IndyLambdas def removeFromCallGraph(insn: AbstractInsnNode): Unit = insn match { case mi: MethodInsnNode => callGraph.removeCallsite(mi, method) case _ => @@ -649,7 +649,7 @@ final class BoxUnbox(backendUtils: BackendUtils, callGraph: CallGraph, ts: WellK val receiverProds = prodCons.producersForValueAt(mi, prodCons.frameAt(mi).stackTop - numArgs) if (receiverProds.size == 1) { val prod = receiverProds.head - if (backendUtils.isPredefLoad(prod) && prodCons.consumersOfOutputsFrom(prod) == Set(mi)) return Some(prod) + if (optimizerUtils.isPredefLoad(prod) && prodCons.consumersOfOutputsFrom(prod) == Set(mi)) return Some(prod) } None } @@ -690,14 +690,14 @@ final class BoxUnbox(backendUtils: BackendUtils, callGraph: CallGraph, ts: WellK insn match { case mi: MethodInsnNode => - if (backendUtils.isScalaBox(mi) || backendUtils.isJavaBox(mi)) checkKind(mi).map((StaticFactory(mi, loadInitialValues = None), _)) - else if (backendUtils.isPredefAutoBox(mi)) + if (optimizerUtils.isScalaBox(mi) || optimizerUtils.isJavaBox(mi)) checkKind(mi).map((StaticFactory(mi, loadInitialValues = None), _)) + else if (optimizerUtils.isPredefAutoBox(mi)) for (predefLoad <- BoxKind.checkReceiverPredefLoad(mi, prodCons); kind <- checkKind(mi)) yield (ModuleFactory(predefLoad, mi), kind) else None case ti: TypeInsnNode if ti.getOpcode == NEW => - for ((dupOp, initCall) <- BoxKind.checkInstanceCreation(ti, prodCons) if backendUtils.isPrimitiveBoxConstructor(initCall); kind <- checkKind(initCall)) + for ((dupOp, initCall) <- BoxKind.checkInstanceCreation(ti, prodCons) if optimizerUtils.isPrimitiveBoxConstructor(initCall); kind <- checkKind(initCall)) yield (InstanceCreation(ti, dupOp, initCall), kind) case _ => None @@ -708,8 +708,8 @@ final class BoxUnbox(backendUtils: BackendUtils, callGraph: CallGraph, ts: WellK def typeOK(mi: MethodInsnNode) = kind.boxedType == Type.getReturnType(mi.desc) insn match { case mi: MethodInsnNode => - if ((backendUtils.isScalaUnbox(mi) || backendUtils.isJavaUnbox(mi)) && typeOK(mi)) Some(StaticGetterOrInstanceRead(mi)) - else if (backendUtils.isPredefAutoUnbox(mi) && typeOK(mi)) BoxKind.checkReceiverPredefLoad(mi, prodCons).map(ModuleGetter(_, mi)) + if ((optimizerUtils.isScalaUnbox(mi) || optimizerUtils.isJavaUnbox(mi)) && typeOK(mi)) Some(StaticGetterOrInstanceRead(mi)) + else if (optimizerUtils.isPredefAutoUnbox(mi) && typeOK(mi)) BoxKind.checkReceiverPredefLoad(mi, prodCons).map(ModuleGetter(_, mi)) else None case ti: TypeInsnNode if insn.getOpcode == INSTANCEOF => @@ -734,9 +734,9 @@ final class BoxUnbox(backendUtils: BackendUtils, callGraph: CallGraph, ts: WellK } private object Ref { - private def boxedType(mi: MethodInsnNode): Type = backendUtils.runtimeRefClassBoxedType(mi.owner) + private def boxedType(mi: MethodInsnNode): Type = optimizerUtils.runtimeRefClassBoxedType(mi.owner) private def refClass(mi: MethodInsnNode): InternalName = mi.owner - private def loadZeroValue(refZeroCall: MethodInsnNode): List[AbstractInsnNode] = List(loadZeroForTypeSort(backendUtils.runtimeRefClassBoxedType(refZeroCall.owner).getSort)) + private def loadZeroValue(refZeroCall: MethodInsnNode): List[AbstractInsnNode] = List(loadZeroForTypeSort(optimizerUtils.runtimeRefClassBoxedType(refZeroCall.owner).getSort)) private val refSupertypes = Set(ts.jiSerializableRef, ts.ObjectRef).map(_.internalName) @@ -748,12 +748,12 @@ final class BoxUnbox(backendUtils: BackendUtils, callGraph: CallGraph, ts: WellK insn match { case mi: MethodInsnNode => - if (backendUtils.isRefCreate(mi)) checkKind(mi).map((StaticFactory(mi, loadInitialValues = None), _)) - else if (backendUtils.isRefZero(mi)) checkKind(mi).map((StaticFactory(mi, loadInitialValues = Some(loadZeroValue(mi))), _)) + if (optimizerUtils.isRefCreate(mi)) checkKind(mi).map((StaticFactory(mi, loadInitialValues = None), _)) + else if (optimizerUtils.isRefZero(mi)) checkKind(mi).map((StaticFactory(mi, loadInitialValues = Some(loadZeroValue(mi))), _)) else None case ti: TypeInsnNode if ti.getOpcode == NEW => - for ((dupOp, initCall) <- BoxKind.checkInstanceCreation(ti, prodCons) if backendUtils.isRuntimeRefConstructor(initCall); kind <- checkKind(initCall)) + for ((dupOp, initCall) <- BoxKind.checkInstanceCreation(ti, prodCons) if optimizerUtils.isRuntimeRefConstructor(initCall); kind <- checkKind(initCall)) yield (InstanceCreation(ti, dupOp, initCall), kind) case _ => None @@ -816,7 +816,7 @@ final class BoxUnbox(backendUtils: BackendUtils, callGraph: CallGraph, ts: WellK ) case ti: TypeInsnNode if ti.getOpcode == NEW => - for ((dupOp, initCall) <- BoxKind.checkInstanceCreation(ti, prodCons) if backendUtils.isTupleConstructor(initCall); kind <- checkKind(initCall)) + for ((dupOp, initCall) <- BoxKind.checkInstanceCreation(ti, prodCons) if optimizerUtils.isTupleConstructor(initCall); kind <- checkKind(initCall)) yield (InstanceCreation(ti, dupOp, initCall), kind) case _ => None @@ -950,8 +950,8 @@ final class BoxUnbox(backendUtils: BackendUtils, callGraph: CallGraph, ts: WellK * `Tuple2` extracts the Integer value and unboxes it. */ def postExtractionAdaptationOps(typeOfExtractedValue: Type): List[AbstractInsnNode] = this match { - case PrimitiveBoxingGetter(_) => List(backendUtils.getScalaBox(typeOfExtractedValue)) - case PrimitiveUnboxingGetter(_, unboxedPrimitive) => List(backendUtils.getScalaUnbox(unboxedPrimitive)) + case PrimitiveBoxingGetter(_) => List(optimizerUtils.getScalaBox(typeOfExtractedValue)) + case PrimitiveUnboxingGetter(_, unboxedPrimitive) => List(optimizerUtils.getScalaUnbox(unboxedPrimitive)) case BoxedPrimitiveTypeCheck(_, success) => getPop(typeOfExtractedValue.getSize) :: new InsnNode(if (success) ICONST_1 else ICONST_0) :: diff --git a/compiler/src/dotty/tools/backend/jvm/opt/CallGraph.scala b/compiler/src/dotty/tools/backend/jvm/opt/CallGraph.scala index ed04e73dc57d..5ebebd751396 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/CallGraph.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/CallGraph.scala @@ -21,15 +21,14 @@ import scala.jdk.CollectionConverters.* import scala.tools.asm.tree.* import scala.tools.asm.{Opcodes, Type} import dotty.tools.backend.jvm.BTypes.InternalName -import dotty.tools.backend.jvm.BackendUtils.LambdaMetaFactoryCall import dotty.tools.backend.jvm.analysis.TypeFlowInterpreter.{LMFValue, ParamValue} -import dotty.tools.backend.jvm.analysis.* +import dotty.tools.backend.jvm.analysis.{AnalysisUtils, *} +import AnalysisUtils.LambdaMetaFactoryCall import BCodeUtils.* -import dotty.tools.dotc.util.{SourcePosition, NoSourcePosition} -import dotty.tools.backend.jvm.PostProcessorFrontendAccess.Lazy +import dotty.tools.dotc.util.{NoSourcePosition, SourcePosition} +import dotty.tools.dotc.ast.Positioned -class CallGraph(frontendAccess: PostProcessorFrontendAccess, - byteCodeRepository: BCodeRepository, bTypesFromClassfile: BTypesFromClassfile) { +class CallGraph(byteCodeRepository: BCodeRepository, bTypesFromClassfile: BTypesFromClassfile) { /** * The call graph contains the callsites in the program being compiled. @@ -51,7 +50,7 @@ class CallGraph(frontendAccess: PostProcessorFrontendAccess, * The call graph is less problematic because only methods being called are kept alive, not entire * classes. But we should keep an eye on this. */ - val callsites: Lazy[mutable.Map[MethodNode, Map[MethodInsnNode, Callsite]]] = frontendAccess.perRunLazy(concurrent.TrieMap.empty withDefaultValue Map.empty) + val callsites: mutable.Map[MethodNode, Map[MethodInsnNode, Callsite]] = concurrent.TrieMap.empty withDefaultValue Map.empty /** * Closure instantiations in the program being compiled. @@ -60,14 +59,13 @@ class CallGraph(frontendAccess: PostProcessorFrontendAccess, * optimizer: finding callsites to re-write requires running a producers-consumers analysis on * the method. Here the closure instantiations are already grouped by method. */ - //currently single threaded access only - val closureInstantiations: Lazy[mutable.Map[MethodNode, Map[InvokeDynamicInsnNode, ClosureInstantiation]]] = frontendAccess.perRunLazy(concurrent.TrieMap.empty withDefaultValue Map.empty) + val closureInstantiations: mutable.Map[MethodNode, Map[InvokeDynamicInsnNode, ClosureInstantiation]] = concurrent.TrieMap.empty withDefaultValue Map.empty /** * Store the position of every MethodInsnNode during code generation. This allows each callsite * in the call graph to remember its source position, which is required for inliner warnings. */ - val callsitePositions: Lazy[concurrent.Map[MethodInsnNode, SourcePosition]] = frontendAccess.perRunLazy(TrieMap.empty) + val callsitePositions: concurrent.Map[MethodInsnNode, SourcePosition] = TrieMap.empty /** * Stores callsite instructions of invocations annotated `f(): @inline/noinline`. @@ -75,39 +73,42 @@ class CallGraph(frontendAccess: PostProcessorFrontendAccess, * when building the CallGraph, every Callsite object has an annotated(No)Inline field. */ //currently single threaded access only - private val inlineAnnotatedCallsites: Lazy[mutable.Set[MethodInsnNode]] = frontendAccess.perRunLazy(mutable.Set.empty) + private val inlineAnnotatedCallsites: mutable.Set[MethodInsnNode] = mutable.Set.empty //currently single threaded access only - private val noInlineAnnotatedCallsites: Lazy[mutable.Set[MethodInsnNode]] = frontendAccess.perRunLazy(mutable.Set.empty) + private val noInlineAnnotatedCallsites: mutable.Set[MethodInsnNode] = mutable.Set.empty // Contains `INVOKESPECIAL` instructions that were cloned by the inliner and need to be resolved // statically by the call graph. See Inliner.maybeInlinedLater. - val staticallyResolvedInvokespecial: Lazy[mutable.Set[MethodInsnNode]] = frontendAccess.perRunLazy(mutable.Set.empty) + val staticallyResolvedInvokespecial: mutable.Set[MethodInsnNode] = mutable.Set.empty def isStaticCallsite(call: MethodInsnNode): Boolean = { val opc = call.getOpcode - opc == Opcodes.INVOKESTATIC || opc == Opcodes.INVOKESPECIAL && staticallyResolvedInvokespecial.get(call) + opc == Opcodes.INVOKESTATIC || opc == Opcodes.INVOKESPECIAL && staticallyResolvedInvokespecial(call) } def removeCallsite(invocation: MethodInsnNode, methodNode: MethodNode): Option[Callsite] = { - val methodCallsites = callsites.get(methodNode) + val methodCallsites = callsites(methodNode) val newCallsites = methodCallsites - invocation - if (newCallsites.isEmpty) callsites.get.subtractOne(methodNode) - else callsites.get(methodNode) = newCallsites + if (newCallsites.isEmpty) callsites.subtractOne(methodNode) + else callsites(methodNode) = newCallsites methodCallsites.get(invocation) } def addCallsite(callsite: Callsite): Unit = { - val methodCallsites = callsites.get(callsite.callsiteMethod) - callsites.get(callsite.callsiteMethod) = methodCallsites + (callsite.callsiteInstruction -> callsite) + val methodCallsites = callsites(callsite.callsiteMethod) + callsites(callsite.callsiteMethod) = methodCallsites + (callsite.callsiteInstruction -> callsite) } - def containsCallsite(callsite: Callsite): Boolean = callsites.get(callsite.callsiteMethod).contains(callsite.callsiteInstruction) + def recordCallsitePosition(m: MethodInsnNode, pos: SourcePosition): Unit = + callsitePositions(m) = pos + + def containsCallsite(callsite: Callsite): Boolean = callsites(callsite.callsiteMethod).contains(callsite.callsiteInstruction) def removeClosureInstantiation(indy: InvokeDynamicInsnNode, methodNode: MethodNode): Option[ClosureInstantiation] = { - val methodClosureInits = closureInstantiations.get(methodNode) + val methodClosureInits = closureInstantiations(methodNode) val newClosureInits = methodClosureInits - indy - if (newClosureInits.isEmpty) closureInstantiations.get.subtractOne(methodNode) - else closureInstantiations.get(methodNode) = newClosureInits + if (newClosureInits.isEmpty) closureInstantiations.subtractOne(methodNode) + else closureInstantiations(methodNode) = newClosureInits methodClosureInits.get(indy) } @@ -117,8 +118,8 @@ class CallGraph(frontendAccess: PostProcessorFrontendAccess, } def refresh(methodNode: MethodNode, definingClass: ClassBType): Unit = { - callsites.get.subtractOne(methodNode) - closureInstantiations.get.subtractOne(methodNode) + callsites.subtractOne(methodNode) + closureInstantiations.subtractOne(methodNode) // callsitePositions, inlineAnnotatedCallsites, noInlineAnnotatedCallsites, staticallyResolvedInvokespecial // are left unchanged. They contain individual instructions, the state for those remains valid in case // the inliner performs a rollback. @@ -194,7 +195,7 @@ class CallGraph(frontendAccess: PostProcessorFrontendAccess, // graph (or when inlining). val receiverNotNull = call.getOpcode == Opcodes.INVOKESTATIC - val pos = callsitePositions.get.getOrElse(call, NoSourcePosition) + val pos = callsitePositions.getOrElse(call, NoSourcePosition) methodCallsites += call -> callee.fold( w => UnknownCallsite( callsiteInstruction = call, @@ -213,8 +214,8 @@ class CallGraph(frontendAccess: PostProcessorFrontendAccess, callsiteStackHeight = typeAnalyzer.frameAt(call).getStackSize, receiverKnownNotNull = receiverNotNull, callsitePosition = pos, - annotatedInline = inlineAnnotatedCallsites.get(call), - annotatedNoInline = noInlineAnnotatedCallsites.get(call) + annotatedInline = inlineAnnotatedCallsites(call), + annotatedNoInline = noInlineAnnotatedCallsites(call) ) ) @@ -231,8 +232,8 @@ class CallGraph(frontendAccess: PostProcessorFrontendAccess, case _ => } - callsites.get(methodNode) = methodCallsites - closureInstantiations.get(methodNode) = methodClosureInstantiations + callsites(methodNode) = methodCallsites + closureInstantiations(methodNode) = methodClosureInstantiations } } @@ -259,13 +260,13 @@ class CallGraph(frontendAccess: PostProcessorFrontendAccess, if (ins.getType == AbstractInsnNode.METHOD_INSN) { val mi = ins.asInstanceOf[MethodInsnNode] val clonedMi = cloned.asInstanceOf[MethodInsnNode] - callsitePositions.get(clonedMi) = callsitePos - if (inlineAnnotatedCallsites.get(mi)) - inlineAnnotatedCallsites.get += clonedMi - if (noInlineAnnotatedCallsites.get(mi)) - noInlineAnnotatedCallsites.get += clonedMi - if (staticallyResolvedInvokespecial.get(mi)) - staticallyResolvedInvokespecial.get += clonedMi + callsitePositions(clonedMi) = callsitePos + if (inlineAnnotatedCallsites(mi)) + inlineAnnotatedCallsites += clonedMi + if (noInlineAnnotatedCallsites(mi)) + noInlineAnnotatedCallsites += clonedMi + if (staticallyResolvedInvokespecial(mi)) + staticallyResolvedInvokespecial += clonedMi } else if (BCodeUtils.isStore(ins)) { val vi = ins.asInstanceOf[VarInsnNode] writtenLocals += vi.`var` @@ -310,7 +311,7 @@ class CallGraph(frontendAccess: PostProcessorFrontendAccess, } argInfo.map((index, _)) } - val isArrayLoadOrUpdateOnKnownArray = BackendUtils.isRuntimeArrayLoadOrUpdate(consumerInsn) && + val isArrayLoadOrUpdateOnKnownArray = AnalysisUtils.isRuntimeArrayLoadOrUpdate(consumerInsn) && consumerFrame.getValue(firstConsumedSlot + 1).getType.getSort == Type.ARRAY if (isArrayLoadOrUpdateOnKnownArray) samInfos.updated(1, StaticallyKnownArray) else samInfos diff --git a/compiler/src/dotty/tools/backend/jvm/opt/ClosureOptimizer.scala b/compiler/src/dotty/tools/backend/jvm/opt/ClosureOptimizer.scala index 5a03f42bf0cc..9decaffa84d0 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/ClosureOptimizer.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/ClosureOptimizer.scala @@ -21,16 +21,14 @@ import scala.jdk.CollectionConverters.* import scala.tools.asm.Opcodes.* import scala.tools.asm.Type import scala.tools.asm.tree.* -import dotty.tools.dotc.core.Decorators.em import dotty.tools.dotc.util.NoSourcePosition import dotty.tools.backend.jvm.BTypes.InternalName -import dotty.tools.backend.jvm.BackendUtils.* -import dotty.tools.backend.jvm.analysis.{AsmAnalyzer, ProdConsAnalyzer} +import dotty.tools.backend.jvm.analysis.{AnalysisUtils, AsmAnalyzer, ProdConsAnalyzer} import BCodeUtils.* -class ClosureOptimizer(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, +class ClosureOptimizer(optimizerUtils: OptimizerUtils, indyTracker: IndyLambdaImplTracker, byteCodeRepository: BCodeRepository, callGraph: CallGraph, - ts: WellKnownBTypes, bTypesFromClassfile: BTypesFromClassfile, + ts: OptimizerKnownBTypes, bTypesFromClassfile: BTypesFromClassfile, settings: OptimizerSettings) { import ClosureOptimizer.* @@ -86,7 +84,7 @@ class ClosureOptimizer(ppa: PostProcessorFrontendAccess, backendUtils: BackendUt * instantiations. * @return The changed methods. The order of the resulting sequence is deterministic. */ - def rewriteClosureApplyInvocations(methods: Option[Iterable[MethodNode]], inlinerState: mutable.Map[MethodNode, MethodInlinerState]): mutable.LinkedHashSet[MethodNode] = { + def rewriteClosureApplyInvocations(methods: Option[Iterable[MethodNode]], inlinerState: mutable.Map[MethodNode, MethodInlinerState], issueSink: OptimizerIssue => Unit): mutable.LinkedHashSet[MethodNode] = { // sort all closure invocations to rewrite to ensure bytecode stability given Ordering[ClosureInstantiation] = closureInitOrdering @@ -98,27 +96,27 @@ class ClosureOptimizer(ppa: PostProcessorFrontendAccess, backendUtils: BackendUt // the `toList` prevents modifying closureInstantiations while iterating it. // minimalRemoveUnreachableCode (called in the loop) removes elements - val methodsToRewrite = methods.getOrElse(callGraph.closureInstantiations.get.keysIterator.toList) + val methodsToRewrite = methods.getOrElse(callGraph.closureInstantiations.keysIterator.toList) // For each closure instantiation find callsites of the closure and add them to the toRewrite // buffer (cannot change a method's bytecode while still looking for further invocations to // rewrite, the frame indices of the ProdCons analysis would get out of date). If a callsite // cannot be rewritten, e.g., because the lambda body method is not accessible, issue a warning. - for (method <- methodsToRewrite if Limits.sizeOKForBasicValue(method)) callGraph.closureInstantiations.get.get(method) match { + for (method <- methodsToRewrite if Limits.sizeOKForBasicValue(method)) callGraph.closureInstantiations.get(method) match { case Some(closureInitsBeforeDCE) if closureInitsBeforeDCE.nonEmpty => val ownerClass = closureInitsBeforeDCE.head._2.ownerClass.internalName // Advanced ProdCons queries (initialProducersForValueAt) expect no unreachable code. - LocalOptImpls.minimalRemoveUnreachableCode(method, ownerClass, callGraph, backendUtils) + LocalOptImpls.minimalRemoveUnreachableCode(method, ownerClass, callGraph, indyTracker) - if (Limits.sizeOKForSourceValue(method)) callGraph.closureInstantiations.get.get(method) match { + if (Limits.sizeOKForSourceValue(method)) callGraph.closureInstantiations.get(method) match { case Some(closureInits) => // A lazy val to ensure the analysis only runs if necessary (the value is passed by name to `closureCallsites`) lazy val prodCons = new ProdConsAnalyzer(method, ownerClass) for (init <- closureInits.valuesIterator) closureCallsites(init, prodCons) foreach { case Left(warning) => - ppa.optimizerWarning(em"${warning.toString}", BackendUtils.siteString(ownerClass, method.name), warning.pos) + issueSink(OptimizerIssue(warning.toString, OptimizerUtils.siteString(ownerClass, method.name), warning.pos)) case Right((invocation, stackHeight)) => addRewrite(init, invocation, stackHeight) @@ -195,7 +193,7 @@ class ClosureOptimizer(ppa: PostProcessorFrontendAccess, backendUtils: BackendUt Inliner.memberIsAccessible(bodyMethodNode.access, declClassBType, lambdaOwnerBType, ownerClass) } - def pos = callGraph.callsites.get(ownerMethod).get(invocation).map(_.callsitePosition).getOrElse(NoSourcePosition) + def pos = callGraph.callsites(ownerMethod).get(invocation).map(_.callsitePosition).getOrElse(NoSourcePosition) val stackSize: Either[RewriteClosureApplyToClosureBodyFailed, Int] = bodyAccessible match { case Left(w) => Left(RewriteClosureAccessCheckFailed(pos, w)) case Right(false) => Left(RewriteClosureIllegalAccess(pos, ownerClass.internalName)) @@ -303,9 +301,9 @@ class ClosureOptimizer(ppa: PostProcessorFrontendAccess, backendUtils: BackendUt if (invokeArgTypes(i) == implMethodArgTypes(i)) { res(i) = None } else if (isPrimitiveType(implMethodArgTypes(i)) && invokeArgTypes(i).getDescriptor == ts.ObjectRef.descriptor) { - res(i) = Some(backendUtils.getScalaUnbox(implMethodArgTypes(i))) + res(i) = Some(optimizerUtils.getScalaUnbox(implMethodArgTypes(i))) } else if (isPrimitiveType(invokeArgTypes(i)) && implMethodArgTypes(i).getDescriptor == ts.ObjectRef.descriptor) { - res(i) = Some(backendUtils.getScalaBox(invokeArgTypes(i))) + res(i) = Some(optimizerUtils.getScalaBox(invokeArgTypes(i))) } else { assert(!isPrimitiveType(invokeArgTypes(i)), invokeArgTypes(i)) assert(!isPrimitiveType(implMethodArgTypes(i)), implMethodArgTypes(i)) @@ -390,12 +388,12 @@ class ClosureOptimizer(ppa: PostProcessorFrontendAccess, backendUtils: BackendUt if (isPrimitiveType(invocationReturnType) && bodyReturnType.getDescriptor == ts.ObjectRef.descriptor) { val op = if (invocationReturnType.getSort == Type.VOID) getPop(1) - else backendUtils.getScalaUnbox(invocationReturnType) + else optimizerUtils.getScalaUnbox(invocationReturnType) ownerMethod.instructions.insertBefore(invocation, op) } else if (isPrimitiveType(bodyReturnType) && invocationReturnType.getDescriptor == ts.ObjectRef.descriptor) { val op = - if (bodyReturnType.getSort == Type.VOID) backendUtils.getBoxedUnit - else backendUtils.getScalaBox(bodyReturnType) + if (bodyReturnType.getSort == Type.VOID) optimizerUtils.getBoxedUnit + else optimizerUtils.getScalaBox(bodyReturnType) ownerMethod.instructions.insertBefore(invocation, op) } else { // see comment of that method @@ -460,7 +458,7 @@ class ClosureOptimizer(ppa: PostProcessorFrontendAccess, backendUtils: BackendUt // Rewriting a closure invocation may render code unreachable. For example, the body method of // (x: T) => ??? has return type Nothing$, and an ATHROW is added (see fixLoadedNothingOrNullValue). - BackendUtils.clearDceDone(ownerMethod) + OptimizerUtils.clearDceDone(ownerMethod) } /** @@ -519,11 +517,11 @@ class ClosureOptimizer(ppa: PostProcessorFrontendAccess, backendUtils: BackendUt /** * A list of local variables. Each local stores information about its type, see class [[Local]]. */ - case class LocalsList(locals: List[Local]) { + private case class LocalsList(locals: List[Local]) { val size = locals.iterator.map(_.size).sum } - object LocalsList { + private object LocalsList { /** * A list of local variables starting at `firstLocal` that can hold values of the types in the * `types` parameter. @@ -553,7 +551,7 @@ class ClosureOptimizer(ppa: PostProcessorFrontendAccess, backendUtils: BackendUt * The xLOAD / xSTORE opcodes are in the following sequence: I, L, F, D, A, so the offset for * a local variable holding a reference (`A`) is 4. See also method `getOpcode` in [[scala.tools.asm.Type]]. */ - case class Local(local: Int, opcodeOffset: Int) { + private case class Local(local: Int, opcodeOffset: Int) { def size = if (loadOpcode == LLOAD || loadOpcode == DLOAD) 2 else 1 def loadOpcode = ILOAD + opcodeOffset @@ -562,6 +560,6 @@ class ClosureOptimizer(ppa: PostProcessorFrontendAccess, backendUtils: BackendUt } object ClosureOptimizer { - val primitives = "BSIJCFDZV" - val specializationSuffix = s"(\\$$mc[$primitives]+\\$$sp)".r + private val primitives = "BSIJCFDZV" + private val specializationSuffix = s"(\\$$mc[$primitives]+\\$$sp)".r } diff --git a/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala b/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala index b22ab2113854..2ec40d39390b 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala @@ -23,14 +23,13 @@ import scala.tools.asm.tree.* import dotty.tools.backend.jvm.BTypes.InternalName import dotty.tools.backend.jvm.analysis.* import BCodeUtils.* -import dotty.tools.backend.jvm.BackendUtils.* import scala.tools.asm -class CopyProp(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inliner, ts: WellKnownBTypes, settings: OptimizerSettings) { +class CopyProp(optimizerUtils: OptimizerUtils, indyTracker: IndyLambdaImplTracker, callGraph: CallGraph, inliner: Inliner, ts: OptimizerKnownBTypes, settings: OptimizerSettings) { private val modulesAllowSkipInitialization = - if settings.optAllowSkipCoreModuleInit then backendUtils.modulesAllowSkipInitialization else Set.empty + if settings.optAllowSkipCoreModuleInit then optimizerUtils.modulesAllowSkipInitialization else Set.empty /** * For every `xLOAD n`, find all local variable slots that are aliases of `n` using an @@ -208,7 +207,7 @@ class CopyProp(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inline if (receiverProds.size == 1) { toReplace(receiverProds.head) = List(receiverProds.head, getPop(1)) toReplace(mi) = List(newArrayInstr) - toInline ++= prodCons.ultimateConsumersOfOutputsFrom(mi).collect({case i if isRuntimeArrayLoadOrUpdate(i) => i.asInstanceOf[MethodInsnNode]}) + toInline ++= prodCons.ultimateConsumersOfOutputsFrom(mi).collect({case i if AnalysisUtils.isRuntimeArrayLoadOrUpdate(i) => i.asInstanceOf[MethodInsnNode]}) } } @@ -278,7 +277,7 @@ class CopyProp(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inline } if (toInline.nonEmpty) { - val methodCallsites = callGraph.callsites.get(method).collect { case (k, v: KnownCallsite) => (k, v) } + val methodCallsites = callGraph.callsites(method).collect { case (k, v: KnownCallsite) => (k, v) } var css = toInline.flatMap(methodCallsites.get).toList.sorted(using callsiteOrdering) while (css.nonEmpty) { val cs = css.head @@ -399,7 +398,7 @@ class CopyProp(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inline case INVOKESPECIAL => val mi = insn.asInstanceOf[MethodInsnNode] - if (backendUtils.isSideEffectFreeConstructorCall(mi)) sideEffectFreeConstructorCalls += mi + if (optimizerUtils.isSideEffectFreeConstructorCall(mi)) sideEffectFreeConstructorCalls += mi case _ => } @@ -432,7 +431,7 @@ class CopyProp(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inline def handleClosureInst(indy: InvokeDynamicInsnNode): Unit = { toRemove += indy callGraph.removeClosureInstantiation(indy, method) - backendUtils.removeIndyLambdaImplMethod(owner, method, indy) + indyTracker.remove(owner, method, indy) handleInputs(indy, Type.getArgumentTypes(indy.desc).length) } @@ -476,24 +475,24 @@ class CopyProp(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inline handleInputs(prod, 1) case GETFIELD | GETSTATIC => - if (backendUtils.isBoxedUnit(prod) || BackendUtils.isJavaLangStaticLoad(prod) || BackendUtils.isModuleLoad(prod, modulesAllowSkipInitialization)) toRemove += prod + if (optimizerUtils.isBoxedUnit(prod) || AnalysisUtils.isJavaLangStaticLoad(prod) || AnalysisUtils.isModuleLoad(prod, modulesAllowSkipInitialization)) toRemove += prod else popAfterProd() // keep potential class initialization (static field) or NPE (instance field) case INVOKEVIRTUAL | INVOKESPECIAL | INVOKESTATIC | INVOKEINTERFACE => val methodInsn = prod.asInstanceOf[MethodInsnNode] - if (backendUtils.isSideEffectFreeCall(methodInsn)) { + if (optimizerUtils.isSideEffectFreeCall(methodInsn)) { toRemove += prod callGraph.removeCallsite(methodInsn, method) val receiver = if (methodInsn.getOpcode == INVOKESTATIC) 0 else 1 handleInputs(prod, Type.getArgumentTypes(methodInsn.desc).length + receiver) - } else if (backendUtils.isScalaUnbox(methodInsn)) { - val tp = backendUtils.primitiveAsmTypeSortToBType(Type.getReturnType(methodInsn.desc).getSort) + } else if (optimizerUtils.isScalaUnbox(methodInsn)) { + val tp = optimizerUtils.primitiveAsmTypeSortToBType(Type.getReturnType(methodInsn.desc).getSort) val boxTp = ts.boxedClassOfPrimitive(tp) toInsertBefore(methodInsn) = List(new TypeInsnNode(CHECKCAST, boxTp.internalName), new InsnNode(POP)) toRemove += prod callGraph.removeCallsite(methodInsn, method) castAdded = true - } else if (backendUtils.isJavaUnbox(methodInsn)) { + } else if (optimizerUtils.isJavaUnbox(methodInsn)) { val nullCheck = mutable.ListBuffer.empty[AbstractInsnNode] val nonNullLabel = newLabelNode nullCheck += new JumpInsnNode(IFNONNULL, nonNullLabel) @@ -510,12 +509,12 @@ class CopyProp(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inline case INVOKEDYNAMIC => prod match { - case LambdaMetaFactoryCall(indy, _, _, _, _) => handleClosureInst(indy) + case AnalysisUtils.LambdaMetaFactoryCall(indy, _, _, _, _) => handleClosureInst(indy) case _ => popAfterProd() } case NEW => - if (backendUtils.isNewForSideEffectFreeConstructor(prod)) toRemove += prod + if (optimizerUtils.isNewForSideEffectFreeConstructor(prod)) toRemove += prod else popAfterProd() case LDC => diff --git a/compiler/src/dotty/tools/backend/jvm/opt/IndyLambdaImplTracker.scala b/compiler/src/dotty/tools/backend/jvm/opt/IndyLambdaImplTracker.scala new file mode 100644 index 000000000000..3faae6ef1f29 --- /dev/null +++ b/compiler/src/dotty/tools/backend/jvm/opt/IndyLambdaImplTracker.scala @@ -0,0 +1,43 @@ +package dotty.tools.backend.jvm.opt + +import dotty.tools.backend.jvm.BTypes.InternalName + +import java.util.concurrent.ConcurrentHashMap +import scala.collection.mutable +import scala.tools.asm +import scala.tools.asm.Handle +import scala.tools.asm.tree.{InvokeDynamicInsnNode, MethodNode} + +final class IndyLambdaImplTracker { + private val indyLambdaImplMethods = + new ConcurrentHashMap[InternalName, mutable.Map[MethodNode, mutable.Map[InvokeDynamicInsnNode, asm.Handle]]] + + private def onMethods[T](hostClass: InternalName)(action: mutable.Map[MethodNode, mutable.Map[InvokeDynamicInsnNode, asm.Handle]] => T): T = { + val methods = indyLambdaImplMethods.computeIfAbsent(hostClass, _ => mutable.Map.empty) + methods.synchronized(action(methods)) + } + + def add(hostClass: InternalName, method: MethodNode, indy: InvokeDynamicInsnNode, handle: asm.Handle): Unit = { + onMethods(hostClass)(_.getOrElseUpdate(method, mutable.Map.empty)(indy) = handle) + } + + def remove(hostClass: InternalName, method: MethodNode, indy: InvokeDynamicInsnNode): Unit = { + onMethods(hostClass)(_.get(method).foreach(_.remove(indy))) + } + + def reset(hostClass: InternalName, method: MethodNode, values: mutable.Map[InvokeDynamicInsnNode, Handle]): Unit = { + onMethods(hostClass)(ms => { + if values.isEmpty then + ms.remove(method) + else + ms(method) = values + }) + } + + /** + * The methods used as lambda bodies for IndyLambda instructions within `method` of `hostClass`. + */ + def get(hostClass: InternalName, method: MethodNode): mutable.Map[InvokeDynamicInsnNode, Handle] = { + onMethods(hostClass)(ms => ms.getOrElseUpdate(method, mutable.Map.empty)) + } +} diff --git a/compiler/src/dotty/tools/backend/jvm/opt/Inliner.scala b/compiler/src/dotty/tools/backend/jvm/opt/Inliner.scala index f6f8de4c6e4d..fed3b248c77e 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/Inliner.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/Inliner.scala @@ -22,15 +22,13 @@ import scala.tools.asm.Opcodes.* import scala.tools.asm.Type import scala.tools.asm.tree.* import scala.tools.asm.tree.analysis.Value -import dotty.tools.dotc.core.Decorators.em import dotty.tools.backend.jvm.BTypes.InternalName -import dotty.tools.backend.jvm.BackendUtils import dotty.tools.backend.jvm.analysis.* -import dotty.tools.backend.jvm.BackendUtils.LambdaMetaFactoryCall +import AnalysisUtils.LambdaMetaFactoryCall import BCodeUtils.* -class Inliner(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, - callGraph: CallGraph, bTypeLoader: BTypeLoader, bTypesFromClassfile: BTypesFromClassfile, byteCodeRepository: BCodeRepository, +class Inliner(indyTracker: IndyLambdaImplTracker, + callGraph: CallGraph, classBTypeCache: ClassBType.Cache, bTypesFromClassfile: BTypesFromClassfile, byteCodeRepository: BCodeRepository, heuristics: InlinerHeuristics, closureOptimizer: ClosureOptimizer, settings: OptimizerSettings) { @@ -51,7 +49,7 @@ class Inliner(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, // avoided, it needs to resolve to T.f, no matter in which class the invocation appears. def hasMethod(c: ClassNode): Boolean = { val r = c.methods.iterator.asScala.exists(m => m.name == mi.name && m.desc == mi.desc) - if (r) callGraph.staticallyResolvedInvokespecial.get += mi + if (r) callGraph.staticallyResolvedInvokespecial += mi r } @@ -63,9 +61,9 @@ class Inliner(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, }) } - def runInlinerAndClosureOptimizer(): Unit = { - val runClosureOptimizer = settings.optClosureInvocations + def runInlinerAndClosureOptimizer(issueSink: OptimizerIssue => Unit): Unit = { var round = 0 + var changedByInliner = Iterable.empty[MethodNode] var changedByClosureOptimizer = mutable.LinkedHashSet.empty[MethodNode] val inlinerState = mutable.Map.empty[MethodNode, MethodInlinerState] @@ -73,13 +71,15 @@ class Inliner(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, // Don't try again to inline failed callsites val failedToInline = mutable.Set.empty[MethodInsnNode] - while (round < 10 && (round == 0 || changedByClosureOptimizer.nonEmpty)) { - val specificMethodsForInlining = if (round == 0) None else Some(changedByClosureOptimizer) - val changedByInliner = runInliner(specificMethodsForInlining, inlinerState, failedToInline) + while (round < 10 && (round == 0 || (changedByClosureOptimizer.nonEmpty && settings.optInlinerEnabled))) { + if (settings.optInlinerEnabled) { + val specificMethodsForInlining = if (round == 0) None else Some(changedByClosureOptimizer) + changedByInliner = runInliner(specificMethodsForInlining, inlinerState, failedToInline, issueSink) + } - if (runClosureOptimizer) { + if (settings.optClosureInvocations) { val specificMethodsForClosureRewriting = if (round == 0) None else Some(changedByInliner) - changedByClosureOptimizer = closureOptimizer.rewriteClosureApplyInvocations(specificMethodsForClosureRewriting, inlinerState) + changedByClosureOptimizer = closureOptimizer.rewriteClosureApplyInvocations(specificMethodsForClosureRewriting, inlinerState, issueSink) } var logs = List.empty[(MethodNode, InlineLog)] @@ -101,11 +101,11 @@ class Inliner(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, * @param methods The methods to check for callsites to inline. If not defined, check all methods. * @return The set of changed methods, in no deterministic order. */ - private def runInliner(methods: Option[mutable.LinkedHashSet[MethodNode]], inlinerState: mutable.Map[MethodNode, MethodInlinerState], failed: mutable.Set[MethodInsnNode]): Iterable[MethodNode] = { + private def runInliner(methods: Option[mutable.LinkedHashSet[MethodNode]], inlinerState: mutable.Map[MethodNode, MethodInlinerState], failed: mutable.Set[MethodInsnNode], issueSink: OptimizerIssue => Unit): Iterable[MethodNode] = { // Inline requests are grouped by method for performance: we only update the call graph (which // runs analyzers) once all callsites are inlined. val requests: mutable.Queue[(MethodNode, List[InlineRequest])] = - if (methods.isEmpty) collectAndOrderInlineRequests + if (methods.isEmpty) collectAndOrderInlineRequests(issueSink) else mutable.Queue.empty // Methods that were changed (inlined into), they will be checked for more callsites to inline @@ -124,9 +124,9 @@ class Inliner(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, def inlineChainSuffix(callsite: KnownCallsite, chain: List[KnownCallsite]): String = if (chain.isEmpty) "" else s""" - |Note that this callsite was itself inlined into ${BackendUtils.methodSignature(callsite.callsiteClass.internalName, callsite.callsiteMethod)} + |Note that this callsite was itself inlined into ${OptimizerUtils.methodSignature(callsite.callsiteClass.internalName, callsite.callsiteMethod)} |by inlining the following methods: - |${chain.map(cs => BackendUtils.methodSignature(cs.callee.calleeDeclarationClass.internalName, cs.callee.callee)).mkString(" - ", "\n - ", "")}""".stripMargin + |${chain.map(cs => OptimizerUtils.methodSignature(cs.callee.calleeDeclarationClass.internalName, cs.callee.callee)).mkString(" - ", "\n - ", "")}""".stripMargin while (requests.nonEmpty || changedMethods.nonEmpty) { // First inline all requests that were initially collected. Then check methods that changed @@ -178,7 +178,7 @@ class Inliner(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, case Some(w: IllegalAccessInstructions) if maybeInlinedLater(r.callsite, w.instructions) => if (state.undoLog.isEmpty) { - val undo = new UndoLog(backendUtils, callGraph) + val undo = new UndoLog(indyTracker, callGraph) val currentState = state.clone() // undo actions for the method and global state undo.saveMethodState(r.callsite.callsiteClass, method) @@ -208,17 +208,17 @@ class Inliner(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, case Some(inlinedCallsite) => val rw = inlinedCallsite.warning.get if (rw.emitWarning(settings)) { - ppa.optimizerWarning( - em"${rw.toString + inlineChainSuffix(r.callsite, state.inlineChain(inlinedCallsite.eliminatedCallsite.callsiteInstruction, skipForwarders = true))}", - BackendUtils.siteString(inlinedCallsite.eliminatedCallsite.callsiteClass.internalName, inlinedCallsite.eliminatedCallsite.callsiteMethod.name), - inlinedCallsite.eliminatedCallsite.callsitePosition) + issueSink(OptimizerIssue( + s"${rw.toString + inlineChainSuffix(r.callsite, state.inlineChain(inlinedCallsite.eliminatedCallsite.callsiteInstruction, skipForwarders = true))}", + OptimizerUtils.siteString(inlinedCallsite.eliminatedCallsite.callsiteClass.internalName, inlinedCallsite.eliminatedCallsite.callsiteMethod.name), + inlinedCallsite.eliminatedCallsite.callsitePosition)) } case _ => if (w.emitWarning(settings)) - ppa.optimizerWarning( - em"${w.toString + inlineChainSuffix(r.callsite, state.inlineChain(r.callsite.callsiteInstruction, skipForwarders = true))}", - BackendUtils.siteString(r.callsite.callsiteClass.internalName, r.callsite.callsiteMethod.name), - r.callsite.callsitePosition) + issueSink(OptimizerIssue( + s"${w.toString + inlineChainSuffix(r.callsite, state.inlineChain(r.callsite.callsiteInstruction, skipForwarders = true))}", + OptimizerUtils.siteString(r.callsite.callsiteClass.internalName, r.callsite.callsiteMethod.name), + r.callsite.callsitePosition)) } } } @@ -245,7 +245,7 @@ class Inliner(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, } val rs = mutable.ListBuffer.empty[InlineRequest] - callGraph.callsites.get(method).valuesIterator foreach { + callGraph.callsites(method).valuesIterator foreach { // Don't inline: recursive calls, callsites that failed inlining before case cs: KnownCallsite if !failed(cs.callsiteInstruction) && !isLoop(cs.callsiteInstruction, cs.callee) => heuristics.inlineRequest(cs) match { @@ -269,10 +269,10 @@ class Inliner(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, val w = inlinedCallsite.warning.get state.inlineLog.logRollback(callsite, s"Instruction ${LogUtils.textify(notInlinedIllegalInsn)} would cause an IllegalAccessError, and is not selected for (or failed) inlining", state.outerCallsite(notInlinedIllegalInsn)) if (w.emitWarning(settings)) - ppa.optimizerWarning( - em"${w.toString + inlineChainSuffix(callsite, state.inlineChain(callsite.callsiteInstruction, skipForwarders = true))}", - BackendUtils.siteString(callsite.callsiteClass.internalName, callsite.callsiteMethod.name), - callsite.callsitePosition) + issueSink(OptimizerIssue( + s"${w.toString + inlineChainSuffix(callsite, state.inlineChain(callsite.callsiteInstruction, skipForwarders = true))}", + OptimizerUtils.siteString(callsite.callsiteClass.internalName, callsite.callsiteMethod.name), + callsite.callsitePosition)) case _ => // TODO: replace by dev warning after testing assert(false, "should not happen") @@ -296,8 +296,8 @@ class Inliner(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, * The resulting list is sorted such that the leaves of the inline request graph are on the left. * Once these leaves are inlined, the successive elements will be leaves, etc. */ - private def collectAndOrderInlineRequests: mutable.Queue[(MethodNode, List[InlineRequest])] = { - val requestsByMethod = heuristics.selectCallsitesForInlining.withDefaultValue(Set.empty) + private def collectAndOrderInlineRequests(issueSink: OptimizerIssue => Unit): mutable.Queue[(MethodNode, List[InlineRequest])] = { + val requestsByMethod = heuristics.selectCallsitesForInlining(issueSink).withDefaultValue(Set.empty) val elided = mutable.Set.empty[InlineRequest] @@ -415,7 +415,7 @@ class Inliner(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, // def g = f; println() // println is unreachable after inlining f // If we have an inline request for a call to g, and f has been already inlined into g, we // need to run DCE on g's body before inlining g. - LocalOptImpls.minimalRemoveUnreachableCode(callee, calleeDeclarationClass.internalName, callGraph, backendUtils) + LocalOptImpls.minimalRemoveUnreachableCode(callee, calleeDeclarationClass.internalName, callGraph, indyTracker) // If the callsite was eliminated by DCE, do nothing. if (!callGraph.containsCallsite(callsite)) return Map.empty @@ -696,14 +696,14 @@ class Inliner(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, callsite.callsiteMethod.maxStack = math.max(MethodMax.maxStack(callsite.callsiteMethod), math.max(stackHeightAtNullCheck, maxStackOfInlinedCode)) - lazy val callsiteLambdaBodyMethods = backendUtils.onIndyLambdaImplMethod(callsite.callsiteClass.internalName)(_.getOrElseUpdate(callsite.callsiteMethod, mutable.Map.empty)) - backendUtils.onIndyLambdaImplMethodIfPresent(calleeDeclarationClass.internalName)(methods => methods.getOrElse(callee, Nil) foreach { + lazy val callsiteLambdaBodyMethods = indyTracker.get(callsite.callsiteClass.internalName, callsite.callsiteMethod) + indyTracker.get(calleeDeclarationClass.internalName, callee).foreach { case (indy, handle) => instructionMap.get(indy) match { case Some(clonedIndy: InvokeDynamicInsnNode) => callsiteLambdaBodyMethods(clonedIndy) = handle case _ => } - }) + } // Don't remove the inlined instruction from callsitePositions, inlineAnnotatedCallsites so that // the information is still there in case the method is rolled back (UndoLog). @@ -711,7 +711,7 @@ class Inliner(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, if (updateCallGraph) callGraph.refresh(callsite.callsiteMethod, callsite.callsiteClass) // Inlining a method body can render some code unreachable, see example above in this method. - BackendUtils.clearDceDone(callsite.callsiteMethod) + OptimizerUtils.clearDceDone(callsite.callsiteMethod) instructionMap } @@ -795,7 +795,7 @@ class Inliner(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, private val isInternalCache = mutable.Map.empty[String, Either[OptimizerWarning, Boolean]] private def isInternal(name: String): Either[OptimizerWarning, Boolean] = { isInternalCache.getOrElseUpdate(name, - bTypeLoader.previouslyConstructedClassBType(name) match + classBTypeCache.previouslyConstructedClassBType(name) match case Some(ct) => Right(!ct.info.inlineInfo.isAccessible) case None => bTypesFromClassfile.classBTypeFromParsedClassfile(name) match case Left(l) => Left(l) @@ -1056,7 +1056,7 @@ object Inliner { } } -class UndoLog(backendUtils: BackendUtils, callGraph: CallGraph) { +class UndoLog(indyTracker: IndyLambdaImplTracker, callGraph: CallGraph) { import java.util.{ArrayList => JArrayList} @@ -1073,7 +1073,7 @@ class UndoLog(backendUtils: BackendUtils, callGraph: CallGraph) { val currentMaxLocals = methodNode.maxLocals val currentMaxStack = methodNode.maxStack - val currentIndyLambdaBodyMethods = backendUtils.indyLambdaBodyMethods(ownerClass.internalName, methodNode) + val currentIndyLambdaBodyMethods = indyTracker.get(ownerClass.internalName, methodNode) // Instead of saving / restoring the CallGraph's callsites / closureInstantiations, we call // callGraph.refresh on rollback. The call graph might not be up to date at the point where @@ -1102,12 +1102,10 @@ class UndoLog(backendUtils: BackendUtils, callGraph: CallGraph) { methodNode.maxLocals = currentMaxLocals methodNode.maxStack = currentMaxStack - BackendUtils.clearDceDone(methodNode) + OptimizerUtils.clearDceDone(methodNode) callGraph.refresh(methodNode, ownerClass) - backendUtils.onIndyLambdaImplMethodIfPresent(ownerClass.internalName)(_.subtractOne(methodNode)) - if (currentIndyLambdaBodyMethods.nonEmpty) - backendUtils.onIndyLambdaImplMethod(ownerClass.internalName)(ms => ms(methodNode) = mutable.Map.empty ++= currentIndyLambdaBodyMethods) + indyTracker.reset(ownerClass.internalName, methodNode, currentIndyLambdaBodyMethods) } } } @@ -1185,7 +1183,7 @@ final class MethodInlinerState(optLogInline: Option[String]) { @tailrec def impl(insn: AbstractInsnNode, res: List[KnownCallsite]): List[KnownCallsite] = inlinedCalls.get(insn) match { case Some(inlinedCallsite) => val cs = inlinedCallsite.eliminatedCallsite - val res1 = if (skipForwarders && BackendUtils.isTraitSuperAccessorOrMixinForwarder(cs.callee.callee, cs.callee.calleeDeclarationClass)) res else cs :: res + val res1 = if (skipForwarders && AnalysisUtils.isTraitSuperAccessorOrMixinForwarder(cs.callee.callee, cs.callee.calleeDeclarationClass)) res else cs :: res impl(cs.callsiteInstruction, res1) case _ => res @@ -1203,7 +1201,7 @@ final class MethodInlinerState(optLogInline: Option[String]) { // If the chain has only forwarders, `returnForwarderIfNoOther` determines whether to return `None` // or the last inlined forwarder. def rootInlinedCallsiteWithWarning(call: AbstractInsnNode, returnForwarderIfNoOther: Boolean): Option[InlinedCallsite] = { - def isForwarder(callsite: KnownCallsite) = BackendUtils.isTraitSuperAccessorOrMixinForwarder(callsite.callee.callee, callsite.callee.calleeDeclarationClass) + def isForwarder(callsite: KnownCallsite) = AnalysisUtils.isTraitSuperAccessorOrMixinForwarder(callsite.callee.callee, callsite.callee.calleeDeclarationClass) def result(res: Option[InlinedCallsite]) = res match { case Some(r) if returnForwarderIfNoOther || !isForwarder(r.eliminatedCallsite) => res diff --git a/compiler/src/dotty/tools/backend/jvm/opt/InlinerHeuristics.scala b/compiler/src/dotty/tools/backend/jvm/opt/InlinerHeuristics.scala index fc17c8054d96..1aaf2ab42387 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/InlinerHeuristics.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/InlinerHeuristics.scala @@ -20,16 +20,14 @@ import scala.collection.mutable import scala.jdk.CollectionConverters.* import scala.tools.asm.Type import scala.tools.asm.tree.MethodNode -import dotty.tools.dotc.core.Decorators.em import dotty.tools.backend.jvm.BTypes.InternalName -import dotty.tools.backend.jvm.BackendUtils import dotty.tools.backend.jvm.opt.InlinerHeuristics.* -import PostProcessorFrontendAccess.Lazy import dotty.tools.backend.jvm.BCodeUtils.{isStrictfpMethod, isSynchronizedMethod} +import dotty.tools.backend.jvm.analysis.AnalysisUtils import dotty.tools.dotc.report -class InlinerHeuristics(ppa: PostProcessorFrontendAccess, backendUtils: BackendUtils, byteCodeRepository: BCodeRepository, - callGraph: CallGraph, ts: WellKnownBTypes, +class InlinerHeuristics(optimizerUtils: OptimizerUtils, byteCodeRepository: BCodeRepository, + callGraph: CallGraph, ts: OptimizerKnownBTypes, settings: OptimizerSettings) { private lazy val inlineSourceMatcher: InlineSourceMatcher = new InlineSourceMatcher(settings.optInlineFrom) @@ -43,7 +41,7 @@ class InlinerHeuristics(ppa: PostProcessorFrontendAccess, backendUtils: BackendU * Select callsites from the call graph that should be inlined, grouped by the containing method. * Cyclic inlining requests are allowed, the inliner will eliminate requests to break cycles. */ - def selectCallsitesForInlining: Map[MethodNode, Set[InlineRequest]] = { + def selectCallsitesForInlining(issueSink: OptimizerIssue => Unit): Map[MethodNode, Set[InlineRequest]] = { // We should only create inlining requests for callsites being compiled (not for callsites in // classes on the classpath). The call graph may contain callsites of classes parsed from the // classpath. In order to get only the callsites being compiled, we start at the map of @@ -55,25 +53,25 @@ class InlinerHeuristics(ppa: PostProcessorFrontendAccess, backendUtils: BackendU compilingMethods.map(methodNode => { var requests = Set.empty[InlineRequest] - callGraph.callsites.get(methodNode).valuesIterator foreach { + callGraph.callsites(methodNode).valuesIterator foreach { case callsite @ KnownCallsite(_, _, _, Callee(callee, _, _, _, _, _, _, callsiteWarning), _, _, _, pos, _, _) => inlineRequest(callsite) match { case Some(Right(req)) => requests += req case Some(Left(w)) => if (w.emitWarning(settings)) { - ppa.optimizerWarning(em"${w.toString}", BackendUtils.siteString(callsite.callsiteClass.internalName, callsite.callsiteMethod.name), callsite.callsitePosition) + issueSink(OptimizerIssue(w.toString, OptimizerUtils.siteString(callsite.callsiteClass.internalName, callsite.callsiteMethod.name), callsite.callsitePosition)) } case None => if (callsiteWarning.exists(_.emitWarning(settings))) { - ppa.optimizerWarning(em"there was a problem determining if method ${callee.name} can be inlined: \n${callsiteWarning.get.toString}", BackendUtils.siteString(callsite.callsiteClass.internalName, callsite.callsiteMethod.name), pos) + issueSink(OptimizerIssue(s"there was a problem determining if method ${callee.name} can be inlined: \n${callsiteWarning.get.toString}", OptimizerUtils.siteString(callsite.callsiteClass.internalName, callsite.callsiteMethod.name), pos)) } } case callsite @ UnknownCallsite(ins, meth, clas, pos, _, warning) => if (warning.emitWarning(settings)) { - ppa.optimizerWarning(em"failed to determine if ${ins.name} should be inlined:\n${warning.toString}", BackendUtils.siteString(clas.internalName, meth.name), pos) + issueSink(OptimizerIssue(s"failed to determine if ${ins.name} should be inlined:\n${warning.toString}", OptimizerUtils.siteString(clas.internalName, meth.name), pos)) } } (methodNode, requests) @@ -185,7 +183,7 @@ class InlinerHeuristics(ppa: PostProcessorFrontendAccess, backendUtils: BackendU // or aliases, because otherwise it's too confusing for users looking at generated code, they will // write a small test method and think the inliner doesn't work correctly. val isGeneratedForwarder = - BCodeUtils.isSyntheticMethod(callsite.callsiteMethod) && backendUtils.looksLikeForwarderOrFactoryOrTrivial(callsite.callsiteMethod, callsite.callsiteClass.internalName, allowPrivateCalls = true) > 0 + BCodeUtils.isSyntheticMethod(callsite.callsiteMethod) && optimizerUtils.looksLikeForwarderOrFactoryOrTrivial(callsite.callsiteMethod, callsite.callsiteClass.internalName, allowPrivateCalls = true) > 0 if (isGeneratedForwarder) None else { @@ -218,26 +216,26 @@ class InlinerHeuristics(ppa: PostProcessorFrontendAccess, backendUtils: BackendU else None def shouldInlineArrayOp = - if (BackendUtils.isRuntimeArrayLoadOrUpdate(callsite.callsiteInstruction) && callsite.argInfos.get(1).contains(StaticallyKnownArray)) Some(KnownArrayOp) + if (AnalysisUtils.isRuntimeArrayLoadOrUpdate(callsite.callsiteInstruction) && callsite.argInfos.get(1).contains(StaticallyKnownArray)) Some(KnownArrayOp) else None def shouldInlineForwarder = Option { // In general, we cannot inline calls to methods that contain private calls here. // However (scala-dev#618) we should inline them if they call something that is itself trivial, as it will also be inlined. - val calleeCallsites = callGraph.callsites.get(callee.callee) + val calleeCallsites = callGraph.callsites(callee.callee) val allowPrivateCalls = calleeCallsites.size == 1 && (calleeCallsites.head match case (_, nestedCallsite: KnownCallsite) => - backendUtils.looksLikeForwarderOrFactoryOrTrivial( + optimizerUtils.looksLikeForwarderOrFactoryOrTrivial( nestedCallsite.callee.callee, nestedCallsite.callee.calleeDeclarationClass.internalName, allowPrivateCalls = false ) > 0 case _ => false ) - val forwarderKind = backendUtils.looksLikeForwarderOrFactoryOrTrivial(callee.callee, callee.calleeDeclarationClass.internalName, allowPrivateCalls) + val forwarderKind = optimizerUtils.looksLikeForwarderOrFactoryOrTrivial(callee.callee, callee.calleeDeclarationClass.internalName, allowPrivateCalls) if (forwarderKind < 0) null - else if (BCodeUtils.isSyntheticMethod(callee.callee) || BackendUtils.isMixinForwarder(callee.callee, callee.calleeDeclarationClass)) + else if (BCodeUtils.isSyntheticMethod(callee.callee) || AnalysisUtils.isMixinForwarder(callee.callee, callee.calleeDeclarationClass)) SyntheticForwarder else forwarderKind match { case 1 => TrivialMethod diff --git a/compiler/src/dotty/tools/backend/jvm/opt/Limits.scala b/compiler/src/dotty/tools/backend/jvm/opt/Limits.scala index e308e9c881a5..c9a35a2a439f 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/Limits.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/Limits.scala @@ -1,7 +1,5 @@ package dotty.tools.backend.jvm.opt -import dotty.tools.backend.jvm.BackendUtils - import scala.tools.asm.tree.MethodNode /** diff --git a/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala b/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala index 63df69271d1e..7f3ac25f1448 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala @@ -24,7 +24,6 @@ import scala.tools.asm.tree.analysis.Frame import dotty.tools.backend.jvm.BTypes.InternalName import dotty.tools.backend.jvm.analysis.* import BCodeUtils.* -import dotty.tools.backend.jvm.BackendUtils.isArrayGetLength /** * Optimizations within a single method. Certain optimizations enable others, for example removing @@ -150,14 +149,14 @@ import dotty.tools.backend.jvm.BackendUtils.isArrayGetLength * Note on updating the call graph: whenever an optimization eliminates a callsite or a closure * instantiation, we eliminate the corresponding entry from the call graph. */ -class LocalOpt(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inliner, - ts: WellKnownBTypes, bTypesFromClassfile: BTypesFromClassfile, +class LocalOpt(optimizerUtils: OptimizerUtils, indyTracker: IndyLambdaImplTracker, callGraph: CallGraph, inliner: Inliner, + ts: OptimizerKnownBTypes, bTypesFromClassfile: BTypesFromClassfile, settings: OptimizerSettings) { import LocalOptImpls.* - private val boxUnbox = new BoxUnbox(backendUtils, callGraph, ts) - private val copyProp = new CopyProp(backendUtils, callGraph, inliner, ts, settings) + private val boxUnbox = new BoxUnbox(optimizerUtils, callGraph, ts) + private val copyProp = new CopyProp(optimizerUtils, indyTracker, callGraph, inliner, ts, settings) /** * Remove unreachable instructions from all (non-abstract) methods and apply various other @@ -254,7 +253,7 @@ class LocalOpt(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inline val runDCE = (settings.optUnreachableCode && (requestDCE || nullnessOptChanged)) || settings.optBoxUnbox || settings.optCopyPropagation - val codeRemoved = if (runDCE) LocalOptImpls.removeUnreachableCodeImpl(method, ownerClassName, callGraph, backendUtils) else false + val codeRemoved = if (runDCE) LocalOptImpls.removeUnreachableCodeImpl(method, ownerClassName, callGraph, indyTracker) else false traceIfChanged("dce") // BOX-UNBOX @@ -343,7 +342,7 @@ class LocalOpt(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inline requestPushPop = true, requestStoreLoad = true) - if (settings.optUnreachableCode) BackendUtils.setDceDone(method) + if (settings.optUnreachableCode) OptimizerUtils.setDceDone(method) // (*) Removing stale local variable descriptors is required for correctness, see comment in `methodOptimizations` val localsRemoved = @@ -365,8 +364,8 @@ class LocalOpt(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inline assert(nullOrEmpty(method.invisibleLocalVariableAnnotations), method.invisibleLocalVariableAnnotations) // clear the non-official "access" flags once we're done and no longer look at them - // BackendUtils.clearMaxsComputed(method) - BackendUtils.clearDceDone(method) + // OptimizerUtils.clearMaxsComputed(method) + OptimizerUtils.clearDceDone(method) nullnessDceBoxesCastsCopypropPushpopOrJumpsChanged || localsRemoved || lineNumbersRemoved } @@ -383,7 +382,7 @@ class LocalOpt(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inline */ private def nullnessOptimizations(method: MethodNode, ownerClassName: InternalName): Boolean = { Limits.sizeOKForNullness(method) && { - lazy val nullnessAnalyzer = new NullnessAnalyzer(method, ownerClassName, backendUtils.isNonNullMethodInvocation, settings.optAssumeModulesNonNull) + lazy val nullnessAnalyzer = new NullnessAnalyzer(method, ownerClassName, optimizerUtils.isNonNullMethodInvocation, settings.optAssumeModulesNonNull) // When running nullness optimizations the method may still have unreachable code. Analyzer // frames of unreachable instructions are `null`. @@ -441,7 +440,7 @@ class LocalOpt(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inline } case mi: MethodInsnNode => - if (backendUtils.isScalaUnbox(mi)) for (frame <- frameAt(mi) if frame.peekStack(0) == NullValue) { + if (optimizerUtils.isScalaUnbox(mi)) for (frame <- frameAt(mi) if frame.peekStack(0) == NullValue) { toReplace(mi) = List( getPop(1), loadZeroForTypeSort(Type.getReturnType(mi.desc).getSort)) @@ -469,7 +468,7 @@ class LocalOpt(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inline } // We don't need to worry about CallGraph.closureInstantiations and - // BackendUtils.indyLambdaImplMethods, the removed instructions are not IndyLambdas + // OptimizerUtils.indyLambdaImplMethods, the removed instructions are not IndyLambdas def removeFromCallGraph(insn: AbstractInsnNode): Unit = insn match { case mi: MethodInsnNode => callGraph.removeCallsite(mi, method) case _ => @@ -488,7 +487,7 @@ class LocalOpt(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inline // Check for an Array.getLength(x) call where x is statically known to be of array type private def isArrayGetLengthOnStaticallyKnownArray(mi: MethodInsnNode, typeAnalyzer: NonLubbingTypeFlowAnalyzer): Boolean = { - BackendUtils.isArrayGetLength(mi) && { + AnalysisUtils.isArrayGetLength(mi) && { val f = typeAnalyzer.frameAt(mi) f.getValue(f.stackTop).getType.getSort == Type.ARRAY } @@ -540,7 +539,7 @@ class LocalOpt(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inline a.length - 2 == b.length && a(0) == 'L' && a.last == ';' && a.regionMatches(1, b, 0, b.length) || b.length - 2 == a.length && b(0) == 'L' && b.last == ';' && b.regionMatches(1, a, 0, a.length) } - sameClass(aDescOrIntN, bDescOrIntN) || sameClass(bDescOrIntN, "java/lang/Object") || { + sameClass(aDescOrIntN, bDescOrIntN) || sameClass(bDescOrIntN, ClassBType.javaLangObjectInternalName) || { val aType = bTypesFromClassfile.bTypeForDescriptorOrInternalNameFromClassfile(aDescOrIntN) val bType = bTypesFromClassfile.bTypeForDescriptorOrInternalNameFromClassfile(bDescOrIntN) // TODO instead of getOrElse, we should bubble the warning up... @@ -574,7 +573,7 @@ class LocalOpt(backendUtils: BackendUtils, callGraph: CallGraph, inliner: Inline } lazy val typeAnalyzer = new NonLubbingTypeFlowAnalyzer(method, owner) - lazy val nullnessAnalyzer = new NullnessAnalyzer(method, owner, backendUtils.isNonNullMethodInvocation, settings.optAssumeModulesNonNull) + lazy val nullnessAnalyzer = new NullnessAnalyzer(method, owner, optimizerUtils.isNonNullMethodInvocation, settings.optAssumeModulesNonNull) // cannot remove instructions while iterating, it gets the analysis out of synch (indexed by instructions) val toReplace = mutable.Map.empty[AbstractInsnNode, List[AbstractInsnNode]] @@ -653,18 +652,18 @@ object LocalOptImpls { * * @return A set containing the eliminated instructions */ - def minimalRemoveUnreachableCode(method: MethodNode, ownerClassName: InternalName, callGraph: CallGraph, backendUtils: BackendUtils): Boolean = { + def minimalRemoveUnreachableCode(method: MethodNode, ownerClassName: InternalName, callGraph: CallGraph, indyTracker: IndyLambdaImplTracker): Boolean = { // In principle, for the inliner, a single removeUnreachableCodeImpl would be enough. But that // would potentially leave behind stale handlers (empty try block) which is not legal in the // classfile. So we run both removeUnreachableCodeImpl and removeEmptyExceptionHandlers. if (method.instructions.size == 0) return false // fast path for abstract methods - if (BackendUtils.isDceDone(method)) return false // we know there is no unreachable code + if (OptimizerUtils.isDceDone(method)) return false // we know there is no unreachable code // For correctness, after removing unreachable code, we have to eliminate empty exception // handlers, see scaladoc of def methodOptimizations. Removing a live handler may render more // code unreachable and therefore requires running another round. def removalRound(): Boolean = { - val insnsRemoved = removeUnreachableCodeImpl(method, ownerClassName, callGraph, backendUtils) + val insnsRemoved = removeUnreachableCodeImpl(method, ownerClassName, callGraph, indyTracker) if (insnsRemoved) { val removeHandlersResult = removeEmptyExceptionHandlers(method) if (removeHandlersResult.liveHandlerRemoved) removalRound() @@ -676,7 +675,7 @@ object LocalOptImpls { val changed = removalRound() if (changed) removeUnusedLocalVariableNodes(method)() - BackendUtils.setDceDone(method) + OptimizerUtils.setDceDone(method) changed } @@ -684,9 +683,9 @@ object LocalOptImpls { * Removes unreachable basic blocks, returns `true` if instructions were removed. * * When this method returns, each `labelNode.getLabel` has a status set whether the label is live - * or not. This can be queried using `BackendUtils.isLabelReachable`. + * or not. This can be queried using `OptimizerUtils.isLabelReachable`. */ - def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: InternalName, callGraph: CallGraph, backendUtils: BackendUtils): Boolean = { + def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: InternalName, callGraph: CallGraph, indyTracker: IndyLambdaImplTracker): Boolean = { val size = method.instructions.size // queue of instruction indices where analysis should start @@ -803,7 +802,7 @@ object LocalOptImpls { insn match { case l: LabelNode => // label nodes are not removed: they might be referenced for example in a LocalVariableNode - if (isLive) BackendUtils.setLabelReachable(l) else BackendUtils.clearLabelReachable(l) + if (isLive) OptimizerUtils.setLabelReachable(l) else OptimizerUtils.clearLabelReachable(l) case _: LineNumberNode => @@ -817,7 +816,7 @@ object LocalOptImpls { case invocation: MethodInsnNode => callGraph.removeCallsite(invocation, method) case indy: InvokeDynamicInsnNode => callGraph.removeClosureInstantiation(indy, method) - backendUtils.removeIndyLambdaImplMethod(ownerClassName, method, indy) + indyTracker.remove(ownerClassName, method, indy) case _ => } } @@ -843,7 +842,7 @@ object LocalOptImpls { * Returns a pair of booleans (handlerRemoved, liveHandlerRemoved) * * The `liveHandlerRemoved` result depends on `removeUnreachableCode` being executed - * before, so that `BackendUtils.isLabelReachable` gives a correct answer. + * before, so that `OptimizerUtils.isLabelReachable` gives a correct answer. */ def removeEmptyExceptionHandlers(method: MethodNode): RemoveHandlersResult = { /* True if there exists code between start and end. */ @@ -863,7 +862,7 @@ object LocalOptImpls { val handler = handlersIter.next() if (!containsExecutableCode(handler.start, handler.end)) { if (!result.handlerRemoved) result = RemoveHandlersResult.HandlerRemoved - if (!result.liveHandlerRemoved && BackendUtils.isLabelReachable(handler.start)) + if (!result.liveHandlerRemoved && OptimizerUtils.isLabelReachable(handler.start)) result = RemoveHandlersResult.LiveHandlerRemoved handlersIter.remove() } @@ -1016,7 +1015,7 @@ object LocalOptImpls { while (iterator.hasNext) { iterator.next match { case label: LabelNode => - BackendUtils.clearLabelReachable(label) + OptimizerUtils.clearLabelReachable(label) previousLabel = label case line: LineNumberNode if isEmpty(line) => assert(line.start == previousLabel) diff --git a/compiler/src/dotty/tools/backend/jvm/opt/OptimizerKnownBTypes.scala b/compiler/src/dotty/tools/backend/jvm/opt/OptimizerKnownBTypes.scala new file mode 100644 index 000000000000..6a55cd458e55 --- /dev/null +++ b/compiler/src/dotty/tools/backend/jvm/opt/OptimizerKnownBTypes.scala @@ -0,0 +1,164 @@ +package dotty.tools.backend.jvm.opt + +import dotty.tools.backend.jvm.BTypes.InternalName +import dotty.tools.backend.jvm.{BType, BTypeLoader, ClassBType, KnownBTypes, MethodBType, MethodNameAndType, UNIT} +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.StdNames.nme +import dotty.tools.dotc.core.Symbols +import dotty.tools.dotc.core.Symbols.{defn, requiredClass} + +import scala.annotation.constructorOnly + +final class OptimizerKnownBTypes(ts: BTypeLoader)(using @constructorOnly initctx: Context) extends KnownBTypes(ts) { + + val srNothingRef: ClassBType = ts.classBTypeFromSymbol(defn.RuntimeNothingClass) + + val boxedClasses: Set[ClassBType] = boxedClassOfPrimitive.values.toSet + + val srBoxedUnitRef: ClassBType = ts.classBTypeFromSymbol(requiredClass[scala.runtime.BoxedUnit]) + + val PredefRef: ClassBType = ts.classBTypeFromSymbol(defn.ScalaPredefModuleClass) + + val jlCloneableRef: ClassBType = ts.classBTypeFromSymbol(defn.JavaCloneableClass) + + val jiSerializableRef: ClassBType = ts.classBTypeFromSymbol(requiredClass[java.io.Serializable]) + + // java/lang/Boolean -> MethodNameAndType(valueOf,(Z)Ljava/lang/Boolean;) + val javaBoxMethods: Map[InternalName, MethodNameAndType] = _javaBoxMethods(using initctx) + private def _javaBoxMethods(using Context): Map[InternalName, MethodNameAndType] = { + Map.from(defn.ScalaValueClassesNoUnit().map(primitive => { + val boxed = defn.boxedClass(primitive) + val unboxed = ts.bTypeFromSymbol(primitive) + val method = MethodNameAndType("valueOf", MethodBType(List(unboxed), boxedClassOfPrimitive(unboxed))) + (ts.classBTypeFromSymbol(boxed).internalName, method) + })) + } + + // java/lang/Boolean -> MethodNameAndType(booleanValue,()Z) + val javaUnboxMethods: Map[InternalName, MethodNameAndType] = _javaUnboxMethods(using initctx) + private def _javaUnboxMethods(using Context): Map[InternalName, MethodNameAndType] = { + Map.from(defn.ScalaValueClassesNoUnit().map(primitive => { + val boxed = defn.boxedClass(primitive) + val name = primitive.name.toString.toLowerCase + "Value" + (ts.classBTypeFromSymbol(boxed).internalName, MethodNameAndType(name, MethodBType(Nil, ts.bTypeFromSymbol(primitive)))) + })) + } + + private def predefBoxingMethods(isBox: Boolean, getName: (String, String) => String)(using Context): Map[String, MethodBType] = + Map.from(defn.ScalaValueClassesNoUnit().map(primitive => { + val unboxed = ts.bTypeFromSymbol(primitive) + val boxed = boxedClassOfPrimitive(unboxed) + val name = getName(primitive.name.toString, defn.boxedClass(primitive).name.toString) + (name, MethodBType(List(if isBox then unboxed else boxed), if isBox then boxed else unboxed)) + })) + + // boolean2Boolean -> (Z)Ljava/lang/Boolean; + val predefAutoBoxMethods: Map[String, MethodBType] = _predefAutoBoxMethods(using initctx) + private def _predefAutoBoxMethods(using Context): Map[String, MethodBType] = predefBoxingMethods(true, (primitive, boxed) => primitive.toLowerCase + "2" + boxed) + + // Boolean2boolean -> (Ljava/lang/Boolean;)Z + val predefAutoUnboxMethods: Map[String, MethodBType] = _predefAutoUnboxMethods(using initctx) + private def _predefAutoUnboxMethods(using Context): Map[String, MethodBType] = predefBoxingMethods(false, (primitive, boxed) => boxed + "2" + primitive.toLowerCase) + + // scala/runtime/BooleanRef -> MethodNameAndType(create,(Z)Lscala/runtime/BooleanRef;) + val srRefCreateMethods: Map[InternalName, MethodNameAndType] = _srRefCreateMethods(using initctx) + private def _srRefCreateMethods(using Context): Map[InternalName, MethodNameAndType] = { + Map.from(defn.ScalaValueClassesNoUnit().union(Set(defn.ObjectClass)).flatMap(primitive => { + val boxed = if primitive == defn.ObjectClass then primitive else defn.boxedClass(primitive) + val unboxed = if primitive == defn.ObjectClass then ObjectRef else ts.bTypeFromSymbol(primitive) + val refClass = Symbols.requiredClass("scala.runtime." + primitive.name.toString + "Ref") + val volatileRefClass = Symbols.requiredClass("scala.runtime.Volatile" + primitive.name.toString + "Ref") + List( + (ts.classBTypeFromSymbol(refClass).internalName, MethodNameAndType(nme.create.toString, MethodBType(List(unboxed), ts.bTypeFromSymbol(refClass)))), + (ts.classBTypeFromSymbol(volatileRefClass).internalName, MethodNameAndType(nme.create.toString, MethodBType(List(unboxed), ts.bTypeFromSymbol(volatileRefClass)))) + ) + })) + } + + // scala/runtime/BooleanRef -> MethodNameAndType(zero,()Lscala/runtime/BooleanRef;) + val srRefZeroMethods: Map[InternalName, MethodNameAndType] = _srRefZeroMethods(using initctx) + private def _srRefZeroMethods(using Context): Map[InternalName, MethodNameAndType] = { + Map.from(defn.ScalaValueClassesNoUnit().union(Set(defn.ObjectClass)).flatMap(primitive => { + val boxed = if primitive == defn.ObjectClass then primitive else defn.boxedClass(primitive) + val refClass = Symbols.requiredClass("scala.runtime." + primitive.name.toString + "Ref") + val volatileRefClass = Symbols.requiredClass("scala.runtime.Volatile" + primitive.name.toString + "Ref") + List( + (ts.classBTypeFromSymbol(refClass).internalName, MethodNameAndType(nme.zero.toString, MethodBType(List(), ts.bTypeFromSymbol(refClass)))), + (ts.classBTypeFromSymbol(volatileRefClass).internalName, MethodNameAndType(nme.zero.toString, MethodBType(List(), ts.bTypeFromSymbol(volatileRefClass)))) + ) + })) + } + + // java/lang/Boolean -> MethodNameAndType(,(Z)V) + val primitiveBoxConstructors: Map[InternalName, MethodNameAndType] = _primitiveBoxConstructors(using initctx) + private def _primitiveBoxConstructors(using Context): Map[InternalName, MethodNameAndType] = { + Map.from(defn.ScalaValueClassesNoUnit().map(primitive => { + val boxed = defn.boxedClass(primitive) + val unboxed = ts.bTypeFromSymbol(primitive) + (ts.classBTypeFromSymbol(boxed).internalName, MethodNameAndType(nme.CONSTRUCTOR.toString, MethodBType(List(unboxed), UNIT))) + })) + } + + // Z -> MethodNameAndType(boxToBoolean,(Z)Ljava/lang/Boolean;) + val srBoxesRuntimeBoxToMethods: Map[BType, MethodNameAndType] = _srBoxesRuntimeBoxToMethods(using initctx) + private def _srBoxesRuntimeBoxToMethods(using Context): Map[BType, MethodNameAndType] = { + Map.from(defn.ScalaValueClassesNoUnit().map(primitive => { + val bType = ts.bTypeFromSymbol(primitive) + val boxed = boxedClassOfPrimitive(bType) + val name = "boxTo" + defn.boxedClass(primitive).name.toString + (bType, MethodNameAndType(name, MethodBType(List(bType), boxed))) + })) + } + + // Z -> MethodNameAndType(unboxToBoolean,(Ljava/lang/Object;)Z) + val srBoxesRuntimeUnboxToMethods: Map[BType, MethodNameAndType] = _srBoxesRuntimeUnboxToMethods(using initctx) + private def _srBoxesRuntimeUnboxToMethods(using Context): Map[BType, MethodNameAndType] = { + Map.from(defn.ScalaValueClassesNoUnit().map(primitive => { + val bType = ts.bTypeFromSymbol(primitive) + val name = "unboxTo" + primitive.name.toString + (bType, MethodNameAndType(name, MethodBType(List(ObjectRef), bType))) + })) + } + + // scala/runtime/BooleanRef -> MethodNameAndType(,(Z)V) + val srRefConstructors: Map[InternalName, MethodNameAndType] = _srRefConstructors(using initctx) + private def _srRefConstructors(using Context): Map[InternalName, MethodNameAndType] = { + Map.from(defn.ScalaValueClassesNoUnit().union(Set(defn.ObjectClass)).flatMap(primitive => { + val boxed = if primitive == defn.ObjectClass then primitive else defn.boxedClass(primitive) + val unboxed = if primitive == defn.ObjectClass then ObjectRef else ts.bTypeFromSymbol(primitive) + val refClass = Symbols.requiredClass("scala.runtime." + primitive.name.toString + "Ref") + val volatileRefClass = Symbols.requiredClass("scala.runtime.Volatile" + primitive.name.toString + "Ref") + List( + (ts.classBTypeFromSymbol(refClass).internalName, MethodNameAndType(nme.CONSTRUCTOR.toString, MethodBType(List(unboxed), UNIT))), + (ts.classBTypeFromSymbol(volatileRefClass).internalName, MethodNameAndType(nme.CONSTRUCTOR.toString, MethodBType(List(unboxed), UNIT))) + ) + })) + } + + // scala/Tuple3 -> MethodNameAndType(,(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V) + // scala/Tuple2$mcZC$sp -> MethodNameAndType(,(ZC)V) + // ... this was easy in scala2, but now we don't specialize them so we have to know each name + // tuple1 is specialized for D, I, J + // tuple2 is specialized for C, D, I, J, Z in each parameter + val tupleClassConstructors: Map[InternalName, MethodNameAndType] = _tupleClassConstructors(using initctx) + private def _tupleClassConstructors(using Context): Map[InternalName, MethodNameAndType] = { + val spec1 = List(defn.DoubleClass, defn.IntClass, defn.LongClass) + val spec2 = List(defn.CharClass, defn.DoubleClass, defn.IntClass, defn.LongClass, defn.BooleanClass) + Map.from( + Iterator.concat( + (1 to 22).map { n => + ("scala/Tuple" + n, MethodNameAndType(nme.CONSTRUCTOR.toString, MethodBType(List.fill(n)(ObjectRef), UNIT))) + }, + spec1.map { sp1 => + val prim = ts.bTypeFromSymbol(sp1) + ("scala/Tuple1$mc" + prim.descriptor + "$sp", MethodNameAndType(nme.CONSTRUCTOR.toString, MethodBType(List(), UNIT))) + }, + for sp2a <- spec2; sp2b <- spec2 yield { + val primA = ts.bTypeFromSymbol(sp2a) + val primB = ts.bTypeFromSymbol(sp2b) + ("scala/Tuple2$mc" + primA.descriptor + primB.descriptor + "$sp", MethodNameAndType(nme.CONSTRUCTOR.toString, MethodBType(List(primA, primB), UNIT))) + } + ) + ) + } +} diff --git a/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala b/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala new file mode 100644 index 000000000000..2758f66116cc --- /dev/null +++ b/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala @@ -0,0 +1,272 @@ +package dotty.tools.backend.jvm.opt + +import dotty.tools.backend.jvm.BTypes.InternalName +import dotty.tools.backend.jvm.* +import dotty.tools.backend.jvm.analysis.AnalysisUtils +import dotty.tools.dotc.core.Definitions + +import java.util.concurrent.ConcurrentHashMap +import scala.annotation.switch +import scala.collection.{BitSet, mutable} +import scala.jdk.CollectionConverters.* +import scala.tools.asm +import scala.tools.asm.tree.* +import scala.tools.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) { + + def isPredefLoad(insn: AbstractInsnNode): Boolean = AnalysisUtils.isModuleLoad(insn, _ == ts.PredefRef.internalName) + + // ============================================================================================== + + val primitiveAsmTypeSortToBType: Map[Int, PrimitiveBType] = Map( + asm.Type.BOOLEAN -> BOOL, + asm.Type.BYTE -> BYTE, + asm.Type.CHAR -> CHAR, + asm.Type.SHORT -> SHORT, + asm.Type.INT -> INT, + asm.Type.LONG -> LONG, + asm.Type.FLOAT -> FLOAT, + asm.Type.DOUBLE -> DOUBLE + ) + + def isScalaBox(insn: MethodInsnNode): Boolean = + insn.owner == ts.srBoxesRuntimeRef.internalName && { + val args = asm.Type.getArgumentTypes(insn.desc) + args.length == 1 && (primitiveAsmTypeSortToBType.get(args(0).getSort) match + case Some(prim) => + val MethodNameAndType(name, tp) = ts.srBoxesRuntimeBoxToMethods(prim) + name == insn.name && tp.descriptor == insn.desc + case None => false) + } + + def getScalaBox(primitiveType: asm.Type): MethodInsnNode = { + val bType = primitiveAsmTypeSortToBType(primitiveType.getSort) + val MethodNameAndType(name, methodBType) = ts.srBoxesRuntimeBoxToMethods(bType) + new MethodInsnNode(Opcodes.INVOKESTATIC, ts.srBoxesRuntimeRef.internalName, name, methodBType.descriptor, /*itf =*/ false) + } + + def getScalaUnbox(primitiveType: asm.Type): MethodInsnNode = { + val bType = primitiveAsmTypeSortToBType(primitiveType.getSort) + val MethodNameAndType(name, methodBType) = ts.srBoxesRuntimeUnboxToMethods(bType) + new MethodInsnNode(Opcodes.INVOKESTATIC, ts.srBoxesRuntimeRef.internalName, name, methodBType.descriptor, /*itf =*/ false) + } + + def isScalaUnbox(insn: MethodInsnNode): Boolean = { + insn.owner == ts.srBoxesRuntimeRef.internalName && (primitiveAsmTypeSortToBType.get(asm.Type.getReturnType(insn.desc).getSort) match { + case Some(prim) => + val MethodNameAndType(name, tp) = ts.srBoxesRuntimeUnboxToMethods(prim) + name == insn.name && tp.descriptor == insn.desc + case _ => false + }) + } + + private def calleeInMap(insn: MethodInsnNode, map: Map[InternalName, MethodNameAndType]): Boolean = map.get(insn.owner) match { + case Some(MethodNameAndType(name, tp)) => insn.name == name && insn.desc == tp.descriptor + case _ => false + } + + def isJavaBox(insn: MethodInsnNode): Boolean = calleeInMap(insn, ts.javaBoxMethods) + def isJavaUnbox(insn: MethodInsnNode): Boolean = calleeInMap(insn, ts.javaUnboxMethods) + + def isPredefAutoBox(insn: MethodInsnNode): Boolean = { + insn.owner == ts.PredefRef.internalName && (ts.predefAutoBoxMethods.get(insn.name) match { + case Some(tp) => insn.desc == tp.descriptor + case _ => false + }) + } + + def isPredefAutoUnbox(insn: MethodInsnNode): Boolean = { + insn.owner == ts.PredefRef.internalName && (ts.predefAutoUnboxMethods.get(insn.name) match { + case Some(tp) => insn.desc == tp.descriptor + case _ => false + }) + } + + def getBoxedUnit: FieldInsnNode = + new FieldInsnNode(Opcodes.GETSTATIC, ts.srBoxedUnitRef.internalName, "UNIT", ts.srBoxedUnitRef.descriptor) + + def isBoxedUnit(insn: AbstractInsnNode): Boolean = { + insn.getOpcode == Opcodes.GETSTATIC && { + val fi = insn.asInstanceOf[FieldInsnNode] + fi.owner == ts.srBoxedUnitRef.internalName && fi.name == "UNIT" && fi.desc == ts.srBoxedUnitRef.descriptor + } + } + + def isRefCreate(insn: MethodInsnNode): Boolean = calleeInMap(insn, ts.srRefCreateMethods) + def isRefZero(insn: MethodInsnNode): Boolean = calleeInMap(insn, ts.srRefZeroMethods) + + def isPrimitiveBoxConstructor(insn: MethodInsnNode): Boolean = calleeInMap(insn, ts.primitiveBoxConstructors) + def isRuntimeRefConstructor(insn: MethodInsnNode): Boolean = calleeInMap(insn, ts.srRefConstructors) + def isTupleConstructor(insn: MethodInsnNode): Boolean = calleeInMap(insn, ts.tupleClassConstructors) + def isTupleApply(insn: MethodInsnNode): Boolean = insn.owner.startsWith("scala/Tuple") && insn.owner.endsWith("$") && insn.name == "apply" + + def runtimeRefClassBoxedType(refClass: InternalName): asm.Type = asm.Type.getArgumentTypes(ts.srRefCreateMethods(refClass).methodType.descriptor)(0) + + def isSideEffectFreeConstructorCall(insn: MethodInsnNode): Boolean = { + insn.name == GenBCode.INSTANCE_CONSTRUCTOR_NAME && sideEffectFreeConstructors((insn.owner, insn.desc)) + } + + def isNewForSideEffectFreeConstructor(insn: AbstractInsnNode): Boolean = { + insn.getOpcode == Opcodes.NEW && { + val ti = insn.asInstanceOf[TypeInsnNode] + classesOfSideEffectFreeConstructors.contains(ti.desc) + } + } + + def isSideEffectFreeCall(mi: MethodInsnNode): Boolean = { + isScalaBox(mi) || // not Scala unbox, it may CCE + isJavaBox(mi) || // not Java unbox, it may NPE + isSideEffectFreeConstructorCall(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) + } + + // unused objects created by these constructors are eliminated by pushPop + private lazy val sideEffectFreeConstructors: Set[(String, String)] = + val ownerDesc = (p: (InternalName, MethodNameAndType)) => (p._1, p._2.methodType.descriptor) + ts.primitiveBoxConstructors.map(ownerDesc).toSet ++ + ts.srRefConstructors.map(ownerDesc) ++ + ts.tupleClassConstructors.map(ownerDesc) ++ Set( + (ts.ObjectRef.internalName, MethodBType(Nil, UNIT).descriptor), + (ts.StringRef.internalName, MethodBType(Nil, UNIT).descriptor), + (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) + + private val nonForwarderInstructionTypes: BitSet = { + import AbstractInsnNode.* + BitSet(FIELD_INSN, INVOKE_DYNAMIC_INSN, JUMP_INSN, IINC_INSN, TABLESWITCH_INSN, LOOKUPSWITCH_INSN) + } + + /** + * Identify forwarders, aliases, anonfun\$adapted methods, bridges, trivial methods (x + y), etc + * Returns + * -1 : no match + * 1 : trivial (no method calls), but not field getters + * 2 : factory + * 3 : forwarder with boxing adaptation + * 4 : generic forwarder / alias + * + * TODO: should delay some checks to `canInline` (during inlining) + * problem is: here we don't have access to the callee / accessed field, so we can't check accessibility + * - INVOKESPECIAL is not the only way to call private methods, INVOKESTATIC is also possible + * - the body of the callee can change between here (we're in inliner heuristics) and the point + * when we actually inline it (code may have been inlined into the callee) + * - methods accessing a public field could be inlined. on the other hand, methods accessing a private + * static field should not be inlined. + */ + def looksLikeForwarderOrFactoryOrTrivial(method: MethodNode, owner: InternalName, allowPrivateCalls: Boolean): Int = { + val paramTypes = Type.getArgumentTypes(method.desc) + val numPrimitives = paramTypes.count(_.getSort < Type.ARRAY) + (if (Type.getReturnType(method.desc).getSort < Type.ARRAY) 1 else 0) + + val maxSize = + 3 + // forwardee call, return + paramTypes.length + // param load + numPrimitives * 2 + // box / unbox call, for example Predef.int2Integer + paramTypes.length + 2 // some slack: +1 for each parameter, receiver, return value. allow things like casts. + + if (method.instructions.iterator.asScala.count(_.getOpcode > 0) > maxSize) return -1 + + var numBoxConv = 0 + var numCallsOrNew = 0 + var callMi: MethodInsnNode | Null = null + val it = method.instructions.iterator + while (it.hasNext && numCallsOrNew < 2) { + val i = it.next() + 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 != GenBCode.INSTANCE_CONSTRUCTOR_NAME) { + 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)) + numBoxConv += 1 + else { + numCallsOrNew += 1 + callMi = mi + } + } + } else if (nonForwarderInstructionTypes(t)) { + if (i.getOpcode == Opcodes.GETSTATIC) { + if (!allowPrivateCalls && owner == i.asInstanceOf[FieldInsnNode].owner) + numCallsOrNew = 2 // stop here: not forwarder or trivial + } else { + numCallsOrNew = 2 // stop here: not forwarder or trivial + } + } + } + if (numCallsOrNew > 1 || numBoxConv > paramTypes.length + 1) -1 + else if (numCallsOrNew == 0) if (numBoxConv == 0) 1 else 3 + else if (callMi.nn.name == GenBCode.INSTANCE_CONSTRUCTOR_NAME) 2 // if numCallsOrNew > 0 then callMi is nonnull + else if (numBoxConv > 0) 3 + else 4 + } +} + +object OptimizerUtils { + + + /** + * A pseudo-flag indicating if a MethodNode's unreachable code has been eliminated. + * + * The ASM Analyzer class does not compute any frame information for unreachable instructions. + * Transformations that use an analyzer (including inlining) therefore require unreachable code + * to be eliminated. + * + * This flag allows running dead code elimination whenever an analyzer is used. If the method + * is already optimized, DCE can return early. + */ + private val ACC_DCE_DONE = 0x2000000 + def isDceDone(method: MethodNode): Boolean = (method.access & ACC_DCE_DONE) != 0 + def setDceDone(method: MethodNode): Unit = method.access |= ACC_DCE_DONE + def clearDceDone(method: MethodNode): Unit = method.access &= ~ACC_DCE_DONE + + private val LABEL_REACHABLE_STATUS = 0x1000000 + private def isLabelFlagSet(l: LabelNode1, f: Int): Boolean = (l.flags & f) != 0 + private def setLabelFlag(l: LabelNode1, f: Int): Unit = l.flags |= f + private def clearLabelFlag(l: LabelNode1, f: Int): Unit = l.flags &= ~f + def isLabelReachable(label: LabelNode): Boolean = isLabelFlagSet(label.asInstanceOf[LabelNode1], LABEL_REACHABLE_STATUS) + def setLabelReachable(label: LabelNode): Unit = setLabelFlag(label.asInstanceOf[LabelNode1], LABEL_REACHABLE_STATUS) + def clearLabelReachable(label: LabelNode): Unit = clearLabelFlag(label.asInstanceOf[LabelNode1], LABEL_REACHABLE_STATUS) + + // === + + def methodSignature(classInternalName: InternalName, name: String, desc: String): String = { + classInternalName + "::" + name + desc + } + + def methodSignature(classInternalName: InternalName, method: MethodNode): String = { + methodSignature(classInternalName, method.name, method.desc) + } + + def siteString(owner: String, method: String): String = { + val c = owner.replace('/', '.').replaceAll("\\$+", ".").replaceAll("\\.$", "") + if (method.isEmpty) c + else s"$c.$method" + } +} \ No newline at end of file diff --git a/compiler/src/dotty/tools/backend/jvm/opt/OptimizerWarning.scala b/compiler/src/dotty/tools/backend/jvm/opt/OptimizerWarning.scala index 259f1d874e50..bd6f37100b67 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/OptimizerWarning.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/OptimizerWarning.scala @@ -1,11 +1,12 @@ package dotty.tools.backend.jvm.opt -import dotty.tools.backend.jvm.BackendUtils import dotty.tools.backend.jvm.BTypes.InternalName -import dotty.tools.dotc.util.SourcePosition +import dotty.tools.dotc.reporting.Message +import dotty.tools.dotc.util.{SourcePosition, SrcPos} import scala.tools.asm.tree.AbstractInsnNode +final class OptimizerIssue(val msg: String, val site: String, val pos: SrcPos) sealed trait OptimizerWarning { def emitWarning(settings: OptimizerSettings): Boolean @@ -67,7 +68,7 @@ sealed trait CalleeInfoWarning extends OptimizerWarning { def descriptor: String - private def warningMessageSignature = BackendUtils.methodSignature(declarationClass, name, descriptor) + private def warningMessageSignature = OptimizerUtils.methodSignature(declarationClass, name, descriptor) override def toString: String = this match { case MethodInlineInfoIncomplete(_, _, _, cause) => @@ -107,7 +108,7 @@ sealed trait CannotInlineWarning extends OptimizerWarning { /** Either the callee or the callsite is annotated @inline */ def annotatedInline: Boolean - private def calleeMethodSig = BackendUtils.methodSignature(calleeDeclarationClass, name, descriptor) + private def calleeMethodSig = OptimizerUtils.methodSignature(calleeDeclarationClass, name, descriptor) override def toString: String = { val annotWarn = if (annotatedInline) " is annotated @inline but" else "" @@ -126,7 +127,7 @@ sealed trait CannotInlineWarning extends OptimizerWarning { |$cause""" case MethodWithHandlerCalledOnNonEmptyStack(_, _, _, _, callsiteClass, callsiteName, callsiteDesc) => - s"""|The operand stack at the callsite in ${BackendUtils.methodSignature(callsiteClass, callsiteName, callsiteDesc)} contains more values than the + s"""|The operand stack at the callsite in ${OptimizerUtils.methodSignature(callsiteClass, callsiteName, callsiteDesc)} contains more values than the |arguments expected by the callee $calleeMethodSig. These values would be discarded |when entering an exception handler declared in the inlined method.""" @@ -137,12 +138,12 @@ sealed trait CannotInlineWarning extends OptimizerWarning { s"Method $calleeMethodSig cannot be inlined because it does not have any instructions, even though it is not abstract. The class may come from a signature jar file (such as a Bazel 'hjar')." case StrictfpMismatch(_, _, _, _, callsiteClass, callsiteName, callsiteDesc) => - s"""The callsite method ${BackendUtils.methodSignature(callsiteClass, callsiteName, callsiteDesc)} + s"""The callsite method ${OptimizerUtils.methodSignature(callsiteClass, callsiteName, callsiteDesc)} |does not have the same strictfp mode as the callee $calleeMethodSig. """.stripMargin case ResultingMethodTooLarge(_, _, _, _, callsiteClass, callsiteName, callsiteDesc) => - s"""The size of the callsite method ${BackendUtils.methodSignature(callsiteClass, callsiteName, callsiteDesc)} + s"""The size of the callsite method ${OptimizerUtils.methodSignature(callsiteClass, callsiteName, callsiteDesc)} |would exceed the JVM method size limit after inlining $calleeMethodSig. """.stripMargin } diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index fb77ef3a1e8b..b5edccfa07e4 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -69,7 +69,7 @@ extends ImplicitRunInfo, ConstraintRunInfo, cc.CaptureRunInfo { private var myUnits: List[CompilationUnit] = Nil private var myUnitsCached: List[CompilationUnit] = Nil - private var myFiles: Set[AbstractFile] = uninitialized + private var myFiles: Set[AbstractFile] = Set.empty // `@nowarn` annotations by source file, populated during typer private val mySuppressions: mutable.LinkedHashMap[SourceFile, ListBuffer[Suppression]] = mutable.LinkedHashMap.empty @@ -296,17 +296,9 @@ extends ImplicitRunInfo, ConstraintRunInfo, cc.CaptureRunInfo { private def syncAsyncTasty()(using Context): Unit = for async <- _asyncTasty - bufferedReporter <- async.sync() - report <- bufferedReporter.resetReports() + errorMessage <- async.sync() do - import reporting.Diagnostic - report match - case FileWriters.Report.Error(msg, pos) => - ctx.reporter.report(Diagnostic.Error(msg(ctx), pos)) - case FileWriters.Report.Warning(msg, pos) => - ctx.reporter.report(Diagnostic.Warning(msg(ctx), pos)) - case FileWriters.Report.Log(msg) => - ctx.reporter.report(Diagnostic.Info(msg, NoSourcePosition)) + report.error(errorMessage, NoSourcePosition) /** Will be set to true if any of the compiled compilation units contains * a pureFunctions language import. diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 152b9a55cde4..92d208e46855 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -36,6 +36,15 @@ object Definitions { * else without affecting the set of programs that can be compiled. */ val MaxImplementedFunctionArity: Int = MaxTupleArity + + /* + * RuntimeNothingClass and RuntimeNullClass exist at run-time only. + * They are the run-time manifestation (in method signatures only) + * of what shows up as NothingClass (scala.Nothing) resp. NullClass (scala.Null) in Scala ASTs. + * Therefore, when NothingClass or NullClass are to be emitted, a mapping is needed. + */ + val RuntimeNothingName: String = "scala.runtime.Nothing$" + val RuntimeNullName: String = "scala.runtime.Null$" } /** A class defining symbols and types of standard definitions @@ -484,14 +493,8 @@ class Definitions { } def NullType: TypeRef = NullClass.typeRef - /* - * RuntimeNothingClass and RuntimeNullClass exist at run-time only. - * They are the run-time manifestation (in method signatures only) - * of what shows up as NothingClass (scala.Nothing) resp. NullClass (scala.Null) in Scala ASTs. - * Therefore, when NothingClass or NullClass are to be emitted, a mapping is needed. - */ - @tu lazy val RuntimeNothingClass: Symbol = requiredClass("scala.runtime.Nothing$") - @tu lazy val RuntimeNullClass: Symbol = requiredClass("scala.runtime.Null$") + @tu lazy val RuntimeNothingClass: Symbol = requiredClass(RuntimeNothingName) + @tu lazy val RuntimeNullClass: Symbol = requiredClass(RuntimeNullName) @tu lazy val InvokerModule = requiredModule("scala.runtime.coverage.Invoker") @tu lazy val InvokedMethodRef = InvokerModule.requiredMethodRef("invoked") diff --git a/compiler/src/dotty/tools/dotc/report.scala b/compiler/src/dotty/tools/dotc/report.scala index f68507ffb6a1..359e3e17e21a 100644 --- a/compiler/src/dotty/tools/dotc/report.scala +++ b/compiler/src/dotty/tools/dotc/report.scala @@ -55,8 +55,8 @@ object report: else issueWarning(new FeatureWarning(msg, pos.sourcePos)) end featureWarning - def optimizerWarning(msg: Message, site: String, pos: SrcPos)(using Context): Unit = - issueWarning(new OptimizerWarning(msg, site, addInlineds(pos))) + def optimizerWarning(msg: String, site: String, pos: SrcPos)(using Context): Unit = + issueWarning(new OptimizerWarning(em"$msg", site, addInlineds(pos))) def warning(msg: Message, pos: SrcPos, origin: String)(using Context): Unit = issueWarning(LintWarning(msg, addInlineds(pos), origin)) diff --git a/compiler/src/dotty/tools/dotc/reporting/Message.scala b/compiler/src/dotty/tools/dotc/reporting/Message.scala index 7de9c0b640fa..e712cc9a7293 100644 --- a/compiler/src/dotty/tools/dotc/reporting/Message.scala +++ b/compiler/src/dotty/tools/dotc/reporting/Message.scala @@ -334,7 +334,7 @@ end Message * @param errorId a unique id identifying the message, this will be * used to reference documentation online * - * Messages modify the rendendering of interpolated strings in several ways: + * Messages modify the rendering of interpolated strings in several ways: * * 1. The size of the printed code is limited with a MessageLimiter. If the message * would get too large or too deeply nested, a `...` is printed instead. @@ -349,7 +349,7 @@ end Message * * Messages inheriting from the NoDisambiguation trait or returned from the * `noDisambiguation()` method skip point (3) above. This makes sense if the - * message already exolains where different occurrences of the same identifier + * message already explains where different occurrences of the same identifier * are located. Examples are NamingMsgs such as double definition errors, * overriding errors, and ambiguous implicit errors. * diff --git a/compiler/src/dotty/tools/dotc/sbt/package.scala b/compiler/src/dotty/tools/dotc/sbt/package.scala index 1b4eca3ca95a..14692bfa88b2 100644 --- a/compiler/src/dotty/tools/dotc/sbt/package.scala +++ b/compiler/src/dotty/tools/dotc/sbt/package.scala @@ -5,26 +5,15 @@ import dotty.tools.dotc.core.Symbols.Symbol import dotty.tools.dotc.core.NameOps.stripModuleClassSuffix import dotty.tools.dotc.core.Names.Name import dotty.tools.dotc.core.Names.termName - import interfaces.IncrementalCallback -import dotty.tools.io.FileWriters.BufferingReporter -import dotty.tools.dotc.core.Decorators.em inline val TermNameHash = 1987 // 300th prime inline val TypeNameHash = 1993 // 301st prime inline val InlineParamHash = 1997 // 302nd prime -def asyncZincPhasesCompleted(cb: IncrementalCallback, pending: Option[BufferingReporter]): BufferingReporter = - val zincReporter = pending match - case Some(buffered) => buffered - case None => BufferingReporter() - try - cb.apiPhaseCompleted() - cb.dependencyPhaseCompleted() - catch - case t: Exception => - zincReporter.exception(em"signaling API and Dependencies phases completion", t) - zincReporter +def asyncZincPhasesCompleted(cb: IncrementalCallback): Unit = + cb.apiPhaseCompleted() + cb.dependencyPhaseCompleted() extension (sym: Symbol) diff --git a/compiler/src/dotty/tools/dotc/transform/Pickler.scala b/compiler/src/dotty/tools/dotc/transform/Pickler.scala index e82f8e651636..904617bf9387 100644 --- a/compiler/src/dotty/tools/dotc/transform/Pickler.scala +++ b/compiler/src/dotty/tools/dotc/transform/Pickler.scala @@ -8,27 +8,31 @@ import Decorators.* import tasty.* import config.Printers.{noPrinter, pickling} import config.Feature + import java.io.PrintStream -import io.FileWriters.{TastyWriter, ReadOnlyContext} -import StdNames.{str, nme} +import io.FileWriters.TastyWriter +import StdNames.{nme, str} import Periods.* import Phases.* import Symbols.* import Flags.Module -import reporting.{ThrowingReporter, Profile, Message} +import reporting.{Message, Profile, Reporter, ThrowingReporter} import collection.mutable import util.concurrent.Executor + import compiletime.uninitialized -import dotty.tools.io.{JarArchive, AbstractFile} +import dotty.tools.io.{AbstractFile, JarArchive} import dotty.tools.dotc.printing.OutlinePrinter +import dotty.tools.dotc.report.error + import scala.annotation.constructorOnly import scala.concurrent.Promise import dotty.tools.dotc.transform.Pickler.writeSigFilesAsync - -import dotty.tools.io.FileWriters.{EagerReporter, BufferingReporter} import dotty.tools.dotc.sbt.interfaces.IncrementalCallback import dotty.tools.dotc.sbt.asyncZincPhasesCompleted +import dotty.tools.dotc.util.NoSourcePosition import dotty.tools.dotc.util.chaining.* + import scala.concurrent.ExecutionContext import java.util.concurrent.atomic.AtomicBoolean import java.nio.file.Files @@ -43,6 +47,10 @@ object Pickler { */ inline val ParallelPickling = true + private def exceptionMessage(reason: Context ?=> Message, throwable: Throwable)(using Context): Message = + val trace = throwable.getStackTrace().mkString("\n ") + em"An unhandled exception was thrown in the compiler while\n ${reason.message}.\n${throwable}\n $trace" + /**A holder for synchronization points and reports when writing TASTy asynchronously. * The callbacks should only be called once. */ @@ -74,26 +82,29 @@ object Pickler { if incCallback == null then Promise.successful(Signal.Done) // no need to wait for API completion else Promise[Signal]() - private val backendFuture: StdFuture[Option[BufferingReporter]] = + private val backendFuture: StdFuture[Option[Exception]] = val asyncState = asyncTastyWritten.future .zipWith(asyncAPIComplete.future)((state, api) => state.filterNot(_ => api == Signal.Cancelled)) asyncState.map: optState => optState.flatMap: state => if incCallback != null && state.done && !state.hasErrors then - asyncZincPhasesCompleted(incCallback, state.pending).toBuffered - else state.pending + try + asyncZincPhasesCompleted(incCallback) + None + catch case e: Exception => Some(e) + else None - /** awaits the state of async TASTy operations indefinitely, returns optionally any buffered reports. */ - def sync(): Option[BufferingReporter] = - Await.result(backendFuture, Duration.Inf) + /** awaits the state of async TASTy operations indefinitely, returns the error message if any. */ + def sync()(using Context): Option[Message] = + Await.result(backendFuture, Duration.Inf).map(e => exceptionMessage(em"signaling API and Dependencies phases completion", e)) def signalAPIComplete(): Unit = if incCallback != null then asyncAPIComplete.trySuccess(Signal.Done) /** should only be called once */ - def signalAsyncTastyWritten()(using ctx: ReadOnlyContext): Unit = - val done = !ctx.run.suspendedAtTyperPhase + def signalAsyncTastyWritten()(using ctx: Context): Unit = + val done = !ctx.run.nn.suspendedAtTyperPhase if done then try // when we are done, i.e. no suspended units, @@ -104,14 +115,14 @@ object Pickler { case _ => catch case ex: Exception => - ctx.reporter.error(em"Error closing early output: $ex") + report.error(em"Error closing early output: $ex") asyncTastyWritten.trySuccess: Some( AsyncTastyHolder.State( hasErrors = ctx.reporter.hasErrors, done = done, - pending = ctx.reporter.toBuffered + pending = ctx.reporter ) ) end signalAsyncTastyWritten @@ -121,7 +132,7 @@ object Pickler { /** The state after writing async tasty. Any errors should have been reported, or pending. * if suspendedUnits is true, then we can't signal Zinc yet. */ - private class State(val hasErrors: Boolean, val done: Boolean, val pending: Option[BufferingReporter]) + private class State(val hasErrors: Boolean, val done: Boolean, val pending: Reporter) private enum Signal: case Done, Cancelled @@ -154,24 +165,24 @@ object Pickler { def writeSigFilesAsync( tasks: List[(String, Array[Byte])], writer: EarlyFileWriter, - async: AsyncTastyHolder)(using ctx: ReadOnlyContext): Unit = { + async: AsyncTastyHolder)(using ctx: Context): Unit = { try try for (internalName, pickled) <- tasks do if !async.cancelled then val _ = writer.writeTasty(internalName, pickled) catch - case ex: Exception => ctx.reporter.exception(em"writing TASTy to early output", ex) + case ex: Exception => report.error(exceptionMessage(em"writing TASTy to early output", ex), NoSourcePosition) finally writer.close() catch - case ex: Exception => ctx.reporter.exception(em"closing early output writer", ex) + case ex: Exception => report.error(exceptionMessage(em"closing early output writer", ex), NoSourcePosition) finally async.signalAsyncTastyWritten() } class EarlyFileWriter private (writer: TastyWriter): - def this(dest: AbstractFile)(using @constructorOnly ctx: ReadOnlyContext) = this(TastyWriter(dest)) + def this(dest: AbstractFile)(using @constructorOnly ctx: Context) = this(TastyWriter(dest)) export writer.{writeTasty, close} } @@ -383,7 +394,6 @@ class Pickler extends Phase { ctx.run.nn.asyncTasty.map: async => fastDoAsyncTasty = true () => - given ReadOnlyContext = if useExecutor then ReadOnlyContext.buffered else ReadOnlyContext.eager val writer = Pickler.EarlyFileWriter(async.earlyOut) writeSigFilesAsync(serialized.result(), writer, async) diff --git a/compiler/src/dotty/tools/io/FileWriters.scala b/compiler/src/dotty/tools/io/FileWriters.scala index 84a86aa233d3..3b8ed8fb686a 100644 --- a/compiler/src/dotty/tools/io/FileWriters.scala +++ b/compiler/src/dotty/tools/io/FileWriters.scala @@ -19,134 +19,14 @@ import java.util.zip.CRC32 import java.util.zip.Deflater import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream -import scala.collection.mutable import dotty.tools.dotc.core.Contexts.* import dotty.tools.dotc.core.Decorators.em -import dotty.tools.dotc.util.{NoSourcePosition, SourcePosition} -import dotty.tools.dotc.reporting.Message import dotty.tools.dotc.report -import scala.annotation.constructorOnly -import java.util.concurrent.atomic.AtomicReference -import java.util.concurrent.atomic.AtomicBoolean -import java.util.ConcurrentModificationException - object FileWriters { private def classRelativePath(className: String, suffix: String): String = className.replace('.', '/') + suffix - inline def ctx(using ReadOnlyContext): ReadOnlyContext = summon[ReadOnlyContext] - - sealed trait DelayedReporter { - def hasErrors: Boolean - def error(message: Context ?=> Message, position: SourcePosition): Unit - def warning(message: Context ?=> Message, position: SourcePosition): Unit - def log(message: String): Unit - - final def toBuffered: Option[BufferingReporter] = this match - case buffered: BufferingReporter => - if buffered.hasReports then Some(buffered) else None - case _: EagerReporter => None - - def error(message: Context ?=> Message): Unit = error(message, NoSourcePosition) - def warning(message: Context ?=> Message): Unit = warning(message, NoSourcePosition) - final def exception(reason: Context ?=> Message, throwable: Throwable): Unit = - error({ - val trace = throwable.getStackTrace().mkString("\n ") - em"An unhandled exception was thrown in the compiler while\n ${reason.message}.\n${throwable}\n $trace" - }, NoSourcePosition) - } - - final class EagerReporter(using captured: Context) extends DelayedReporter: - private var _hasErrors = false - - def hasErrors: Boolean = _hasErrors - - def error(message: Context ?=> Message, position: SourcePosition): Unit = - report.error(message, position) - _hasErrors = true - - def warning(message: Context ?=> Message, position: SourcePosition): Unit = - report.warning(message, position) - - def log(message: String): Unit = report.echo(message) - - enum Report: - case Error(message: Context => Message, position: SourcePosition) - case Warning(message: Context => Message, position: SourcePosition) - case Log(message: String) - - final class BufferingReporter extends DelayedReporter { - // We optimise access to the buffered reports for the common case - that there are no warning/errors to report - // We could use a listBuffer etc - but that would be extra allocation in the common case - // buffered logs are updated atomically. - - private val _bufferedReports = AtomicReference(List.empty[Report]) - private val _hasErrors = AtomicBoolean(false) - - - /** Atomically record that an error occurred */ - private def recordError(): Unit = - _hasErrors.set(true) - - /** Atomically add a report to the log */ - private def recordReport(report: Report): Unit = - _bufferedReports.getAndUpdate(report :: _) - - /** atomically extract and clear the buffered reports, must only be called at a synchronization point. */ - def resetReports(): List[Report] = - val curr = _bufferedReports.get() - if curr.nonEmpty && !_bufferedReports.compareAndSet(curr, Nil) then - throw ConcurrentModificationException("concurrent modification of buffered reports") - else curr - - def hasErrors: Boolean = _hasErrors.get() - def hasReports: Boolean = _bufferedReports.get().nonEmpty - - def error(message: Context ?=> Message, position: SourcePosition): Unit = - recordReport(Report.Error({case given Context => message}, position)) - recordError() - - def warning(message: Context ?=> Message, position: SourcePosition): Unit = - recordReport(Report.Warning({case given Context => message}, position)) - - def log(message: String): Unit = - recordReport(Report.Log(message)) - } - - trait ReadOnlySettings: - def jarCompressionLevel: Int - def debug: Boolean - - trait ReadOnlyRun: - def suspendedAtTyperPhase: Boolean - - trait ReadOnlyContext: - val run: ReadOnlyRun - val settings: ReadOnlySettings - val reporter: DelayedReporter - - trait BufferedReadOnlyContext extends ReadOnlyContext: - val reporter: BufferingReporter - - object ReadOnlyContext: - def readSettings(using ctx: Context): ReadOnlySettings = new: - val jarCompressionLevel = ctx.settings.XjarCompressionLevel.value - val debug = ctx.settings.Ydebug.value - - def readRun(using ctx: Context): ReadOnlyRun = new: - val suspendedAtTyperPhase = ctx.run.nn.suspendedAtTyperPhase - - def buffered(using Context): BufferedReadOnlyContext = new: - val settings = readSettings - val reporter = BufferingReporter() - val run = readRun - - def eager(using Context): ReadOnlyContext = new: - val settings = readSettings - val reporter = EagerReporter() - val run = readRun - /** * The interface to writing classfiles. GeneratedClassHandler calls these methods to generate the * directory and files that are created, and eventually calls `close` when the writing is complete. @@ -162,7 +42,7 @@ object FileWriters { * * @param name the internal name of the class, e.g. "scala.Option" */ - def writeTasty(name: String, bytes: Array[Byte])(using ReadOnlyContext): AbstractFile + def writeTasty(name: String, bytes: Array[Byte])(using Context): AbstractFile /** * Close the writer. Behavior is undefined after a call to `close`. @@ -172,14 +52,14 @@ object FileWriters { object TastyWriter { - def apply(output: AbstractFile)(using ReadOnlyContext): TastyWriter = + def apply(output: AbstractFile)(using Context): TastyWriter = // In Scala 2 depending on cardinality of distinct output dirs MultiClassWriter could have been used // In Dotty we always use single output directory new SingleTastyWriter(FileWriter(output, None)) private final class SingleTastyWriter(underlying: FileWriter) extends TastyWriter { - override def writeTasty(className: String, bytes: Array[Byte])(using ReadOnlyContext): AbstractFile = { + override def writeTasty(className: String, bytes: Array[Byte])(using Context): AbstractFile = { underlying.writeFile(classRelativePath(className, ".tasty"), bytes) } @@ -202,7 +82,7 @@ object FileWriters { /** * Write a classfile */ - def writeClass(name: String, bytes: Array[Byte])(using ReadOnlyContext): AbstractFile + def writeClass(name: String, bytes: Array[Byte])(using Context): AbstractFile /** * Close the writer. Behavior is undefined after a call to `close`. @@ -211,7 +91,7 @@ object FileWriters { } object ClassfileWriter { - def apply(output: AbstractFile, jarManifestMainClass: Option[String], dumpClassesPath: Option[AbstractFile])(using ReadOnlyContext): ClassfileWriter = { + def apply(output: AbstractFile, jarManifestMainClass: Option[String], dumpClassesPath: Option[AbstractFile])(using Context): ClassfileWriter = { // In Scala 2 depending on cardinality of distinct output dirs MultiClassWriter could have been used // In Dotty we always use single output directory val basicClassWriter = new SingleClassWriter(FileWriter(output, jarManifestMainClass)) @@ -221,11 +101,11 @@ object FileWriters { } private final class SingleClassWriter(underlying: FileWriter) extends ClassfileWriter { - override def writeClass(className: String, bytes: Array[Byte])(using ReadOnlyContext): AbstractFile = { + override def writeClass(className: String, bytes: Array[Byte])(using Context): AbstractFile = { underlying.writeFile(classRelativePath(className, ".class"), bytes) } - override def writeTasty(className: String, bytes: Array[Byte])(using ReadOnlyContext): AbstractFile = { + override def writeTasty(className: String, bytes: Array[Byte])(using Context): AbstractFile = { underlying.writeFile(classRelativePath(className, ".tasty"), bytes) } @@ -233,13 +113,13 @@ object FileWriters { } private final class DebugClassWriter(basic: ClassfileWriter, dump: FileWriter) extends ClassfileWriter { - override def writeClass(className: String, bytes: Array[Byte])(using ReadOnlyContext): AbstractFile = { + override def writeClass(className: String, bytes: Array[Byte])(using Context): AbstractFile = { val outFile = basic.writeClass(className, bytes) dump.writeFile(classRelativePath(className, ".class"), bytes) outFile } - override def writeTasty(className: String, bytes: Array[Byte])(using ReadOnlyContext): AbstractFile = { + override def writeTasty(className: String, bytes: Array[Byte])(using Context): AbstractFile = { basic.writeTasty(className, bytes) } @@ -252,14 +132,14 @@ object FileWriters { sealed trait FileWriter { - def writeFile(relativePath: String, bytes: Array[Byte])(using ReadOnlyContext): AbstractFile + def writeFile(relativePath: String, bytes: Array[Byte])(using Context): AbstractFile def close(): Unit } object FileWriter { - def apply(file: AbstractFile, jarManifestMainClass: Option[String])(using ReadOnlyContext): FileWriter = + def apply(file: AbstractFile, jarManifestMainClass: Option[String])(using Context): FileWriter = if (file.isInstanceOf[JarArchive]) { - val jarCompressionLevel = ctx.settings.jarCompressionLevel + val jarCompressionLevel = ctx.settings.XjarCompressionLevel.value // Writing to non-empty JAR might be an undefined behaviour, e.g. in case if other files where // created using `AbstractFile.bufferedOutputStream`instead of JarWriter val jarFile = file.underlyingSource.getOrElse{ @@ -278,9 +158,9 @@ object FileWriters { import java.util.jar.Attributes.Name.{MANIFEST_VERSION, MAIN_CLASS} import java.util.jar.{JarOutputStream, Manifest} - val storeOnly = compressionLevel == Deflater.NO_COMPRESSION + private val storeOnly = compressionLevel == Deflater.NO_COMPRESSION - val jarWriter: JarOutputStream = { + private val jarWriter: JarOutputStream = { import scala.util.Properties.* val manifest = new Manifest val attrs = manifest.getMainAttributes @@ -294,9 +174,9 @@ object FileWriters { jar } - lazy val crc = new CRC32 + private lazy val crc = new CRC32 - override def writeFile(relativePath: String, bytes: Array[Byte])(using ReadOnlyContext): AbstractFile = this.synchronized { + override def writeFile(relativePath: String, bytes: Array[Byte])(using Context): AbstractFile = this.synchronized { val entry = new ZipEntry(relativePath) if (storeOnly) { // When using compression method `STORED`, the ZIP spec requires the CRC and compressed/ @@ -326,18 +206,18 @@ object FileWriters { } private final class DirEntryWriter(base: Path) extends FileWriter { - val builtPaths = new ConcurrentHashMap[Path, java.lang.Boolean]() - val noAttributes = Array.empty[FileAttribute[?]] + private val builtPaths = new ConcurrentHashMap[Path, java.lang.Boolean]() + private val noAttributes = Array.empty[FileAttribute[?]] private val isWindows = scala.util.Properties.isWin - private def checkName(component: Path)(using ReadOnlyContext): Unit = if (isWindows) { + private def checkName(component: Path)(using Context): Unit = if (isWindows) { val specials = raw"(?i)CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9]".r val name = component.toString - def warnSpecial(): Unit = ctx.reporter.warning(em"path component is special Windows device: ${name}") + def warnSpecial(): Unit = report.warning(em"path component is special Windows device: $name") specials.findPrefixOf(name).foreach(prefix => if (prefix.length == name.length || name(prefix.length) == '.') warnSpecial()) } - def ensureDirForPath(baseDir: Path, filePath: Path)(using ReadOnlyContext): Unit = { + private def ensureDirForPath(baseDir: Path, filePath: Path)(using Context): Unit = { import java.lang.Boolean.TRUE val parent = filePath.getParent if (!builtPaths.containsKey(parent)) { @@ -346,7 +226,7 @@ object FileWriters { catch { case e: FileAlreadyExistsException => // `createDirectories` reports this exception if `parent` is an existing symlink to a directory - // but that's fine for us (and common enough, `scalac -d /tmp` on mac targets symlink). + // but that's fine for us (and common enough, `scalac -d /tmp` on Mac targets symlink). if (!Files.isDirectory(parent)) throw new FileConflictException(s"Can't create directory $parent; there is an existing (non-directory) file in its path", e) } @@ -356,18 +236,18 @@ object FileWriters { current = current.getParent } } - checkName(filePath.getFileName()) + checkName(filePath.getFileName) } // the common case is that we are creating a new file, and on MS Windows the create and truncate is expensive - // because there is not an options in the windows API that corresponds to this so the truncate is applied as a separate call + // because there is not an options in the Windows API that corresponds to this so the truncate is applied as a separate call // even if the file is new. // as this is rare, it's best to always try to create a new file, and it that fails, then open with truncate if that fails private val fastOpenOptions = util.EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE) private val fallbackOpenOptions = util.EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING) - override def writeFile(relativePath: String, bytes: Array[Byte])(using ReadOnlyContext): AbstractFile = { + override def writeFile(relativePath: String, bytes: Array[Byte])(using Context): AbstractFile = { val path = base.resolve(relativePath) try { ensureDirForPath(base, path) @@ -388,10 +268,10 @@ object FileWriters { os.close() } catch { case e: FileConflictException => - ctx.reporter.error(em"error writing ${path.toString}: ${e.getMessage}") + report.error(em"error writing ${path.toString}: ${e.getMessage}") case e: java.nio.file.FileSystemException => - if (ctx.settings.debug) e.printStackTrace() - ctx.reporter.error(em"error writing ${path.toString}: ${e.getClass.getName} ${e.getMessage}") + if (ctx.debug) e.printStackTrace() + report.error(em"error writing ${path.toString}: ${e.getClass.getName} ${e.getMessage}") } AbstractFile.getFile(path).nn // we just wrote to it so it better still exist } @@ -407,8 +287,8 @@ object FileWriters { val components = path.split('/') var dir = base for i <- 0 until components.length - 1 do - dir = ensureDirectory(dir).subdirectoryNamed(components(i).toString) - ensureDirectory(dir).fileNamed(components.last.toString) + dir = ensureDirectory(dir).subdirectoryNamed(components(i)) + ensureDirectory(dir).fileNamed(components.last) } private def writeBytes(outFile: AbstractFile, bytes: Array[Byte]): Unit = { @@ -417,7 +297,7 @@ object FileWriters { finally out.close() } - override def writeFile(relativePath: String, bytes: Array[Byte])(using ReadOnlyContext): AbstractFile = { + override def writeFile(relativePath: String, bytes: Array[Byte])(using Context): AbstractFile = { val outFile = getFile(base, relativePath) writeBytes(outFile, bytes) outFile diff --git a/tests/warn/optimizer-warnings.check b/tests/warn/optimizer-warnings.check new file mode 100644 index 000000000000..c5a93a5ebdd6 --- /dev/null +++ b/tests/warn/optimizer-warnings.check @@ -0,0 +1,30 @@ +-- Warning: tests/warn/optimizer-warnings.scala:3:7 -------------------------------------------------------------------- +3 |object Test: // warn + | ^ + | there was a problem determining if method can be inlined: + | The inline information for scala/runtime/ModuleSerializationProxy::(Ljava/lang/Class;)V may be incomplete: + | The Scala classfile scala/runtime/ModuleSerializationProxy does not have a ScalaInlineInfo attribute. +-- Warning: tests/warn/optimizer-warnings.scala:5:37 ------------------------------------------------------------------- +5 | val (a, b) = n match { case 0 => (1, 2); case _ => (3, 4) }; a + b // warn // warn // warn // warn + | ^^^^^^ + | there was a problem determining if method can be inlined: + | The inline information for scala/Tuple2$mcII$sp::(II)V may be incomplete: + | The Scala classfile scala/Tuple2$mcII$sp does not have a ScalaInlineInfo attribute. +-- Warning: tests/warn/optimizer-warnings.scala:5:55 ------------------------------------------------------------------- +5 | val (a, b) = n match { case 0 => (1, 2); case _ => (3, 4) }; a + b // warn // warn // warn // warn + | ^^^^^^ + | there was a problem determining if method can be inlined: + | The inline information for scala/Tuple2$mcII$sp::(II)V may be incomplete: + | The Scala classfile scala/Tuple2$mcII$sp does not have a ScalaInlineInfo attribute. +-- Warning: tests/warn/optimizer-warnings.scala:5:9 -------------------------------------------------------------------- +5 | val (a, b) = n match { case 0 => (1, 2); case _ => (3, 4) }; a + b // warn // warn // warn // warn + | ^ + | there was a problem determining if method _1$mcI$sp can be inlined: + | The inline information for scala/Tuple2$mcII$sp::_1$mcI$sp()I may be incomplete: + | The Scala classfile scala/Tuple2$mcII$sp does not have a ScalaInlineInfo attribute. +-- Warning: tests/warn/optimizer-warnings.scala:5:12 ------------------------------------------------------------------- +5 | val (a, b) = n match { case 0 => (1, 2); case _ => (3, 4) }; a + b // warn // warn // warn // warn + | ^ + | there was a problem determining if method _2$mcI$sp can be inlined: + | The inline information for scala/Tuple2$mcII$sp::_2$mcI$sp()I may be incomplete: + | The Scala classfile scala/Tuple2$mcII$sp does not have a ScalaInlineInfo attribute. diff --git a/tests/warn/optimizer-warnings.scala b/tests/warn/optimizer-warnings.scala new file mode 100644 index 000000000000..a51a71960887 --- /dev/null +++ b/tests/warn/optimizer-warnings.scala @@ -0,0 +1,5 @@ +//> using options -opt -opt-inline:** -Wopt:all + +object Test: // warn + def foo(n: Int): Int = + val (a, b) = n match { case 0 => (1, 2); case _ => (3, 4) }; a + b // warn // warn // warn // warn