diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala b/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala index 86b00e31ff80..c36c8c546635 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala @@ -51,10 +51,9 @@ trait BCodeHelpers extends BCodeIdiomatic { import int.{_, given} import DottyBackendInterface._ - protected def backendUtils = genBCodePhase match { - case genBCode: GenBCode => genBCode.postProcessor.backendUtils - case _ => null - } + // We need to access GenBCode phase to get access to post-processor components. + // At this point it should always be initialized already. + protected lazy val backendUtils = genBCodePhase.asInstanceOf[GenBCode].postProcessor.backendUtils def ScalaATTRName: String = "Scala" def ScalaSignatureATTRName: String = "ScalaSig" diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala index 1c8233d20c60..9c26de7988d7 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala @@ -37,7 +37,7 @@ trait BCodeSkelBuilder extends BCodeHelpers { import bTypes._ import coreBTypes._ import bCodeAsmCommon._ - + lazy val NativeAttr: Symbol = requiredClass[scala.native] /** The destination of a value generated by `genLoadTo`. */ diff --git a/compiler/src/dotty/tools/backend/jvm/BTypes.scala b/compiler/src/dotty/tools/backend/jvm/BTypes.scala index 4ed2cff88f0b..5539bf44aa17 100644 --- a/compiler/src/dotty/tools/backend/jvm/BTypes.scala +++ b/compiler/src/dotty/tools/backend/jvm/BTypes.scala @@ -860,7 +860,6 @@ abstract class BTypes { self => * Just a named pair, used in CoreBTypes.asmBoxTo/asmUnboxTo. */ /*final*/ case class MethodNameAndType(name: String, methodType: MethodBType) - } object BTypes { @@ -870,6 +869,4 @@ object BTypes { * But that would create overhead in a Collection[InternalName]. */ type InternalName = String - - -} \ No newline at end of file +} diff --git a/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala b/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala index 0127bbbb2c33..884dd19ee64f 100644 --- a/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala +++ b/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala @@ -36,7 +36,7 @@ class BTypesFromSymbols[I <: DottyBackendInterface](val int: I, val frontendAcce } import coreBTypes._ - @threadUnsafe protected lazy val classBTypeFromInternalNameMap = + @threadUnsafe protected lazy val classBTypeFromInternalNameMap = collection.concurrent.TrieMap.empty[String, ClassBType] /** diff --git a/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala b/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala index 0a1650129906..d54364b1675f 100644 --- a/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala +++ b/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala @@ -36,7 +36,7 @@ class BackendUtils(val postProcessor: PostProcessor) { case "19" => asm.Opcodes.V19 case "20" => asm.Opcodes.V20 } - + lazy val extraProc: Int = { import GenBCodeOps.addFlagIf val majorVersion: Int = (classfileVersion & 0xFF) @@ -68,27 +68,27 @@ class BackendUtils(val postProcessor: PostProcessor) { } /* - * 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. - */ + * 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._ import bTypes._ @@ -161,7 +161,7 @@ class BackendUtils(val postProcessor: PostProcessor) { * `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, + * This method serializes in the InnerClasses JVM attribute in an appropriate order, * not necessarily that given by `refedInnerClasses`. * * can-multi-thread diff --git a/compiler/src/dotty/tools/backend/jvm/ClassfileWriter.scala b/compiler/src/dotty/tools/backend/jvm/ClassfileWriter.scala index e50f3ad4e5c9..08e84de92dca 100644 --- a/compiler/src/dotty/tools/backend/jvm/ClassfileWriter.scala +++ b/compiler/src/dotty/tools/backend/jvm/ClassfileWriter.scala @@ -7,6 +7,7 @@ import java.util.jar.Attributes.Name import scala.tools.asm.ClassReader import scala.tools.asm.tree.ClassNode import dotty.tools.io.* +import dotty.tools.dotc.core.Decorators.* import dotty.tools.dotc.util.NoSourcePosition import java.nio.charset.StandardCharsets import java.nio.channels.ClosedByInterruptException @@ -15,36 +16,39 @@ import scala.language.unsafeNulls class ClassfileWriter(frontendAccess: PostProcessorFrontendAccess) { import frontendAccess.{backendReporting, compilerSettings} - + // if non-null, classfiles are additionally written to this directory private val dumpOutputDir: AbstractFile = getDirectoryOrNull(compilerSettings.dumpClassesDirectory) // if non-null, classfiles are written to a jar instead of the output directory - private val jarWriter: JarWriter = - val f = compilerSettings.outputDirectory - if f.hasExtension("jar") then - // If no main class was specified, see if there's only one - // entry point among the classes going into the jar. - val mainClass = compilerSettings.mainClass match { - case c @ Some(m) => - backendReporting.log(s"Main-Class was specified: ${m}") - c - case None => frontendAccess.getEntryPoints match { - case Nil => - backendReporting.log("No Main-Class designated or discovered.") - None + private val jarWriter: JarWriter | Null = compilerSettings.outputDirectory match { + case jar: JarArchive => + val mainClass = compilerSettings.mainClass.orElse { + // If no main class was specified, see if there's only one + // entry point among the classes going into the jar. + frontendAccess.getEntryPoints match { case name :: Nil => - backendReporting.log(s"Unique entry point: setting Main-Class to $name") + backendReporting.log(i"Unique entry point: setting Main-Class to $name") Some(name) case names => - backendReporting.log(s"No Main-Class due to multiple entry points:\n ${names.mkString("\n ")}") + if names.isEmpty then backendReporting.warning(em"No Main-Class designated or discovered.") + else backendReporting.warning(em"No Main-Class due to multiple entry points:\n ${names.mkString("\n ")}") None } } + jar.underlyingSource.map{ source => + if jar.isEmpty then + val jarMainAttrs = mainClass.map(Name.MAIN_CLASS -> _).toList + new Jar(source.file).jarWriter(jarMainAttrs: _*) + else + // Writing to non-empty JAR might be an undefined behaviour, e.g. in case if other files where + // created using `AbstractFile.bufferedOutputStream`instead of JarWritter + backendReporting.warning(em"Tried to write to non-empty JAR: $source") + null + }.orNull - val jarMainAttrs = mainClass.map(c => Name.MAIN_CLASS -> c).toList - new Jar(f.file).jarWriter(jarMainAttrs: _*) - else null + case _ => null + } private def getDirectoryOrNull(dir: Option[String]): AbstractFile = dir.map(d => new PlainDirectory(Directory(d))).orNull @@ -88,20 +92,9 @@ class ClassfileWriter(frontendAccess: PostProcessorFrontendAccess) { } } - def write(className: InternalName, bytes: Array[Byte], sourceFile: AbstractFile): AbstractFile | Null = try { + def writeClass(className: InternalName, bytes: Array[Byte], sourceFile: AbstractFile): AbstractFile | Null = try { // val writeStart = Statistics.startTimer(BackendStats.bcodeWriteTimer) - val outFile = if (jarWriter == null) { - val outFolder = compilerSettings.outputDirectory - val outFile = getFile(outFolder, className, ".class") - writeBytes(outFile, bytes) - outFile - } else { - val path = className + ".class" - val out = jarWriter.newOutputStream(path) - try out.write(bytes, 0, bytes.length) - finally out.flush() - null - } + val outFile = writeToJarOrFile(className, bytes, ".class") // Statistics.stopTimer(BackendStats.bcodeWriteTimer, writeStart) if (dumpOutputDir != null) { @@ -111,22 +104,34 @@ class ClassfileWriter(frontendAccess: PostProcessorFrontendAccess) { outFile } catch { case e: FileConflictException => - backendReporting.error(s"error writing $className: ${e.getMessage}", NoSourcePosition) + backendReporting.error(em"error writing $className: ${e.getMessage}") null case e: java.nio.file.FileSystemException => if compilerSettings.debug then e.printStackTrace() - backendReporting.error(s"error writing $className: ${e.getClass.getName} ${e.getMessage}", NoSourcePosition) + backendReporting.error(em"error writing $className: ${e.getClass.getName} ${e.getMessage}") null } - def writeTasty(className: InternalName, bytes: Array[Byte]): Unit = - val outFolder = compilerSettings.outputDirectory - val outFile = getFile(outFolder, className, ".tasty") - try writeBytes(outFile, bytes) - catch case ex: ClosedByInterruptException => - try outFile.delete() // don't leave an empty or half-written tastyfile around after an interrupt - catch case _: Throwable => () - finally throw ex + def writeTasty(className: InternalName, bytes: Array[Byte]): Unit = + writeToJarOrFile(className, bytes, ".tasty") + + private def writeToJarOrFile(className: InternalName, bytes: Array[Byte], suffix: String): AbstractFile | Null = { + if jarWriter == null then + val outFolder = compilerSettings.outputDirectory + val outFile = getFile(outFolder, className, suffix) + try writeBytes(outFile, bytes) + catch case ex: ClosedByInterruptException => + try outFile.delete() // don't leave an empty or half-written files around after an interrupt + catch case _: Throwable => () + finally throw ex + outFile + else + val path = className + suffix + val out = jarWriter.newOutputStream(path) + try out.write(bytes, 0, bytes.length) + finally out.flush() + null + } def close(): Unit = { if (jarWriter != null) jarWriter.close() @@ -134,4 +139,4 @@ class ClassfileWriter(frontendAccess: PostProcessorFrontendAccess) { } /** Can't output a file due to the state of the file system. */ -class FileConflictException(msg: String, val file: AbstractFile) extends IOException(msg) \ No newline at end of file +class FileConflictException(msg: String, val file: AbstractFile) extends IOException(msg) diff --git a/compiler/src/dotty/tools/backend/jvm/CodeGen.scala b/compiler/src/dotty/tools/backend/jvm/CodeGen.scala index 71e751287a72..c9f9e4e23d90 100644 --- a/compiler/src/dotty/tools/backend/jvm/CodeGen.scala +++ b/compiler/src/dotty/tools/backend/jvm/CodeGen.scala @@ -40,51 +40,72 @@ class CodeGen(val int: DottyBackendInterface, val primitives: DottyPrimitives)( private lazy val mirrorCodeGen = Impl.JMirrorBuilder() - def gen(unit: CompilationUnit): Unit = { - val postProcessor = Phases.genBCodePhase match { - case genBCode: GenBCode => genBCode.postProcessor - case _ => null - } + def genUnit(unit: CompilationUnit): GeneratedDefs = { + val generatedClasses = mutable.ListBuffer.empty[GeneratedClass] + val generatedTasty = mutable.ListBuffer.empty[GeneratedTasty] def genClassDef(cd: TypeDef): Unit = try val sym = cd.symbol val sourceFile = unit.source.file - def registerGeneratedClass(classNode: ClassNode, isArtifact: Boolean) = if (classNode != null){ - postProcessor.generatedClasses += GeneratedClass(classNode, sourceFile, isArtifact, onFileCreated(classNode, sym, unit.source)) - } + def registerGeneratedClass(classNode: ClassNode, isArtifact: Boolean): Unit = + generatedClasses += GeneratedClass(classNode, sourceFile, isArtifact, onFileCreated(classNode, sym, unit.source)) val plainC = genClass(cd, unit) registerGeneratedClass(plainC, isArtifact = false) - val mirrorC = - if !sym.isTopLevelModuleClass then null - else if sym.companionClass == NoSymbol then genMirrorClass(sym, unit) + val attrNode = + if !sym.isTopLevelModuleClass then plainC + else if sym.companionClass == NoSymbol then + val mirrorC = genMirrorClass(sym, unit) + registerGeneratedClass(mirrorC, isArtifact = true) + mirrorC else report.log(s"No mirror class for module with linked class: ${sym.fullName}", NoSourcePosition) - null - registerGeneratedClass(mirrorC, isArtifact = true) + plainC if sym.isClass then - val attrNode = if (mirrorC ne null) mirrorC else plainC - setTastyAttributes(sym, attrNode, postProcessor, unit) + genTastyAndSetAttributes(sym, attrNode) catch case ex: Throwable => ex.printStackTrace() report.error(s"Error while emitting ${unit.source}\n${ex.getMessage}", NoSourcePosition) + + def genTastyAndSetAttributes(claszSymbol: Symbol, store: ClassNode): Unit = + import Impl.createJAttribute + for (binary <- unit.pickled.get(claszSymbol.asClass)) { + generatedTasty += GeneratedTasty(store, binary) + val tasty = + val uuid = new TastyHeaderUnpickler(binary()).readHeader() + val lo = uuid.getMostSignificantBits + val hi = uuid.getLeastSignificantBits + + // TASTY attribute is created but only the UUID bytes are stored in it. + // A TASTY attribute has length 16 if and only if the .tasty file exists. + val buffer = new TastyBuffer(16) + buffer.writeUncompressedLong(lo) + buffer.writeUncompressedLong(hi) + buffer.bytes + + val dataAttr = createJAttribute(nme.TASTYATTR.mangledString, tasty, 0, tasty.length) + store.visitAttribute(dataAttr) + } + def genClassDefs(tree: Tree): Unit = tree match { case EmptyTree => () case PackageDef(_, stats) => stats foreach genClassDefs - case ValDef(_,_,_) => () // module val not emmited + case ValDef(_, _, _) => () // module val not emitted case td: TypeDef => genClassDef(td) } genClassDefs(unit.tpdTree) + GeneratedDefs(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: interfaces.SourceFile): AbstractFile => Unit = clsFile => { val (fullClassName, isLocal) = atPhase(sbtExtractDependenciesPhase) { (ExtractDependencies.classNameAsString(claszSymbol), claszSymbol.isLocal) @@ -112,28 +133,8 @@ class CodeGen(val int: DottyBackendInterface, val primitives: DottyPrimitives)( override def jfile = Optional.ofNullable(absfile.file) } - private def setTastyAttributes(claszSymbol: Symbol, store: ClassNode, postProcessor: PostProcessor, unit: CompilationUnit): Unit = - import Impl.createJAttribute - for (binary <- unit.pickled.get(claszSymbol.asClass)) { - postProcessor.generatedTasty += GeneratedTasty(store, binary) - val tasty = - val uuid = new TastyHeaderUnpickler(binary()).readHeader() - val lo = uuid.getMostSignificantBits - val hi = uuid.getLeastSignificantBits - - // TASTY attribute is created but only the UUID bytes are stored in it. - // A TASTY attribute has length 16 if and only if the .tasty file exists. - val buffer = new TastyBuffer(16) - buffer.writeUncompressedLong(lo) - buffer.writeUncompressedLong(hi) - buffer.bytes - - val dataAttr = createJAttribute(nme.TASTYATTR.mangledString, tasty, 0, tasty.length) - store.visitAttribute(dataAttr) - } - private def genClass(cd: TypeDef, unit: CompilationUnit): ClassNode = { - val b = new Impl.SyncAndTryBuilder(unit) {} + val b = new Impl.PlainClassBuilder(unit) b.genPlainClass(cd) val cls = b.cnode checkForCaseConflict(cls.name, cd.symbol) @@ -174,5 +175,7 @@ class CodeGen(val int: DottyBackendInterface, val primitives: DottyPrimitives)( val bTypes: self.bTypes.type = self.bTypes protected val primitives: DottyPrimitives = self.primitives } - object Impl extends ImplEarlyInit with BCodeSyncAndTry -} \ No newline at end of file + object Impl extends ImplEarlyInit with BCodeSyncAndTry { + class PlainClassBuilder(unit: CompilationUnit) extends SyncAndTryBuilder(unit) + } +} diff --git a/compiler/src/dotty/tools/backend/jvm/CoreBTypes.scala b/compiler/src/dotty/tools/backend/jvm/CoreBTypes.scala index 18fced615c73..30ad6b29b9f0 100644 --- a/compiler/src/dotty/tools/backend/jvm/CoreBTypes.scala +++ b/compiler/src/dotty/tools/backend/jvm/CoreBTypes.scala @@ -65,18 +65,16 @@ abstract class CoreBTypesFromSymbols[I <: DottyBackendInterface] extends CoreBTy * the first use of `classBTypeFromSymbol` because that method looks at the map. */ lazy val primitiveTypeMap: Map[Symbol, PrimitiveBType] = Map( - defn.UnitClass -> UNIT, - defn.BooleanClass -> BOOL, - defn.CharClass -> CHAR, - defn.ByteClass -> BYTE, - defn.ShortClass -> SHORT, - defn.IntClass -> INT, - defn.LongClass -> LONG, - defn.FloatClass -> FLOAT, - defn.DoubleClass -> DOUBLE - ) - - + defn.UnitClass -> UNIT, + defn.BooleanClass -> BOOL, + defn.CharClass -> CHAR, + defn.ByteClass -> BYTE, + defn.ShortClass -> SHORT, + defn.IntClass -> INT, + defn.LongClass -> LONG, + defn.FloatClass -> FLOAT, + defn.DoubleClass -> DOUBLE + ) /** * Map from primitive types to their boxed class type. Useful when pushing class literals onto the @@ -126,33 +124,33 @@ abstract class CoreBTypesFromSymbols[I <: DottyBackendInterface] extends CoreBTy * names of NothingClass and NullClass can't be emitted as-is. * TODO @lry Once there's a 2.11.3 starr, use the commented argument list. The current starr crashes on the type literal `scala.runtime.Nothing$` */ - lazy val srNothingRef : ClassBType = classBTypeFromSymbol(requiredClass("scala.runtime.Nothing$")) - lazy val srNullRef : ClassBType = classBTypeFromSymbol(requiredClass("scala.runtime.Null$")) - - lazy val ObjectRef : ClassBType = classBTypeFromSymbol(defn.ObjectClass) - lazy val StringRef : ClassBType = classBTypeFromSymbol(defn.StringClass) - - lazy val jlStringBuilderRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.StringBuilder]) - lazy val jlStringBufferRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.StringBuffer]) - lazy val jlCharSequenceRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.CharSequence]) - lazy val jlClassRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.Class[_]]) - lazy val jlThrowableRef : ClassBType = classBTypeFromSymbol(defn.ThrowableClass) - lazy val jlCloneableRef : ClassBType = classBTypeFromSymbol(defn.JavaCloneableClass) - lazy val jiSerializableRef : ClassBType = classBTypeFromSymbol(requiredClass[java.io.Serializable]) - lazy val jlClassCastExceptionRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.ClassCastException]) - lazy val jlIllegalArgExceptionRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.IllegalArgumentException]) - lazy val jliSerializedLambdaRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.SerializedLambda]) - - lazy val srBoxesRuntimeRef: ClassBType = classBTypeFromSymbol(requiredClass[scala.runtime.BoxesRunTime]) - - private lazy val jliCallSiteRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.CallSite]) - private lazy val jliLambdaMetafactoryRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.LambdaMetafactory]) - private lazy val jliMethodHandleRef : ClassBType = classBTypeFromSymbol(defn.MethodHandleClass) - private lazy val jliMethodHandlesLookupRef : ClassBType = classBTypeFromSymbol(defn.MethodHandlesLookupClass) - private lazy val jliMethodTypeRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.MethodType]) - private lazy val jliStringConcatFactoryRef : ClassBType = classBTypeFromSymbol(requiredClass("java.lang.invoke.StringConcatFactory")) // since JDK 9 - - lazy val srLambdaDeserialize : ClassBType = classBTypeFromSymbol(requiredClass[scala.runtime.LambdaDeserialize]) + lazy val srNothingRef : ClassBType = classBTypeFromSymbol(requiredClass("scala.runtime.Nothing$")) + lazy val srNullRef : ClassBType = classBTypeFromSymbol(requiredClass("scala.runtime.Null$")) + + lazy val ObjectRef : ClassBType = classBTypeFromSymbol(defn.ObjectClass) + lazy val StringRef : ClassBType = classBTypeFromSymbol(defn.StringClass) + + lazy val jlStringBuilderRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.StringBuilder]) + lazy val jlStringBufferRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.StringBuffer]) + lazy val jlCharSequenceRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.CharSequence]) + lazy val jlClassRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.Class[_]]) + lazy val jlThrowableRef : ClassBType = classBTypeFromSymbol(defn.ThrowableClass) + lazy val jlCloneableRef : ClassBType = classBTypeFromSymbol(defn.JavaCloneableClass) + lazy val jiSerializableRef : ClassBType = classBTypeFromSymbol(requiredClass[java.io.Serializable]) + lazy val jlClassCastExceptionRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.ClassCastException]) + lazy val jlIllegalArgExceptionRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.IllegalArgumentException]) + lazy val jliSerializedLambdaRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.SerializedLambda]) + + lazy val srBoxesRuntimeRef: ClassBType = classBTypeFromSymbol(requiredClass[scala.runtime.BoxesRunTime]) + + private lazy val jliCallSiteRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.CallSite]) + private lazy val jliLambdaMetafactoryRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.LambdaMetafactory]) + private lazy val jliMethodHandleRef : ClassBType = classBTypeFromSymbol(defn.MethodHandleClass) + private lazy val jliMethodHandlesLookupRef : ClassBType = classBTypeFromSymbol(defn.MethodHandlesLookupClass) + private lazy val jliMethodTypeRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.MethodType]) + private lazy val jliStringConcatFactoryRef : ClassBType = classBTypeFromSymbol(requiredClass("java.lang.invoke.StringConcatFactory")) // since JDK 9 + + lazy val srLambdaDeserialize : ClassBType = classBTypeFromSymbol(requiredClass[scala.runtime.LambdaDeserialize]) lazy val jliLambdaMetaFactoryMetafactoryHandle = new Handle( Opcodes.H_INVOKESTATIC, @@ -193,7 +191,7 @@ abstract class CoreBTypesFromSymbols[I <: DottyBackendInterface] extends CoreBTy jliCallSiteRef ).descriptor, /* itf = */ false) - + /** * Methods in scala.runtime.BoxesRuntime */ @@ -222,15 +220,15 @@ abstract class CoreBTypesFromSymbols[I <: DottyBackendInterface] extends CoreBTy 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)) : _* + (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)) : _* ) } } diff --git a/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala b/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala index 652e06871292..de74f14f2849 100644 --- a/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala +++ b/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala @@ -25,7 +25,7 @@ import StdNames.nme import NameKinds.LazyBitMapName import Names.Name -class DottyBackendInterface(val superCallsMap: ReadOnlyMap[Symbol, Set[ClassSymbol]])(using var ctx: Context) { +class DottyBackendInterface(val superCallsMap: ReadOnlyMap[Symbol, Set[ClassSymbol]])(using val ctx: Context) { private val desugared = new java.util.IdentityHashMap[Type, tpd.Select] diff --git a/compiler/src/dotty/tools/backend/jvm/GenBCode.scala b/compiler/src/dotty/tools/backend/jvm/GenBCode.scala index 77eb9a4f8e25..469a6ea57679 100644 --- a/compiler/src/dotty/tools/backend/jvm/GenBCode.scala +++ b/compiler/src/dotty/tools/backend/jvm/GenBCode.scala @@ -22,14 +22,17 @@ class GenBCode extends Phase { self => superCallsMap.update(sym, old + calls) } - private[jvm] val entryPoints = new mutable.HashSet[String]() - def registerEntryPoint(s: String): Unit = - entryPoints += s + private val entryPoints = new mutable.HashSet[String]() + def registerEntryPoint(s: String): Unit = entryPoints += s private var _backendInterface: DottyBackendInterface = _ - def backendInterface(using Context): DottyBackendInterface = { + def backendInterface(using ctx: Context): DottyBackendInterface = { if _backendInterface eq null then - _backendInterface = DottyBackendInterface(superCallsMap) + // Enforce usage of FreshContext so we would be able to modify compilation unit between runs + val backendCtx = ctx match + case fc: FreshContext => fc + case ctx => ctx.fresh + _backendInterface = DottyBackendInterface(superCallsMap)(using backendCtx) _backendInterface } @@ -49,26 +52,30 @@ class GenBCode extends Phase { self => _bTypes } - private var _frontendAccess: PostProcessorFrontendAccess = _ + private var _frontendAccess: PostProcessorFrontendAccess | Null = _ def frontendAccess(using Context): PostProcessorFrontendAccess = { if _frontendAccess eq null then - _frontendAccess = PostProcessorFrontendAccess.Impl(backendInterface) - _frontendAccess + _frontendAccess = PostProcessorFrontendAccess.Impl(backendInterface, entryPoints) + _frontendAccess.nn } - private var _postProcessor: PostProcessor = _ + private var _postProcessor: PostProcessor | Null = _ def postProcessor(using Context): PostProcessor = { if _postProcessor eq null then _postProcessor = new PostProcessor(frontendAccess, bTypes) - _postProcessor + _postProcessor.nn } - override def run(using Context): Unit = - backendInterface.ctx = summon[Context] + override def run(using ctx: Context): Unit = + // CompilationUnit is the only component that will differ between each run invocation + // We need to update it to have correct source positions. + // FreshContext is always enforced when creating backend interface + backendInterface.ctx + .asInstanceOf[FreshContext] + .setCompilationUnit(ctx.compilationUnit) val generated = codeGen.genUnit(ctx.compilationUnit) - // In Scala 2 backend depening on either global optimizations are enabled - // post processing might be delayed to build the call graph, or run imiedietlly if opts are disabled - // In Scala 3 we don't perform backend optimizations and though always post process without the delay + // In Scala 2, the backend might use global optimizations which might delay post-processing to build the call graph. + // In Scala 3, we don't perform backend optimizations and always perform post-processing immediately. // https://github.com/scala/scala/pull/6057 postProcessor.postProcessAndSendToDisk(generated) (ctx.compilerCallback: CompilerCallback | Null) match { @@ -77,21 +84,22 @@ class GenBCode extends Phase { self => } override def runOn(units: List[CompilationUnit])(using ctx:Context): List[CompilationUnit] = { - try - val result = super.runOn(units) - postProcessor.postProcessAndSendToDisk() - result + try super.runOn(units) finally - frontendAccess.compilerSettings.outputDirectory match { - case jar: JarArchive => - if (ctx.run.nn.suspendedUnits.nonEmpty) - // If we close the jar the next run will not be able to write on the jar. - // But if we do not close it we cannot use it as part of the macro classpath of the suspended files. - report.error("Can not suspend and output to a jar at the same time. See suspension with -Xprint-suspension.") + // frontendAccess and postProcessor are created lazilly, clean them up only if they were initialized + if _frontendAccess ne null then + frontendAccess.compilerSettings.outputDirectory match { + case jar: JarArchive => + if (ctx.run.nn.suspendedUnits.nonEmpty) + // If we close the jar the next run will not be able to write on the jar. + // But if we do not close it we cannot use it as part of the macro classpath of the suspended files. + report.error("Can not suspend and output to a jar at the same time. See suspension with -Xprint-suspension.") - jar.close() - case _ => - } + jar.close() + case _ => () + } + if _postProcessor ne null then + postProcessor.classfileWriter.close() } } diff --git a/compiler/src/dotty/tools/backend/jvm/GenericSignatureVisitor.scala b/compiler/src/dotty/tools/backend/jvm/GenericSignatureVisitor.scala index e9e532933290..c16bc70fc3b0 100644 --- a/compiler/src/dotty/tools/backend/jvm/GenericSignatureVisitor.scala +++ b/compiler/src/dotty/tools/backend/jvm/GenericSignatureVisitor.scala @@ -185,13 +185,13 @@ abstract class GenericSignatureVisitor(nestedOnly: Boolean) { } // Backported from scala/scala, commit sha: 724be0e9425b9ad07c244d25efdad695d75abbcf -// https://github.com/scala/scala/blob/724be0e9425b9ad07c244d25efdad695d75abbcf/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala#L790 +// https://github.com/scala/scala/blob/724be0e9425b9ad07c244d25efdad695d75abbcf/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala#L790 abstract class NestedClassesCollector[T](nestedOnly: Boolean) extends GenericSignatureVisitor(nestedOnly) { - type InternalName = String + type InternalName = String def declaredNestedClasses(internalName: InternalName): List[T] def getClassIfNested(internalName: InternalName): Option[T] - + val declaredInnerClasses = mutable.Set.empty[T] val referredInnerClasses = mutable.Set.empty[T] diff --git a/compiler/src/dotty/tools/backend/jvm/PostProcessor.scala b/compiler/src/dotty/tools/backend/jvm/PostProcessor.scala index 379a2ade1a16..606b5645aa24 100644 --- a/compiler/src/dotty/tools/backend/jvm/PostProcessor.scala +++ b/compiler/src/dotty/tools/backend/jvm/PostProcessor.scala @@ -4,6 +4,7 @@ import scala.collection.mutable.ListBuffer import dotty.tools.dotc.util.{SourcePosition, NoSourcePosition} import dotty.tools.io.AbstractFile import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.Decorators.em import scala.tools.asm.ClassWriter import scala.tools.asm.tree.ClassNode @@ -16,45 +17,39 @@ class PostProcessor(val frontendAccess: PostProcessorFrontendAccess, val bTypes: import bTypes.* import frontendAccess.{backendReporting, compilerSettings} import int.given - + val backendUtils = new BackendUtils(this) - lazy val classfileWriter = ClassfileWriter(frontendAccess) - lazy val generatedClasses = ListBuffer.empty[GeneratedClass] - lazy val generatedTasty = ListBuffer.empty[GeneratedTasty] + val classfileWriter = ClassfileWriter(frontendAccess) - def postProcessAndSendToDisk(): Unit = { - for (GeneratedClass(classNode, sourceFile, isArtifact, onFileCreated) <- generatedClasses) { - val bytes = - try + def postProcessAndSendToDisk(generatedDefs: GeneratedDefs): Unit = { + val GeneratedDefs(classes, tasty) = generatedDefs + for (GeneratedClass(classNode, sourceFile, isArtifact, onFileCreated) <- classes) { + val bytes = + try if !isArtifact then setSerializableLambdas(classNode) setInnerClasses(classNode) serializeClass(classNode) - catch + catch case e: java.lang.RuntimeException if e.getMessage != null && e.getMessage.nn.contains("too large!") => - backendReporting.error( - s"Could not write class ${classNode.name} because it exceeds JVM code size limits. ${e.getMessage}", - NoSourcePosition - ) + backendReporting.error(em"Could not write class ${classNode.name} because it exceeds JVM code size limits. ${e.getMessage}") null case ex: Throwable => ex.printStackTrace() - backendReporting.error(s"Error while emitting ${classNode.name}\n${ex.getMessage}",NoSourcePosition) + backendReporting.error(em"Error while emitting ${classNode.name}\n${ex.getMessage}") null if (bytes != null) { if (AsmUtils.traceSerializedClassEnabled && classNode.name.nn.contains(AsmUtils.traceSerializedClassPattern)) AsmUtils.traceClass(bytes) - val clsFile = classfileWriter.write(classNode.name.nn, bytes, sourceFile) + val clsFile = classfileWriter.writeClass(classNode.name.nn, bytes, sourceFile) if clsFile != null then onFileCreated(clsFile) } } - for (GeneratedTasty(classNode, binaryGen) <- generatedTasty){ + for (GeneratedTasty(classNode, binaryGen) <- tasty){ classfileWriter.writeTasty(classNode.name.nn, binaryGen()) } - - classfileWriter.close() } private def setSerializableLambdas(classNode: ClassNode): Unit = { @@ -113,7 +108,7 @@ class PostProcessor(val frontendAccess: PostProcessorFrontendAccess, val bTypes: */ case class GeneratedClass(classNode: ClassNode, sourceFile: AbstractFile, isArtifact: Boolean, onFileCreated: AbstractFile => Unit) case class GeneratedTasty(classNode: ClassNode, tastyGen: () => Array[Byte]) - +case class GeneratedDefs(classes: List[GeneratedClass], tasty: List[GeneratedTasty]) // Temporary class, will be refactored in a future commit trait ClassWriterForPostProcessor { diff --git a/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala b/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala index c194bd415975..80ee68bc94c3 100644 --- a/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala +++ b/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala @@ -1,7 +1,8 @@ package dotty.tools.backend.jvm -import scala.collection.mutable.Clearable +import scala.collection.mutable.{Clearable, HashSet} import dotty.tools.dotc.util.* +import dotty.tools.dotc.reporting.Message import dotty.tools.io.AbstractFile import java.util.{Collection => JCollection, Map => JMap} import dotty.tools.dotc.core.Contexts.Context @@ -20,7 +21,7 @@ sealed abstract class PostProcessorFrontendAccess { def getEntryPoints: List[String] private val frontendLock: AnyRef = new Object() - @inline final def frontendSynch[T](x: => T): T = frontendLock.synchronized(x) + inline final def frontendSynch[T](inline x: => T): T = frontendLock.synchronized(x) } object PostProcessorFrontendAccess { @@ -35,17 +36,18 @@ object PostProcessorFrontendAccess { } sealed trait BackendReporting { - def error(message: String, pos: SourcePosition): Unit - def log(message: String): Unit + def error(message: Context ?=> Message): Unit + def warning(message: Context ?=> Message): Unit + def log(message: Context ?=> String): Unit } - class Impl[I <: DottyBackendInterface](val int: I) extends PostProcessorFrontendAccess { + class Impl[I <: DottyBackendInterface](val int: I, entryPoints: HashSet[String]) extends PostProcessorFrontendAccess { import int.given lazy val compilerSettings: CompilerSettings = buildCompilerSettings() private def buildCompilerSettings(): CompilerSettings = new CompilerSettings { extension [T](s: dotty.tools.dotc.config.Settings.Setting[T]) - def valueSetByUser: Option[T] = + def valueSetByUser: Option[T] = Option(s.value).filter(_ != s.default) def s = ctx.settings @@ -67,13 +69,11 @@ object PostProcessorFrontendAccess { } object backendReporting extends BackendReporting { - def error(message: String, pos: SourcePosition): Unit = frontendSynch(report.error(message, pos)) - def log(message: String): Unit = frontendSynch(report.log(message)) + def error(message: Context ?=> Message): Unit = frontendSynch(report.error(message)) + def warning(message: Context ?=> Message): Unit = frontendSynch(report.warning(message)) + def log(message: Context ?=> String): Unit = frontendSynch(report.log(message)) } - def getEntryPoints: List[String] = frontendSynch(Phases.genBCodePhase match { - case genBCode: GenBCode => genBCode.entryPoints.toList - case _ => Nil - }) + def getEntryPoints: List[String] = frontendSynch(entryPoints.toList) } } \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 5ae99ec7e6fa..29431c089db0 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -15,7 +15,7 @@ import scala.util.chaining._ class ScalaSettings extends SettingGroup with AllScalaSettings object ScalaSettings: - // Keep synchronized with `classfileVersion` in `BCodeIdiomatic` + // Keep synchronized with `classfileVersion` in `BackendUtils` private val minTargetVersion = 8 private val maxTargetVersion = 20 diff --git a/docs/_docs/internals/backend.md b/docs/_docs/internals/backend.md index e3215c3993ae..660f6e1f41e5 100644 --- a/docs/_docs/internals/backend.md +++ b/docs/_docs/internals/backend.md @@ -6,8 +6,13 @@ title: "Backend Internals" The code for the JVM backend is split up by functionality and assembled in `GenBCode.scala`. This file defines class `GenBCode`, the compiler phase. +The workflow is split into `CodeGen.scala` Scala compilation context aware responsible for emitting bytecode, +and `PostProcessor.scala` which can be used for parallelized, context agnostic processing. In Scala 2 `PostProcessor`, +was responsible for performing bytecode optimization, e.g. inlining method calls. In Scala 3 it is only used for writing +Class files and Tasty to disk. + ``` -class GenBCodePipeline -[defines]--> PlainClassBuilder +class CodeGen.Impl -[defines]--> PlainClassBuilder | | [extends] [extends] | | @@ -18,14 +23,14 @@ BCodeBodyBuilder ----------------> PlainBodyBuilder BCodeSkelBuilder ----------------> PlainSkelBuilder | / | \ BCodeHelpers ----------------> BCClassGen BCAnnotGen ... (more components) - | | \ - | | \-------------> helper methods - | | \------------> JMirrorBuilder, JBeanInfoBuilder (uses some components, e.g. BCInnerClassGen) - | | - | BytecodeWriters ---------> methods and classes to write byte code files + | \ + | \-------------> helper methods + | \------------> JMirrorBuilder, JAndroidBuilder (uses some components, e.g. BCInnerClassGen) + | \-----------> `backendUtils`: utility for bytecode related ops, contains mapping for supported classfile version | BCodeIdiomatic ----------------> utilities for code generation, e.g. genPrimitiveArithmetic \--------------> `bTypes`: maps and fields for common BTypes + \-------------> `int`: synchronized interface between PostProcessor and compiltion ctx ``` The `BTypes.scala` class contains the `BType` class and predefined BTypes @@ -34,28 +39,33 @@ The `BTypes.scala` class contains the `BType` class and predefined BTypes Compiler creates a `GenBCode` `Phase`, calls `runOn(compilationUnits)`, which calls `run(context)`. This: -* initializes `myPrimitives` defined in `DottyPrimitives` (maps primitive - members, like `int.+`, to bytecode instructions) -* creates a `GenBCodePipeline` and calls `run(tree)` - -`GenBCodePipeline` now: - -* initializes the `bTypes` field of `GenBCodePipeline` defined in `BCodeIdiomatic` - (BType maps, common BTypes like `StringRef`) -* creates `BytecodeWriter` and `JMirrorBuilder` instances (on each compiler run) -* `buildAndSendToDisk(units)`: uses work queues, see below. - - `GenBCodePipeline.feedPipeline1` adds ClassDefs to `q1` - - `Worker1.run` creates ASM `ClassNodes`, adds to `q2`. It creates one - `PlainClassBuilder` for each compilation unit. - - `Worker2.run` adds byte arrays (one for each class) to `q3` - - `GenBCodePipeline.drainQ3` writes byte arrays to disk +* initializes lazily components reused by all `compilationUnits` using same instance of Context: + - `bTypes`, used by `CodeGen` and `PostProcessro`, defined in `BCodeIdiomatic` (BType maps, common BTypes like `StringRef`) + - `backendInterface:` - proxy to Context specific operations + - `codeGen: CodeGen` - uses `backendInterface`, `bTypes`, initializes instance of `DottyPrimitives` and defines `JMirrorBuilder` instance and implements bytecode generation flow (maps primitive members, like `int.+`, to bytecode instructions) + - `fontendAccess` - synchronized `PostProcessor` interface to compiler settings, reporting and GenBCode context (e.g. list of entrypoints) + - `postProcessor` - compilation context agnostic module dedicated to parallel processing of produced bytecode. Currently used only for writing Tasty and Class files. Defines `backendUtils` and `classfileWriter` +* sets context of current compilation unit to the shared context instance +* calls `codeGen.genUnit(ctx.compilation)` which returns structure with generated definitions (both Class files and Tasty) +* calls postProcessing of generated definition in `postProcessor` +* calls registered callbacks if needed for every generated class + +Upon calling `codeGen.genUnit` it: +* creates `PlainClassBuilder` instance for each generated `TypeDef` and creates ASM `ClassNode` +* creates optional mirror class if needed +* generates Tasty file content and store its attributes in either mirror or plain class node + +`PostProcessor` is later: +* enriching `ClassNode` with collected serializable lambdas +* sets its inner classes +* serializes class and writes it to file, optionally it can execute register callbacks for each generated file +* writes generated Tasty to file ## Architecture ## The architecture of `GenBCode` is the same as in Scalac. It can be partitioned into weakly coupled components (called "subsystems" below): - ### (a) The queue subsystem ### Queues mediate between processors, queues don't know what each processor does. @@ -126,4 +136,4 @@ emitting: ### (f) Building an ASM ClassNode given an AST TypeDef ### -It's done by `PlainClassBuilder`(see `GenBCode.scala`). +It's done by `PlainClassBuilder`(see `CodeGen.scala`).