diff options
18 files changed, 805 insertions, 199 deletions
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala index 32a421c570..e40e928761 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala @@ -103,6 +103,8 @@ abstract class BCodeSkelBuilder extends BCodeHelpers { isCZRemote = isRemote(claszSymbol) thisName = internalName(claszSymbol) + val classBType = classBTypeFromSymbol(claszSymbol) + cnode = new asm.tree.ClassNode() initJClass(cnode) @@ -120,10 +122,12 @@ abstract class BCodeSkelBuilder extends BCodeHelpers { addClassFields() - innerClassBufferASM ++= classBTypeFromSymbol(claszSymbol).info.nestedClasses + innerClassBufferASM ++= classBType.info.nestedClasses gen(cd.impl) addInnerClassesASM(cnode, innerClassBufferASM.toList) + cnode.visitAttribute(classBType.inlineInfoAttribute) + if (AsmUtils.traceClassEnabled && cnode.name.contains(AsmUtils.traceClassPattern)) AsmUtils.traceClass(cnode) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index 81d8adb7de..51a17b7fe4 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -10,8 +10,8 @@ import scala.annotation.switch import scala.tools.asm import asm.Opcodes import scala.tools.asm.tree.{InnerClassNode, ClassNode} -import scala.tools.nsc.backend.jvm.BTypes.{MethodInlineInfo, InlineInfo} -import scala.tools.nsc.backend.jvm.opt.{CallGraph, ByteCodeRepository, Inliner} +import scala.tools.nsc.backend.jvm.BTypes.{InlineInfo, MethodInlineInfo} +import scala.tools.nsc.backend.jvm.opt._ import opt.OptimizerReporting._ import scala.collection.convert.decorateAsScala._ @@ -47,6 +47,9 @@ abstract class BTypes { // When building the call graph, we need to know if global inlining is allowed (the component doesn't have a global) def inlineGlobalEnabled: Boolean + // When the inliner is not enabled, there's no point in adding InlineInfos to all ClassBTypes + def inlinerEnabled: Boolean + /** * A map from internal names to ClassBTypes. Every ClassBType is added to this map on its * construction. @@ -61,23 +64,6 @@ abstract class BTypes { val classBTypeFromInternalName: collection.concurrent.Map[InternalName, ClassBType] = recordPerRunCache(collection.concurrent.TrieMap.empty[InternalName, ClassBType]) /** - * Build the [[InlineInfo]] for the methods of a class, given its internal name. - * - * The InlineInfo is part of the ClassBType's [[ClassInfo]]. Note that there are two ways to build - * a ClassBType: from a class symbol (methods in [[BTypesFromSymbols]]) or from a [[ClassNode]]. - * The InlineInfo however contains information that can only be retrieved from the symbol of - * the class (e.g., is a method annotated @inline). - * - * This method (implemented in [[BTypesFromSymbols]]) looks up the class symbol in the symbol - * table, using the classfile name of the class. - * - * The method tries to undo some of the name mangling, but the lookup does not succeed for all - * classes. In case it fails, the resulting ClassBType will simply not have an InlineInfo, and - * we won't be able to inline its methods. - */ - def inlineInfosFromSymbolLookup(internalName: InternalName): Map[String, MethodInlineInfo] - - /** * Obtain the BType for a type descriptor or internal name. For class descriptors, the ClassBType * is constructed by parsing the corresponding classfile. * @@ -187,11 +173,53 @@ abstract class BTypes { NestedInfo(enclosingClass, Option(innerEntry.outerName), Option(innerEntry.innerName), staticFlag) } - classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfosFromSymbolLookup(classBType.internalName)) + val inlineInfo = inlineInfoFromClassfile(classNode) + + classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfo) classBType } /** + * Build the InlineInfo for a class. For Scala classes, the information is stored in the + * ScalaInlineInfo attribute. If the attribute is missing, the InlineInfo is built using the + * metadata available in the classfile (ACC_FINAL flags, etc). + */ + def inlineInfoFromClassfile(classNode: ClassNode): InlineInfo = { + def fromClassfileAttribute: Option[InlineInfo] = { + // TODO: if this is a scala class and there's no attribute, emit an inliner warning if the InlineInfo is used + if (classNode.attrs == null) None + else classNode.attrs.asScala.collect({ case a: InlineInfoAttribute => a}).headOption.map(_.inlineInfo) + } + + def fromClassfileWithoutAttribute = { + // when building MethodInlineInfos for the members of a ClassSymbol, we exclude those methods + // in scalaPrimitives. This is necessary because some of them have non-erased types, which would + // require special handling. Excluding is OK because they are never inlined. + // Here we are parsing from a classfile and we don't need to do anything special. Many of these + // primitives don't even exist, for example Any.isInstanceOf. + val methodInfos = classNode.methods.asScala.map(methodNode => { + val info = MethodInlineInfo( + effectivelyFinal = BytecodeUtils.isFinalMethod(methodNode), + traitMethodWithStaticImplementation = false, + annotatedInline = false, + annotatedNoInline = false) + (methodNode.name + methodNode.desc, info) + }).toMap + InlineInfo( + traitImplClassSelfType = None, + isEffectivelyFinal = BytecodeUtils.isFinalClass(classNode), + methodInfos = methodInfos, + warning = None) + } + + // The InlineInfo is built from the classfile (not from the symbol) for all classes that are NOT + // being compiled. For those classes, the info is only needed if the inliner is enabled, othewise + // we can save the memory. + if (!inlinerEnabled) BTypes.EmptyInlineInfo + else fromClassfileAttribute getOrElse fromClassfileWithoutAttribute + } + + /** * A BType is either a primitive type, a ClassBType, an ArrayBType of one of these, or a MethodType * referring to BTypes. */ @@ -831,6 +859,8 @@ abstract class BTypes { ) } + def inlineInfoAttribute: InlineInfoAttribute = InlineInfoAttribute(info.inlineInfo) + def isSubtypeOf(other: ClassBType): Boolean = { if (this == other) return true @@ -945,13 +975,11 @@ abstract class BTypes { * @param nestedClasses Classes nested in this class. Those need to be added to the * InnerClass table, see the InnerClass spec summary above. * @param nestedInfo If this describes a nested class, information for the InnerClass table. - * @param inlineInfos The [[InlineInfo]]s for the methods declared in this class. The map is - * indexed by the string s"$name$descriptor" (to disambiguate overloads). - * Entries may be missing, see comment on [[inlineInfosFromSymbolLookup]]. + * @param inlineInfo Information about this class for the inliner. */ final case class ClassInfo(superClass: Option[ClassBType], interfaces: List[ClassBType], flags: Int, nestedClasses: List[ClassBType], nestedInfo: Option[NestedInfo], - inlineInfos: Map[String, MethodInlineInfo]) + inlineInfo: InlineInfo) /** * Information required to add a class to an InnerClass table. diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala index 9fdb92b47c..a217e54ed8 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala @@ -7,10 +7,8 @@ package scala.tools.nsc package backend.jvm import scala.tools.asm -import scala.tools.asm.tree.ClassNode -import scala.tools.nsc.backend.jvm.opt.ByteCodeRepository.Source import scala.tools.nsc.backend.jvm.opt.{CallGraph, Inliner, ByteCodeRepository} -import scala.tools.nsc.backend.jvm.BTypes.{MethodInlineInfo, InlineInfo, InternalName} +import scala.tools.nsc.backend.jvm.BTypes.{InlineInfo, MethodInlineInfo, InternalName} /** * This class mainly contains the method classBTypeFromSymbol, which extracts the necessary @@ -42,48 +40,6 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { val callGraph: CallGraph[this.type] = new CallGraph(this) - /** - * See doc in [[BTypes.inlineInfosFromSymbolLookup]]. - * TODO: once the optimzier uses parallelism, lock before symbol table accesses - */ - def inlineInfosFromSymbolLookup(internalName: InternalName): Map[String, MethodInlineInfo] = { - val name = internalName.replace('/', '.') - - // TODO: de-mangle more class names - - def inEmptyPackage = name.indexOf('.') == -1 - def isModule = name.endsWith("$") - def isTopLevel = { - // TODO: this is conservative, there's also $'s introduced by name mangling, e.g., $colon$colon - // for this, use NameTransformer.decode - if (isModule) name.indexOf('$') == (name.length - 1) - else name.indexOf('$') == -1 - } - - val lookupName = { - if (isModule) newTermName(name.substring(0, name.length - 1)) - else newTypeName(name) - } - - // for now we only try classes that look like top-level - val classSym = if (!isTopLevel) NoSymbol else { - val member = { - if (inEmptyPackage) { - // rootMirror.getClassIfDefined fails for classes / modules in the empty package. - // maybe that should be fixed. - rootMirror.EmptyPackageClass.info.member(lookupName) - } else { - if (isModule) rootMirror.getModuleIfDefined(lookupName) - else rootMirror.getClassIfDefined(lookupName) - } - } - if (isModule) member.moduleClass else member - } - - if (classSym == NoSymbol) Map.empty - else buildInlineInfos(classSym) - } - final def initializeCoreBTypes(): Unit = { coreBTypes.setBTypes(new CoreBTypes[this.type](this)) } @@ -92,6 +48,8 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { def inlineGlobalEnabled: Boolean = settings.YoptInlineGlobal + def inlinerEnabled: Boolean = settings.YoptInlinerEnabled + // helpers that need access to global. // TODO @lry create a separate component, they don't belong to BTypesFromSymbols @@ -168,27 +126,6 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { } /** - * This is a hack to work around SI-9111. The completer of `methodSym` may report type errors. We - * cannot change the typer context of the completer at this point and make it silent: the context - * captured when creating the completer in the namer. However, we can temporarily replace - * global.reporter (it's a var) to store errors. - */ - def completeSilentlyAndCheckErroneous(sym: Symbol): Boolean = { - if (sym.rawInfo.isComplete) false - else { - val originalReporter = global.reporter - val storeReporter = new reporters.StoreReporter() - try { - global.reporter = storeReporter - sym.info - } finally { - global.reporter = originalReporter - } - storeReporter.infos.exists(_.severity == storeReporter.ERROR) - } - } - - /** * Builds a [[MethodBType]] for a method symbol. */ final def methodBTypeFromSymbol(methodSymbol: Symbol): MethodBType = { @@ -401,9 +338,9 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { val nestedInfo = buildNestedInfo(classSym) - val inlineInfos = buildInlineInfos(classSym) + val inlineInfo = buildInlineInfo(classSym, classBType.internalName) - classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfos) + classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfo) classBType } @@ -458,30 +395,39 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { } } - private def buildInlineInfos(classSym: Symbol): Map[String, MethodInlineInfo] = { - if (!settings.YoptInlinerEnabled) Map.empty + /** + * Build the InlineInfo for a ClassBType from the class symbol. + * + * Note that the InlineInfo is only built from the symbolic information for classes that are being + * compiled. For all other classes we delegate to inlineInfoFromClassfile. The reason is that + * mixed-in methods are only added to class symbols being compiled, but not to other classes + * extending traits. Creating the InlineInfo from the symbol would prevent these mixins from being + * inlined. + * + * So for classes being compiled, the InlineInfo is created here and stored in the ScalaInlineInfo + * classfile attribute. + */ + private def buildInlineInfo(classSym: Symbol, internalName: InternalName): InlineInfo = { + def buildFromSymbol = buildInlineInfoFromClassSymbol(classSym, classBTypeFromSymbol(_).internalName, methodBTypeFromSymbol(_).descriptor) + + // phase travel required, see implementation of `compiles`. for nested classes, it checks if the + // enclosingTopLevelClass is being compiled. after flatten, all classes are considered top-level, + // so `compiles` would return `false`. + if (exitingPickler(currentRun.compiles(classSym))) buildFromSymbol else { - // Primitve methods cannot be inlined, so there's no point in building an InlineInfo. Also, some - // primitive methods (e.g., `isInstanceOf`) have non-erased types, which confuses [[typeToBType]]. - classSym.info.decls.iterator.filter(m => m.isMethod && !scalaPrimitives.isPrimitive(m)).flatMap({ - case methodSym => - if (completeSilentlyAndCheckErroneous(methodSym)) { - // Happens due to SI-9111. Just don't provide any InlineInfo for that method, we don't - // need fail the compiler. - None - } else { - val methodBType = methodBTypeFromSymbol(methodSym) - val name = methodSym.javaSimpleName.toString // same as in genDefDef - val signature = name + methodBType.descriptor - val info = MethodInlineInfo( - effectivelyFinal = methodSym.isEffectivelyFinalOrNotOverridden, - traitMethodWithStaticImplementation = false, // temporary, fixed in future commit - annotatedInline = methodSym.hasAnnotation(ScalaInlineClass), - annotatedNoInline = methodSym.hasAnnotation(ScalaNoInlineClass) - ) - Some((signature, info)) - } - }).toMap + // For classes not being compiled, the InlineInfo is read from the classfile attribute. This + // fixes an issue with mixed-in methods: the mixin phase enters mixin methods only to class + // symbols being compiled. For non-compiled classes, we could not build MethodInlineInfos + // for those mixin members, which prevents inlining. + byteCodeRepository.classNode(internalName) match { + case Some(classNode) => + inlineInfoFromClassfile(classNode) + case None => + // TODO: inliner warning if the InlineInfo for that class is being used + // We can still use the inline information built from the symbol, even though mixin + // members will be missing. + buildFromSymbol + } } } @@ -503,7 +449,7 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { flags = asm.Opcodes.ACC_SUPER | asm.Opcodes.ACC_PUBLIC | asm.Opcodes.ACC_FINAL, nestedClasses = nested, nestedInfo = None, - Map.empty // no InlineInfo needed, scala never invokes methods on the mirror class + InlineInfo(None, true, Map.empty, None) // no InlineInfo needed, scala never invokes methods on the mirror class ) c }) @@ -538,8 +484,8 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { } // legacy, to be removed when the @remote annotation gets removed - final def isRemote(s: Symbol) = (s hasAnnotation definitions.RemoteAttr) - final def hasPublicBitSet(flags: Int) = ((flags & asm.Opcodes.ACC_PUBLIC) != 0) + final def isRemote(s: Symbol) = s hasAnnotation definitions.RemoteAttr + final def hasPublicBitSet(flags: Int) = (flags & asm.Opcodes.ACC_PUBLIC) != 0 /** * Return the Java modifiers for the given symbol. diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala index ea4dd0c032..fb58f1b189 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala @@ -10,6 +10,7 @@ package opt import scala.tools.asm import asm.tree._ import scala.collection.convert.decorateAsScala._ +import scala.tools.asm.Attribute import scala.tools.nsc.io.AbstractFile import scala.tools.nsc.util.ClassFileLookup import OptimizerReporting._ @@ -64,6 +65,7 @@ class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val class */ def methodNode(ownerInternalNameOrArrayDescriptor: String, name: String, descriptor: String): Option[(MethodNode, InternalName)] = { // In a MethodInsnNode, the `owner` field may be an array descriptor, for exmple when invoking `clone`. + // We don't inline array methods (they are native anyway), so just return None. if (ownerInternalNameOrArrayDescriptor.charAt(0) == '[') None else { classNode(ownerInternalNameOrArrayDescriptor).flatMap(c => @@ -80,9 +82,13 @@ class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val class classPath.findClassFile(fullName) map { classFile => val classNode = new asm.tree.ClassNode() val classReader = new asm.ClassReader(classFile.toByteArray) + + // Passing the InlineInfoAttributePrototype makes the ClassReader invoke the specific `read` + // method of the InlineInfoAttribute class, instead of putting the byte array into a generic + // Attribute. // We don't need frames when inlining, but we want to keep the local variable table, so we // don't use SKIP_DEBUG. - classReader.accept(classNode, asm.ClassReader.SKIP_FRAMES) + classReader.accept(classNode, Array[Attribute](InlineInfoAttributePrototype), asm.ClassReader.SKIP_FRAMES) // SKIP_FRAMES leaves line number nodes. Remove them because they are not correct after // inlining. // TODO: we need to remove them also for classes that are not parsed from classfiles, why not simplify and do it once when inlining? diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala index 74f46d04f9..e221eef636 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala @@ -83,6 +83,10 @@ object BytecodeUtils { def isSynchronizedMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_SYNCHRONIZED) != 0 + def isFinalClass(classNode: ClassNode): Boolean = (classNode.access & Opcodes.ACC_FINAL) != 0 + + def isFinalMethod(methodNode: MethodNode): Boolean = (methodNode.access & (Opcodes.ACC_FINAL | Opcodes.ACC_PRIVATE)) != 0 + def nextExecutableInstruction(instruction: AbstractInsnNode, alsoKeep: AbstractInsnNode => Boolean = Set()): Option[AbstractInsnNode] = { var result = instruction do { result = result.getNext } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala index ac40ab8904..020db738e8 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala @@ -9,7 +9,9 @@ package opt import scala.tools.asm.tree._ import scala.collection.convert.decorateAsScala._ +import scala.tools.nsc.backend.jvm.BTypes.InternalName import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.BasicAnalyzer +import ByteCodeRepository.{Source, CompilationUnit} class CallGraph[BT <: BTypes](val btypes: BT) { import btypes._ @@ -22,6 +24,43 @@ class CallGraph[BT <: BTypes](val btypes: BT) { } def analyzeCallsites(methodNode: MethodNode, definingClass: ClassBType): List[Callsite] = { + + /** + * Analyze a callsite and gather meta-data that can be used for inlining decisions. + * + * @return Three booleans indicating whether + * 1. the callsite can be safely inlined + * 2. the callee is annotated `@inline` + * 3. the callee is annotated `@noinline` + */ + def analyzeCallsite(calleeMethodNode: MethodNode, calleeDeclarationClassBType: ClassBType, receiverTypeInternalName: InternalName, calleeSource: Source): (Boolean, Boolean, Boolean) = { + val methodSignature = calleeMethodNode.name + calleeMethodNode.desc + + // The inlineInfo.methodInfos of a ClassBType holds an InlineInfo for each method *declared* + // within a class (not for inherited methods). Since we already have the classBType of the + // callee, we only check there for the methodInlineInfo, we should find it there. + calleeDeclarationClassBType.info.inlineInfo.methodInfos.find(_._1 == methodSignature) match { + case Some((_, methodInlineInfo)) => + val canInlineFromSource = inlineGlobalEnabled || calleeSource == CompilationUnit + // A non-final method can be inline if the receiver type is a final subclass. Example: + // class A { @inline def f = 1 }; object B extends A; B.f // can be inlined + def isStaticallyResolved: Boolean = { + // TODO: type analysis can render more calls statically resolved + // Example: `new A.f` can be inlined, the receiver type is known to be exactly A. + methodInlineInfo.effectivelyFinal || { + // TODO: inline warning when the receiver class cannot be found on the classpath + classBTypeFromParsedClassfile(receiverTypeInternalName).exists(_.info.inlineInfo.isEffectivelyFinal) + } + } + + (canInlineFromSource && isStaticallyResolved, methodInlineInfo.annotatedInline, methodInlineInfo.annotatedNoInline) + + case None => + // TODO: issue inliner warning + (false, false, false) + } + } + // TODO: run dataflow analyses to make the call graph more precise // - producers to get forwarded parameters (ForwardedParam) // - typeAnalysis for more precise argument types, more precise callee @@ -40,16 +79,7 @@ class CallGraph[BT <: BTypes](val btypes: BT) { byteCodeRepository.classNodeAndSource(declarationClass) map { case (declarationClassNode, source) => val declarationClassBType = classBTypeFromClassNode(declarationClassNode) - val methodSignature = method.name + method.desc - val (safeToInline, annotatedInline, annotatedNoInline) = declarationClassBType.info.inlineInfos.get(methodSignature) match { - case Some(inlineInfo) => - val canInlineFromSource = inlineGlobalEnabled || source == ByteCodeRepository.CompilationUnit - // TODO: for now, we consider a callee safeToInline only if it's final - // type analysis can render more calls safeToInline (e.g. when the precise receiver type is known) - (canInlineFromSource && inlineInfo.effectivelyFinal, Some(inlineInfo.annotatedInline), Some(inlineInfo.annotatedNoInline)) - case None => - (false, None, None) - } + val (safeToInline, annotatedInline, annotatedNoInline) = analyzeCallsite(method, declarationClassBType, call.owner, source) Callee( callee = method, calleeDeclarationClass = declarationClassBType, @@ -81,17 +111,21 @@ class CallGraph[BT <: BTypes](val btypes: BT) { /** * A callsite in the call graph. + * * @param callsiteInstruction The invocation instruction * @param callsiteMethod The method containing the callsite * @param callsiteClass The class containing the callsite - * @param callee The callee. For virtual calls, an override of the callee might be invoked. + * @param callee The callee, as it appears in the invocation instruction. For virtual + * calls, an override of the callee might be invoked. Also, the callee + * can be abstract. `None` if the callee MethodNode cannot be found in + * the bytecode repository. * @param argInfos Information about the invocation receiver and arguments * @param callsiteStackHeight The stack height at the callsite, required by the inliner */ final case class Callsite(callsiteInstruction: MethodInsnNode, callsiteMethod: MethodNode, callsiteClass: ClassBType, callee: Option[Callee], argInfos: List[ArgInfo], callsiteStackHeight: Int) { - override def toString = s"Invocation of ${callsiteInstruction.name + callsiteInstruction.desc}@${callsiteMethod.instructions.indexOf(callsiteInstruction)} in ${callsiteMethod.name}" + override def toString = s"Invocation of ${callee.map(_.calleeDeclarationClass.internalName).getOrElse("?")}.${callsiteInstruction.name + callsiteInstruction.desc}@${callsiteMethod.instructions.indexOf(callsiteInstruction)} in ${callsiteClass.internalName}.${callsiteMethod.name}" } /** @@ -104,14 +138,17 @@ class CallGraph[BT <: BTypes](val btypes: BT) { /** * A callee in the call graph. - * @param callee The called method. For virtual calls, an override may actually be invoked. + * + * @param callee The callee, as it appears in the invocation instruction. For + * virtual calls, an override of the callee might be invoked. Also, + * the callee can be abstract. * @param calleeDeclarationClass The class in which the callee is declared * @param safeToInline True if the callee can be safely inlined: it cannot be overridden, * and the inliner settings (project / global) allow inlining it. - * @param annotatedInline Defined if it is known whether the callee is annotated @inline - * @param annotatedNoInline Defined if it is known whether the callee is annotated @noinline + * @param annotatedInline True if the callee is annotated @inline + * @param annotatedNoInline True if the callee is annotated @noinline */ final case class Callee(callee: MethodNode, calleeDeclarationClass: ClassBType, safeToInline: Boolean, - annotatedInline: Option[Boolean], annotatedNoInline: Option[Boolean]) + annotatedInline: Boolean, annotatedNoInline: Boolean) } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala index 2ca8e8b8c4..970cc6803a 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -16,7 +16,6 @@ import scala.collection.convert.decorateAsJava._ import AsmUtils._ import BytecodeUtils._ import OptimizerReporting._ -import scala.tools.asm.tree.analysis._ import collection.mutable class Inliner[BT <: BTypes](val btypes: BT) { @@ -24,6 +23,8 @@ class Inliner[BT <: BTypes](val btypes: BT) { import callGraph._ def runInliner(): Unit = { + rewriteFinalTraitMethodInvocations() + for (request <- collectAndOrderInlineRequests) { val Some(callee) = request.callee inline(request.callsiteInstruction, request.callsiteStackHeight, request.callsiteMethod, request.callsiteClass, @@ -58,18 +59,74 @@ class Inliner[BT <: BTypes](val btypes: BT) { * requests is allowed to have cycles, and the callsites can appear in any order. */ def selectCallsitesForInlining: List[Callsite] = { - callsites.iterator.filter({ - case (_, callsite) => callsite.callee match { - case Some(Callee(callee, _, safeToInline, Some(annotatedInline), _)) => - // TODO: fix inlining from traits. - // For trait methods the callee is abstract: "trait T { @inline final def f = 1}". - // A callsite (t: T).f is `safeToInline` (effectivelyFinal is true), but the callee is the - // abstract method in the interface. - !isAbstractMethod(callee) && safeToInline && annotatedInline - case _ => false - } + callsites.valuesIterator.filter({ + case Callsite(_, _, _, Some(Callee(callee, _, safeToInline, annotatedInline, _)), _, _) => + // For trait methods the callee is abstract: "trait T { @inline final def f = 1}". + // A callsite (t: T).f is `safeToInline` (effectivelyFinal is true), but the callee is the + // abstract method in the interface. + // Even though we such invocations are re-written using `rewriteFinalTraitMethodInvocation`, + // the guard is kept here for the cases where the rewrite fails. + !isAbstractMethod(callee) && safeToInline && annotatedInline + case _ => false - }).map(_._2).toList + }).toList + } + + def rewriteFinalTraitMethodInvocations(): Unit = { + // Rewriting final trait method callsites to the implementation class enables inlining. + // We cannot just iterate over the values of the `callsites` map because the rewrite changes the + // map. Therefore we first copy the values to a list. + callsites.values.toList.foreach(rewriteFinalTraitMethodInvocation) + } + + /** + * Rewrite the INVOKEINTERFACE callsite of a final trait method invocation to INVOKESTATIC of the + * corresponding method in the implementation class. This enables inlining final trait methods. + * + * In a final trait method callsite, the callee is safeToInline and the callee method is abstract + * (the receiver type is the interface, so the method is abstract). + */ + def rewriteFinalTraitMethodInvocation(callsite: Callsite): Unit = callsite.callee match { + case Some(Callee(callee, calleeDeclarationClass, true, true, annotatedNoInline)) if isAbstractMethod(callee) => + assert(calleeDeclarationClass.isInterface, s"expected interface call (final trait method) when inlining abstract method: $callsite") + + val traitMethodArgumentTypes = asm.Type.getArgumentTypes(callee.desc) + + val selfParamTypeName = calleeDeclarationClass.info.inlineInfo.traitImplClassSelfType.getOrElse(calleeDeclarationClass.internalName) + val selfParamType = asm.Type.getObjectType(selfParamTypeName) + + val implClassMethodDescriptor = asm.Type.getMethodDescriptor(asm.Type.getReturnType(callee.desc), selfParamType +: traitMethodArgumentTypes: _*) + val implClassInternalName = calleeDeclarationClass.internalName + "$class" + + // The rewrite reading the implementation class and the implementation method from the bytecode + // repository. If either of the two fails, the rewrite is not performed. + for { + // TODO: inline warnings if impl class or method cannot be found + (implClassMethod, _) <- byteCodeRepository.methodNode(implClassInternalName, callee.name, implClassMethodDescriptor) + implClassBType <- classBTypeFromParsedClassfile(implClassInternalName) + } yield { + val newCallsiteInstruction = new MethodInsnNode(INVOKESTATIC, implClassInternalName, callee.name, implClassMethodDescriptor, false) + callsite.callsiteMethod.instructions.insert(callsite.callsiteInstruction, newCallsiteInstruction) + callsite.callsiteMethod.instructions.remove(callsite.callsiteInstruction) + + callGraph.callsites.remove(callsite.callsiteInstruction) + val staticCallsite = Callsite( + callsiteInstruction = newCallsiteInstruction, + callsiteMethod = callsite.callsiteMethod, + callsiteClass = callsite.callsiteClass, + callee = Some(Callee( + callee = implClassMethod, + calleeDeclarationClass = implClassBType, + safeToInline = true, + annotatedInline = true, + annotatedNoInline = annotatedNoInline)), + argInfos = Nil, + callsiteStackHeight = callsite.callsiteStackHeight + ) + callGraph.callsites(newCallsiteInstruction) = staticCallsite + } + + case _ => } /** diff --git a/src/compiler/scala/tools/nsc/transform/Delambdafy.scala b/src/compiler/scala/tools/nsc/transform/Delambdafy.scala index 1f832ba81e..94e88589f5 100644 --- a/src/compiler/scala/tools/nsc/transform/Delambdafy.scala +++ b/src/compiler/scala/tools/nsc/transform/Delambdafy.scala @@ -255,6 +255,8 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre val name = unit.freshTypeName(s"$oldClassPart$suffix".replace("$anon", "$nestedInAnon")) val lambdaClass = pkg newClassSymbol(name, originalFunction.pos, FINAL | SYNTHETIC) addAnnotation SerialVersionUIDAnnotation + // make sure currentRun.compiles(lambdaClass) is true (AddInterfaces does the same for trait impl classes) + currentRun.symSource(lambdaClass) = funOwner.sourceFile lambdaClass setInfo ClassInfoType(parents, newScope, lambdaClass) assert(!lambdaClass.isAnonymousClass && !lambdaClass.isAnonymousFunction, "anonymous class name: "+ lambdaClass.name) assert(lambdaClass.isDelambdafyFunction, "not lambda class name: " + lambdaClass.name) diff --git a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala index e94f33db3d..c64f6e7f10 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala @@ -14,6 +14,7 @@ import scala.tools.nsc.settings.{MutableSettings, ScalaSettings} import scala.tools.nsc.{Settings, Global} import scala.tools.partest.ASMConverters import scala.collection.JavaConverters._ +import scala.tools.testing.TempDir object CodeGenTools { import ASMConverters._ @@ -42,20 +43,27 @@ object CodeGenTools { } def newCompiler(defaultArgs: String = "-usejavacp", extraArgs: String = ""): Global = { + val compiler = newCompilerWithoutVirtualOutdir(defaultArgs, extraArgs) + resetOutput(compiler) + compiler + } + + def newCompilerWithoutVirtualOutdir(defaultArgs: String = "-usejavacp", extraArgs: String = ""): Global = { val settings = new Settings() val args = (CommandLineParser tokenize defaultArgs) ++ (CommandLineParser tokenize extraArgs) settings.processArguments(args, processAll = true) - val compiler = new Global(settings) - resetOutput(compiler) - compiler + new Global(settings) } - def compile(compiler: Global)(code: String): List[(String, Array[Byte])] = { + def newRun(compiler: Global): compiler.Run = { compiler.reporter.reset() resetOutput(compiler) - val run = new compiler.Run() - run.compileSources(List(new BatchSourceFile("unitTestSource.scala", code))) - val outDir = compiler.settings.outputDirs.getSingleOutput.get + new compiler.Run() + } + + def makeSourceFile(code: String, filename: String): BatchSourceFile = new BatchSourceFile(filename, code) + + def getGeneratedClassfiles(outDir: AbstractFile): List[(String, Array[Byte])] = { def files(dir: AbstractFile): List[(String, Array[Byte])] = { val res = ListBuffer.empty[(String, Array[Byte])] for (f <- dir.iterator) { @@ -67,8 +75,46 @@ object CodeGenTools { files(outDir) } - def compileClasses(compiler: Global)(code: String): List[ClassNode] = { - compile(compiler)(code).map(p => AsmUtils.readClass(p._2)).sortBy(_.name) + def compile(compiler: Global)(scalaCode: String, javaCode: List[(String, String)] = Nil): List[(String, Array[Byte])] = { + val run = newRun(compiler) + run.compileSources(makeSourceFile(scalaCode, "unitTestSource.scala") :: javaCode.map(p => makeSourceFile(p._1, p._2))) + getGeneratedClassfiles(compiler.settings.outputDirs.getSingleOutput.get) + } + + /** + * Compile multiple Scala files separately into a single output directory. + * + * Note that a new compiler instance is created for compiling each file because symbols survive + * across runs. This makes separate compilation slower. + * + * The output directory is a physical directory, I have not figured out if / how it's possible to + * add a VirtualDirectory to the classpath of a compiler. + */ + def compileSeparately(codes: List[String], extraArgs: String = ""): List[(String, Array[Byte])] = { + val outDir = AbstractFile.getDirectory(TempDir.createTempDir()) + val outDirPath = outDir.canonicalPath + val argsWithOutDir = extraArgs + s" -d $outDirPath -cp $outDirPath" + + for (code <- codes) { + val compiler = newCompilerWithoutVirtualOutdir(extraArgs = argsWithOutDir) + new compiler.Run().compileSources(List(makeSourceFile(code, "unitTestSource.scala"))) + } + + val classfiles = getGeneratedClassfiles(outDir) + outDir.delete() + classfiles + } + + def compileClassesSeparately(codes: List[String], extraArgs: String = "") = { + readAsmClasses(compileSeparately(codes, extraArgs)) + } + + def readAsmClasses(classfiles: List[(String, Array[Byte])]) = { + classfiles.map(p => AsmUtils.readClass(p._2)).sortBy(_.name) + } + + def compileClasses(compiler: Global)(code: String, javaCode: List[(String, String)] = Nil): List[ClassNode] = { + readAsmClasses(compile(compiler)(code, javaCode)) } def compileMethods(compiler: Global)(code: String): List[MethodNode] = { diff --git a/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala b/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala index 3b1b009037..94877fb037 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala @@ -78,4 +78,17 @@ class DirectCompileTest extends ClearAfterClass { Label(11) )) } + + @Test + def testSeparateCompilation(): Unit = { + val codeA = "class A { def f = 1 }" + val codeB = "class B extends A { def g = f }" + val List(a, b) = compileClassesSeparately(List(codeA, codeB)) + val ins = getSingleMethod(b, "g").instructions + assert(ins exists { + case Invoke(_, "B", "f", _, _) => true + case _ => false + }, ins) + + } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala index f7c9cab284..761f214f82 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala @@ -59,17 +59,12 @@ class BTypesFromClassfileTest { else (fromSym.flags | ACC_PRIVATE | ACC_PUBLIC) == (fromClassfile.flags | ACC_PRIVATE | ACC_PUBLIC) }, s"class flags differ\n$fromSym\n$fromClassfile") - // when parsing from classfile, the inline infos are obtained through the classSymbol, which - // is searched based on the classfile name. this lookup can fail. - assert(fromSym.inlineInfos.size == fromClassfile.inlineInfos.size || fromClassfile.inlineInfos.isEmpty, - s"wrong # of inline infos:\n${fromSym.inlineInfos.keys.toList.sorted}\n${fromClassfile.inlineInfos.keys.toList.sorted}") - fromClassfile.inlineInfos foreach { - case (signature, inlineInfo) => - assert(fromSym.inlineInfos(signature) == inlineInfo, s"inline infos differ for $signature:\n$inlineInfo\n${fromClassfile.inlineInfos(signature)}") - } + // we don't compare InlineInfos in this test: in both cases (from symbol and from classfile) they + // are actually created by looking at the classfile members, not the symbol's. InlineInfos are only + // built from symbols for classes that are being compiled, which is not the case here. Instead + // there's a separate InlineInfoTest. val chk1 = sameBTypes(fromSym.superClass, fromClassfile.superClass, checked) - val chk2 = sameBTypes(fromSym.interfaces, fromClassfile.interfaces, chk1) // The fromSym info has only member classes, no local or anonymous. The symbol is read from the diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala index 69bd92b4ba..16f09db189 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala @@ -89,8 +89,8 @@ class CallGraphTest { assert(callee.callee == target) assert(callee.calleeDeclarationClass == calleeDeclClass) assert(callee.safeToInline == safeToInline) - assert(callee.annotatedInline.get == atInline) - assert(callee.annotatedNoInline.get == atNoInline) + assert(callee.annotatedInline == atInline) + assert(callee.annotatedNoInline == atNoInline) assert(callsite.argInfos == List()) // not defined yet } catch { diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala index fc748196d0..76492cfa23 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala @@ -17,8 +17,8 @@ class CompactLocalVariablesTest { // recurse-unreachable-jumps is required for eliminating catch blocks, in the first dce round they // are still live.only after eliminating the empty handler the catch blocks become unreachable. - val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code,recurse-unreachable-jumps,compact-locals") - val noCompactVarsCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code,recurse-unreachable-jumps") + val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code,compact-locals") + val noCompactVarsCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code") @Test def compactUnused(): Unit = { diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala new file mode 100644 index 0000000000..4e12ed757e --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala @@ -0,0 +1,65 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.collection.generic.Clearable +import org.junit.Assert._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ +import scala.tools.testing.ClearAfterClass + +import scala.collection.convert.decorateAsScala._ + +object InlineInfoTest extends ClearAfterClass.Clearable { + var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:classpath") + def clear(): Unit = { compiler = null } + + def notPerRun: List[Clearable] = List(compiler.genBCode.bTypes.classBTypeFromInternalName, compiler.genBCode.bTypes.byteCodeRepository.classes) + notPerRun foreach compiler.perRunCaches.unrecordCache +} + +@RunWith(classOf[JUnit4]) +class InlineInfoTest { + val compiler = InlineInfoTest.compiler + + def compile(code: String) = { + InlineInfoTest.notPerRun.foreach(_.clear()) + compileClasses(compiler)(code) + } + + @Test + def inlineInfosFromSymbolAndAttribute(): Unit = { + val code = + """trait T { + | @inline def f: Int + | @noinline final def g = 0 + |} + |trait U { self: T => + | @inline def f = 0 + | final def h = 0 + | final class K { + | @inline def i = 0 + | } + |} + |sealed trait V { + | @inline def j = 0 + |} + |class C extends T with U + """.stripMargin + val classes = compile(code) + val fromSyms = classes.map(c => compiler.genBCode.bTypes.classBTypeFromInternalName(c.name).info.inlineInfo) + + val fromAttrs = classes.map(c => { + assert(c.attrs.asScala.exists(_.isInstanceOf[InlineInfoAttribute]), c.attrs) + compiler.genBCode.bTypes.inlineInfoFromClassfile(c) + }) + + assert(fromSyms == fromAttrs) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala new file mode 100644 index 0000000000..58a262c401 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala @@ -0,0 +1,114 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ + +import scala.collection.convert.decorateAsScala._ + +object InlinerSeparateCompilationTest { + val args = "-Ybackend:GenBCode -Yopt:l:classpath" +} + +@RunWith(classOf[JUnit4]) +class InlinerSeparateCompilationTest { + import InlinerSeparateCompilationTest._ + import InlinerTest.{listStringLines, assertInvoke, assertNoInvoke} + + @Test + def inlnieMixedinMember(): Unit = { + val codeA = + """trait T { + | @inline def f = 0 + |} + |object O extends T { + | @inline def g = 1 + |} + """.stripMargin + + val codeB = + """class C { + | def t1(t: T) = t.f + | def t2 = O.f + | def t3 = O.g + |} + """.stripMargin + + val List(c, o, oMod, t, tCls) = compileClassesSeparately(List(codeA, codeB), args) + assertInvoke(getSingleMethod(c, "t1"), "T", "f") + assertNoInvoke(getSingleMethod(c, "t2")) + assertNoInvoke(getSingleMethod(c, "t3")) + } + + @Test + def inlineSealedMember(): Unit = { + val codeA = + """sealed trait T { + | @inline def f = 1 + |} + """.stripMargin + + val codeB = + """class C { + | def t1(t: T) = t.f + |} + """.stripMargin + + val List(c, t, tCls) = compileClassesSeparately(List(codeA, codeB), args) + assertNoInvoke(getSingleMethod(c, "t1")) + } + + @Test + def inlineInheritedMember(): Unit = { + val codeA = + """trait T { + | @inline final def f = 1 + |} + |trait U extends T { + | @inline final def g = f + |} + """.stripMargin + + val codeB = + """class C extends U { + | def t1 = this.f + | def t2 = this.g + | def t3(t: T) = t.f + |} + """.stripMargin + + val List(c, t, tCls, u, uCls) = compileClassesSeparately(List(codeA, codeB), args) + for (m <- List("t1", "t2", "t3")) assertNoInvoke(getSingleMethod(c, m)) + } + + @Test + def inlineWithSelfType(): Unit = { + val assembly = + """trait Assembly extends T { + | @inline final def g = 1 + | @inline final def n = m + |} + """.stripMargin + + val codeA = + s"""trait T { self: Assembly => + | @inline final def f = g + | @inline final def m = 1 + |} + |$assembly + """.stripMargin + + val List(a, aCls, t, tCls) = compileClassesSeparately(List(codeA, assembly), args) + assertNoInvoke(getSingleMethod(tCls, "f")) + assertNoInvoke(getSingleMethod(aCls, "n")) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 4e7a2399a2..694dff8dee 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -33,18 +33,37 @@ object InlinerTest extends ClearAfterClass.Clearable { notPerRun foreach compiler.perRunCaches.unrecordCache def clear(): Unit = { compiler = null } + + implicit class listStringLines[T](val l: List[T]) extends AnyVal { + def stringLines = l.mkString("\n") + } + + def assertNoInvoke(m: Method): Unit = assertNoInvoke(m.instructions) + def assertNoInvoke(ins: List[Instruction]): Unit = { + assert(!ins.exists(_.isInstanceOf[Invoke]), ins.stringLines) + } + + def assertInvoke(m: Method, receiver: String, method: String): Unit = assertInvoke(m.instructions, receiver, method) + def assertInvoke(l: List[Instruction], receiver: String, method: String): Unit = { + assert(l.exists { + case Invoke(_, `receiver`, `method`, _, _) => true + case _ => false + }, l.stringLines) + } } @RunWith(classOf[JUnit4]) class InlinerTest extends ClearAfterClass { ClearAfterClass.stateToClear = InlinerTest + import InlinerTest.{listStringLines, assertInvoke, assertNoInvoke} + val compiler = InlinerTest.compiler import compiler.genBCode.bTypes._ - def compile(code: String): List[ClassNode] = { + def compile(scalaCode: String, javaCode: List[(String, String)] = Nil): List[ClassNode] = { InlinerTest.notPerRun.foreach(_.clear()) - compileClasses(compiler)(code) + compileClasses(compiler)(scalaCode, javaCode) } def checkCallsite(callsite: callGraph.Callsite, callee: MethodNode) = { @@ -229,8 +248,8 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin val List(cCls) = compile(code) - val instructions = instructionsFromMethod(cCls.methods.asScala.find(_.name == "test").get) - assert(instructions.contains(Op(ICONST_0)), instructions mkString "\n") + val instructions = getSingleMethod(cCls, "test").instructions + assert(instructions.contains(Op(ICONST_0)), instructions.stringLines) assert(!instructions.contains(Op(ICONST_1)), instructions) } @@ -282,16 +301,22 @@ class InlinerTest extends ClearAfterClass { def arraycopy(): Unit = { // also tests inlining of a void-returning method (no return value on the stack) val code = - """class C { + """// can't use the `compat.Platform.arraycopy` from the std lib for now, because the classfile doesn't have a ScalaInlineInfo attribute + |object Platform { + | @inline def arraycopy(src: AnyRef, srcPos: Int, dest: AnyRef, destPos: Int, length: Int) { + | System.arraycopy(src, srcPos, dest, destPos, length) + | } + |} + |class C { | def f(src: AnyRef, srcPos: Int, dest: AnyRef, destPos: Int, length: Int): Unit = { - | compat.Platform.arraycopy(src, srcPos, dest, destPos, length) + | Platform.arraycopy(src, srcPos, dest, destPos, length) | } |} """.stripMargin - val List(c) = compile(code) - val ins = instructionsFromMethod(c.methods.asScala.find(_.name == "f").get) + val List(c, _, _) = compile(code) + val ins = getSingleMethod(c, "f").instructions val invokeSysArraycopy = Invoke(INVOKESTATIC, "java/lang/System", "arraycopy", "(Ljava/lang/Object;ILjava/lang/Object;II)V", false) - assert(ins contains invokeSysArraycopy, ins mkString "\n") + assert(ins contains invokeSysArraycopy, ins.stringLines) } @Test @@ -311,7 +336,7 @@ class InlinerTest extends ClearAfterClass { } @Test - def atInlineInTraitDoesNotCrash(): Unit = { + def atInlineInTrait(): Unit = { val code = """trait T { | @inline final def f = 0 @@ -321,10 +346,7 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin val List(c, t, tClass) = compile(code) - val ins = instructionsFromMethod(c.methods.asScala.find(_.name == "g").get) - val invokeF = Invoke(INVOKEINTERFACE, "T", "f", "()I", true) - // no inlining yet - assert(ins contains invokeF, ins mkString "\n") + assertNoInvoke(getSingleMethod(c, "g")) } @Test @@ -336,10 +358,8 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin val List(c) = compile(code) - val ins = instructionsFromMethod(c.methods.asScala.find(_.name == "g").get) - println(ins) // no more invoke, f is inlined - assert(ins.count(_.isInstanceOf[Invoke]) == 0, ins mkString "\n") + assertNoInvoke(getSingleMethod(c, "g")) } @Test @@ -373,11 +393,11 @@ class InlinerTest extends ClearAfterClass { val ins = instructionsFromMethod(f) // no invocations, lowestOneBit is inlined - assert(ins.count(_.isInstanceOf[Invoke]) == 0, ins mkString "\n") + assertNoInvoke(ins) // no null check when inlining a static method ins foreach { - case Jump(IFNONNULL, _) => assert(false, ins mkString "\n") + case Jump(IFNONNULL, _) => assert(false, ins.stringLines) case _ => } } @@ -437,16 +457,267 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin - InlinerTest.notPerRun.foreach(_.clear()) - compiler.reporter.reset() - compiler.settings.outputDirs.setSingleOutput(new VirtualDirectory("(memory)", None)) - val run = new compiler.Run() - run.compileSources(List(new BatchSourceFile("A.java", javaCode), new BatchSourceFile("B.scala", scalaCode))) - val outDir = compiler.settings.outputDirs.getSingleOutput.get - val List(b) = outDir.iterator.map(f => AsmUtils.readClass(f.toByteArray)).toList.sortBy(_.name) + val List(b) = compile(scalaCode, List((javaCode, "A.java"))) val ins = getSingleMethod(b, "g").instructions val invokeFlop = Invoke(INVOKEVIRTUAL, "B", "flop", "()I", false) - assert(ins contains invokeFlop, ins mkString "\n") + assert(ins contains invokeFlop, ins.stringLines) + } + + @Test + def inlineFromTraits(): Unit = { + val code = + """trait T { + | @inline final def f = g + | @inline final def g = 1 + |} + | + |class C extends T { + | def t1(t: T) = t.f + | def t2(c: C) = c.f + |} + """.stripMargin + val List(c, t, tClass) = compile(code) + // both are just `return 1`, no more calls + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + } + + @Test + def inlineMixinMethods(): Unit = { + val code = + """trait T { + | @inline final def f = 1 + |} + |class C extends T + """.stripMargin + val List(c, t, tClass) = compile(code) + // the static implementaiton method is inlined into the mixin, so there's no invocation in the mixin + assertNoInvoke(getSingleMethod(c, "f")) + } + + @Test + def inlineTraitInherited(): Unit = { + val code = + """trait T { + | @inline final def f = 1 + |} + |trait U extends T { + | @inline final def g = f + |} + |class C extends U { + | def t1 = f + | def t2 = g + |} + """.stripMargin + val List(c, t, tClass, u, uClass) = compile(code) + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + } + + @Test + def virtualTraitNoInline(): Unit = { + val code = + """trait T { + | @inline def f = 1 + |} + |class C extends T { + | def t1(t: T) = t.f + | def t2 = this.f + |} + """.stripMargin + val List(c, t, tClass) = compile(code) + assertInvoke(getSingleMethod(c, "t1"), "T", "f") + assertInvoke(getSingleMethod(c, "t2"), "C", "f") + } + + @Test + def sealedTraitInline(): Unit = { + val code = + """sealed trait T { + | @inline def f = 1 + |} + |class C { + | def t1(t: T) = t.f + |} + """.stripMargin + val List(c, t, tClass) = compile(code) + assertNoInvoke(getSingleMethod(c, "t1")) + } + + @Test + def inlineFromObject(): Unit = { + val code = + """trait T { + | @inline def f = 0 + |} + |object O extends T { + | @inline def g = 1 + | // mixin generates `def f = T$class.f(this)`, which is inlined here (we get ICONST_0) + |} + |class C { + | def t1 = O.f // the mixin method of O is inlined, so we directly get the ICONST_0 + | def t2 = O.g // object members are inlined + | def t3(t: T) = t.f // no inlining here + |} + """.stripMargin + val List(c, oMirror, oModule, t, tClass) = compile(code) + + assertNoInvoke(getSingleMethod(oModule, "f")) + + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + assertInvoke(getSingleMethod(c, "t3"), "T", "f") + } + + @Test + def selfTypeInline(): Unit = { + val code = + """trait T { self: Assembly => + | @inline final def f = g + | @inline final def m = 1 + |} + |trait Assembly extends T { + | @inline final def g = 1 + | @inline final def n = m // inlined. (*) + | // (*) the declaration class of m is T. the signature of T$class.m is m(LAssembly;)I. so we need the self type to build the + | // signature. then we can look up the MethodNode of T$class.m and then rewrite the INVOKEINTERFACE to INVOKESTATIC. + |} + |class C { + | def t1(a: Assembly) = a.f // like above, decl class is T, need self-type of T to rewrite the interface call to static. + | def t2(a: Assembly) = a.n + |} + """.stripMargin + + val List(assembly, assemblyClass, c, t, tClass) = compile(code) + + assertNoInvoke(getSingleMethod(tClass, "f")) + + assertNoInvoke(getSingleMethod(assemblyClass, "n")) + + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + } + + @Test + def selfTypeInline2(): Unit = { + // There are some interesting things going on here with the self types. Here's a short version: + // + // trait T1 { def f = 1 } + // trait T2a { self: T1 with T2a => // self type in the backend: T1 + // def f = 2 + // def g = f // resolved to T2a.f + // } + // trait T2b { self: T2b with T1 => // self type in the backend: T2b + // def f = 2 + // def g = f // resolved to T1.f + // } + // + // scala> val t = typeOf[T2a]; exitingMixin(t.typeOfThis.typeSymbol) // self type of T2a is T1 + // res28: $r.intp.global.Symbol = trait T1 + // + // scala> typeOf[T2a].typeOfThis.member(newTermName("f")).owner // f in T2a is resolved as T2a.f + // res29: $r.intp.global.Symbol = trait T2a + // + // scala> val t = typeOf[T2b]; exitingMixin(t.typeOfThis.typeSymbol) // self type of T2b is T1 + // res30: $r.intp.global.Symbol = trait T2b + // + // scala> typeOf[T2b].typeOfThis.member(newTermName("f")).owner // f in T2b is resolved as T1.f + // res31: $r.intp.global.Symbol = trait T1 + + val code = + """trait T1 { + | @inline def f: Int = 0 + | @inline def g1 = f // not inlined: f not final, so T1$class.g1 has an interface call T1.f + |} + | + |// erased self-type (used in impl class for `self` parameter): T1 + |trait T2a { self: T1 with T2a => + | @inline override final def f = 1 + | @inline def g2a = f // inlined: resolved as T2a.f, which is re-written to T2a$class.f, so T2a$class.g2a has ICONST_1 + |} + | + |final class Ca extends T1 with T2a { + | // mixin generates accessors like `def g1 = T1$class.g1`, the impl class method call is inlined into the accessor. + | + | def m1a = g1 // call to accessor, inlined, we get the interface call T1.f + | def m2a = g2a // call to accessor, inlined, we get ICONST_1 + | def m3a = f // call to accessor, inlined, we get ICONST_1 + | + | def m4a(t: T1) = t.f // T1.f is not final, so not inlined, interface call to T1.f + | def m5a(t: T2a) = t.f // re-written to T2a$class.f, inlined, ICONST_1 + |} + | + |// erased self-type: T2b + |trait T2b { self: T2b with T1 => + | @inline override final def f = 1 + | @inline def g2b = f // not inlined: resolved as T1.f, so T2b$class.g2b has an interface call T1.f + |} + | + |final class Cb extends T1 with T2b { + | def m1b = g1 // inlined, we get the interface call to T1.f + | def m2b = g2b // inlined, we get the interface call to T1.f + | def m3b = f // inlined, we get ICONST_1 + | + | def m4b(t: T1) = t.f // T1.f is not final, so not inlined, interface call to T1.f + | def m5b(t: T2b) = t.f // re-written to T2b$class.f, inlined, ICONST_1 + |} + """.stripMargin + val List(ca, cb, t1, t1C, t2a, t2aC, t2b, t2bC) = compile(code) + + val t2aCfDesc = t2aC.methods.asScala.find(_.name == "f").get.desc + assert(t2aCfDesc == "(LT1;)I", t2aCfDesc) // self-type of T2a is T1 + + val t2bCfDesc = t2bC.methods.asScala.find(_.name == "f").get.desc + assert(t2bCfDesc == "(LT2b;)I", t2bCfDesc) // self-type of T2b is T2b + + assertNoInvoke(getSingleMethod(t2aC, "g2a")) + assertInvoke(getSingleMethod(t2bC, "g2b"), "T1", "f") + + assertInvoke(getSingleMethod(ca, "m1a"), "T1", "f") + assertNoInvoke(getSingleMethod(ca, "m2a")) // no invoke, see comment on def g2a + assertNoInvoke(getSingleMethod(ca, "m3a")) + assertInvoke(getSingleMethod(ca, "m4a"), "T1", "f") + assertNoInvoke(getSingleMethod(ca, "m5a")) + + assertInvoke(getSingleMethod(cb, "m1b"), "T1", "f") + assertInvoke(getSingleMethod(cb, "m2b"), "T1", "f") // invoke, see comment on def g2b + assertNoInvoke(getSingleMethod(cb, "m3b")) + assertInvoke(getSingleMethod(cb, "m4b"), "T1", "f") + assertNoInvoke(getSingleMethod(cb, "m5b")) + } + + @Test + def finalSubclassInline(): Unit = { + val code = + """class C { + | @inline def f = 0 + | @inline final def g = 1 + |} + |final class D extends C + |object E extends C + |class T { + | def t1(d: D) = d.f + d.g + E.f + E.g // d.f can be inlined because the reciever type is D, which is final. + |} // so d.f can be resolved statically. same for E.f + """.stripMargin + val List(c, d, e, eModule, t) = compile(code) + assertNoInvoke(getSingleMethod(t, "t1")) + } + + @Test + def inlineFromNestedClasses(): Unit = { + val code = + """class C { + | trait T { @inline final def f = 1 } + | class D extends T{ + | def m(t: T) = t.f + | } + | + | def m(d: D) = d.f + |} + """.stripMargin + val List(c, d, t, tC) = compile(code) + assertNoInvoke(getSingleMethod(d, "m")) + assertNoInvoke(getSingleMethod(c, "m")) } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala index 7c9636b8b7..c2e2a1b883 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala @@ -22,8 +22,8 @@ object UnreachableCodeTest extends ClearAfterClass.Clearable { var dceCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code") var noOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:none") - // jvm-1.5 disables computing stack map frames, and it emits dead code as-is. - var noOptNoFramesCompiler = newCompiler(extraArgs = "-target:jvm-1.5 -Ybackend:GenBCode -Yopt:l:none") + // jvm-1.5 disables computing stack map frames, and it emits dead code as-is. note that this flag triggers a deprecation warning + var noOptNoFramesCompiler = newCompiler(extraArgs = "-target:jvm-1.5 -Ybackend:GenBCode -Yopt:l:none -deprecation") def clear(): Unit = { methodOptCompiler = null diff --git a/test/junit/scala/tools/testing/TempDir.scala b/test/junit/scala/tools/testing/TempDir.scala new file mode 100644 index 0000000000..475de8c4a2 --- /dev/null +++ b/test/junit/scala/tools/testing/TempDir.scala @@ -0,0 +1,18 @@ +package scala.tools.testing + +import java.io.{IOException, File} + +object TempDir { + final val TEMP_DIR_ATTEMPTS = 10000 + def createTempDir(): File = { + val baseDir = new File(System.getProperty("java.io.tmpdir")) + val baseName = System.currentTimeMillis() + "-" + var c = 0 + while (c < TEMP_DIR_ATTEMPTS) { + val tempDir = new File(baseDir, baseName + c) + if (tempDir.mkdir()) return tempDir + c += 1 + } + throw new IOException(s"Failed to create directory") + } +} |