summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukas Rytz <lukas.rytz@gmail.com>2015-03-11 12:09:21 -0700
committerLukas Rytz <lukas.rytz@gmail.com>2015-03-11 13:47:07 -0700
commitf8bb3d5289e5eb84ccd94386e5c3df1bdf8b91bc (patch)
treed7745fc7de76b699304e1ddac255e531134400ff
parent027e97981d9b6a3783e9ab247cc898017b3de821 (diff)
downloadscala-f8bb3d5289e5eb84ccd94386e5c3df1bdf8b91bc.tar.gz
scala-f8bb3d5289e5eb84ccd94386e5c3df1bdf8b91bc.tar.bz2
scala-f8bb3d5289e5eb84ccd94386e5c3df1bdf8b91bc.zip
Inline final methods defined in traits
In order to inline a final trait method, callsites of such methods are first re-written from interface calls to static calls of the trait's implementation class. Then inlining proceeds as ususal. One problem that came up during development was that mixin methods are added to class symbols only for classes being compiled, but not for others. In order to inline a mixin method, we need the InlineInfo, which so far was built using the class (and method) symbols. So we had a problem with separate compilation. Looking up the symbol from a given classfile name was already known to be brittle (it's also one of the weak points of the current inliner), so we changed the strategy. Now the InlineInfo for every class is encoded in a new classfile attribute. This classfile attribute is relatively small, because all strings it references (class internal names, method names, method descriptors) would exist anyway in the constant pool, so it just adds a few references. When building the InlineInfo for a class symbol, we only look at the symbol properties for symbols being compiled in the current run. For unpickled symbols, we build the InlineInfo by reading the classfile attribute. This change also adds delambdafy:method classes to currentRun.symSource. Otherwise, currentRun.compiles(lambdaClass) is false.
-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")
+ }
+}