summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala6
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala76
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala134
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala8
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala4
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala69
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala81
-rw-r--r--src/compiler/scala/tools/nsc/transform/Delambdafy.scala2
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala64
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala13
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala13
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala4
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala4
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala65
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala114
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala325
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala4
-rw-r--r--test/junit/scala/tools/testing/TempDir.scala18
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")
+ }
+}