summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrzegorz Kossakowski <grzegorz.kossakowski@gmail.com>2015-03-20 15:02:58 -0700
committerGrzegorz Kossakowski <grzegorz.kossakowski@gmail.com>2015-03-20 15:02:58 -0700
commit3c7b1e3d953703582aae81506f8b27732a603bfe (patch)
treecfdc28b39cf590da5529d6a375772b566ecae2bf
parent13ed36201ff48549b60367b6594c2477db5e0729 (diff)
parentc2ab768287cc02b5e01342ac993d6c2b6e7ee2aa (diff)
downloadscala-3c7b1e3d953703582aae81506f8b27732a603bfe.tar.gz
scala-3c7b1e3d953703582aae81506f8b27732a603bfe.tar.bz2
scala-3c7b1e3d953703582aae81506f8b27732a603bfe.zip
Merge pull request #4312 from lrytz/opt/inlining
Inliner for GenBCode
-rwxr-xr-xbuild.xml9
-rw-r--r--src/asm/scala/tools/asm/tree/MethodInsnNode.java1
-rw-r--r--src/compiler/scala/tools/nsc/Reporting.scala12
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala18
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala7
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala58
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala141
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala42
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala26
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala322
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala208
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala265
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala5
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala29
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala123
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala132
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala189
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/InlineInfoAttribute.scala3
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala648
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala88
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala24
-rw-r--r--src/compiler/scala/tools/nsc/settings/ScalaSettings.scala33
-rw-r--r--src/compiler/scala/tools/nsc/transform/Delambdafy.scala2
-rw-r--r--src/reflect/scala/reflect/internal/SymbolTable.scala8
-rw-r--r--test/files/pos/t9111-inliner-workaround.flags1
-rw-r--r--test/files/pos/t9111-inliner-workaround/A_1.java13
-rw-r--r--test/files/pos/t9111-inliner-workaround/Test_1.scala10
-rw-r--r--test/files/run/bcodeInlinerMixed.flags1
-rw-r--r--test/files/run/bcodeInlinerMixed/A_1.java3
-rw-r--r--test/files/run/bcodeInlinerMixed/B_1.scala20
-rw-r--r--test/files/run/bcodeInlinerMixed/Test.scala16
-rw-r--r--test/files/run/colltest1.scala2
-rw-r--r--test/files/run/compiler-asSeenFrom.scala2
-rw-r--r--test/files/run/existentials-in-compiler.scala2
-rw-r--r--test/files/run/is-valid-num.scala2
-rw-r--r--test/files/run/iterator-from.scala2
-rw-r--r--test/files/run/mapConserve.scala2
-rw-r--r--test/files/run/pc-conversions.scala2
-rw-r--r--test/files/run/stringinterpolation_macro-run.scala2
-rw-r--r--test/files/run/synchronized.check4
-rw-r--r--test/files/run/t7096.scala2
-rw-r--r--test/files/run/t7582.check4
-rw-r--r--test/files/run/t7582b.check4
-rw-r--r--test/junit/scala/collection/IterableViewLikeTest.scala1
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala52
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala106
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala29
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala16
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala152
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala4
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala19
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala67
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala146
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala198
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala115
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala953
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala15
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala39
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala12
-rw-r--r--test/junit/scala/tools/testing/ClearAfterClass.java20
-rw-r--r--test/junit/scala/tools/testing/TempDir.scala18
61 files changed, 3994 insertions, 455 deletions
diff --git a/build.xml b/build.xml
index 5f6b04b8e4..ee6a045bda 100755
--- a/build.xml
+++ b/build.xml
@@ -1448,6 +1448,15 @@ TODO:
<target name="test.junit.comp" depends="pack.done">
<stopwatch name="test.junit.compiler.timer"/>
<mkdir dir="${test.junit.classes}"/>
+ <javac
+ debug="true"
+ srcdir="${test.junit.src}"
+ destdir="${test.junit.classes}"
+ classpathref="test.junit.compiler.build.path"
+ target="1.6"
+ source="1.5"
+ compiler="javac1.6"
+ includes="**/*.java"/>
<scalacfork
destdir="${test.junit.classes}"
compilerpathref="quick.compiler.path"
diff --git a/src/asm/scala/tools/asm/tree/MethodInsnNode.java b/src/asm/scala/tools/asm/tree/MethodInsnNode.java
index 1ec46d473d..30c7854646 100644
--- a/src/asm/scala/tools/asm/tree/MethodInsnNode.java
+++ b/src/asm/scala/tools/asm/tree/MethodInsnNode.java
@@ -45,6 +45,7 @@ public class MethodInsnNode extends AbstractInsnNode {
/**
* The internal name of the method's owner class (see
* {@link scala.tools.asm.Type#getInternalName() getInternalName}).
+ * For methods of arrays, e.g., clone(), the array type descriptor.
*/
public String owner;
diff --git a/src/compiler/scala/tools/nsc/Reporting.scala b/src/compiler/scala/tools/nsc/Reporting.scala
index 4d7e9e753f..72a4b69536 100644
--- a/src/compiler/scala/tools/nsc/Reporting.scala
+++ b/src/compiler/scala/tools/nsc/Reporting.scala
@@ -26,7 +26,7 @@ trait Reporting extends scala.reflect.internal.Reporting { self: ast.Positions w
protected def PerRunReporting = new PerRunReporting
class PerRunReporting extends PerRunReportingBase {
/** Collects for certain classes of warnings during this run. */
- private class ConditionalWarning(what: String, option: Settings#BooleanSetting) {
+ private class ConditionalWarning(what: String, option: Settings#BooleanSetting)(reRunFlag: String = option.name) {
val warnings = mutable.LinkedHashMap[Position, String]()
def warn(pos: Position, msg: String) =
if (option) reporter.warning(pos, msg)
@@ -37,16 +37,16 @@ trait Reporting extends scala.reflect.internal.Reporting { self: ast.Positions w
val warningVerb = if (numWarnings == 1) "was" else "were"
val warningCount = countElementsAsString(numWarnings, s"$what warning")
- reporter.warning(NoPosition, s"there $warningVerb $warningCount; re-run with ${option.name} for details")
+ reporter.warning(NoPosition, s"there $warningVerb $warningCount; re-run with $reRunFlag for details")
}
}
// This change broke sbt; I gave it the thrilling name of uncheckedWarnings0 so
// as to recover uncheckedWarnings for its ever-fragile compiler interface.
- private val _deprecationWarnings = new ConditionalWarning("deprecation", settings.deprecation)
- private val _uncheckedWarnings = new ConditionalWarning("unchecked", settings.unchecked)
- private val _featureWarnings = new ConditionalWarning("feature", settings.feature)
- private val _inlinerWarnings = new ConditionalWarning("inliner", settings.YinlinerWarnings)
+ private val _deprecationWarnings = new ConditionalWarning("deprecation", settings.deprecation)()
+ private val _uncheckedWarnings = new ConditionalWarning("unchecked", settings.unchecked)()
+ private val _featureWarnings = new ConditionalWarning("feature", settings.feature)()
+ private val _inlinerWarnings = new ConditionalWarning("inliner", settings.YinlinerWarnings)(if (settings.isBCodeAskedFor) settings.YoptWarnings.name else settings.YinlinerWarnings.name)
private val _allConditionalWarnings = List(_deprecationWarnings, _uncheckedWarnings, _featureWarnings, _inlinerWarnings)
// TODO: remove in favor of the overload that takes a Symbol, give that argument a default (NoSymbol)
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala
index d3f09217cd..0df1b2029d 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala
@@ -7,8 +7,8 @@ package scala.tools.nsc.backend.jvm
import scala.tools.asm.tree.{InsnList, AbstractInsnNode, ClassNode, MethodNode}
import java.io.{StringWriter, PrintWriter}
-import scala.tools.asm.util.{TraceClassVisitor, TraceMethodVisitor, Textifier}
-import scala.tools.asm.{Attribute, ClassReader}
+import scala.tools.asm.util.{CheckClassAdapter, TraceClassVisitor, TraceMethodVisitor, Textifier}
+import scala.tools.asm.{ClassWriter, Attribute, ClassReader}
import scala.collection.convert.decorateAsScala._
import scala.tools.nsc.backend.jvm.opt.InlineInfoAttributePrototype
@@ -106,4 +106,18 @@ object AsmUtils {
* Returns a human-readable representation of the given instruction sequence.
*/
def textify(insns: InsnList): String = textify(insns.iterator().asScala)
+
+ /**
+ * Run ASM's CheckClassAdapter over a class. Returns None if no problem is found, otherwise
+ * Some(msg) with the verifier's error message.
+ */
+ def checkClass(classNode: ClassNode): Option[String] = {
+ val cw = new ClassWriter(ClassWriter.COMPUTE_MAXS)
+ classNode.accept(cw)
+ val sw = new StringWriter()
+ val pw = new PrintWriter(sw)
+ CheckClassAdapter.verify(new ClassReader(cw.toByteArray), false, pw)
+ val res = sw.toString
+ if (res.isEmpty) None else Some(res)
+ }
}
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala
index 2ebf338f5e..162da4236a 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala
@@ -7,7 +7,8 @@ package scala.tools.nsc
package backend.jvm
import scala.tools.nsc.Global
-import scala.tools.nsc.backend.jvm.BTypes.{MethodInlineInfo, InlineInfo, InternalName}
+import scala.tools.nsc.backend.jvm.BTypes.{InternalName, MethodInlineInfo, InlineInfo}
+import BackendReporting.ClassSymbolInfoFailureSI9111
/**
* This trait contains code shared between GenBCode and GenASM that depends on types defined in
@@ -336,7 +337,7 @@ final class BCodeAsmCommon[G <: Global](val global: G) {
val isEffectivelyFinal = classSym.isEffectivelyFinal
- var warning = Option.empty[String]
+ var warning = Option.empty[ClassSymbolInfoFailureSI9111]
// Primitive methods cannot be inlined, so there's no point in building a MethodInlineInfo. Also, some
// primitive methods (e.g., `isInstanceOf`) have non-erased types, which confuses [[typeToBType]].
@@ -345,7 +346,7 @@ final class BCodeAsmCommon[G <: Global](val global: G) {
if (completeSilentlyAndCheckErroneous(methodSym)) {
// Happens due to SI-9111. Just don't provide any MethodInlineInfo for that method, we don't need fail the compiler.
if (!classSym.isJavaDefined) devWarning("SI-9111 should only be possible for Java classes")
- warning = Some(s"Failed to get the type of a method of class symbol ${classSym.fullName} due to SI-9111")
+ warning = Some(ClassSymbolInfoFailureSI9111(classSym.fullName))
None
} else {
val name = methodSym.javaSimpleName.toString // same as in genDefDef
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala
index 062daa4eac..15b014bdd3 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala
@@ -12,6 +12,8 @@ package jvm
import scala.annotation.switch
import scala.tools.asm
+import GenBCode._
+import BackendReporting._
/*
*
@@ -92,7 +94,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
val thrownKind = tpeTK(expr)
// `throw null` is valid although scala.Null (as defined in src/libray-aux) isn't a subtype of Throwable.
// Similarly for scala.Nothing (again, as defined in src/libray-aux).
- assert(thrownKind.isNullType || thrownKind.isNothingType || thrownKind.asClassBType.isSubtypeOf(ThrowableReference))
+ assert(thrownKind.isNullType || thrownKind.isNothingType || thrownKind.asClassBType.isSubtypeOf(ThrowableReference).get)
genLoad(expr, thrownKind)
lineNumber(expr)
emit(asm.Opcodes.ATHROW) // ICode enters here into enterIgnoreMode, we'll rely instead on DCE at ClassNode level.
@@ -229,7 +231,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
if (isArithmeticOp(code)) genArithmeticOp(tree, code)
else if (code == scalaPrimitives.CONCAT) genStringConcat(tree)
- else if (code == scalaPrimitives.HASH) genScalaHash(receiver)
+ else if (code == scalaPrimitives.HASH) genScalaHash(receiver, tree.pos)
else if (isArrayOp(code)) genArrayOp(tree, code, expectedType)
else if (isLogicalOp(code) || isComparisonOp(code)) {
val success, failure, after = new asm.Label
@@ -583,7 +585,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
// if (fun.symbol.isConstructor) Static(true) else SuperCall(mix);
mnode.visitVarInsn(asm.Opcodes.ALOAD, 0)
genLoadArguments(args, paramTKs(app))
- genCallMethod(fun.symbol, invokeStyle, pos = app.pos)
+ genCallMethod(fun.symbol, invokeStyle, app.pos)
generatedType = asmMethodType(fun.symbol).returnType
// 'new' constructor call: Note: since constructors are
@@ -613,7 +615,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
*/
for (i <- args.length until dims) elemKind = ArrayBType(elemKind)
}
- (argsSize : @switch) match {
+ argsSize match {
case 1 => bc newarray elemKind
case _ =>
val descr = ('[' * argsSize) + elemKind.descriptor // denotes the same as: arrayN(elemKind, argsSize).descriptor
@@ -625,7 +627,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
mnode.visitTypeInsn(asm.Opcodes.NEW, rt.internalName)
bc dup generatedType
genLoadArguments(args, paramTKs(app))
- genCallMethod(ctor, icodes.opcodes.Static(onInstance = true))
+ genCallMethod(ctor, icodes.opcodes.Static(onInstance = true), app.pos)
case _ =>
abort(s"Cannot instantiate $tpt of kind: $generatedType")
@@ -635,7 +637,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
val nativeKind = tpeTK(expr)
genLoad(expr, nativeKind)
val MethodNameAndType(mname, methodType) = asmBoxTo(nativeKind)
- bc.invokestatic(BoxesRunTime.internalName, mname, methodType.descriptor)
+ bc.invokestatic(BoxesRunTime.internalName, mname, methodType.descriptor, app.pos)
generatedType = boxResultType(fun.symbol) // was toTypeKind(fun.symbol.tpe.resultType)
case Apply(fun @ _, List(expr)) if currentRun.runDefinitions.isUnbox(fun.symbol) =>
@@ -643,7 +645,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
val boxType = unboxResultType(fun.symbol) // was toTypeKind(fun.symbol.owner.linkedClassOfClass.tpe)
generatedType = boxType
val MethodNameAndType(mname, methodType) = asmUnboxTo(boxType)
- bc.invokestatic(BoxesRunTime.internalName, mname, methodType.descriptor)
+ bc.invokestatic(BoxesRunTime.internalName, mname, methodType.descriptor, app.pos)
case app @ Apply(fun, args) =>
val sym = fun.symbol
@@ -694,10 +696,10 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
// descriptor (instead of a class internal name):
// invokevirtual #2; //Method "[I".clone:()Ljava/lang/Object
val target: String = targetTypeKind.asRefBType.classOrArrayType
- bc.invokevirtual(target, "clone", "()Ljava/lang/Object;")
+ bc.invokevirtual(target, "clone", "()Ljava/lang/Object;", app.pos)
}
else {
- genCallMethod(sym, invokeStyle, hostClass, app.pos)
+ genCallMethod(sym, invokeStyle, app.pos, hostClass)
}
} // end of genNormalMethodCall()
@@ -809,7 +811,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
}
def adapt(from: BType, to: BType) {
- if (!from.conformsTo(to)) {
+ if (!from.conformsTo(to).get) {
to match {
case UNIT => bc drop from
case _ => bc.emitT2T(from, to)
@@ -975,23 +977,23 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
// Optimization for expressions of the form "" + x. We can avoid the StringBuilder.
case List(Literal(Constant("")), arg) =>
genLoad(arg, ObjectReference)
- genCallMethod(String_valueOf, icodes.opcodes.Static(onInstance = false))
+ genCallMethod(String_valueOf, icodes.opcodes.Static(onInstance = false), arg.pos)
case concatenations =>
- bc.genStartConcat
+ bc.genStartConcat(tree.pos)
for (elem <- concatenations) {
val kind = tpeTK(elem)
genLoad(elem, kind)
- bc.genStringConcat(kind)
+ bc.genStringConcat(kind, elem.pos)
}
- bc.genEndConcat
+ bc.genEndConcat(tree.pos)
}
StringReference
}
- def genCallMethod(method: Symbol, style: InvokeStyle, hostClass0: Symbol = null, pos: Position = NoPosition) {
+ def genCallMethod(method: Symbol, style: InvokeStyle, pos: Position, hostClass0: Symbol = null) {
val siteSymbol = claszSymbol
val hostSymbol = if (hostClass0 == null) method.owner else hostClass0
@@ -1035,26 +1037,26 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
}
if (style.isStatic) {
- if (style.hasInstance) { bc.invokespecial (jowner, jname, mdescr) }
- else { bc.invokestatic (jowner, jname, mdescr) }
+ if (style.hasInstance) { bc.invokespecial (jowner, jname, mdescr, pos) }
+ else { bc.invokestatic (jowner, jname, mdescr, pos) }
}
else if (style.isDynamic) {
- if (needsInterfaceCall(receiver)) { bc.invokeinterface(jowner, jname, mdescr) }
- else { bc.invokevirtual (jowner, jname, mdescr) }
+ if (needsInterfaceCall(receiver)) { bc.invokeinterface(jowner, jname, mdescr, pos) }
+ else { bc.invokevirtual (jowner, jname, mdescr, pos) }
}
else {
assert(style.isSuper, s"An unknown InvokeStyle: $style")
- bc.invokespecial(jowner, jname, mdescr)
+ bc.invokespecial(jowner, jname, mdescr, pos)
initModule()
}
} // end of genCallMethod()
/* Generate the scala ## method. */
- def genScalaHash(tree: Tree): BType = {
+ def genScalaHash(tree: Tree, applyPos: Position): BType = {
genLoadModule(ScalaRunTimeModule) // TODO why load ScalaRunTimeModule if ## has InvokeStyle of Static(false) ?
genLoad(tree, ObjectReference)
- genCallMethod(hashMethodSym, icodes.opcodes.Static(onInstance = false))
+ genCallMethod(hashMethodSym, icodes.opcodes.Static(onInstance = false), applyPos)
INT
}
@@ -1186,8 +1188,8 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
// TODO !!!!!!!!!! isReferenceType, in the sense of TypeKind? (ie non-array, non-boxed, non-nothing, may be null)
if (scalaPrimitives.isUniversalEqualityOp(code) && tpeTK(lhs).isClass) {
// `lhs` has reference type
- if (code == EQ) genEqEqPrimitive(lhs, rhs, success, failure)
- else genEqEqPrimitive(lhs, rhs, failure, success)
+ if (code == EQ) genEqEqPrimitive(lhs, rhs, success, failure, tree.pos)
+ else genEqEqPrimitive(lhs, rhs, failure, success, tree.pos)
}
else if (scalaPrimitives.isComparisonOp(code))
genComparisonOp(lhs, rhs, code)
@@ -1207,7 +1209,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
* @param l left-hand-side of the '=='
* @param r right-hand-side of the '=='
*/
- def genEqEqPrimitive(l: Tree, r: Tree, success: asm.Label, failure: asm.Label) {
+ def genEqEqPrimitive(l: Tree, r: Tree, success: asm.Label, failure: asm.Label, pos: Position) {
/* True if the equality comparison is between values that require the use of the rich equality
* comparator (scala.runtime.Comparator.equals). This is the case when either side of the
@@ -1231,7 +1233,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
}
genLoad(l, ObjectReference)
genLoad(r, ObjectReference)
- genCallMethod(equalsMethod, icodes.opcodes.Static(onInstance = false))
+ genCallMethod(equalsMethod, icodes.opcodes.Static(onInstance = false), pos)
genCZJUMP(success, failure, icodes.NE, BOOL)
}
else {
@@ -1247,7 +1249,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
// SI-7852 Avoid null check if L is statically non-null.
genLoad(l, ObjectReference)
genLoad(r, ObjectReference)
- genCallMethod(Object_equals, icodes.opcodes.Dynamic)
+ genCallMethod(Object_equals, icodes.opcodes.Dynamic, pos)
genCZJUMP(success, failure, icodes.NE, BOOL)
} else {
// l == r -> if (l eq null) r eq null else l.equals(r)
@@ -1268,7 +1270,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
markProgramPoint(lNonNull)
locals.load(eqEqTempLocal)
- genCallMethod(Object_equals, icodes.opcodes.Dynamic)
+ genCallMethod(Object_equals, icodes.opcodes.Dynamic, pos)
genCZJUMP(success, failure, icodes.NE, BOOL)
}
}
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala
index ccee230191..3b7dbc18da 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala
@@ -10,6 +10,8 @@ package backend.jvm
import scala.tools.asm
import scala.collection.mutable
import scala.tools.nsc.io.AbstractFile
+import GenBCode._
+import BackendReporting._
/*
* Traits encapsulating functionality to convert Scala AST Trees into ASM ClassNodes.
@@ -66,7 +68,7 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters {
override def getCommonSuperClass(inameA: String, inameB: String): String = {
val a = classBTypeFromInternalName(inameA)
val b = classBTypeFromInternalName(inameB)
- val lub = a.jvmWiseLUB(b)
+ val lub = a.jvmWiseLUB(b).get
val lubName = lub.internalName
assert(lubName != "scala/Any")
lubName // ASM caches the answer during the lifetime of a ClassWriter. We outlive that. Not sure whether caching on our side would improve things.
@@ -204,12 +206,12 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters {
* can-multi-thread
*/
final def addInnerClassesASM(jclass: asm.ClassVisitor, refedInnerClasses: List[ClassBType]) {
- val allNestedClasses = refedInnerClasses.flatMap(_.enclosingNestedClassesChain).distinct
+ val allNestedClasses = refedInnerClasses.flatMap(_.enclosingNestedClassesChain.get).distinct
// sorting ensures nested classes are listed after their enclosing class thus satisfying the Eclipse Java compiler
for (nestedClass <- allNestedClasses.sortBy(_.internalName.toString)) {
// Extract the innerClassEntry - we know it exists, enclosingNestedClassesChain only returns nested classes.
- val Some(e) = nestedClass.innerClassAttributeEntry
+ val Some(e) = nestedClass.innerClassAttributeEntry.get
jclass.visitInnerClass(e.name, e.outerName, e.innerName, e.flags)
}
}
@@ -331,125 +333,42 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters {
getClassBTypeAndRegisterInnerClass(classSym).internalName
}
- private def assertClassNotArray(sym: Symbol): Unit = {
- assert(sym.isClass, sym)
- assert(sym != definitions.ArrayClass || isCompilingArray, sym)
- }
-
- private def assertClassNotArrayNotPrimitive(sym: Symbol): Unit = {
- assertClassNotArray(sym)
- assert(!primitiveTypeMap.contains(sym) || isCompilingPrimitive, sym)
- }
-
/**
* The ClassBType for a class symbol. If the class is nested, the ClassBType is added to the
* innerClassBufferASM.
*
- * The class symbol scala.Nothing is mapped to the class scala.runtime.Nothing$. Similarly,
- * scala.Null is mapped to scala.runtime.Null$. This is because there exist no class files
- * for the Nothing / Null. If used for example as a parameter type, we use the runtime classes
- * in the classfile method signature.
- *
- * Note that the referenced class symbol may be an implementation class. For example when
- * compiling a mixed-in method that forwards to the static method in the implementation class,
- * the class descriptor of the receiver (the implementation class) is obtained by creating the
- * ClassBType.
+ * TODO: clean up the way we track referenced inner classes.
+ * doing it during code generation is not correct when the optimizer changes the code.
*/
final def getClassBTypeAndRegisterInnerClass(sym: Symbol): ClassBType = {
- assertClassNotArrayNotPrimitive(sym)
-
- if (sym == definitions.NothingClass) RT_NOTHING
- else if (sym == definitions.NullClass) RT_NULL
- else {
- val r = classBTypeFromSymbol(sym)
- if (r.isNestedClass) innerClassBufferASM += r
- r
- }
+ val r = classBTypeFromSymbol(sym)
+ if (r.isNestedClass.get) innerClassBufferASM += r
+ r
}
/**
- * This method returns the BType for a type reference, for example a parameter type.
- *
- * If the result is a ClassBType for a nested class, it is added to the innerClassBufferASM.
- *
- * If `t` references a class, toTypeKind ensures that the class is not an implementation class.
- * See also comment on getClassBTypeAndRegisterInnerClass, which is invoked for implementation
- * classes.
+ * The BType for a type reference. If the result is a ClassBType for a nested class, it is added
+ * to the innerClassBufferASM.
+ * TODO: clean up the way we track referenced inner classes.
*/
- final def toTypeKind(t: Type): BType = {
- import definitions.ArrayClass
-
- /**
- * Primitive types are represented as TypeRefs to the class symbol of, for example, scala.Int.
- * The `primitiveTypeMap` maps those class symbols to the corresponding PrimitiveBType.
- */
- def primitiveOrClassToBType(sym: Symbol): BType = {
- assertClassNotArray(sym)
- assert(!sym.isImplClass, sym)
- primitiveTypeMap.getOrElse(sym, getClassBTypeAndRegisterInnerClass(sym))
- }
-
- /**
- * When compiling Array.scala, the type parameter T is not erased and shows up in method
- * signatures, e.g. `def apply(i: Int): T`. A TyperRef to T is replaced by ObjectReference.
- */
- def nonClassTypeRefToBType(sym: Symbol): ClassBType = {
- assert(sym.isType && isCompilingArray, sym)
- ObjectReference
- }
-
- t.dealiasWiden match {
- case TypeRef(_, ArrayClass, List(arg)) => ArrayBType(toTypeKind(arg)) // Array type such as Array[Int] (kept by erasure)
- case TypeRef(_, sym, _) if !sym.isClass => nonClassTypeRefToBType(sym) // See comment on nonClassTypeRefToBType
- case TypeRef(_, sym, _) => primitiveOrClassToBType(sym) // Common reference to a type such as scala.Int or java.lang.String
- case ClassInfoType(_, _, sym) => primitiveOrClassToBType(sym) // We get here, for example, for genLoadModule, which invokes toTypeKind(moduleClassSymbol.info)
-
- /* AnnotatedType should (probably) be eliminated by erasure. However we know it happens for
- * meta-annotated annotations (@(ann @getter) val x = 0), so we don't emit a warning.
- * The type in the AnnotationInfo is an AnnotatedTpe. Tested in jvm/annotations.scala.
- */
- case a @ AnnotatedType(_, t) =>
- debuglog(s"typeKind of annotated type $a")
- toTypeKind(t)
-
- /* ExistentialType should (probably) be eliminated by erasure. We know they get here for
- * classOf constants:
- * class C[T]
- * class T { final val k = classOf[C[_]] }
- */
- case e @ ExistentialType(_, t) =>
- debuglog(s"typeKind of existential type $e")
- toTypeKind(t)
-
- /* The cases below should probably never occur. They are kept for now to avoid introducing
- * new compiler crashes, but we added a warning. The compiler / library bootstrap and the
- * test suite don't produce any warning.
- */
-
- case tp =>
- currentUnit.warning(tp.typeSymbol.pos,
- s"an unexpected type representation reached the compiler backend while compiling $currentUnit: $tp. " +
- "If possible, please file a bug on issues.scala-lang.org.")
-
- tp match {
- case ThisType(ArrayClass) => ObjectReference // was introduced in 9b17332f11 to fix SI-999, but this code is not reached in its test, or any other test
- case ThisType(sym) => getClassBTypeAndRegisterInnerClass(sym)
- case SingleType(_, sym) => primitiveOrClassToBType(sym)
- case ConstantType(_) => toTypeKind(t.underlying)
- case RefinedType(parents, _) => parents.map(toTypeKind(_).asClassBType).reduceLeft((a, b) => a.jvmWiseLUB(b))
- }
- }
+ final def toTypeKind(t: Type): BType = typeToBType(t) match {
+ case c: ClassBType if c.isNestedClass.get =>
+ innerClassBufferASM += c
+ c
+ case r => r
}
- /*
- * must-single-thread
+ /**
+ * Class components that are nested classes are added to the innerClassBufferASM.
+ * TODO: clean up the way we track referenced inner classes.
*/
final def asmMethodType(msym: Symbol): MethodBType = {
- assert(msym.isMethod, s"not a method-symbol: $msym")
- val resT: BType =
- if (msym.isClassConstructor || msym.isConstructor) UNIT
- else toTypeKind(msym.tpe.resultType)
- MethodBType(msym.tpe.paramTypes map toTypeKind, resT)
+ val r = methodBTypeFromSymbol(msym)
+ (r.returnType :: r.argumentTypes) foreach {
+ case c: ClassBType if c.isNestedClass.get => innerClassBufferASM += c
+ case _ =>
+ }
+ r
}
/**
@@ -796,7 +715,7 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters {
val mirrorClass = new asm.tree.ClassNode
mirrorClass.visit(
classfileVersion,
- bType.info.flags,
+ bType.info.get.flags,
bType.internalName,
null /* no java-generic-signature */,
ObjectReference.internalName,
@@ -812,7 +731,7 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters {
addForwarders(isRemote(moduleClass), mirrorClass, bType.internalName, moduleClass)
- innerClassBufferASM ++= bType.info.nestedClasses
+ innerClassBufferASM ++= bType.info.get.nestedClasses
addInnerClassesASM(mirrorClass, innerClassBufferASM.toList)
mirrorClass.visitEnd()
@@ -928,7 +847,7 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters {
constructor.visitMaxs(0, 0) // just to follow protocol, dummy arguments
constructor.visitEnd()
- innerClassBufferASM ++= classBTypeFromSymbol(cls).info.nestedClasses
+ innerClassBufferASM ++= classBTypeFromSymbol(cls).info.get.nestedClasses
addInnerClassesASM(beanInfoClass, innerClassBufferASM.toList)
beanInfoClass.visitEnd()
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala
index c3db28151b..9993357eee 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala
@@ -10,6 +10,8 @@ package backend.jvm
import scala.tools.asm
import scala.annotation.switch
import scala.collection.mutable
+import GenBCode._
+import scala.tools.asm.tree.MethodInsnNode
/*
* A high-level facade to the ASM API for bytecode generation.
@@ -42,9 +44,6 @@ abstract class BCodeIdiomatic extends SubComponent {
val StringBuilderClassName = "scala/collection/mutable/StringBuilder"
- val CLASS_CONSTRUCTOR_NAME = "<clinit>"
- val INSTANCE_CONSTRUCTOR_NAME = "<init>"
-
val EMPTY_STRING_ARRAY = Array.empty[String]
val EMPTY_INT_ARRAY = Array.empty[Int]
val EMPTY_LABEL_ARRAY = Array.empty[asm.Label]
@@ -107,7 +106,7 @@ abstract class BCodeIdiomatic extends SubComponent {
*/
abstract class JCodeMethodN {
- def jmethod: asm.MethodVisitor
+ def jmethod: asm.tree.MethodNode
import asm.Opcodes;
import icodes.opcodes.{ Static, Dynamic, SuperCall }
@@ -207,20 +206,21 @@ abstract class BCodeIdiomatic extends SubComponent {
/*
* can-multi-thread
*/
- final def genStartConcat {
+ final def genStartConcat(pos: Position): Unit = {
jmethod.visitTypeInsn(Opcodes.NEW, StringBuilderClassName)
jmethod.visitInsn(Opcodes.DUP)
invokespecial(
StringBuilderClassName,
INSTANCE_CONSTRUCTOR_NAME,
- "()V"
+ "()V",
+ pos
)
}
/*
* can-multi-thread
*/
- final def genStringConcat(el: BType) {
+ final def genStringConcat(el: BType, pos: Position): Unit = {
val jtype =
if (el.isArray || el.isClass) ObjectReference
@@ -228,14 +228,14 @@ abstract class BCodeIdiomatic extends SubComponent {
val bt = MethodBType(List(jtype), StringBuilderReference)
- invokevirtual(StringBuilderClassName, "append", bt.descriptor)
+ invokevirtual(StringBuilderClassName, "append", bt.descriptor, pos)
}
/*
* can-multi-thread
*/
- final def genEndConcat {
- invokevirtual(StringBuilderClassName, "toString", "()Ljava/lang/String;")
+ final def genEndConcat(pos: Position): Unit = {
+ invokevirtual(StringBuilderClassName, "toString", "()Ljava/lang/String;", pos)
}
/*
@@ -391,20 +391,26 @@ abstract class BCodeIdiomatic extends SubComponent {
final def rem(tk: BType) { emitPrimitive(JCodeMethodN.remOpcodes, tk) } // can-multi-thread
// can-multi-thread
- final def invokespecial(owner: String, name: String, desc: String) {
- jmethod.visitMethodInsn(Opcodes.INVOKESPECIAL, owner, name, desc, false)
+ final def invokespecial(owner: String, name: String, desc: String, pos: Position) {
+ addInvoke(Opcodes.INVOKESPECIAL, owner, name, desc, false, pos)
}
// can-multi-thread
- final def invokestatic(owner: String, name: String, desc: String) {
- jmethod.visitMethodInsn(Opcodes.INVOKESTATIC, owner, name, desc, false)
+ final def invokestatic(owner: String, name: String, desc: String, pos: Position) {
+ addInvoke(Opcodes.INVOKESTATIC, owner, name, desc, false, pos)
}
// can-multi-thread
- final def invokeinterface(owner: String, name: String, desc: String) {
- jmethod.visitMethodInsn(Opcodes.INVOKEINTERFACE, owner, name, desc, true)
+ final def invokeinterface(owner: String, name: String, desc: String, pos: Position) {
+ addInvoke(Opcodes.INVOKEINTERFACE, owner, name, desc, true, pos)
}
// can-multi-thread
- final def invokevirtual(owner: String, name: String, desc: String) {
- jmethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, owner, name, desc, false)
+ final def invokevirtual(owner: String, name: String, desc: String, pos: Position) {
+ addInvoke(Opcodes.INVOKEVIRTUAL, owner, name, desc, false, pos)
+ }
+
+ private def addInvoke(opcode: Int, owner: String, name: String, desc: String, itf: Boolean, pos: Position) = {
+ val node = new MethodInsnNode(opcode, owner, name, desc, itf)
+ jmethod.instructions.add(node)
+ if (settings.YoptInlinerEnabled) callsitePositions(node) = pos
}
// can-multi-thread
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala
index 142c901c21..2a06c62e37 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala
@@ -4,18 +4,17 @@
*/
-package scala
-package tools.nsc
+package scala.tools.nsc
package backend
package jvm
import scala.collection.{ mutable, immutable }
+import scala.tools.nsc.backend.jvm.opt.ByteCodeRepository
import scala.tools.nsc.symtab._
-import scala.annotation.switch
import scala.tools.asm
-import scala.tools.asm.util.{TraceMethodVisitor, ASMifier}
-import java.io.PrintWriter
+import GenBCode._
+import BackendReporting._
/*
*
@@ -101,6 +100,8 @@ abstract class BCodeSkelBuilder extends BCodeHelpers {
isCZRemote = isRemote(claszSymbol)
thisName = internalName(claszSymbol)
+ val classBType = classBTypeFromSymbol(claszSymbol)
+
cnode = new asm.tree.ClassNode()
initJClass(cnode)
@@ -118,16 +119,21 @@ abstract class BCodeSkelBuilder extends BCodeHelpers {
addClassFields()
- innerClassBufferASM ++= classBTypeFromSymbol(claszSymbol).info.nestedClasses
+ innerClassBufferASM ++= classBType.info.get.nestedClasses
gen(cd.impl)
addInnerClassesASM(cnode, innerClassBufferASM.toList)
+ cnode.visitAttribute(classBType.inlineInfoAttribute.get)
+
if (AsmUtils.traceClassEnabled && cnode.name.contains(AsmUtils.traceClassPattern))
AsmUtils.traceClass(cnode)
- cnode.innerClasses
- assert(cd.symbol == claszSymbol, "Someone messed up BCodePhase.claszSymbol during genPlainClass().")
+ if (settings.YoptInlinerEnabled) {
+ // The inliner needs to find all classes in the code repo, also those being compiled
+ byteCodeRepository.add(cnode, ByteCodeRepository.CompilationUnit)
+ }
+ assert(cd.symbol == claszSymbol, "Someone messed up BCodePhase.claszSymbol during genPlainClass().")
} // end of method genPlainClass()
/*
@@ -137,9 +143,9 @@ abstract class BCodeSkelBuilder extends BCodeHelpers {
val ps = claszSymbol.info.parents
val superClass: String = if (ps.isEmpty) ObjectReference.internalName else internalName(ps.head.typeSymbol)
- val interfaceNames = classBTypeFromSymbol(claszSymbol).info.interfaces map {
+ val interfaceNames = classBTypeFromSymbol(claszSymbol).info.get.interfaces map {
case classBType =>
- if (classBType.isNestedClass) { innerClassBufferASM += classBType }
+ if (classBType.isNestedClass.get) { innerClassBufferASM += classBType }
classBType.internalName
}
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala
index ec26ef9b48..d8a17e975e 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala
@@ -6,18 +6,23 @@
package scala.tools.nsc
package backend.jvm
+import scala.annotation.switch
+import scala.collection.concurrent.TrieMap
+import scala.reflect.internal.util.Position
import scala.tools.asm
import asm.Opcodes
-import scala.tools.asm.tree.{InnerClassNode, ClassNode}
-import opt.ByteCodeRepository
+import scala.tools.asm.tree.{MethodInsnNode, InnerClassNode, ClassNode}
+import scala.tools.nsc.backend.jvm.BTypes.{InlineInfo, MethodInlineInfo}
+import scala.tools.nsc.backend.jvm.BackendReporting._
+import scala.tools.nsc.backend.jvm.opt._
import scala.collection.convert.decorateAsScala._
/**
- * The BTypes component defines The BType class hierarchy. BTypes encapsulate all type information
+ * The BTypes component defines The BType class hierarchy. A BType stores all type information
* that is required after building the ASM nodes. This includes optimizations, generation of
* InnerClass attributes and generation of stack map frames.
*
- * This representation is immutable and independent of the compiler data structures, hence it can
+ * The representation is immutable and independent of the compiler data structures, hence it can
* be queried by concurrent threads.
*/
abstract class BTypes {
@@ -34,9 +39,24 @@ abstract class BTypes {
*/
val byteCodeRepository: ByteCodeRepository
+ val inliner: Inliner[this.type]
+
+ val callGraph: CallGraph[this.type]
+
+ val backendReporting: BackendReporting
+
// Allows to define per-run caches here and in the CallGraph component, which don't have a global
def recordPerRunCache[T <: collection.generic.Clearable](cache: T): T
+ // 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
+
+ // Settings that define what kind of optimizer warnings are emitted.
+ def warnSettings: WarnSettings
+
/**
* A map from internal names to ClassBTypes. Every ClassBType is added to this map on its
* construction.
@@ -48,13 +68,57 @@ abstract class BTypes {
* Concurrent because stack map frames are computed when in the class writer, which might run
* on multiple classes concurrently.
*/
- val classBTypeFromInternalName: collection.concurrent.Map[InternalName, ClassBType] = recordPerRunCache(collection.concurrent.TrieMap.empty[InternalName, ClassBType])
+ val classBTypeFromInternalName: collection.concurrent.Map[InternalName, ClassBType] = recordPerRunCache(TrieMap.empty)
+
+ /**
+ * Store the position of every MethodInsnNode during code generation. This allows each callsite
+ * in the call graph to remember its source position, which is required for inliner warnings.
+ */
+ val callsitePositions: collection.concurrent.Map[MethodInsnNode, Position] = recordPerRunCache(TrieMap.empty)
+
+ /**
+ * Contains the internal names of all classes that are defined in Java source files of the current
+ * compilation run (mixed compilation). Used for more detailed error reporting.
+ */
+ val javaDefinedClasses: collection.mutable.Set[InternalName] = recordPerRunCache(collection.mutable.Set.empty)
+
+ /**
+ * Obtain the BType for a type descriptor or internal name. For class descriptors, the ClassBType
+ * is constructed by parsing the corresponding classfile.
+ *
+ * Some JVM operations use either a full descriptor or only an internal name. Example:
+ * ANEWARRAY java/lang/String // a new array of strings (internal name for the String class)
+ * ANEWARRAY [Ljava/lang/String; // a new array of array of string (full descriptor for the String class)
+ *
+ * This method supports both descriptors and internal names.
+ */
+ def bTypeForDescriptorOrInternalNameFromClassfile(desc: String): BType = (desc(0): @switch) match {
+ case 'V' => UNIT
+ case 'Z' => BOOL
+ case 'C' => CHAR
+ case 'B' => BYTE
+ case 'S' => SHORT
+ case 'I' => INT
+ case 'F' => FLOAT
+ case 'J' => LONG
+ case 'D' => DOUBLE
+ case '[' => ArrayBType(bTypeForDescriptorOrInternalNameFromClassfile(desc.substring(1)))
+ case 'L' if desc.last == ';' => classBTypeFromParsedClassfile(desc.substring(1, desc.length - 1))
+ case _ => classBTypeFromParsedClassfile(desc)
+ }
/**
- * Parse the classfile for `internalName` and construct the [[ClassBType]].
+ * Parse the classfile for `internalName` and construct the [[ClassBType]]. If the classfile cannot
+ * be found in the `byteCodeRepository`, the `info` of the resulting ClassBType is undefined.
*/
def classBTypeFromParsedClassfile(internalName: InternalName): ClassBType = {
- classBTypeFromClassNode(byteCodeRepository.classNode(internalName))
+ classBTypeFromInternalName.getOrElse(internalName, {
+ val res = ClassBType(internalName)
+ byteCodeRepository.classNode(internalName) match {
+ case Left(msg) => res.info = Left(NoClassBTypeInfoMissingBytecode(msg)); res
+ case Right(c) => setClassInfoFromParsedClassfile(c, res)
+ }
+ })
}
/**
@@ -62,11 +126,11 @@ abstract class BTypes {
*/
def classBTypeFromClassNode(classNode: ClassNode): ClassBType = {
classBTypeFromInternalName.getOrElse(classNode.name, {
- setClassInfo(classNode, ClassBType(classNode.name))
+ setClassInfoFromParsedClassfile(classNode, ClassBType(classNode.name))
})
}
- private def setClassInfo(classNode: ClassNode, classBType: ClassBType): ClassBType = {
+ private def setClassInfoFromParsedClassfile(classNode: ClassNode, classBType: ClassBType): ClassBType = {
val superClass = classNode.superName match {
case null =>
assert(classNode.name == ObjectReference.internalName, s"class with missing super type: ${classNode.name}")
@@ -89,11 +153,13 @@ abstract class BTypes {
* For local and anonymous classes, innerClassNode.outerName is null. Such classes are required
* to have an EnclosingMethod attribute declaring the outer class. So we keep those local and
* anonymous classes whose outerClass is classNode.name.
- *
*/
def nestedInCurrentClass(innerClassNode: InnerClassNode): Boolean = {
(innerClassNode.outerName != null && innerClassNode.outerName == classNode.name) ||
- (innerClassNode.outerName == null && byteCodeRepository.classNode(innerClassNode.name).outerClass == classNode.name)
+ (innerClassNode.outerName == null && {
+ val classNodeForInnerClass = byteCodeRepository.classNode(innerClassNode.name).get // TODO: don't get here, but set the info to Left at the end
+ classNodeForInnerClass.outerClass == classNode.name
+ })
}
val nestedClasses: List[ClassBType] = classNode.innerClasses.asScala.collect({
@@ -116,11 +182,58 @@ abstract class BTypes {
val staticFlag = (innerEntry.access & Opcodes.ACC_STATIC) != 0
NestedInfo(enclosingClass, Option(innerEntry.outerName), Option(innerEntry.innerName), staticFlag)
}
- classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo)
+
+ val inlineInfo = inlineInfoFromClassfile(classNode)
+
+ classBType.info = Right(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] = {
+ if (classNode.attrs == null) None
+ else classNode.attrs.asScala.collect({ case a: InlineInfoAttribute => a}).headOption.map(_.inlineInfo)
+ }
+
+ def fromClassfileWithoutAttribute = {
+ val warning = {
+ val isScala = classNode.attrs != null && classNode.attrs.asScala.exists(a => a.`type` == BTypes.ScalaAttributeName || a.`type` == BTypes.ScalaSigAttributeName)
+ if (isScala) Some(NoInlineInfoAttribute(classNode.name))
+ else None
+ }
+ // 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)
+ }
+
+ // 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.
*/
@@ -184,7 +297,7 @@ abstract class BTypes {
* promotions (e.g. BYTE to INT). Its operation can be visualized more easily in terms of the
* Java bytecode type hierarchy.
*/
- final def conformsTo(other: BType): Boolean = {
+ final def conformsTo(other: BType): Either[NoClassBTypeInfo, Boolean] = tryEither(Right({
assert(isRef || isPrimitive, s"conformsTo cannot handle $this")
assert(other.isRef || other.isPrimitive, s"conformsTo cannot handle $other")
@@ -192,7 +305,7 @@ abstract class BTypes {
case ArrayBType(component) =>
if (other == ObjectReference || other == jlCloneableReference || other == jioSerializableReference) true
else other match {
- case ArrayBType(otherComponoent) => component.conformsTo(otherComponoent)
+ case ArrayBType(otherComponoent) => component.conformsTo(otherComponoent).orThrow
case _ => false
}
@@ -201,7 +314,7 @@ abstract class BTypes {
if (other.isBoxed) this == other
else if (other == ObjectReference) true
else other match {
- case otherClassType: ClassBType => classType.isSubtypeOf(otherClassType) // e.g., java/lang/Double conforms to java/lang/Number
+ case otherClassType: ClassBType => classType.isSubtypeOf(otherClassType).orThrow // e.g., java/lang/Double conforms to java/lang/Number
case _ => false
}
} else if (isNullType) {
@@ -211,7 +324,7 @@ abstract class BTypes {
} else if (isNothingType) {
true
} else other match {
- case otherClassType: ClassBType => classType.isSubtypeOf(otherClassType)
+ case otherClassType: ClassBType => classType.isSubtypeOf(otherClassType).orThrow
// case ArrayBType(_) => this.isNullType // documentation only, because `if (isNullType)` above covers this case
case _ =>
// isNothingType || // documentation only, because `if (isNothingType)` above covers this case
@@ -226,7 +339,7 @@ abstract class BTypes {
assert(isPrimitive && other.isPrimitive, s"Expected primitive types $this - $other")
this == other
}
- }
+ }))
/**
* Compute the upper bound of two types.
@@ -245,7 +358,7 @@ abstract class BTypes {
ObjectReference
case _: MethodBType =>
- throw new AssertionError(s"unexpected method type when computing maxType: $this")
+ assertionError(s"unexpected method type when computing maxType: $this")
}
/**
@@ -336,7 +449,7 @@ abstract class BTypes {
*/
final def maxValueType(other: BType): BType = {
- def uncomparable: Nothing = throw new AssertionError(s"Cannot compute maxValueType: $this, $other")
+ def uncomparable: Nothing = assertionError(s"Cannot compute maxValueType: $this, $other")
if (!other.isPrimitive && !other.isNothingType) uncomparable
@@ -665,6 +778,21 @@ abstract class BTypes {
/**
* A ClassBType represents a class or interface type. The necessary information to build a
* ClassBType is extracted from compiler symbols and types, see BTypesFromSymbols.
+ *
+ * The `info` field contains either the class information on an error message why the info could
+ * not be computed. There are two reasons for an erroneous info:
+ * 1. The ClassBType was built from a class symbol that stems from a java source file, and the
+ * symbol's type could not be completed successfully (SI-9111)
+ * 2. The ClassBType should be built from a classfile, but the class could not be found on the
+ * compilation classpath.
+ *
+ * Note that all ClassBTypes required in a non-optimzied run are built during code generation from
+ * the class symbols referenced by the ASTs, so they have a valid info. Therefore the backend
+ * often invokes `info.get` (which asserts the info to exist) when reading data from the ClassBType.
+ *
+ * The inliner on the other hand uses ClassBTypes that are built from classfiles, which may have
+ * a missing info. In order not to crash the compiler unnecessarily, the inliner does not force
+ * infos using `get`, but it reports inliner warnings for missing infos that prevent inlining.
*/
final case class ClassBType(internalName: InternalName) extends RefBType {
/**
@@ -674,14 +802,14 @@ abstract class BTypes {
* B.info.nestedInfo.outerClass == A
* A.info.nestedClasses contains B
*/
- private var _info: ClassInfo = null
+ private var _info: Either[NoClassBTypeInfo, ClassInfo] = null
- def info: ClassInfo = {
+ def info: Either[NoClassBTypeInfo, ClassInfo] = {
assert(_info != null, s"ClassBType.info not yet assigned: $this")
_info
}
- def info_=(i: ClassInfo): Unit = {
+ def info_=(i: Either[NoClassBTypeInfo, ClassInfo]): Unit = {
assert(_info == null, s"Cannot set ClassBType.info multiple times: $this")
_info = i
checkInfoConsistency()
@@ -690,27 +818,29 @@ abstract class BTypes {
classBTypeFromInternalName(internalName) = this
private def checkInfoConsistency(): Unit = {
+ if (info.isLeft) return
+
// we assert some properties. however, some of the linked ClassBType (members, superClass,
// interfaces) may not yet have an `_info` (initialization of cyclic structures). so we do a
- // best-effort verification.
- def ifInit(c: ClassBType)(p: ClassBType => Boolean): Boolean = c._info == null || p(c)
+ // best-effort verification. also we don't report an error if the info is a Left.
+ def ifInit(c: ClassBType)(p: ClassBType => Boolean): Boolean = c._info == null || c.info.isLeft || p(c)
def isJLO(t: ClassBType) = t.internalName == ObjectReference.internalName
assert(!ClassBType.isInternalPhantomType(internalName), s"Cannot create ClassBType for phantom type $this")
assert(
- if (info.superClass.isEmpty) { isJLO(this) || (isCompilingPrimitive && ClassBType.hasNoSuper(internalName)) }
- else if (isInterface) isJLO(info.superClass.get)
- else !isJLO(this) && ifInit(info.superClass.get)(!_.isInterface),
- s"Invalid superClass in $this: ${info.superClass}"
+ if (info.get.superClass.isEmpty) { isJLO(this) || (isCompilingPrimitive && ClassBType.hasNoSuper(internalName)) }
+ else if (isInterface.get) isJLO(info.get.superClass.get)
+ else !isJLO(this) && ifInit(info.get.superClass.get)(!_.isInterface.get),
+ s"Invalid superClass in $this: ${info.get.superClass}"
)
assert(
- info.interfaces.forall(c => ifInit(c)(_.isInterface)),
- s"Invalid interfaces in $this: ${info.interfaces}"
+ info.get.interfaces.forall(c => ifInit(c)(_.isInterface.get)),
+ s"Invalid interfaces in $this: ${info.get.interfaces}"
)
- assert(info.nestedClasses.forall(c => ifInit(c)(_.isNestedClass)), info.nestedClasses)
+ assert(info.get.nestedClasses.forall(c => ifInit(c)(_.isNestedClass.get)), info.get.nestedClasses)
}
/**
@@ -718,20 +848,37 @@ abstract class BTypes {
*/
def simpleName: String = internalName.split("/").last
- def isInterface = (info.flags & asm.Opcodes.ACC_INTERFACE) != 0
+ def isInterface: Either[NoClassBTypeInfo, Boolean] = info.map(i => (i.flags & asm.Opcodes.ACC_INTERFACE) != 0)
+
+ def superClassesTransitive: Either[NoClassBTypeInfo, List[ClassBType]] = info.flatMap(i => i.superClass match {
+ case None => Right(Nil)
+ case Some(sc) => sc.superClassesTransitive.map(sc :: _)
+ })
- def superClassesTransitive: List[ClassBType] = info.superClass match {
- case None => Nil
- case Some(sc) => sc :: sc.superClassesTransitive
+ /**
+ * The prefix of the internal name until the last '/', or the empty string.
+ */
+ def packageInternalName: String = {
+ val name = internalName
+ name.lastIndexOf('/') match {
+ case -1 => ""
+ case i => name.substring(0, i)
+ }
}
- def isNestedClass = info.nestedInfo.isDefined
+ def isPublic: Either[NoClassBTypeInfo, Boolean] = info.map(i => (i.flags & asm.Opcodes.ACC_PUBLIC) != 0)
+
+ def isNestedClass: Either[NoClassBTypeInfo, Boolean] = info.map(_.nestedInfo.isDefined)
- def enclosingNestedClassesChain: List[ClassBType] =
- if (isNestedClass) this :: info.nestedInfo.get.enclosingClass.enclosingNestedClassesChain
- else Nil
+ def enclosingNestedClassesChain: Either[NoClassBTypeInfo, List[ClassBType]] = {
+ isNestedClass.flatMap(isNested => {
+ // if isNested is true, we know that info.get is defined, and nestedInfo.get is also defined.
+ if (isNested) info.get.nestedInfo.get.enclosingClass.enclosingNestedClassesChain.map(this :: _)
+ else Right(Nil)
+ })
+ }
- def innerClassAttributeEntry: Option[InnerClassEntry] = info.nestedInfo map {
+ def innerClassAttributeEntry: Either[NoClassBTypeInfo, Option[InnerClassEntry]] = info.map(i => i.nestedInfo map {
case NestedInfo(_, outerName, innerName, isStaticNestedClass) =>
InnerClassEntry(
internalName,
@@ -739,28 +886,39 @@ abstract class BTypes {
innerName.orNull,
GenBCode.mkFlags(
// the static flag in the InnerClass table has a special meaning, see InnerClass comment
- info.flags & ~Opcodes.ACC_STATIC,
+ i.flags & ~Opcodes.ACC_STATIC,
if (isStaticNestedClass) Opcodes.ACC_STATIC else 0
) & ClassBType.INNER_CLASSES_FLAGS
)
- }
+ })
- def isSubtypeOf(other: ClassBType): Boolean = {
- if (this == other) return true
+ def inlineInfoAttribute: Either[NoClassBTypeInfo, InlineInfoAttribute] = info.map(i => {
+ // InlineInfos are serialized for classes being compiled. For those the info was built by
+ // buildInlineInfoFromClassSymbol, which only adds a warning under SI-9111, which in turn
+ // only happens for class symbols of java source files.
+ // we could put this assertion into InlineInfoAttribute, but it is more safe to put it here
+ // where it affect only GenBCode, and not add any assertion to GenASM in 2.11.6.
+ assert(i.inlineInfo.warning.isEmpty, i.inlineInfo.warning)
+ InlineInfoAttribute(i.inlineInfo)
+ })
- if (isInterface) {
- if (other == ObjectReference) return true // interfaces conform to Object
- if (!other.isInterface) return false // this is an interface, the other is some class other than object. interfaces cannot extend classes, so the result is false.
+ def isSubtypeOf(other: ClassBType): Either[NoClassBTypeInfo, Boolean] = try {
+ if (this == other) return Right(true)
+ if (isInterface.orThrow) {
+ if (other == ObjectReference) return Right(true) // interfaces conform to Object
+ if (!other.isInterface.orThrow) return Right(false) // this is an interface, the other is some class other than object. interfaces cannot extend classes, so the result is false.
// else: this and other are both interfaces. continue to (*)
} else {
- val sc = info.superClass
- if (sc.isDefined && sc.get.isSubtypeOf(other)) return true // the superclass of this class conforms to other
- if (!other.isInterface) return false // this and other are both classes, and the superclass of this does not conform
+ val sc = info.orThrow.superClass
+ if (sc.isDefined && sc.get.isSubtypeOf(other).orThrow) return Right(true) // the superclass of this class conforms to other
+ if (!other.isInterface.orThrow) return Right(false) // this and other are both classes, and the superclass of this does not conform
// else: this is a class, the other is an interface. continue to (*)
}
// (*) check if some interface of this class conforms to other.
- info.interfaces.exists(_.isSubtypeOf(other))
+ Right(info.orThrow.interfaces.exists(_.isSubtypeOf(other).orThrow))
+ } catch {
+ case Invalid(noInfo: NoClassBTypeInfo) => Left(noInfo)
}
/**
@@ -770,34 +928,36 @@ abstract class BTypes {
* http://comments.gmane.org/gmane.comp.java.vm.languages/2293
* https://issues.scala-lang.org/browse/SI-3872
*/
- def jvmWiseLUB(other: ClassBType): ClassBType = {
+ def jvmWiseLUB(other: ClassBType): Either[NoClassBTypeInfo, ClassBType] = {
def isNotNullOrNothing(c: ClassBType) = !c.isNullType && !c.isNothingType
assert(isNotNullOrNothing(this) && isNotNullOrNothing(other), s"jvmWiseLub for null or nothing: $this - $other")
- val res: ClassBType = (this.isInterface, other.isInterface) match {
- case (true, true) =>
- // exercised by test/files/run/t4761.scala
- if (other.isSubtypeOf(this)) this
- else if (this.isSubtypeOf(other)) other
- else ObjectReference
-
- case (true, false) =>
- if (other.isSubtypeOf(this)) this else ObjectReference
-
- case (false, true) =>
- if (this.isSubtypeOf(other)) other else ObjectReference
+ tryEither {
+ val res: ClassBType = (this.isInterface.orThrow, other.isInterface.orThrow) match {
+ case (true, true) =>
+ // exercised by test/files/run/t4761.scala
+ if (other.isSubtypeOf(this).orThrow) this
+ else if (this.isSubtypeOf(other).orThrow) other
+ else ObjectReference
+
+ case (true, false) =>
+ if (other.isSubtypeOf(this).orThrow) this else ObjectReference
+
+ case (false, true) =>
+ if (this.isSubtypeOf(other).orThrow) other else ObjectReference
+
+ case _ =>
+ // TODO @lry I don't really understand the reasoning here.
+ // Both this and other are classes. The code takes (transitively) all superclasses and
+ // finds the first common one.
+ // MOST LIKELY the answer can be found here, see the comments and links by Miguel:
+ // - https://issues.scala-lang.org/browse/SI-3872
+ firstCommonSuffix(this :: this.superClassesTransitive.orThrow, other :: other.superClassesTransitive.orThrow)
+ }
- case _ =>
- // TODO @lry I don't really understand the reasoning here.
- // Both this and other are classes. The code takes (transitively) all superclasses and
- // finds the first common one.
- // MOST LIKELY the answer can be found here, see the comments and links by Miguel:
- // - https://issues.scala-lang.org/browse/SI-3872
- firstCommonSuffix(this :: this.superClassesTransitive, other :: other.superClassesTransitive)
+ assert(isNotNullOrNothing(res), s"jvmWiseLub computed: $res")
+ Right(res)
}
-
- assert(isNotNullOrNothing(res), s"jvmWiseLub computed: $res")
- res
}
private def firstCommonSuffix(as: List[ClassBType], bs: List[ClassBType]): ClassBType = {
@@ -859,9 +1019,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 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])
+ nestedClasses: List[ClassBType], nestedInfo: Option[NestedInfo],
+ inlineInfo: InlineInfo)
/**
* Information required to add a class to an InnerClass table.
@@ -962,11 +1124,15 @@ object BTypes {
* @param methodInfos The [[MethodInlineInfo]]s for the methods declared in this class.
* The map is indexed by the string s"$name$descriptor" (to
* disambiguate overloads).
+ *
+ * @param warning Contains an warning message if an error occured when building this
+ * InlineInfo, for example if some classfile could not be found on
+ * the classpath. This warning can be reported later by the inliner.
*/
final case class InlineInfo(traitImplClassSelfType: Option[InternalName],
isEffectivelyFinal: Boolean,
methodInfos: Map[String, MethodInlineInfo],
- warning: Option[String])
+ warning: Option[ClassInlineInfoWarning])
val EmptyInlineInfo = InlineInfo(None, false, Map.empty, None)
@@ -984,4 +1150,8 @@ object BTypes {
traitMethodWithStaticImplementation: Boolean,
annotatedInline: Boolean,
annotatedNoInline: Boolean)
+
+ // no static way (without symbol table instance) to get to nme.ScalaATTR / ScalaSignatureATTR
+ val ScalaAttributeName = "Scala"
+ val ScalaSigAttributeName = "ScalaSig"
} \ No newline at end of file
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala
index 2af665d31c..eeb6ed24a2 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala
@@ -7,10 +7,9 @@ package scala.tools.nsc
package backend.jvm
import scala.tools.asm
-import opt.ByteCodeRepository
-import scala.tools.asm.tree.ClassNode
-import scala.tools.nsc.backend.jvm.opt.ByteCodeRepository.Source
-import BTypes.InternalName
+import scala.tools.nsc.backend.jvm.opt.{CallGraph, Inliner, ByteCodeRepository}
+import scala.tools.nsc.backend.jvm.BTypes.{InlineInfo, MethodInlineInfo, InternalName}
+import BackendReporting._
/**
* This class mainly contains the method classBTypeFromSymbol, which extracts the necessary
@@ -36,7 +35,13 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes {
val coreBTypes = new CoreBTypesProxy[this.type](this)
import coreBTypes._
- val byteCodeRepository = new ByteCodeRepository(global.classPath, recordPerRunCache(collection.concurrent.TrieMap.empty[InternalName, (ClassNode, Source)]))
+ val byteCodeRepository = new ByteCodeRepository(global.classPath, javaDefinedClasses, recordPerRunCache(collection.concurrent.TrieMap.empty))
+
+ val inliner: Inliner[this.type] = new Inliner(this)
+
+ val callGraph: CallGraph[this.type] = new CallGraph(this)
+
+ val backendReporting: BackendReporting = new BackendReportingImpl(global)
final def initializeCoreBTypes(): Unit = {
coreBTypes.setBTypes(new CoreBTypes[this.type](this))
@@ -44,6 +49,20 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes {
def recordPerRunCache[T <: collection.generic.Clearable](cache: T): T = perRunCaches.recordCache(cache)
+ def inlineGlobalEnabled: Boolean = settings.YoptInlineGlobal
+
+ def inlinerEnabled: Boolean = settings.YoptInlinerEnabled
+
+ def warnSettings: WarnSettings = {
+ val c = settings.YoptWarningsChoices
+ // cannot extract settings.YoptWarnings into a local val due to some dependent typing issue.
+ WarnSettings(
+ !settings.YoptWarnings.isSetByUser || settings.YoptWarnings.contains(c.atInlineFailedSummary.name) || settings.YoptWarnings.contains(c.atInlineFailed.name),
+ settings.YoptWarnings.contains(c.noInlineMixed.name),
+ settings.YoptWarnings.contains(c.noInlineMissingBytecode.name),
+ settings.YoptWarnings.contains(c.noInlineMissingScalaInlineInfoAttr.name))
+ }
+
// helpers that need access to global.
// TODO @lry create a separate component, they don't belong to BTypesFromSymbols
@@ -76,22 +95,131 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes {
// end helpers
/**
- * The ClassBType for a class symbol `sym`.
+ * The ClassBType for a class symbol `classSym`.
+ *
+ * The class symbol scala.Nothing is mapped to the class scala.runtime.Nothing$. Similarly,
+ * scala.Null is mapped to scala.runtime.Null$. This is because there exist no class files
+ * for the Nothing / Null. If used for example as a parameter type, we use the runtime classes
+ * in the classfile method signature.
+ *
+ * Note that the referenced class symbol may be an implementation class. For example when
+ * compiling a mixed-in method that forwards to the static method in the implementation class,
+ * the class descriptor of the receiver (the implementation class) is obtained by creating the
+ * ClassBType.
*/
final def classBTypeFromSymbol(classSym: Symbol): ClassBType = {
assert(classSym != NoSymbol, "Cannot create ClassBType from NoSymbol")
assert(classSym.isClass, s"Cannot create ClassBType from non-class symbol $classSym")
- assert(
- (!primitiveTypeMap.contains(classSym) || isCompilingPrimitive) &&
- (classSym != NothingClass && classSym != NullClass),
- s"Cannot create ClassBType for special class symbol ${classSym.fullName}")
+ assertClassNotArrayNotPrimitive(classSym)
+ assert(!primitiveTypeMap.contains(classSym) || isCompilingPrimitive, s"Cannot create ClassBType for primitive class symbol $classSym")
+ if (classSym == NothingClass) RT_NOTHING
+ else if (classSym == NullClass) RT_NULL
+ else {
+ val internalName = classSym.javaBinaryName.toString
+ classBTypeFromInternalName.getOrElse(internalName, {
+ // The new ClassBType is added to the map in its constructor, before we set its info. This
+ // allows initializing cyclic dependencies, see the comment on variable ClassBType._info.
+ val res = ClassBType(internalName)
+ if (completeSilentlyAndCheckErroneous(classSym)) {
+ res.info = Left(NoClassBTypeInfoClassSymbolInfoFailedSI9111(classSym.fullName))
+ res
+ } else {
+ setClassInfo(classSym, res)
+ }
+ })
+ }
+ }
- val internalName = classSym.javaBinaryName.toString
- classBTypeFromInternalName.getOrElse(internalName, {
- // The new ClassBType is added to the map in its constructor, before we set its info. This
- // allows initializing cyclic dependencies, see the comment on variable ClassBType._info.
- setClassInfo(classSym, ClassBType(internalName))
- })
+ /**
+ * Builds a [[MethodBType]] for a method symbol.
+ */
+ final def methodBTypeFromSymbol(methodSymbol: Symbol): MethodBType = {
+ assert(methodSymbol.isMethod, s"not a method-symbol: $methodSymbol")
+ val resultType: BType =
+ if (methodSymbol.isClassConstructor || methodSymbol.isConstructor) UNIT
+ else typeToBType(methodSymbol.tpe.resultType)
+ MethodBType(methodSymbol.tpe.paramTypes map typeToBType, resultType)
+ }
+
+ /**
+ * This method returns the BType for a type reference, for example a parameter type.
+ *
+ * If `t` references a class, typeToBType ensures that the class is not an implementation class.
+ * See also comment on classBTypeFromSymbol, which is invoked for implementation classes.
+ */
+ final def typeToBType(t: Type): BType = {
+ import definitions.ArrayClass
+
+ /**
+ * Primitive types are represented as TypeRefs to the class symbol of, for example, scala.Int.
+ * The `primitiveTypeMap` maps those class symbols to the corresponding PrimitiveBType.
+ */
+ def primitiveOrClassToBType(sym: Symbol): BType = {
+ assertClassNotArray(sym)
+ assert(!sym.isImplClass, sym)
+ primitiveTypeMap.getOrElse(sym, classBTypeFromSymbol(sym))
+ }
+
+ /**
+ * When compiling Array.scala, the type parameter T is not erased and shows up in method
+ * signatures, e.g. `def apply(i: Int): T`. A TyperRef to T is replaced by ObjectReference.
+ */
+ def nonClassTypeRefToBType(sym: Symbol): ClassBType = {
+ assert(sym.isType && isCompilingArray, sym)
+ ObjectReference
+ }
+
+ t.dealiasWiden match {
+ case TypeRef(_, ArrayClass, List(arg)) => ArrayBType(typeToBType(arg)) // Array type such as Array[Int] (kept by erasure)
+ case TypeRef(_, sym, _) if !sym.isClass => nonClassTypeRefToBType(sym) // See comment on nonClassTypeRefToBType
+ case TypeRef(_, sym, _) => primitiveOrClassToBType(sym) // Common reference to a type such as scala.Int or java.lang.String
+ case ClassInfoType(_, _, sym) => primitiveOrClassToBType(sym) // We get here, for example, for genLoadModule, which invokes typeToBType(moduleClassSymbol.info)
+
+ /* AnnotatedType should (probably) be eliminated by erasure. However we know it happens for
+ * meta-annotated annotations (@(ann @getter) val x = 0), so we don't emit a warning.
+ * The type in the AnnotationInfo is an AnnotatedTpe. Tested in jvm/annotations.scala.
+ */
+ case a @ AnnotatedType(_, t) =>
+ debuglog(s"typeKind of annotated type $a")
+ typeToBType(t)
+
+ /* ExistentialType should (probably) be eliminated by erasure. We know they get here for
+ * classOf constants:
+ * class C[T]
+ * class T { final val k = classOf[C[_]] }
+ */
+ case e @ ExistentialType(_, t) =>
+ debuglog(s"typeKind of existential type $e")
+ typeToBType(t)
+
+ /* The cases below should probably never occur. They are kept for now to avoid introducing
+ * new compiler crashes, but we added a warning. The compiler / library bootstrap and the
+ * test suite don't produce any warning.
+ */
+
+ case tp =>
+ currentUnit.warning(tp.typeSymbol.pos,
+ s"an unexpected type representation reached the compiler backend while compiling $currentUnit: $tp. " +
+ "If possible, please file a bug on issues.scala-lang.org.")
+
+ tp match {
+ case ThisType(ArrayClass) => ObjectReference // was introduced in 9b17332f11 to fix SI-999, but this code is not reached in its test, or any other test
+ case ThisType(sym) => classBTypeFromSymbol(sym)
+ case SingleType(_, sym) => primitiveOrClassToBType(sym)
+ case ConstantType(_) => typeToBType(t.underlying)
+ case RefinedType(parents, _) => parents.map(typeToBType(_).asClassBType).reduceLeft((a, b) => a.jvmWiseLUB(b).get)
+ }
+ }
+ }
+
+ def assertClassNotArray(sym: Symbol): Unit = {
+ assert(sym.isClass, sym)
+ assert(sym != definitions.ArrayClass || isCompilingArray, sym)
+ }
+
+ def assertClassNotArrayNotPrimitive(sym: Symbol): Unit = {
+ assertClassNotArray(sym)
+ assert(!primitiveTypeMap.contains(sym) || isCompilingPrimitive, sym)
}
private def setClassInfo(classSym: Symbol, classBType: ClassBType): ClassBType = {
@@ -215,7 +343,9 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes {
val nestedInfo = buildNestedInfo(classSym)
- classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo)
+ val inlineInfo = buildInlineInfo(classSym, classBType.internalName)
+
+ classBType.info = Right(ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfo))
classBType
}
@@ -271,6 +401,40 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes {
}
/**
+ * 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 // InlineInfo required for classes being compiled, we have to create the classfile attribute
+ else if (!inlinerEnabled) BTypes.EmptyInlineInfo // For other classes, we need the InlineInfo only inf the inliner is enabled.
+ else {
+ // 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 Right(classNode) =>
+ inlineInfoFromClassfile(classNode)
+ case Left(missingClass) =>
+ InlineInfo(None, false, Map.empty, Some(ClassNotFoundWhenBuildingInlineInfoFromSymbol(missingClass)))
+ }
+ }
+ }
+
+ /**
* For top-level objects without a companion class, the compilere generates a mirror class with
* static forwarders (Java compat). There's no symbol for the mirror class, but we still need a
* ClassBType (its info.nestedClasses will hold the InnerClass entries, see comment in BTypes).
@@ -282,13 +446,13 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes {
val c = ClassBType(internalName)
// class info consistent with BCodeHelpers.genMirrorClass
val nested = exitingPickler(memberClassesForInnerClassTable(moduleClassSym)) map classBTypeFromSymbol
- c.info = ClassInfo(
+ c.info = Right(ClassInfo(
superClass = Some(ObjectReference),
interfaces = Nil,
flags = asm.Opcodes.ACC_SUPER | asm.Opcodes.ACC_PUBLIC | asm.Opcodes.ACC_FINAL,
nestedClasses = nested,
- nestedInfo = None
- )
+ nestedInfo = None,
+ InlineInfo(None, true, Map.empty, None))) // no InlineInfo needed, scala never invokes methods on the mirror class
c
})
}
@@ -322,8 +486,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/BackendReporting.scala b/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala
new file mode 100644
index 0000000000..a06fb4bab8
--- /dev/null
+++ b/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala
@@ -0,0 +1,265 @@
+package scala.tools.nsc
+package backend.jvm
+
+import scala.tools.asm.tree.{AbstractInsnNode, MethodNode}
+import scala.tools.nsc.backend.jvm.BTypes.InternalName
+import scala.reflect.internal.util.Position
+
+/**
+ * Interface for emitting inline warnings. The interface is required because the implementation
+ * depends on Global, which is not available in BTypes (only in BTypesFromSymbols).
+ */
+sealed abstract class BackendReporting {
+ def inlinerWarning(pos: Position, message: String): Unit
+}
+
+final class BackendReportingImpl(val global: Global) extends BackendReporting {
+ import global._
+
+ def inlinerWarning(pos: Position, message: String): Unit = {
+ currentRun.reporting.inlinerWarning(pos, message)
+ }
+}
+
+/**
+ * Utilities for error reporting.
+ *
+ * Defines some tools to make error reporting with Either easier. Would be subsumed by a right-biased
+ * Either in the standard library (or scalaz \/) (Validation is different, it accumulates multiple
+ * errors).
+ */
+object BackendReporting {
+ def methodSignature(classInternalName: InternalName, name: String, desc: String) = {
+ classInternalName + "::" + name + desc
+ }
+
+ def methodSignature(classInternalName: InternalName, method: MethodNode): String = {
+ methodSignature(classInternalName, method.name, method.desc)
+ }
+
+ def assertionError(message: String): Nothing = throw new AssertionError(message)
+
+ implicit class RightBiasedEither[A, B](val v: Either[A, B]) extends AnyVal {
+ def map[U](f: B => U) = v.right.map(f)
+ def flatMap[BB](f: B => Either[A, BB]) = v.right.flatMap(f)
+ def filter(f: B => Boolean)(implicit empty: A): Either[A, B] = v match {
+ case Left(_) => v
+ case Right(e) => if (f(e)) v else Left(empty) // scalaz.\/ requires an implicit Monoid m to get m.empty
+ }
+ def foreach[U](f: B => U) = v.right.foreach(f)
+
+ def getOrElse[BB >: B](alt: => BB): BB = v.right.getOrElse(alt)
+
+ /**
+ * Get the value, fail with an assertion if this is an error.
+ */
+ def get: B = {
+ assert(v.isRight, v.left.get)
+ v.right.get
+ }
+
+ /**
+ * Get the right value of an `Either` by throwing a potential error message. Can simplify the
+ * implementation of methods that act on multiple `Either` instances. Instead of flat-mapping,
+ * the first error can be collected as
+ *
+ * tryEither {
+ * eitherOne.orThrow .... eitherTwo.orThrow ... eitherThree.orThrow
+ * }
+ */
+ def orThrow: B = v match {
+ case Left(m) => throw Invalid(m)
+ case Right(t) => t
+ }
+ }
+
+ case class Invalid[A](e: A) extends Exception
+
+ /**
+ * See documentation of orThrow above.
+ */
+ def tryEither[A, B](op: => Either[A, B]): Either[A, B] = try { op } catch { case Invalid(e) => Left(e.asInstanceOf[A]) }
+
+ final case class WarnSettings(atInlineFailed: Boolean, noInlineMixed: Boolean, noInlineMissingBytecode: Boolean, noInlineMissingScalaInlineInfoAttr: Boolean)
+
+ sealed trait OptimizerWarning {
+ def emitWarning(settings: WarnSettings): Boolean
+ }
+
+ // Method filter in RightBiasedEither requires an implicit empty value. Taking the value here
+ // in scope allows for-comprehensions that desugar into filter calls (for example when using a
+ // tuple de-constructor).
+ implicit object emptyOptimizerWarning extends OptimizerWarning {
+ def emitWarning(settings: WarnSettings): Boolean = false
+ }
+
+ sealed trait MissingBytecodeWarning extends OptimizerWarning {
+ override def toString = this match {
+ case ClassNotFound(internalName, definedInJavaSource) =>
+ s"The classfile for $internalName could not be found on the compilation classpath." + {
+ if (definedInJavaSource) "\nThe class is defined in a Java source file that is being compiled (mixed compilation), therefore no bytecode is available."
+ else ""
+ }
+
+ case MethodNotFound(name, descriptor, ownerInternalName, missingClasses) =>
+ val (javaDef, others) = missingClasses.partition(_.definedInJavaSource)
+ s"The method $name$descriptor could not be found in the class $ownerInternalName or any of its parents." +
+ (if (others.isEmpty) "" else others.map(_.internalName).mkString("\nNote that the following parent classes could not be found on the classpath: ", ", ", "")) +
+ (if (javaDef.isEmpty) "" else javaDef.map(_.internalName).mkString("\nNote that the following parent classes are defined in Java sources (mixed compilation), no bytecode is available: ", ",", ""))
+
+ case FieldNotFound(name, descriptor, ownerInternalName, missingClass) =>
+ s"The field node $name$descriptor could not be found because the classfile $ownerInternalName cannot be found on the classpath." +
+ missingClass.map(c => s" Reason:\n$c").getOrElse("")
+ }
+
+ def emitWarning(settings: WarnSettings): Boolean = this match {
+ case ClassNotFound(_, javaDefined) =>
+ if (javaDefined) settings.noInlineMixed
+ else settings.noInlineMissingBytecode
+
+ case m @ MethodNotFound(_, _, _, missing) =>
+ if (m.isArrayMethod) false
+ else settings.noInlineMissingBytecode || missing.exists(_.emitWarning(settings))
+
+ case FieldNotFound(_, _, _, missing) =>
+ settings.noInlineMissingBytecode || missing.exists(_.emitWarning(settings))
+ }
+ }
+
+ case class ClassNotFound(internalName: InternalName, definedInJavaSource: Boolean) extends MissingBytecodeWarning
+ case class MethodNotFound(name: String, descriptor: String, ownerInternalNameOrArrayDescriptor: InternalName, missingClasses: List[ClassNotFound]) extends MissingBytecodeWarning {
+ def isArrayMethod = ownerInternalNameOrArrayDescriptor.charAt(0) == '['
+ }
+ case class FieldNotFound(name: String, descriptor: String, ownerInternalName: InternalName, missingClass: Option[ClassNotFound]) extends MissingBytecodeWarning
+
+ sealed trait NoClassBTypeInfo extends OptimizerWarning {
+ override def toString = this match {
+ case NoClassBTypeInfoMissingBytecode(cause) =>
+ cause.toString
+
+ case NoClassBTypeInfoClassSymbolInfoFailedSI9111(classFullName) =>
+ s"Failed to get the type of class symbol $classFullName due to SI-9111."
+ }
+
+ def emitWarning(settings: WarnSettings): Boolean = this match {
+ case NoClassBTypeInfoMissingBytecode(cause) => cause.emitWarning(settings)
+ case NoClassBTypeInfoClassSymbolInfoFailedSI9111(_) => settings.noInlineMissingBytecode
+ }
+ }
+
+ case class NoClassBTypeInfoMissingBytecode(cause: MissingBytecodeWarning) extends NoClassBTypeInfo
+ case class NoClassBTypeInfoClassSymbolInfoFailedSI9111(classFullName: String) extends NoClassBTypeInfo
+
+ /**
+ * Used in the CallGraph for nodes where an issue occurred determining the callee information.
+ */
+ sealed trait CalleeInfoWarning extends OptimizerWarning {
+ def declarationClass: InternalName
+ def name: String
+ def descriptor: String
+
+ def warningMessageSignature = BackendReporting.methodSignature(declarationClass, name, descriptor)
+
+ override def toString = this match {
+ case MethodInlineInfoIncomplete(_, _, _, cause) =>
+ s"The inline information for $warningMessageSignature may be incomplete:\n" + cause
+
+ case MethodInlineInfoMissing(_, _, _, cause) =>
+ s"No inline information for method $warningMessageSignature could be found." +
+ cause.map(" Possible reason:\n" + _).getOrElse("")
+
+ case MethodInlineInfoError(_, _, _, cause) =>
+ s"Error while computing the inline information for method $warningMessageSignature:\n" + cause
+
+ case RewriteTraitCallToStaticImplMethodFailed(_, _, _, cause) =>
+ cause.toString
+ }
+
+ def emitWarning(settings: WarnSettings): Boolean = this match {
+ case MethodInlineInfoIncomplete(_, _, _, cause) => cause.emitWarning(settings)
+
+ case MethodInlineInfoMissing(_, _, _, Some(cause)) => cause.emitWarning(settings)
+ case MethodInlineInfoMissing(_, _, _, None) => settings.noInlineMissingBytecode
+
+ case MethodInlineInfoError(_, _, _, cause) => cause.emitWarning(settings)
+
+ case RewriteTraitCallToStaticImplMethodFailed(_, _, _, cause) => cause.emitWarning(settings)
+ }
+ }
+
+ case class MethodInlineInfoIncomplete(declarationClass: InternalName, name: String, descriptor: String, cause: ClassInlineInfoWarning) extends CalleeInfoWarning
+ case class MethodInlineInfoMissing(declarationClass: InternalName, name: String, descriptor: String, cause: Option[ClassInlineInfoWarning]) extends CalleeInfoWarning
+ case class MethodInlineInfoError(declarationClass: InternalName, name: String, descriptor: String, cause: NoClassBTypeInfo) extends CalleeInfoWarning
+ case class RewriteTraitCallToStaticImplMethodFailed(declarationClass: InternalName, name: String, descriptor: String, cause: OptimizerWarning) extends CalleeInfoWarning
+
+ sealed trait CannotInlineWarning extends OptimizerWarning {
+ def calleeDeclarationClass: InternalName
+ def name: String
+ def descriptor: String
+
+ def calleeMethodSig = BackendReporting.methodSignature(calleeDeclarationClass, name, descriptor)
+
+ override def toString = this match {
+ case IllegalAccessInstruction(_, _, _, callsiteClass, instruction) =>
+ s"The callee $calleeMethodSig contains the instruction ${AsmUtils.textify(instruction)}" +
+ s"\nthat would cause an IllegalAccessError when inlined into class $callsiteClass."
+
+ case IllegalAccessCheckFailed(_, _, _, callsiteClass, instruction, cause) =>
+ s"Failed to check if $calleeMethodSig can be safely inlined to $callsiteClass without causing an IllegalAccessError. Checking instruction ${AsmUtils.textify(instruction)} failed:\n" + cause
+
+ case MethodWithHandlerCalledOnNonEmptyStack(_, _, _, callsiteClass, callsiteName, callsiteDesc) =>
+ s"""The operand stack at the callsite in ${BackendReporting.methodSignature(callsiteClass, callsiteName, callsiteDesc)} contains more values than the
+ |arguments expected by the callee $calleeMethodSig. These values would be discarded
+ |when entering an exception handler declared in the inlined method.""".stripMargin
+
+ case SynchronizedMethod(_, _, _) =>
+ s"Method $calleeMethodSig cannot be inlined because it is synchronized."
+ }
+
+ def emitWarning(settings: WarnSettings): Boolean = this match {
+ case _: IllegalAccessInstruction | _: MethodWithHandlerCalledOnNonEmptyStack | _: SynchronizedMethod =>
+ settings.atInlineFailed
+
+ case IllegalAccessCheckFailed(_, _, _, _, _, cause) =>
+ cause.emitWarning(settings)
+ }
+ }
+ case class IllegalAccessInstruction(calleeDeclarationClass: InternalName, name: String, descriptor: String,
+ callsiteClass: InternalName, instruction: AbstractInsnNode) extends CannotInlineWarning
+ case class IllegalAccessCheckFailed(calleeDeclarationClass: InternalName, name: String, descriptor: String,
+ callsiteClass: InternalName, instruction: AbstractInsnNode, cause: OptimizerWarning) extends CannotInlineWarning
+ case class MethodWithHandlerCalledOnNonEmptyStack(calleeDeclarationClass: InternalName, name: String, descriptor: String,
+ callsiteClass: InternalName, callsiteName: String, callsiteDesc: String) extends CannotInlineWarning
+ case class SynchronizedMethod(calleeDeclarationClass: InternalName, name: String, descriptor: String) extends CannotInlineWarning
+
+ /**
+ * Used in the InlineInfo of a ClassBType, when some issue occurred obtaining the inline information.
+ */
+ sealed trait ClassInlineInfoWarning extends OptimizerWarning {
+ override def toString = this match {
+ case NoInlineInfoAttribute(internalName) =>
+ s"The Scala classfile $internalName does not have a ScalaInlineInfo attribute."
+
+ case ClassSymbolInfoFailureSI9111(classFullName) =>
+ s"Failed to get the type of a method of class symbol $classFullName due to SI-9111."
+
+ case ClassNotFoundWhenBuildingInlineInfoFromSymbol(missingClass) =>
+ s"Failed to build the inline information: $missingClass."
+
+ case UnknownScalaInlineInfoVersion(internalName, version) =>
+ s"Cannot read ScalaInlineInfo version $version in classfile $internalName. Use a more recent compiler."
+ }
+
+ def emitWarning(settings: WarnSettings): Boolean = this match {
+ case NoInlineInfoAttribute(_) => settings.noInlineMissingScalaInlineInfoAttr
+ case ClassNotFoundWhenBuildingInlineInfoFromSymbol(cause) => cause.emitWarning(settings)
+ case ClassSymbolInfoFailureSI9111(_) => settings.noInlineMissingBytecode
+ case UnknownScalaInlineInfoVersion(_, _) => settings.noInlineMissingScalaInlineInfoAttr
+ }
+ }
+
+ case class NoInlineInfoAttribute(internalName: InternalName) extends ClassInlineInfoWarning
+ case class ClassSymbolInfoFailureSI9111(classFullName: String) extends ClassInlineInfoWarning
+ case class ClassNotFoundWhenBuildingInlineInfoFromSymbol(missingClass: ClassNotFound) extends ClassInlineInfoWarning
+ case class UnknownScalaInlineInfoVersion(internalName: InternalName, version: Int) extends ClassInlineInfoWarning
+}
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala
index 246235f395..492fe3ae79 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala
@@ -99,10 +99,9 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) {
*
* Therefore, when RT_NOTHING or RT_NULL are to be emitted, a mapping is needed: the internal
* names of NothingClass and NullClass can't be emitted as-is.
- * TODO @lry Once there's a 2.11.3 starr, use the commented argument list. The current starr crashes on the type literal `scala.runtime.Nothing$`
*/
- lazy val RT_NOTHING : ClassBType = classBTypeFromSymbol(rootMirror.getRequiredClass("scala.runtime.Nothing$")) // (requiredClass[scala.runtime.Nothing$])
- lazy val RT_NULL : ClassBType = classBTypeFromSymbol(rootMirror.getRequiredClass("scala.runtime.Null$")) // (requiredClass[scala.runtime.Null$])
+ lazy val RT_NOTHING : ClassBType = classBTypeFromSymbol(requiredClass[scala.runtime.Nothing$])
+ lazy val RT_NULL : ClassBType = classBTypeFromSymbol(requiredClass[scala.runtime.Null$])
lazy val ObjectReference : ClassBType = classBTypeFromSymbol(ObjectClass)
lazy val objArrayReference : ArrayBType = ArrayBType(ObjectReference)
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala
index d5e95c47cf..be1595dc29 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala
@@ -217,11 +217,26 @@ abstract class GenBCode extends BCodeSyncAndTry {
class Worker2 {
lazy val localOpt = new LocalOpt(settings)
+ def runGlobalOptimizations(): Unit = {
+ import scala.collection.convert.decorateAsScala._
+ q2.asScala foreach {
+ case Item2(_, _, plain, _, _) =>
+ // skip mirror / bean: wd don't inline into tem, and they are not used in the plain class
+ if (plain != null) {
+ localOpt.minimalRemoveUnreachableCode(plain)
+ callGraph.addClass(plain)
+ }
+ }
+ bTypes.inliner.runInliner()
+ }
+
def localOptimizations(classNode: ClassNode): Unit = {
BackendStats.timed(BackendStats.methodOptTimer)(localOpt.methodOptimizations(classNode))
}
def run() {
+ if (settings.YoptInlinerEnabled) runGlobalOptimizations()
+
while (true) {
val item = q2.poll
if (item.isPoison) {
@@ -269,7 +284,12 @@ abstract class GenBCode extends BCodeSyncAndTry {
var arrivalPos = 0
- /*
+ /**
+ * The `run` method is overridden because the backend has a different data flow than the default
+ * phase: the backend does not transform compilation units one by one, but on all units in the
+ * same run. This allows cross-unit optimizations and running some stages of the backend
+ * concurrently on multiple units.
+ *
* A run of the BCodePhase phase comprises:
*
* (a) set-up steps (most notably supporting maps in `BCodeTypes`,
@@ -287,6 +307,10 @@ abstract class GenBCode extends BCodeSyncAndTry {
arrivalPos = 0 // just in case
scalaPrimitives.init()
bTypes.initializeCoreBTypes()
+ bTypes.javaDefinedClasses.clear()
+ bTypes.javaDefinedClasses ++= currentRun.symSource collect {
+ case (sym, _) if sym.isJavaDefined => sym.javaBinaryName.toString
+ }
Statistics.stopTimer(BackendStats.bcodeInitTimer, initStart)
// initBytecodeWriter invokes fullName, thus we have to run it before the typer-dependent thread is activated.
@@ -410,4 +434,7 @@ object GenBCode {
final val PublicStatic = asm.Opcodes.ACC_PUBLIC | asm.Opcodes.ACC_STATIC
final val PublicStaticFinal = asm.Opcodes.ACC_PUBLIC | asm.Opcodes.ACC_STATIC | asm.Opcodes.ACC_FINAL
+
+ val CLASS_CONSTRUCTOR_NAME = "<clinit>"
+ val INSTANCE_CONSTRUCTOR_NAME = "<init>"
}
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 7b424d2107..607b7145d6 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala
@@ -10,11 +10,14 @@ package opt
import scala.tools.asm
import asm.tree._
import scala.collection.convert.decorateAsScala._
+import scala.tools.asm.Attribute
+import scala.tools.nsc.backend.jvm.BackendReporting._
import scala.tools.nsc.io.AbstractFile
import scala.tools.nsc.util.ClassFileLookup
-import OptimizerReporting._
+import BytecodeUtils._
import ByteCodeRepository._
import BTypes.InternalName
+import java.util.concurrent.atomic.AtomicLong
/**
* The ByteCodeRepository provides utilities to read the bytecode of classfiles from the compilation
@@ -24,58 +27,125 @@ import BTypes.InternalName
* @param classes Cache for parsed ClassNodes. Also stores the source of the bytecode:
* [[Classfile]] if read from `classPath`, [[CompilationUnit]] if the bytecode
* corresponds to a class being compiled.
+ * The `Long` field encodes the age of the node in the map, which allows removing
+ * old entries when the map grows too large.
+ * For Java classes in mixed compilation, the map contains an error message: no
+ * ClassNode is generated by the backend and also no classfile that could be parsed.
*/
-class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val classes: collection.concurrent.Map[InternalName, (ClassNode, Source)]) {
+class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val isJavaSourceDefined: InternalName => Boolean, val classes: collection.concurrent.Map[InternalName, Either[ClassNotFound, (ClassNode, Source, Long)]]) {
+
+ private val maxCacheSize = 1500
+ private val targetSize = 500
+
+ private val idCounter = new AtomicLong(0)
+
+ /**
+ * Prevent the code repository from growing too large. Profiling reveals that the average size
+ * of a ClassNode is about 30 kb. I observed having 17k+ classes in the cache, i.e., 500 mb.
+ *
+ * We can only remove classes with `Source == Classfile`, those can be parsed again if requested.
+ */
+ private def limitCacheSize(): Unit = {
+ if (classes.count(c => c._2.isRight && c._2.right.get._2 == Classfile) > maxCacheSize) {
+ val removeId = idCounter.get - targetSize
+ val toRemove = classes.iterator.collect({
+ case (name, Right((_, Classfile, id))) if id < removeId => name
+ }).toList
+ toRemove foreach classes.remove
+ }
+ }
+
+ def add(classNode: ClassNode, source: Source) = {
+ classes(classNode.name) = Right((classNode, source, idCounter.incrementAndGet()))
+ }
+
/**
* The class node and source for an internal name. If the class node is not yet available, it is
* parsed from the classfile on the compile classpath.
*/
- def classNodeAndSource(internalName: InternalName): (ClassNode, Source) = {
- classes.getOrElseUpdate(internalName, (parseClass(internalName), Classfile))
+ def classNodeAndSource(internalName: InternalName): Either[ClassNotFound, (ClassNode, Source)] = {
+ val r = classes.getOrElseUpdate(internalName, {
+ limitCacheSize()
+ parseClass(internalName).map((_, Classfile, idCounter.incrementAndGet()))
+ })
+ r.map(v => (v._1, v._2))
}
/**
* The class node for an internal name. If the class node is not yet available, it is parsed from
* the classfile on the compile classpath.
*/
- def classNode(internalName: InternalName) = classNodeAndSource(internalName)._1
+ def classNode(internalName: InternalName): Either[ClassNotFound, ClassNode] = classNodeAndSource(internalName).map(_._1)
/**
* The field node for a field matching `name` and `descriptor`, accessed in class `classInternalName`.
* The declaration of the field may be in one of the superclasses.
*
- * @return The [[FieldNode]] of the requested field and the [[InternalName]] of its declaring class.
+ * @return The [[FieldNode]] of the requested field and the [[InternalName]] of its declaring
+ * class, or an error message if the field could not be found
*/
- def fieldNode(classInternalName: InternalName, name: String, descriptor: String): Option[(FieldNode, InternalName)] = {
- val c = classNode(classInternalName)
- c.fields.asScala.find(f => f.name == name && f.desc == descriptor).map((_, classInternalName)) orElse {
- Option(c.superName).flatMap(n => fieldNode(n, name, descriptor))
+ def fieldNode(classInternalName: InternalName, name: String, descriptor: String): Either[FieldNotFound, (FieldNode, InternalName)] = {
+ def fieldNodeImpl(parent: InternalName): Either[FieldNotFound, (FieldNode, InternalName)] = {
+ def msg = s"The field node $name$descriptor could not be found in class $classInternalName or any of its superclasses."
+ classNode(parent) match {
+ case Left(e) => Left(FieldNotFound(name, descriptor, classInternalName, Some(e)))
+ case Right(c) =>
+ c.fields.asScala.find(f => f.name == name && f.desc == descriptor) match {
+ case Some(f) => Right((f, parent))
+ case None =>
+ if (c.superName == null) Left(FieldNotFound(name, descriptor, classInternalName, None))
+ else fieldNode(c.superName, name, descriptor)
+ }
+ }
}
+ fieldNodeImpl(classInternalName)
}
/**
* The method node for a method matching `name` and `descriptor`, accessed in class `classInternalName`.
* The declaration of the method may be in one of the parents.
*
- * @return The [[MethodNode]] of the requested method and the [[InternalName]] of its declaring class.
+ * @return The [[MethodNode]] of the requested method and the [[InternalName]] of its declaring
+ * class, or an error message if the method could not be found.
*/
- def methodNode(classInternalName: InternalName, name: String, descriptor: String): Option[(MethodNode, InternalName)] = {
- val c = classNode(classInternalName)
- c.methods.asScala.find(m => m.name == name && m.desc == descriptor).map((_, classInternalName)) orElse {
- val parents = Option(c.superName) ++ c.interfaces.asScala
- // `view` to stop at the first result
- parents.view.flatMap(methodNode(_, name, descriptor)).headOption
+ def methodNode(ownerInternalNameOrArrayDescriptor: String, name: String, descriptor: String): Either[MethodNotFound, (MethodNode, InternalName)] = {
+ // on failure, returns a list of class names that could not be found on the classpath
+ def methodNodeImpl(ownerInternalName: InternalName): Either[List[ClassNotFound], (MethodNode, InternalName)] = {
+ classNode(ownerInternalName) match {
+ case Left(e) => Left(List(e))
+ case Right(c) =>
+ c.methods.asScala.find(m => m.name == name && m.desc == descriptor) match {
+ case Some(m) => Right((m, ownerInternalName))
+ case None => findInParents(Option(c.superName) ++: c.interfaces.asScala.toList, Nil)
+ }
+ }
}
+
+ // find the MethodNode in one of the parent classes
+ def findInParents(parents: List[InternalName], failedClasses: List[ClassNotFound]): Either[List[ClassNotFound], (MethodNode, InternalName)] = parents match {
+ case x :: xs => methodNodeImpl(x).left.flatMap(failed => findInParents(xs, failed ::: failedClasses))
+ case Nil => Left(failedClasses)
+ }
+
+ // In a MethodInsnNode, the `owner` field may be an array descriptor, for exmple when invoking `clone`. We don't have a method node to return in this case.
+ if (ownerInternalNameOrArrayDescriptor.charAt(0) == '[')
+ Left(MethodNotFound(name, descriptor, ownerInternalNameOrArrayDescriptor, Nil))
+ else
+ methodNodeImpl(ownerInternalNameOrArrayDescriptor).left.map(MethodNotFound(name, descriptor, ownerInternalNameOrArrayDescriptor, _))
}
- private def parseClass(internalName: InternalName): ClassNode = {
+ private def parseClass(internalName: InternalName): Either[ClassNotFound, ClassNode] = {
val fullName = internalName.replace('/', '.')
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?
@@ -85,18 +155,9 @@ class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val class
// https://jcp.org/aboutJava/communityprocess/final/jsr045/index.html
removeLineNumberNodes(classNode)
classNode
- } getOrElse {
- inlineFailure(s"Class file for class $fullName not found.")
- }
- }
-
- private def removeLineNumberNodes(classNode: ClassNode): Unit = {
- for (method <- classNode.methods.asScala) {
- val iter = method.instructions.iterator()
- while (iter.hasNext) iter.next() match {
- case _: LineNumberNode => iter.remove()
- case _ =>
- }
+ } match {
+ case Some(node) => Right(node)
+ case None => Left(ClassNotFound(internalName, isJavaSourceDefined(internalName)))
}
}
}
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 6b4047c0a7..14e8cccc60 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala
@@ -10,9 +10,14 @@ package opt
import scala.annotation.{tailrec, switch}
import scala.collection.mutable
import scala.reflect.internal.util.Collections._
-import scala.tools.asm.Opcodes
+import scala.tools.asm.tree.analysis._
+import scala.tools.asm.{MethodWriter, ClassWriter, Label, Opcodes}
import scala.tools.asm.tree._
import scala.collection.convert.decorateAsScala._
+import GenBCode._
+import scala.collection.convert.decorateAsScala._
+import scala.collection.convert.decorateAsJava._
+import scala.tools.nsc.backend.jvm.BTypes._
object BytecodeUtils {
@@ -68,6 +73,20 @@ object BytecodeUtils {
def isExecutable(instruction: AbstractInsnNode): Boolean = instruction.getOpcode >= 0
+ def isConstructor(methodNode: MethodNode): Boolean = {
+ methodNode.name == INSTANCE_CONSTRUCTOR_NAME || methodNode.name == CLASS_CONSTRUCTOR_NAME
+ }
+
+ def isStaticMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_STATIC) != 0
+
+ def isAbstractMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_ABSTRACT) != 0
+
+ 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 | Opcodes.ACC_STATIC)) != 0
+
def nextExecutableInstruction(instruction: AbstractInsnNode, alsoKeep: AbstractInsnNode => Boolean = Set()): Option[AbstractInsnNode] = {
var result = instruction
do { result = result.getNext }
@@ -181,4 +200,115 @@ object BytecodeUtils {
if (handler.end == from) handler.end = to
}
}
+
+ /**
+ * In order to run an Analyzer, the maxLocals / maxStack fields need to be available. The ASM
+ * framework only computes these values during bytecode generation.
+ *
+ * Since there's currently no better way, we run a bytecode generator on the method and extract
+ * the computed values. This required changes to the ASM codebase:
+ * - the [[MethodWriter]] class was made public
+ * - accessors for maxLocals / maxStack were added to the MethodWriter class
+ *
+ * We could probably make this faster (and allocate less memory) by hacking the ASM framework
+ * more: create a subclass of MethodWriter with a /dev/null byteVector. Another option would be
+ * to create a separate visitor for computing those values, duplicating the functionality from the
+ * MethodWriter.
+ */
+ def computeMaxLocalsMaxStack(method: MethodNode) {
+ val cw = new ClassWriter(ClassWriter.COMPUTE_MAXS)
+ val excs = method.exceptions.asScala.toArray
+ val mw = cw.visitMethod(method.access, method.name, method.desc, method.signature, excs).asInstanceOf[MethodWriter]
+ method.accept(mw)
+ method.maxLocals = mw.getMaxLocals
+ method.maxStack = mw.getMaxStack
+ }
+
+ def removeLineNumberNodes(classNode: ClassNode): Unit = {
+ for (m <- classNode.methods.asScala) removeLineNumberNodes(m.instructions)
+ }
+
+ def removeLineNumberNodes(instructions: InsnList): Unit = {
+ val iter = instructions.iterator()
+ while (iter.hasNext) iter.next() match {
+ case _: LineNumberNode => iter.remove()
+ case _ =>
+ }
+ }
+
+ def cloneLabels(methodNode: MethodNode): Map[LabelNode, LabelNode] = {
+ methodNode.instructions.iterator().asScala.collect({
+ case labelNode: LabelNode => (labelNode, newLabelNode)
+ }).toMap
+ }
+
+ /**
+ * Create a new [[LabelNode]] with a correctly associated [[Label]].
+ */
+ def newLabelNode: LabelNode = {
+ val label = new Label
+ val labelNode = new LabelNode(label)
+ label.info = labelNode
+ labelNode
+ }
+
+ /**
+ * Clone the instructions in `methodNode` into a new [[InsnList]], mapping labels according to
+ * the `labelMap`. Returns the new instruction list and a map from old to new instructions.
+ */
+ def cloneInstructions(methodNode: MethodNode, labelMap: Map[LabelNode, LabelNode]): (InsnList, Map[AbstractInsnNode, AbstractInsnNode]) = {
+ val javaLabelMap = labelMap.asJava
+ val result = new InsnList
+ var map = Map.empty[AbstractInsnNode, AbstractInsnNode]
+ for (ins <- methodNode.instructions.iterator.asScala) {
+ val cloned = ins.clone(javaLabelMap)
+ result add cloned
+ map += ((ins, cloned))
+ }
+ (result, map)
+ }
+
+ /**
+ * Clone the local variable descriptors of `methodNode` and map their `start` and `end` labels
+ * according to the `labelMap`.
+ */
+ def cloneLocalVariableNodes(methodNode: MethodNode, labelMap: Map[LabelNode, LabelNode], prefix: String): List[LocalVariableNode] = {
+ methodNode.localVariables.iterator().asScala.map(localVariable => new LocalVariableNode(
+ prefix + localVariable.name,
+ localVariable.desc,
+ localVariable.signature,
+ labelMap(localVariable.start),
+ labelMap(localVariable.end),
+ localVariable.index
+ )).toList
+ }
+
+ /**
+ * Clone the local try/catch blocks of `methodNode` and map their `start` and `end` and `handler`
+ * labels according to the `labelMap`.
+ */
+ def cloneTryCatchBlockNodes(methodNode: MethodNode, labelMap: Map[LabelNode, LabelNode]): List[TryCatchBlockNode] = {
+ methodNode.tryCatchBlocks.iterator().asScala.map(tryCatch => new TryCatchBlockNode(
+ labelMap(tryCatch.start),
+ labelMap(tryCatch.end),
+ labelMap(tryCatch.handler),
+ tryCatch.`type`
+ )).toList
+ }
+
+ /**
+ * A wrapper to make ASM's Analyzer a bit easier to use.
+ */
+ class AsmAnalyzer[V <: Value](methodNode: MethodNode, classInternalName: InternalName, interpreter: Interpreter[V] = new BasicInterpreter) {
+ val analyzer = new Analyzer(interpreter)
+ analyzer.analyze(classInternalName, methodNode)
+ def frameAt(instruction: AbstractInsnNode): Frame[V] = analyzer.getFrames()(methodNode.instructions.indexOf(instruction))
+ }
+
+ implicit class `frame extensions`[V <: Value](val frame: Frame[V]) extends AnyVal {
+ def peekDown(n: Int): V = {
+ val topIndex = frame.getStackSize - 1
+ frame.getStack(topIndex - n)
+ }
+ }
}
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala
new file mode 100644
index 0000000000..47d32c94cb
--- /dev/null
+++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala
@@ -0,0 +1,189 @@
+/* NSC -- new Scala compiler
+ * Copyright 2005-2014 LAMP/EPFL
+ * @author Martin Odersky
+ */
+
+package scala.tools.nsc
+package backend.jvm
+package opt
+
+import scala.reflect.internal.util.{NoPosition, Position}
+import scala.tools.asm.tree._
+import scala.collection.convert.decorateAsScala._
+import scala.tools.nsc.backend.jvm.BTypes.{MethodInlineInfo, InternalName}
+import scala.tools.nsc.backend.jvm.BackendReporting._
+import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.AsmAnalyzer
+import ByteCodeRepository.{Source, CompilationUnit}
+
+class CallGraph[BT <: BTypes](val btypes: BT) {
+ import btypes._
+
+ val callsites: collection.concurrent.Map[MethodInsnNode, Callsite] = recordPerRunCache(collection.concurrent.TrieMap.empty[MethodInsnNode, Callsite])
+
+ def addClass(classNode: ClassNode): Unit = {
+ for (m <- classNode.methods.asScala; callsite <- analyzeCallsites(m, classBTypeFromClassNode(classNode)))
+ callsites(callsite.callsiteInstruction) = callsite
+ }
+
+ def analyzeCallsites(methodNode: MethodNode, definingClass: ClassBType): List[Callsite] = {
+
+ case class CallsiteInfo(safeToInline: Boolean, safeToRewrite: Boolean,
+ annotatedInline: Boolean, annotatedNoInline: Boolean,
+ warning: Option[CalleeInfoWarning])
+
+ /**
+ * Analyze a callsite and gather meta-data that can be used for inlining decisions.
+ */
+ def analyzeCallsite(calleeMethodNode: MethodNode, calleeDeclarationClassBType: ClassBType, receiverTypeInternalName: InternalName, calleeSource: Source): CallsiteInfo = {
+ val methodSignature = calleeMethodNode.name + calleeMethodNode.desc
+
+ try {
+ // 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.orThrow.inlineInfo.methodInfos.get(methodSignature) match {
+ case Some(methodInlineInfo) =>
+ val canInlineFromSource = inlineGlobalEnabled || calleeSource == CompilationUnit
+
+ val isAbstract = BytecodeUtils.isAbstractMethod(calleeMethodNode)
+
+ // (1) A non-final method can be safe to 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
+ //
+ // 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.
+ val isStaticallyResolved: Boolean = {
+ methodInlineInfo.effectivelyFinal ||
+ classBTypeFromParsedClassfile(receiverTypeInternalName).info.orThrow.inlineInfo.isEffectivelyFinal // (1)
+ }
+
+ val isRewritableTraitCall = isStaticallyResolved && methodInlineInfo.traitMethodWithStaticImplementation
+
+ val warning = calleeDeclarationClassBType.info.orThrow.inlineInfo.warning.map(
+ MethodInlineInfoIncomplete(calleeDeclarationClassBType.internalName, calleeMethodNode.name, calleeMethodNode.desc, _))
+
+ // (1) For invocations of final trait methods, the callee isStaticallyResolved but also
+ // abstract. Such a callee is not safe to inline - it needs to be re-written to the
+ // static impl method first (safeToRewrite).
+ // (2) Final trait methods can be rewritten from the interface to the static implementation
+ // method to enable inlining.
+ CallsiteInfo(
+ safeToInline = canInlineFromSource && isStaticallyResolved && !isAbstract, // (1)
+ safeToRewrite = canInlineFromSource && isRewritableTraitCall, // (2)
+ annotatedInline = methodInlineInfo.annotatedInline,
+ annotatedNoInline = methodInlineInfo.annotatedNoInline,
+ warning = warning)
+
+ case None =>
+ val warning = MethodInlineInfoMissing(calleeDeclarationClassBType.internalName, calleeMethodNode.name, calleeMethodNode.desc, calleeDeclarationClassBType.info.orThrow.inlineInfo.warning)
+ CallsiteInfo(false, false, false, false, Some(warning))
+ }
+ } catch {
+ case Invalid(noInfo: NoClassBTypeInfo) =>
+ val warning = MethodInlineInfoError(calleeDeclarationClassBType.internalName, calleeMethodNode.name, calleeMethodNode.desc, noInfo)
+ CallsiteInfo(false, false, false, false, Some(warning))
+ }
+ }
+
+ // 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
+ // - nullAnalysis to skip emitting the receiver-null-check when inlining
+
+ // TODO: for now we run a basic analyzer to get the stack height at the call site.
+ // once we run a more elaborate analyzer (types, nullness), we can get the stack height out of there.
+ val analyzer = new AsmAnalyzer(methodNode, definingClass.internalName)
+
+ methodNode.instructions.iterator.asScala.collect({
+ case call: MethodInsnNode =>
+ val callee: Either[OptimizerWarning, Callee] = for {
+ (method, declarationClass) <- byteCodeRepository.methodNode(call.owner, call.name, call.desc): Either[OptimizerWarning, (MethodNode, InternalName)]
+ (declarationClassNode, source) <- byteCodeRepository.classNodeAndSource(declarationClass): Either[OptimizerWarning, (ClassNode, Source)]
+ declarationClassBType = classBTypeFromClassNode(declarationClassNode)
+ } yield {
+ val CallsiteInfo(safeToInline, safeToRewrite, annotatedInline, annotatedNoInline, warning) = analyzeCallsite(method, declarationClassBType, call.owner, source)
+ Callee(
+ callee = method,
+ calleeDeclarationClass = declarationClassBType,
+ safeToInline = safeToInline,
+ safeToRewrite = safeToRewrite,
+ annotatedInline = annotatedInline,
+ annotatedNoInline = annotatedNoInline,
+ calleeInfoWarning = warning)
+ }
+
+ val argInfos = if (callee.isLeft) Nil else {
+ // TODO: for now it's Nil, because we don't run any data flow analysis
+ // there's no point in using the parameter types, that doesn't add any information.
+ // NOTE: need to run the same analyses after inlining, to re-compute the argInfos for the
+ // new duplicated callsites, see Inliner.inline
+ Nil
+ }
+
+ Callsite(
+ callsiteInstruction = call,
+ callsiteMethod = methodNode,
+ callsiteClass = definingClass,
+ callee = callee,
+ argInfos = argInfos,
+ callsiteStackHeight = analyzer.frameAt(call).getStackSize,
+ callsitePosition = callsitePositions.getOrElse(call, NoPosition)
+ )
+ }).toList
+ }
+
+ /**
+ * 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, as it appears in the invocation instruction. For virtual
+ * calls, an override of the callee might be invoked. Also, the callee
+ * can be abstract. Contains a warning message 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
+ * @param callsitePosition The source position of the callsite, used for inliner warnings.
+ */
+ final case class Callsite(callsiteInstruction: MethodInsnNode, callsiteMethod: MethodNode, callsiteClass: ClassBType,
+ callee: Either[OptimizerWarning, Callee], argInfos: List[ArgInfo],
+ callsiteStackHeight: Int, callsitePosition: Position) {
+ override def toString =
+ "Invocation of" +
+ s" ${callee.map(_.calleeDeclarationClass.internalName).getOrElse("?")}.${callsiteInstruction.name + callsiteInstruction.desc}" +
+ s"@${callsiteMethod.instructions.indexOf(callsiteInstruction)}" +
+ s" in ${callsiteClass.internalName}.${callsiteMethod.name}"
+ }
+
+ /**
+ * Information about invocation arguments, obtained through data flow analysis of the callsite method.
+ */
+ sealed trait ArgInfo
+ final case class ArgTypeInfo(argType: BType, isPrecise: Boolean, knownNotNull: Boolean) extends ArgInfo
+ final case class ForwardedParam(index: Int) extends ArgInfo
+ // can be extended, e.g., with constant types
+
+ /**
+ * A callee in the call graph.
+ *
+ * @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 safeToRewrite True if the callee the interface method of a concrete trait method
+ * that can be safely re-written to the static implementation method.
+ * @param annotatedInline True if the callee is annotated @inline
+ * @param annotatedNoInline True if the callee is annotated @noinline
+ * @param calleeInfoWarning An inliner warning if some information was not available while
+ * gathering the information about this callee.
+ */
+ final case class Callee(callee: MethodNode, calleeDeclarationClass: ClassBType,
+ safeToInline: Boolean, safeToRewrite: Boolean,
+ annotatedInline: Boolean, annotatedNoInline: Boolean,
+ calleeInfoWarning: Option[CalleeInfoWarning]) {
+ assert(!(safeToInline && safeToRewrite), s"A callee of ${callee.name} can be either safeToInline or safeToRewrite, but not both.")
+ }
+}
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/InlineInfoAttribute.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/InlineInfoAttribute.scala
index 4812f2290f..e7dd5abc57 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/opt/InlineInfoAttribute.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/InlineInfoAttribute.scala
@@ -9,6 +9,7 @@ package opt
import scala.tools.asm._
import scala.tools.nsc.backend.jvm.BTypes.{InlineInfo, MethodInlineInfo}
+import scala.tools.nsc.backend.jvm.BackendReporting.UnknownScalaInlineInfoVersion
/**
* This attribute stores the InlineInfo for a ClassBType as an independent classfile attribute.
@@ -119,7 +120,7 @@ case class InlineInfoAttribute(inlineInfo: InlineInfo) extends Attribute(InlineI
InlineInfoAttribute(InlineInfo(self, isFinal, infos, None))
} else {
- val msg = s"Cannot read ScalaInlineInfo version $version in classfile ${cr.getClassName}. Use a more recent compiler."
+ val msg = UnknownScalaInlineInfoVersion(cr.getClassName, version)
InlineInfoAttribute(BTypes.EmptyInlineInfo.copy(warning = Some(msg)))
}
}
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala
new file mode 100644
index 0000000000..e14e57d3ab
--- /dev/null
+++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala
@@ -0,0 +1,648 @@
+/* NSC -- new Scala compiler
+ * Copyright 2005-2014 LAMP/EPFL
+ * @author Martin Odersky
+ */
+
+package scala.tools.nsc
+package backend.jvm
+package opt
+
+import scala.annotation.tailrec
+import scala.tools.asm
+import asm.Opcodes._
+import asm.tree._
+import scala.collection.convert.decorateAsScala._
+import scala.collection.convert.decorateAsJava._
+import AsmUtils._
+import BytecodeUtils._
+import collection.mutable
+import scala.tools.asm.tree.analysis.{SourceInterpreter, Analyzer}
+import BackendReporting._
+import scala.tools.nsc.backend.jvm.BTypes.InternalName
+
+class Inliner[BT <: BTypes](val btypes: BT) {
+ import btypes._
+ import callGraph._
+
+ def runInliner(): Unit = {
+ rewriteFinalTraitMethodInvocations()
+
+ for (request <- collectAndOrderInlineRequests) {
+ val Right(callee) = request.callee // collectAndOrderInlineRequests returns callsites with a known callee
+
+ val r = inline(request.callsiteInstruction, request.callsiteStackHeight, request.callsiteMethod, request.callsiteClass,
+ callee.callee, callee.calleeDeclarationClass,
+ receiverKnownNotNull = false, keepLineNumbers = false)
+
+ for (warning <- r) {
+ if ((callee.annotatedInline && btypes.warnSettings.atInlineFailed) || warning.emitWarning(warnSettings)) {
+ val annotWarn = if (callee.annotatedInline) " is annotated @inline but" else ""
+ val msg = s"${BackendReporting.methodSignature(callee.calleeDeclarationClass.internalName, callee.callee)}$annotWarn could not be inlined:\n$warning"
+ backendReporting.inlinerWarning(request.callsitePosition, msg)
+ }
+ }
+ }
+ }
+
+ /**
+ * Ordering for inline requests. Required to make the inliner deterministic:
+ * - Always remove the same request when breaking inlining cycles
+ * - Perform inlinings in a consistent order
+ */
+ object callsiteOrdering extends Ordering[Callsite] {
+ override def compare(x: Callsite, y: Callsite): Int = {
+ val cls = x.callsiteClass.internalName compareTo y.callsiteClass.internalName
+ if (cls != 0) return cls
+
+ val name = x.callsiteMethod.name compareTo y.callsiteMethod.name
+ if (name != 0) return name
+
+ val desc = x.callsiteMethod.desc compareTo y.callsiteMethod.desc
+ if (desc != 0) return desc
+
+ def pos(c: Callsite) = c.callsiteMethod.instructions.indexOf(c.callsiteInstruction)
+ pos(x) - pos(y)
+ }
+ }
+
+ /**
+ * Select callsites from the call graph that should be inlined. The resulting list of inlining
+ * requests is allowed to have cycles, and the callsites can appear in any order.
+ */
+ def selectCallsitesForInlining: List[Callsite] = {
+ callsites.valuesIterator.filter({
+ case callsite @ Callsite(_, _, _, Right(Callee(callee, calleeDeclClass, safeToInline, _, annotatedInline, _, warning)), _, _, pos) =>
+ val res = doInlineCallsite(callsite)
+
+ if (!res) {
+ if (annotatedInline && btypes.warnSettings.atInlineFailed) {
+ // if the callsite is annotated @inline, we report an inline warning even if the underlying
+ // reason is, for example, mixed compilation (which has a separate -Yopt-warning flag).
+ def initMsg = s"${BackendReporting.methodSignature(calleeDeclClass.internalName, callee)} is annotated @inline but cannot be inlined"
+ def warnMsg = warning.map(" Possible reason:\n" + _).getOrElse("")
+ if (doRewriteTraitCallsite(callsite))
+ backendReporting.inlinerWarning(pos, s"$initMsg: the trait method call could not be rewritten to the static implementation method." + warnMsg)
+ else if (!safeToInline)
+ backendReporting.inlinerWarning(pos, s"$initMsg: the method is not final and may be overridden." + warnMsg)
+ else
+ backendReporting.inlinerWarning(pos, s"$initMsg." + warnMsg)
+ } else if (warning.isDefined && warning.get.emitWarning(warnSettings)) {
+ // when annotatedInline is false, and there is some warning, the callsite metadata is possibly incomplete.
+ backendReporting.inlinerWarning(pos, s"there was a problem determining if method ${callee.name} can be inlined: \n"+ warning.get)
+ }
+ }
+
+ res
+
+ case Callsite(ins, _, _, Left(warning), _, _, pos) =>
+ if (warning.emitWarning(warnSettings))
+ backendReporting.inlinerWarning(pos, s"failed to determine if ${ins.name} should be inlined:\n$warning")
+ false
+ }).toList
+ }
+
+ /**
+ * The current inlining heuristics are simple: inline calls to methods annotated @inline.
+ */
+ def doInlineCallsite(callsite: Callsite): Boolean = callsite match {
+ case Callsite(_, _, _, Right(Callee(callee, calleeDeclClass, safeToInline, _, annotatedInline, _, warning)), _, _, pos) =>
+ annotatedInline && safeToInline
+
+ case _ => false
+ }
+
+ 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)
+ }
+
+ /**
+ * True for statically resolved trait callsites that should be rewritten to the static implementation method.
+ */
+ def doRewriteTraitCallsite(callsite: Callsite) = callsite.callee match {
+ case Right(Callee(callee, calleeDeclarationClass, safeToInline, true, annotatedInline, annotatedNoInline, infoWarning)) => true
+ case _ => false
+ }
+
+ /**
+ * 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 = {
+ if (doRewriteTraitCallsite(callsite)) {
+ val Right(Callee(callee, calleeDeclarationClass, _, _, annotatedInline, annotatedNoInline, infoWarning)) = callsite.callee
+
+ val traitMethodArgumentTypes = asm.Type.getArgumentTypes(callee.desc)
+
+ val implClassInternalName = calleeDeclarationClass.internalName + "$class"
+
+ val selfParamTypeV: Either[OptimizerWarning, ClassBType] = calleeDeclarationClass.info.map(_.inlineInfo.traitImplClassSelfType match {
+ case Some(internalName) => classBTypeFromParsedClassfile(internalName)
+ case None => calleeDeclarationClass
+ })
+
+ def implClassMethodV(implMethodDescriptor: String): Either[OptimizerWarning, MethodNode] = {
+ byteCodeRepository.methodNode(implClassInternalName, callee.name, implMethodDescriptor).map(_._1)
+ }
+
+ // 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.
+ val res = for {
+ selfParamType <- selfParamTypeV
+ implMethodDescriptor = asm.Type.getMethodDescriptor(asm.Type.getReturnType(callee.desc), selfParamType.toASMType +: traitMethodArgumentTypes: _*)
+ implClassMethod <- implClassMethodV(implMethodDescriptor)
+ implClassBType = classBTypeFromParsedClassfile(implClassInternalName)
+ selfTypeOk <- calleeDeclarationClass.isSubtypeOf(selfParamType)
+ } yield {
+
+ // The self parameter type may be incompatible with the trait type.
+ // trait T { self: S => def foo = 1 }
+ // The $self parameter type of T$class.foo is S, which may be unrelated to T. If we re-write
+ // a call to T.foo to T$class.foo, we need to cast the receiver to S, otherwise we get a
+ // VerifyError. We run a `SourceInterpreter` to find all producer instructions of the
+ // receiver value and add a cast to the self type after each.
+ if (!selfTypeOk) {
+ val analyzer = new AsmAnalyzer(callsite.callsiteMethod, callsite.callsiteClass.internalName, new SourceInterpreter)
+ val receiverValue = analyzer.frameAt(callsite.callsiteInstruction).peekDown(traitMethodArgumentTypes.length)
+ for (i <- receiverValue.insns.asScala) {
+ val cast = new TypeInsnNode(CHECKCAST, selfParamType.internalName)
+ callsite.callsiteMethod.instructions.insert(i, cast)
+ }
+ }
+
+ val newCallsiteInstruction = new MethodInsnNode(INVOKESTATIC, implClassInternalName, callee.name, implMethodDescriptor, 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 = Right(Callee(
+ callee = implClassMethod,
+ calleeDeclarationClass = implClassBType,
+ safeToInline = true,
+ safeToRewrite = false,
+ annotatedInline = annotatedInline,
+ annotatedNoInline = annotatedNoInline,
+ calleeInfoWarning = infoWarning)),
+ argInfos = Nil,
+ callsiteStackHeight = callsite.callsiteStackHeight,
+ callsitePosition = callsite.callsitePosition
+ )
+ callGraph.callsites(newCallsiteInstruction) = staticCallsite
+ }
+
+ for (warning <- res.left) {
+ val Right(callee) = callsite.callee
+ val newCallee = callee.copy(calleeInfoWarning = Some(RewriteTraitCallToStaticImplMethodFailed(calleeDeclarationClass.internalName, callee.callee.name, callee.callee.desc, warning)))
+ callGraph.callsites(callsite.callsiteInstruction) = callsite.copy(callee = Right(newCallee))
+ }
+ }
+ }
+
+ /**
+ * Returns the callsites that can be inlined. Ensures that the returned inline request graph does
+ * not contain cycles.
+ *
+ * The resulting list is sorted such that the leaves of the inline request graph are on the left.
+ * Once these leaves are inlined, the successive elements will be leaves, etc.
+ */
+ private def collectAndOrderInlineRequests: List[Callsite] = {
+ val requests = selectCallsitesForInlining
+
+ // This map is an index to look up the inlining requests for a method. The value sets are mutable
+ // to allow removing elided requests (to break inlining cycles). The map itself is mutable to
+ // allow efficient building: requests.groupBy would build values as List[Callsite] that need to
+ // be transformed to mutable sets.
+ val inlineRequestsForMethod: mutable.Map[MethodNode, mutable.Set[Callsite]] = mutable.HashMap.empty.withDefaultValue(mutable.HashSet.empty)
+ for (r <- requests) inlineRequestsForMethod.getOrElseUpdate(r.callsiteMethod, mutable.HashSet.empty) += r
+
+ /**
+ * Break cycles in the inline request graph by removing callsites.
+ *
+ * The list `requests` is traversed left-to-right, removing those callsites that are part of a
+ * cycle. Elided callsites are also removed from the `inlineRequestsForMethod` map.
+ */
+ def breakInlineCycles(requests: List[Callsite]): List[Callsite] = {
+ // is there a path of inline requests from start to goal?
+ def isReachable(start: MethodNode, goal: MethodNode): Boolean = {
+ @tailrec def reachableImpl(check: List[MethodNode], visited: Set[MethodNode]): Boolean = check match {
+ case x :: xs =>
+ if (x == goal) true
+ else if (visited(x)) reachableImpl(xs, visited)
+ else {
+ val callees = inlineRequestsForMethod(x).map(_.callee.get.callee)
+ reachableImpl(xs ::: callees.toList, visited + x)
+ }
+
+ case Nil =>
+ false
+ }
+ reachableImpl(List(start), Set.empty)
+ }
+
+ val result = new mutable.ListBuffer[Callsite]()
+ // sort the inline requests to ensure that removing requests is deterministic
+ for (r <- requests.sorted(callsiteOrdering)) {
+ // is there a chain of inlining requests that would inline the callsite method into the callee?
+ if (isReachable(r.callee.get.callee, r.callsiteMethod))
+ inlineRequestsForMethod(r.callsiteMethod) -= r
+ else
+ result += r
+ }
+ result.toList
+ }
+
+ // sort the remaining inline requests such that the leaves appear first, then those requests
+ // that become leaves, etc.
+ def leavesFirst(requests: List[Callsite], visited: Set[Callsite] = Set.empty): List[Callsite] = {
+ if (requests.isEmpty) Nil
+ else {
+ val (leaves, others) = requests.partition(r => {
+ val inlineRequestsForCallee = inlineRequestsForMethod(r.callee.get.callee)
+ inlineRequestsForCallee.forall(visited)
+ })
+ assert(leaves.nonEmpty, requests)
+ leaves ::: leavesFirst(others, visited ++ leaves)
+ }
+ }
+
+ leavesFirst(breakInlineCycles(requests))
+ }
+
+
+ /**
+ * Copy and adapt the instructions of a method to a callsite.
+ *
+ * Preconditions:
+ * - The maxLocals and maxStack values of the callsite method are correctly computed
+ * - The callsite method contains no unreachable basic blocks, i.e., running an [[Analyzer]]
+ * does not produce any `null` frames
+ *
+ * @param callsiteInstruction The invocation instruction
+ * @param callsiteStackHeight The stack height at the callsite
+ * @param callsiteMethod The method in which the invocation occurs
+ * @param callsiteClass The class in which the callsite method is defined
+ * @param callee The invoked method
+ * @param calleeDeclarationClass The class in which the invoked method is defined
+ * @param receiverKnownNotNull `true` if the receiver is known to be non-null
+ * @param keepLineNumbers `true` if LineNumberNodes should be copied to the call site
+ * @return `Some(message)` if inlining cannot be performed, `None` otherwise
+ */
+ def inline(callsiteInstruction: MethodInsnNode, callsiteStackHeight: Int, callsiteMethod: MethodNode, callsiteClass: ClassBType,
+ callee: MethodNode, calleeDeclarationClass: ClassBType,
+ receiverKnownNotNull: Boolean, keepLineNumbers: Boolean): Option[CannotInlineWarning] = {
+ canInline(callsiteInstruction, callsiteStackHeight, callsiteMethod, callsiteClass, callee, calleeDeclarationClass) orElse {
+ // New labels for the cloned instructions
+ val labelsMap = cloneLabels(callee)
+ val (clonedInstructions, instructionMap) = cloneInstructions(callee, labelsMap)
+ if (!keepLineNumbers) {
+ removeLineNumberNodes(clonedInstructions)
+ }
+
+ // local vars in the callee are shifted by the number of locals at the callsite
+ val localVarShift = callsiteMethod.maxLocals
+ clonedInstructions.iterator.asScala foreach {
+ case varInstruction: VarInsnNode => varInstruction.`var` += localVarShift
+ case _ => ()
+ }
+
+ // add a STORE instruction for each expected argument, including for THIS instance if any
+ val argStores = new InsnList
+ var nextLocalIndex = callsiteMethod.maxLocals
+ if (!isStaticMethod(callee)) {
+ if (!receiverKnownNotNull) {
+ argStores.add(new InsnNode(DUP))
+ val nonNullLabel = newLabelNode
+ argStores.add(new JumpInsnNode(IFNONNULL, nonNullLabel))
+ argStores.add(new InsnNode(ACONST_NULL))
+ argStores.add(new InsnNode(ATHROW))
+ argStores.add(nonNullLabel)
+ }
+ argStores.add(new VarInsnNode(ASTORE, nextLocalIndex))
+ nextLocalIndex += 1
+ }
+
+ // We just use an asm.Type here, no need to create the MethodBType.
+ val calleAsmType = asm.Type.getMethodType(callee.desc)
+
+ for(argTp <- calleAsmType.getArgumentTypes) {
+ val opc = argTp.getOpcode(ISTORE) // returns the correct xSTORE instruction for argTp
+ argStores.insert(new VarInsnNode(opc, nextLocalIndex)) // "insert" is "prepend" - the last argument is on the top of the stack
+ nextLocalIndex += argTp.getSize
+ }
+
+ clonedInstructions.insert(argStores)
+
+ // label for the exit of the inlined functions. xRETURNs are rplaced by GOTOs to this label.
+ val postCallLabel = newLabelNode
+ clonedInstructions.add(postCallLabel)
+
+ // replace xRETURNs:
+ // - store the return value (if any)
+ // - clear the stack of the inlined method (insert DROPs)
+ // - load the return value
+ // - GOTO postCallLabel
+
+ val returnType = calleAsmType.getReturnType
+ val hasReturnValue = returnType.getSort != asm.Type.VOID
+ val returnValueIndex = callsiteMethod.maxLocals + callee.maxLocals
+ nextLocalIndex += returnType.getSize
+
+ def returnValueStore(returnInstruction: AbstractInsnNode) = {
+ val opc = returnInstruction.getOpcode match {
+ case IRETURN => ISTORE
+ case LRETURN => LSTORE
+ case FRETURN => FSTORE
+ case DRETURN => DSTORE
+ case ARETURN => ASTORE
+ }
+ new VarInsnNode(opc, returnValueIndex)
+ }
+
+ // We run an interpreter to know the stack height at each xRETURN instruction and the sizes
+ // of the values on the stack.
+ val analyzer = new AsmAnalyzer(callee, calleeDeclarationClass.internalName)
+
+ for (originalReturn <- callee.instructions.iterator().asScala if isReturn(originalReturn)) {
+ val frame = analyzer.frameAt(originalReturn)
+ var stackHeight = frame.getStackSize
+
+ val inlinedReturn = instructionMap(originalReturn)
+ val returnReplacement = new InsnList
+
+ def drop(slot: Int) = returnReplacement add getPop(frame.peekDown(slot).getSize)
+
+ // for non-void methods, store the stack top into the return local variable
+ if (hasReturnValue) {
+ returnReplacement add returnValueStore(originalReturn)
+ stackHeight -= 1
+ }
+
+ // drop the rest of the stack
+ for (i <- 0 until stackHeight) drop(i)
+
+ returnReplacement add new JumpInsnNode(GOTO, postCallLabel)
+ clonedInstructions.insert(inlinedReturn, returnReplacement)
+ clonedInstructions.remove(inlinedReturn)
+ }
+
+ // Load instruction for the return value
+ if (hasReturnValue) {
+ val retVarLoad = {
+ val opc = returnType.getOpcode(ILOAD)
+ new VarInsnNode(opc, returnValueIndex)
+ }
+ clonedInstructions.insert(postCallLabel, retVarLoad)
+ }
+
+ callsiteMethod.instructions.insert(callsiteInstruction, clonedInstructions)
+ callsiteMethod.instructions.remove(callsiteInstruction)
+
+ callsiteMethod.localVariables.addAll(cloneLocalVariableNodes(callee, labelsMap, callee.name + "_").asJava)
+ callsiteMethod.tryCatchBlocks.addAll(cloneTryCatchBlockNodes(callee, labelsMap).asJava)
+
+ // Add all invocation instructions that were inlined to the call graph
+ callee.instructions.iterator().asScala foreach {
+ case originalCallsiteIns: MethodInsnNode =>
+ callGraph.callsites.get(originalCallsiteIns) match {
+ case Some(originalCallsite) =>
+ val newCallsiteIns = instructionMap(originalCallsiteIns).asInstanceOf[MethodInsnNode]
+ callGraph.callsites(newCallsiteIns) = Callsite(
+ callsiteInstruction = newCallsiteIns,
+ callsiteMethod = callsiteMethod,
+ callsiteClass = callsiteClass,
+ callee = originalCallsite.callee,
+ argInfos = Nil, // TODO: re-compute argInfos for new destination (once we actually compute them)
+ callsiteStackHeight = callsiteStackHeight + originalCallsite.callsiteStackHeight,
+ callsitePosition = originalCallsite.callsitePosition
+ )
+
+ case None =>
+ }
+
+ case _ =>
+ }
+ // Remove the elided invocation from the call graph
+ callGraph.callsites.remove(callsiteInstruction)
+
+ callsiteMethod.maxLocals += returnType.getSize + callee.maxLocals
+ callsiteMethod.maxStack = math.max(callsiteMethod.maxStack, callee.maxStack + callsiteStackHeight)
+
+ None
+ }
+ }
+
+ /**
+ * Check whether an inling can be performed. Parmeters are described in method [[inline]].
+ * @return `Some(message)` if inlining cannot be performed, `None` otherwise
+ */
+ def canInline(callsiteInstruction: MethodInsnNode, callsiteStackHeight: Int, callsiteMethod: MethodNode, callsiteClass: ClassBType,
+ callee: MethodNode, calleeDeclarationClass: ClassBType): Option[CannotInlineWarning] = {
+
+ def calleeDesc = s"${callee.name} of type ${callee.desc} in ${calleeDeclarationClass.internalName}"
+ def methodMismatch = s"Wrong method node for inlining ${textify(callsiteInstruction)}: $calleeDesc"
+ assert(callsiteInstruction.name == callee.name, methodMismatch)
+ assert(callsiteInstruction.desc == callee.desc, methodMismatch)
+ assert(!isConstructor(callee), s"Constructors cannot be inlined: $calleeDesc")
+ assert(!BytecodeUtils.isAbstractMethod(callee), s"Callee is abstract: $calleeDesc")
+ assert(callsiteMethod.instructions.contains(callsiteInstruction), s"Callsite ${textify(callsiteInstruction)} is not an instruction of $calleeDesc")
+
+ // When an exception is thrown, the stack is cleared before jumping to the handler. When
+ // inlining a method that catches an exception, all values that were on the stack before the
+ // call (in addition to the arguments) would be cleared (SI-6157). So we don't inline methods
+ // with handlers in case there are values on the stack.
+ // Alternatively, we could save all stack values below the method arguments into locals, but
+ // that would be inefficient: we'd need to pop all parameters, save the values, and push the
+ // parameters back for the (inlined) invocation. Similarly for the result after the call.
+ def stackHasNonParameters: Boolean = {
+ val expectedArgs = asm.Type.getArgumentTypes(callsiteInstruction.desc).length + (callsiteInstruction.getOpcode match {
+ case INVOKEVIRTUAL | INVOKESPECIAL | INVOKEINTERFACE => 1
+ case INVOKESTATIC => 0
+ case INVOKEDYNAMIC =>
+ assertionError(s"Unexpected opcode, cannot inline ${textify(callsiteInstruction)}")
+ })
+ callsiteStackHeight > expectedArgs
+ }
+
+ if (isSynchronizedMethod(callee)) {
+ // Could be done by locking on the receiver, wrapping the inlined code in a try and unlocking
+ // in finally. But it's probably not worth the effort, scala never emits synchronized methods.
+ Some(SynchronizedMethod(calleeDeclarationClass.internalName, callee.name, callee.desc))
+ } else if (!callee.tryCatchBlocks.isEmpty && stackHasNonParameters) {
+ Some(MethodWithHandlerCalledOnNonEmptyStack(
+ calleeDeclarationClass.internalName, callee.name, callee.desc,
+ callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc))
+ } else findIllegalAccess(callee.instructions, calleeDeclarationClass, callsiteClass) map {
+ case (illegalAccessIns, None) =>
+ IllegalAccessInstruction(
+ calleeDeclarationClass.internalName, callee.name, callee.desc,
+ callsiteClass.internalName, illegalAccessIns)
+
+ case (illegalAccessIns, Some(warning)) =>
+ IllegalAccessCheckFailed(
+ calleeDeclarationClass.internalName, callee.name, callee.desc,
+ callsiteClass.internalName, illegalAccessIns, warning)
+ }
+ }
+
+ /**
+ * Returns the first instruction in the `instructions` list that would cause a
+ * [[java.lang.IllegalAccessError]] when inlined into the `destinationClass`.
+ *
+ * If validity of some instruction could not be checked because an error occurred, the instruction
+ * is returned together with a warning message that describes the problem.
+ */
+ def findIllegalAccess(instructions: InsnList, calleeDeclarationClass: ClassBType, destinationClass: ClassBType): Option[(AbstractInsnNode, Option[OptimizerWarning])] = {
+
+ /**
+ * Check if a type is accessible to some class, as defined in JVMS 5.4.4.
+ * (A1) C is public
+ * (A2) C and D are members of the same run-time package
+ */
+ def classIsAccessible(accessed: BType, from: ClassBType = destinationClass): Either[OptimizerWarning, Boolean] = (accessed: @unchecked) match {
+ // TODO: A2 requires "same run-time package", which seems to be package + classloader (JMVS 5.3.). is the below ok?
+ case c: ClassBType => c.isPublic.map(_ || c.packageInternalName == from.packageInternalName)
+ case a: ArrayBType => classIsAccessible(a.elementType, from)
+ case _: PrimitiveBType => Right(true)
+ }
+
+ /**
+ * Check if a member reference is accessible from the [[destinationClass]], as defined in the
+ * JVMS 5.4.4. Note that the class name in a field / method reference is not necessarily the
+ * class in which the member is declared:
+ *
+ * class A { def f = 0 }; class B extends A { f }
+ *
+ * The INVOKEVIRTUAL instruction uses a method reference "B.f ()I". Therefore this method has
+ * two parameters:
+ *
+ * @param memberDeclClass The class in which the member is declared (A)
+ * @param memberRefClass The class used in the member reference (B)
+ *
+ * JVMS 5.4.4 summary: A field or method R is accessible to a class D (destinationClass) iff
+ * (B1) R is public
+ * (B2) R is protected, declared in C (memberDeclClass) and D is a subclass of C.
+ * If R is not static, R must contain a symbolic reference to a class T (memberRefClass),
+ * such that T is either a subclass of D, a superclass of D, or D itself.
+ * (B3) R is either protected or has default access and declared by a class in the same
+ * run-time package as D.
+ * (B4) R is private and is declared in D.
+ */
+ def memberIsAccessible(memberFlags: Int, memberDeclClass: ClassBType, memberRefClass: ClassBType): Either[OptimizerWarning, Boolean] = {
+ // TODO: B3 requires "same run-time package", which seems to be package + classloader (JMVS 5.3.). is the below ok?
+ def samePackageAsDestination = memberDeclClass.packageInternalName == destinationClass.packageInternalName
+
+ val key = (ACC_PUBLIC | ACC_PROTECTED | ACC_PRIVATE) & memberFlags
+ key match {
+ case ACC_PUBLIC => // B1
+ Right(true)
+
+ case ACC_PROTECTED => // B2
+ tryEither {
+ val condB2 = destinationClass.isSubtypeOf(memberDeclClass).orThrow && {
+ val isStatic = (ACC_STATIC & memberFlags) != 0
+ isStatic || memberRefClass.isSubtypeOf(destinationClass).orThrow || destinationClass.isSubtypeOf(memberRefClass).orThrow
+ }
+ Right(condB2 || samePackageAsDestination) // B3 (protected)
+ }
+
+ case 0 => // B3 (default access)
+ Right(samePackageAsDestination)
+
+ case ACC_PRIVATE => // B4
+ Right(memberDeclClass == destinationClass)
+ }
+ }
+
+ /**
+ * Check if `instruction` can be transplanted to `destinationClass`.
+ *
+ * If the instruction references a class, method or field that cannot be found in the
+ * byteCodeRepository, it is considered as not legal. This is known to happen in mixed
+ * compilation: for Java classes there is no classfile that could be parsed, nor does the
+ * compiler generate any bytecode.
+ *
+ * Returns a warning message describing the problem if checking the legality for the instruction
+ * failed.
+ */
+ def isLegal(instruction: AbstractInsnNode): Either[OptimizerWarning, Boolean] = instruction match {
+ case ti: TypeInsnNode =>
+ // NEW, ANEWARRAY, CHECKCAST or INSTANCEOF. For these instructions, the reference
+ // "must be a symbolic reference to a class, array, or interface type" (JVMS 6), so
+ // it can be an internal name, or a full array descriptor.
+ classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(ti.desc))
+
+ case ma: MultiANewArrayInsnNode =>
+ // "a symbolic reference to a class, array, or interface type"
+ classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(ma.desc))
+
+ case fi: FieldInsnNode =>
+ val fieldRefClass = classBTypeFromParsedClassfile(fi.owner)
+ for {
+ (fieldNode, fieldDeclClassNode) <- byteCodeRepository.fieldNode(fieldRefClass.internalName, fi.name, fi.desc): Either[OptimizerWarning, (FieldNode, InternalName)]
+ fieldDeclClass = classBTypeFromParsedClassfile(fieldDeclClassNode)
+ res <- memberIsAccessible(fieldNode.access, fieldDeclClass, fieldRefClass)
+ } yield {
+ res
+ }
+
+ case mi: MethodInsnNode =>
+ if (mi.owner.charAt(0) == '[') Right(true) // array methods are accessible
+ else {
+ def canInlineCall(opcode: Int, methodFlags: Int, methodDeclClass: ClassBType, methodRefClass: ClassBType): Either[OptimizerWarning, Boolean] = {
+ opcode match {
+ case INVOKESPECIAL if mi.name != GenBCode.INSTANCE_CONSTRUCTOR_NAME =>
+ // invokespecial is used for private method calls, super calls and instance constructor calls.
+ // private method and super calls can only be inlined into the same class.
+ Right(destinationClass == calleeDeclarationClass)
+
+ case _ => // INVOKEVIRTUAL, INVOKESTATIC, INVOKEINTERFACE and INVOKESPECIAL of constructors
+ memberIsAccessible(methodFlags, methodDeclClass, methodRefClass)
+ }
+ }
+
+ val methodRefClass = classBTypeFromParsedClassfile(mi.owner)
+ for {
+ (methodNode, methodDeclClassNode) <- byteCodeRepository.methodNode(methodRefClass.internalName, mi.name, mi.desc): Either[OptimizerWarning, (MethodNode, InternalName)]
+ methodDeclClass = classBTypeFromParsedClassfile(methodDeclClassNode)
+ res <- canInlineCall(mi.getOpcode, methodNode.access, methodDeclClass, methodRefClass)
+ } yield {
+ res
+ }
+ }
+
+ case ivd: InvokeDynamicInsnNode =>
+ // TODO @lry check necessary conditions to inline an indy, instead of giving up
+ Right(false)
+
+ case ci: LdcInsnNode => ci.cst match {
+ case t: asm.Type => classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(t.getInternalName))
+ case _ => Right(true)
+ }
+
+ case _ => Right(true)
+ }
+
+ val it = instructions.iterator.asScala
+ @tailrec def find: Option[(AbstractInsnNode, Option[OptimizerWarning])] = {
+ if (!it.hasNext) None // all instructions are legal
+ else {
+ val i = it.next()
+ isLegal(i) match {
+ case Left(warning) => Some((i, Some(warning))) // checking isLegal for i failed
+ case Right(false) => Some((i, None)) // an illegal instruction was found
+ case _ => find
+ }
+ }
+ }
+ find
+ }
+}
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala
index 87ad715e4d..f6cfc5598b 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala
@@ -8,10 +8,11 @@ package backend.jvm
package opt
import scala.annotation.switch
-import scala.tools.asm.{Opcodes, MethodWriter, ClassWriter}
-import scala.tools.asm.tree.analysis.{Analyzer, BasicValue, BasicInterpreter}
+import scala.tools.asm.Opcodes
+import scala.tools.asm.tree.analysis.{Analyzer, BasicInterpreter}
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._
import scala.tools.nsc.settings.ScalaSettings
@@ -48,6 +49,45 @@ import scala.tools.nsc.settings.ScalaSettings
*/
class LocalOpt(settings: ScalaSettings) {
/**
+ * Remove unreachable code from all methods of `classNode`. See of its overload.
+ *
+ * @param classNode The class to optimize
+ * @return `true` if unreachable code was removed from any method
+ */
+ def minimalRemoveUnreachableCode(classNode: ClassNode): Boolean = {
+ classNode.methods.asScala.foldLeft(false) {
+ case (changed, method) => minimalRemoveUnreachableCode(method, classNode.name) || changed
+ }
+ }
+
+ /**
+ * Remove unreachable code from a method.
+ *
+ * This implementation only removes instructions that are unreachable for an ASM analyzer /
+ * interpreter. This ensures that future analyses will not produce `null` frames. The inliner
+ * and call graph builder depend on this property.
+ */
+ def minimalRemoveUnreachableCode(method: MethodNode, ownerClassName: InternalName): Boolean = {
+ if (method.instructions.size == 0) return false // fast path for abstract methods
+
+ // For correctness, after removing unreachable code, we have to eliminate empty exception
+ // handlers, see scaladoc of def methodOptimizations. Removing an live handler may render more
+ // code unreachable and therefore requires running another round.
+ def removalRound(): Boolean = {
+ val (codeRemoved, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName)
+ if (codeRemoved) {
+ val liveHandlerRemoved = removeEmptyExceptionHandlers(method).exists(h => liveLabels(h.start))
+ if (liveHandlerRemoved) removalRound()
+ }
+ codeRemoved
+ }
+
+ val codeRemoved = removalRound()
+ if (codeRemoved) removeUnusedLocalVariableNodes(method)()
+ codeRemoved
+ }
+
+ /**
* Remove unreachable instructions from all (non-abstract) methods and apply various other
* cleanups to the bytecode.
*
@@ -73,7 +113,7 @@ class LocalOpt(settings: ScalaSettings) {
*
* Returns `true` if the bytecode of `method` was changed.
*/
- private def methodOptimizations(method: MethodNode, ownerClassName: String): Boolean = {
+ def methodOptimizations(method: MethodNode, ownerClassName: InternalName): Boolean = {
if (method.instructions.size == 0) return false // fast path for abstract methods
// unreachable-code also removes unused local variable nodes and empty exception handlers.
@@ -102,9 +142,7 @@ class LocalOpt(settings: ScalaSettings) {
// This triggers "ClassFormatError: Illegal exception table range in class file C". Similar
// for local variables in dead blocks. Maybe that's a bug in the ASM framework.
- var recurse = true
- var codeHandlersOrJumpsChanged = false
- while (recurse) {
+ def removalRound(): Boolean = {
// unreachable-code, empty-handlers and simplify-jumps run until reaching a fixpoint (see doc on class LocalOpt)
val (codeRemoved, handlersRemoved, liveHandlerRemoved) = if (settings.YoptUnreachableCode) {
val (codeRemoved, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName)
@@ -116,15 +154,18 @@ class LocalOpt(settings: ScalaSettings) {
val jumpsChanged = if (settings.YoptSimplifyJumps) simplifyJumps(method) else false
- codeHandlersOrJumpsChanged ||= (codeRemoved || handlersRemoved || jumpsChanged)
+ // Eliminating live handlers and simplifying jump instructions may render more code
+ // unreachable, so we need to run another round.
+ if (liveHandlerRemoved || jumpsChanged) removalRound()
- // The doc comment of class LocalOpt explains why we recurse if jumpsChanged || liveHandlerRemoved
- recurse = settings.YoptRecurseUnreachableJumps && (jumpsChanged || liveHandlerRemoved)
+ codeRemoved || handlersRemoved || jumpsChanged
}
+ val codeHandlersOrJumpsChanged = removalRound()
+
// (*) Removing stale local variable descriptors is required for correctness of unreachable-code
val localsRemoved =
- if (settings.YoptCompactLocals) compactLocalVariables(method)
+ if (settings.YoptCompactLocals) compactLocalVariables(method) // also removes unused
else if (settings.YoptUnreachableCode) removeUnusedLocalVariableNodes(method)() // (*)
else false
@@ -146,10 +187,10 @@ class LocalOpt(settings: ScalaSettings) {
*
* TODO: rewrite, don't use computeMaxLocalsMaxStack (runs a ClassWriter) / Analyzer. Too slow.
*/
- def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: String): (Boolean, Set[LabelNode]) = {
+ def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: InternalName): (Boolean, Set[LabelNode]) = {
// The data flow analysis requires the maxLocals / maxStack fields of the method to be computed.
computeMaxLocalsMaxStack(method)
- val a = new Analyzer[BasicValue](new BasicInterpreter)
+ val a = new Analyzer(new BasicInterpreter)
a.analyze(ownerClassName, method)
val frames = a.getFrames
@@ -319,29 +360,6 @@ class LocalOpt(settings: ScalaSettings) {
}
/**
- * In order to run an Analyzer, the maxLocals / maxStack fields need to be available. The ASM
- * framework only computes these values during bytecode generation.
- *
- * Since there's currently no better way, we run a bytecode generator on the method and extract
- * the computed values. This required changes to the ASM codebase:
- * - the [[MethodWriter]] class was made public
- * - accessors for maxLocals / maxStack were added to the MethodWriter class
- *
- * We could probably make this faster (and allocate less memory) by hacking the ASM framework
- * more: create a subclass of MethodWriter with a /dev/null byteVector. Another option would be
- * to create a separate visitor for computing those values, duplicating the functionality from the
- * MethodWriter.
- */
- private def computeMaxLocalsMaxStack(method: MethodNode) {
- val cw = new ClassWriter(ClassWriter.COMPUTE_MAXS)
- val excs = method.exceptions.asScala.toArray
- val mw = cw.visitMethod(method.access, method.name, method.desc, method.signature, excs).asInstanceOf[MethodWriter]
- method.accept(mw)
- method.maxLocals = mw.getMaxLocals
- method.maxStack = mw.getMaxStack
- }
-
- /**
* Removes LineNumberNodes that don't describe any executable instructions.
*
* This method expects (and asserts) that the `start` label of each LineNumberNode is the
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala
deleted file mode 100644
index 7002e43d98..0000000000
--- a/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala
+++ /dev/null
@@ -1,24 +0,0 @@
-/* NSC -- new Scala compiler
- * Copyright 2005-2014 LAMP/EPFL
- * @author Martin Odersky
- */
-
-package scala.tools.nsc
-package backend.jvm
-
-import scala.tools.asm
-import asm.tree._
-
-/**
- * Reporting utilities used in the optimizer.
- */
-object OptimizerReporting {
- def methodSignature(className: String, methodName: String, methodDescriptor: String): String = {
- className + "::" + methodName + methodDescriptor
- }
-
- def methodSignature(className: String, method: MethodNode): String = methodSignature(className, method.name, method.desc)
-
- def inlineFailure(reason: String): Nothing = MissingRequirementError.signal(reason)
- def assertionError(message: String): Nothing = throw new AssertionError(message)
-}
diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala
index d7f4cca615..d273995e6e 100644
--- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala
+++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala
@@ -218,23 +218,24 @@ trait ScalaSettings extends AbsScalaSettings
object YoptChoices extends MultiChoiceEnumeration {
val unreachableCode = Choice("unreachable-code", "Eliminate unreachable code, exception handlers protecting no instructions, debug information of eliminated variables.")
val simplifyJumps = Choice("simplify-jumps", "Simplify branching instructions, eliminate unnecessary ones.")
- val recurseUnreachableJumps = Choice("recurse-unreachable-jumps", "Recursively apply unreachable-code and simplify-jumps (if enabled) until reaching a fixpoint.")
val emptyLineNumbers = Choice("empty-line-numbers", "Eliminate unnecessary line number information.")
val emptyLabels = Choice("empty-labels", "Eliminate and collapse redundant labels in the bytecode.")
val compactLocals = Choice("compact-locals", "Eliminate empty slots in the sequence of local variables.")
+ val inlineProject = Choice("inline-project", "Inline only methods defined in the files being compiled")
+ val inlineGlobal = Choice("inline-global", "Inline methods from any source, including classfiles on the compile classpath")
val lNone = Choice("l:none", "Don't enable any optimizations.")
private val defaultChoices = List(unreachableCode)
val lDefault = Choice("l:default", "Enable default optimizations: "+ defaultChoices.mkString(","), expandsTo = defaultChoices)
- private val methodChoices = List(unreachableCode, simplifyJumps, recurseUnreachableJumps, emptyLineNumbers, emptyLabels, compactLocals)
+ private val methodChoices = List(unreachableCode, simplifyJumps, emptyLineNumbers, emptyLabels, compactLocals)
val lMethod = Choice("l:method", "Enable intra-method optimizations: "+ methodChoices.mkString(","), expandsTo = methodChoices)
- private val projectChoices = List(lMethod)
+ private val projectChoices = List(lMethod, inlineProject)
val lProject = Choice("l:project", "Enable cross-method optimizations within the current project: "+ projectChoices.mkString(","), expandsTo = projectChoices)
- private val classpathChoices = List(lProject)
+ private val classpathChoices = List(lProject, inlineGlobal)
val lClasspath = Choice("l:classpath", "Enable cross-method optimizations across the entire classpath: "+ classpathChoices.mkString(","), expandsTo = classpathChoices)
}
@@ -247,11 +248,33 @@ trait ScalaSettings extends AbsScalaSettings
def YoptNone = Yopt.isSetByUser && Yopt.value.isEmpty
def YoptUnreachableCode = !Yopt.isSetByUser || Yopt.contains(YoptChoices.unreachableCode)
def YoptSimplifyJumps = Yopt.contains(YoptChoices.simplifyJumps)
- def YoptRecurseUnreachableJumps = Yopt.contains(YoptChoices.recurseUnreachableJumps)
def YoptEmptyLineNumbers = Yopt.contains(YoptChoices.emptyLineNumbers)
def YoptEmptyLabels = Yopt.contains(YoptChoices.emptyLabels)
def YoptCompactLocals = Yopt.contains(YoptChoices.compactLocals)
+ def YoptInlineProject = Yopt.contains(YoptChoices.inlineProject)
+ def YoptInlineGlobal = Yopt.contains(YoptChoices.inlineGlobal)
+ def YoptInlinerEnabled = YoptInlineProject || YoptInlineGlobal
+
+ object YoptWarningsChoices extends MultiChoiceEnumeration {
+ val none = Choice("none" , "No optimizer warnings.")
+ val atInlineFailedSummary = Choice("at-inline-failed-summary" , "One-line summary if there were @inline method calls that could not be inlined.")
+ val atInlineFailed = Choice("at-inline-failed" , "A detailed warning for each @inline method call that could not be inlined.")
+ val noInlineMixed = Choice("no-inline-mixed" , "In mixed compilation, warn at callsites methods defined in java sources (the inlining decision cannot be made without bytecode).")
+ val noInlineMissingBytecode = Choice("no-inline-missing-bytecode" , "Warn if an inlining decision cannot be made because a the bytecode of a class or member cannot be found on the compilation classpath.")
+ val noInlineMissingScalaInlineInfoAttr = Choice("no-inline-missing-attribute", "Warn if an inlining decision cannot be made because a Scala classfile does not have a ScalaInlineInfo attribute.")
+ }
+
+ val YoptWarnings = MultiChoiceSetting(
+ name = "-Yopt-warnings",
+ helpArg = "warnings",
+ descr = "Enable optimizer warnings",
+ domain = YoptWarningsChoices,
+ default = Some(List(YoptWarningsChoices.atInlineFailed.name))) withPostSetHook (self => {
+ if (self.value subsetOf Set(YoptWarningsChoices.none, YoptWarningsChoices.atInlineFailedSummary)) YinlinerWarnings.value = false
+ else YinlinerWarnings.value = true
+ })
+
private def removalIn212 = "This flag is scheduled for removal in 2.12. If you have a case where you need this flag then please report a bug."
object YstatisticsPhases extends MultiChoiceEnumeration { val parser, typer, patmat, erasure, cleanup, jvm = Value }
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/src/reflect/scala/reflect/internal/SymbolTable.scala b/src/reflect/scala/reflect/internal/SymbolTable.scala
index 01e4cdf367..ef63078f90 100644
--- a/src/reflect/scala/reflect/internal/SymbolTable.scala
+++ b/src/reflect/scala/reflect/internal/SymbolTable.scala
@@ -355,6 +355,14 @@ abstract class SymbolTable extends macros.Universe
cache
}
+ /**
+ * Removes a cache from the per-run caches. This is useful for testing: it allows running the
+ * compiler and then inspect the state of a cache.
+ */
+ def unrecordCache[T <: Clearable](cache: T): Unit = {
+ caches = caches.filterNot(_.get eq cache)
+ }
+
def clearAll() = {
debuglog("Clearing " + caches.size + " caches.")
caches foreach (ref => Option(ref.get).foreach(_.clear))
diff --git a/test/files/pos/t9111-inliner-workaround.flags b/test/files/pos/t9111-inliner-workaround.flags
new file mode 100644
index 0000000000..63b5558cfd
--- /dev/null
+++ b/test/files/pos/t9111-inliner-workaround.flags
@@ -0,0 +1 @@
+-Ybackend:GenBCode -Yopt:l:classpath \ No newline at end of file
diff --git a/test/files/pos/t9111-inliner-workaround/A_1.java b/test/files/pos/t9111-inliner-workaround/A_1.java
new file mode 100644
index 0000000000..bc60b68ea6
--- /dev/null
+++ b/test/files/pos/t9111-inliner-workaround/A_1.java
@@ -0,0 +1,13 @@
+public class A_1 {
+ public static class T { }
+
+ public static class Inner {
+ public static class T { }
+
+ public void foo(T t) { }
+
+ public T t = null;
+
+ public class Deeper extends T { }
+ }
+}
diff --git a/test/files/pos/t9111-inliner-workaround/Test_1.scala b/test/files/pos/t9111-inliner-workaround/Test_1.scala
new file mode 100644
index 0000000000..1a00fff833
--- /dev/null
+++ b/test/files/pos/t9111-inliner-workaround/Test_1.scala
@@ -0,0 +1,10 @@
+object Test extends App {
+ println(new A_1.Inner())
+
+ // Accessing foo or Deeper triggers the error of SI-9111.
+ // However, when not referring to those definitions, compilation should
+ // succeed, also if the inliner is enabled.
+
+ // println(i.foo(null))
+ // new i.Deeper()
+}
diff --git a/test/files/run/bcodeInlinerMixed.flags b/test/files/run/bcodeInlinerMixed.flags
new file mode 100644
index 0000000000..63b5558cfd
--- /dev/null
+++ b/test/files/run/bcodeInlinerMixed.flags
@@ -0,0 +1 @@
+-Ybackend:GenBCode -Yopt:l:classpath \ No newline at end of file
diff --git a/test/files/run/bcodeInlinerMixed/A_1.java b/test/files/run/bcodeInlinerMixed/A_1.java
new file mode 100644
index 0000000000..44d7d88eeb
--- /dev/null
+++ b/test/files/run/bcodeInlinerMixed/A_1.java
@@ -0,0 +1,3 @@
+public class A_1 {
+ public static final int bar() { return 100; }
+}
diff --git a/test/files/run/bcodeInlinerMixed/B_1.scala b/test/files/run/bcodeInlinerMixed/B_1.scala
new file mode 100644
index 0000000000..2aadeccb82
--- /dev/null
+++ b/test/files/run/bcodeInlinerMixed/B_1.scala
@@ -0,0 +1,20 @@
+// Partest does proper mixed compilation:
+// 1. scalac *.scala *.java
+// 2. javac *.java
+// 3. scalc *.scala
+//
+// In the second scalc round, the classfile for A_1 is on the classpath.
+// Therefore the inliner has access to the bytecode of `bar`, which means
+// it can verify that the invocation to `bar` can be safely inlined.
+//
+// So both callsites of `flop` are inlined.
+//
+// In a single mixed compilation, `flop` cannot be inlined, see JUnit InlinerTest.scala, def mixedCompilationNoInline.
+
+class B {
+ @inline final def flop = A_1.bar
+ def g = flop
+}
+class C {
+ def h(b: B) = b.flop
+}
diff --git a/test/files/run/bcodeInlinerMixed/Test.scala b/test/files/run/bcodeInlinerMixed/Test.scala
new file mode 100644
index 0000000000..c8c7a9fe2a
--- /dev/null
+++ b/test/files/run/bcodeInlinerMixed/Test.scala
@@ -0,0 +1,16 @@
+import scala.tools.partest.{BytecodeTest, ASMConverters}
+import ASMConverters._
+
+object Test extends BytecodeTest {
+ def show: Unit = {
+ val gIns = instructionsFromMethod(getMethod(loadClassNode("B"), "g"))
+ val hIns = instructionsFromMethod(getMethod(loadClassNode("C"), "h"))
+ // val invocation = Invoke(INVOKESTATIC, A_1, bar, ()I, false)
+ for (i <- List(gIns, hIns)) {
+ assert(i exists {
+ case Invoke(_, _, "bar", "()I", _) => true
+ case _ => false
+ }, i mkString "\n")
+ }
+ }
+}
diff --git a/test/files/run/colltest1.scala b/test/files/run/colltest1.scala
index e0ec378585..de8780a050 100644
--- a/test/files/run/colltest1.scala
+++ b/test/files/run/colltest1.scala
@@ -1,5 +1,5 @@
/*
- * filter: inliner warnings; re-run with -Yinline-warnings for details
+ * filter: inliner warnings; re-run with
*/
import scala.collection._
import scala.language.postfixOps
diff --git a/test/files/run/compiler-asSeenFrom.scala b/test/files/run/compiler-asSeenFrom.scala
index 677dd40ddc..a60c2e8925 100644
--- a/test/files/run/compiler-asSeenFrom.scala
+++ b/test/files/run/compiler-asSeenFrom.scala
@@ -1,5 +1,5 @@
/*
- * filter: inliner warning; re-run with -Yinline-warnings for details
+ * filter: inliner warning; re-run with
*/
import scala.tools.nsc._
import scala.tools.partest.DirectTest
diff --git a/test/files/run/existentials-in-compiler.scala b/test/files/run/existentials-in-compiler.scala
index dfc7048b31..e516eddf95 100644
--- a/test/files/run/existentials-in-compiler.scala
+++ b/test/files/run/existentials-in-compiler.scala
@@ -1,5 +1,5 @@
/*
- * filter: inliner warnings; re-run with -Yinline-warnings for details
+ * filter: inliner warnings; re-run with
*/
import scala.tools.nsc._
import scala.tools.partest.CompilerTest
diff --git a/test/files/run/is-valid-num.scala b/test/files/run/is-valid-num.scala
index 4ab2fac8dd..156121cab5 100644
--- a/test/files/run/is-valid-num.scala
+++ b/test/files/run/is-valid-num.scala
@@ -1,5 +1,5 @@
/*
- * filter: inliner warnings; re-run with -Yinline-warnings for details
+ * filter: inliner warnings; re-run with
*/
object Test {
def x = BigInt("10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
diff --git a/test/files/run/iterator-from.scala b/test/files/run/iterator-from.scala
index e2ca5864ea..e7ba1aeb28 100644
--- a/test/files/run/iterator-from.scala
+++ b/test/files/run/iterator-from.scala
@@ -1,5 +1,5 @@
/* This file tests iteratorFrom, keysIteratorFrom, and valueIteratorFrom on various sorted sets and maps
- * filter: inliner warnings; re-run with -Yinline-warnings for details
+ * filter: inliner warnings; re-run with
*/
import scala.util.{Random => R}
diff --git a/test/files/run/mapConserve.scala b/test/files/run/mapConserve.scala
index f52af3b9f4..c17754283a 100644
--- a/test/files/run/mapConserve.scala
+++ b/test/files/run/mapConserve.scala
@@ -1,5 +1,5 @@
/*
- * filter: inliner warnings; re-run with -Yinline-warnings for details
+ * filter: inliner warnings; re-run with
*/
import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer
diff --git a/test/files/run/pc-conversions.scala b/test/files/run/pc-conversions.scala
index 5fecac9d94..d4ae305aa7 100644
--- a/test/files/run/pc-conversions.scala
+++ b/test/files/run/pc-conversions.scala
@@ -1,5 +1,5 @@
/*
- * filter: inliner warning; re-run with -Yinline-warnings for details
+ * filter: inliner warning; re-run with
*/
import collection._
diff --git a/test/files/run/stringinterpolation_macro-run.scala b/test/files/run/stringinterpolation_macro-run.scala
index e18375d521..ae7c0e5d7a 100644
--- a/test/files/run/stringinterpolation_macro-run.scala
+++ b/test/files/run/stringinterpolation_macro-run.scala
@@ -1,5 +1,5 @@
/*
- * filter: inliner warnings; re-run with -Yinline-warnings for details
+ * filter: inliner warnings; re-run with
*/
object Test extends App {
diff --git a/test/files/run/synchronized.check b/test/files/run/synchronized.check
index eab191b4ed..9add05ea0c 100644
--- a/test/files/run/synchronized.check
+++ b/test/files/run/synchronized.check
@@ -1,4 +1,8 @@
+#partest !-Ybackend:GenBCode
warning: there were 14 inliner warnings; re-run with -Yinline-warnings for details
+#partest -Ybackend:GenBCode
+warning: there were 14 inliner warnings; re-run with -Yopt-warnings for details
+#partest
.|. c1.f1: OK
.|. c1.fi: OK
.|... c1.fv: OK
diff --git a/test/files/run/t7096.scala b/test/files/run/t7096.scala
index 872562dd4d..f723d70abe 100644
--- a/test/files/run/t7096.scala
+++ b/test/files/run/t7096.scala
@@ -1,5 +1,5 @@
/*
- * filter: inliner warning; re-run with -Yinline-warnings for details
+ * filter: inliner warning; re-run with
*/
import scala.tools.partest._
import scala.tools.nsc._
diff --git a/test/files/run/t7582.check b/test/files/run/t7582.check
index cd951d8d4f..2a11210000 100644
--- a/test/files/run/t7582.check
+++ b/test/files/run/t7582.check
@@ -1,2 +1,6 @@
+#partest !-Ybackend:GenBCode
warning: there was one inliner warning; re-run with -Yinline-warnings for details
+#partest -Ybackend:GenBCode
+warning: there was one inliner warning; re-run with -Yopt-warnings for details
+#partest
2
diff --git a/test/files/run/t7582b.check b/test/files/run/t7582b.check
index cd951d8d4f..2a11210000 100644
--- a/test/files/run/t7582b.check
+++ b/test/files/run/t7582b.check
@@ -1,2 +1,6 @@
+#partest !-Ybackend:GenBCode
warning: there was one inliner warning; re-run with -Yinline-warnings for details
+#partest -Ybackend:GenBCode
+warning: there was one inliner warning; re-run with -Yopt-warnings for details
+#partest
2
diff --git a/test/junit/scala/collection/IterableViewLikeTest.scala b/test/junit/scala/collection/IterableViewLikeTest.scala
index ab09c4930b..435a43c215 100644
--- a/test/junit/scala/collection/IterableViewLikeTest.scala
+++ b/test/junit/scala/collection/IterableViewLikeTest.scala
@@ -13,6 +13,7 @@ class IterableViewLikeTest {
def hasCorrectDropAndTakeMethods() {
val iter = Iterable(1, 2, 3)
+ import scala.language.postfixOps
assertEquals(Iterable.empty[Int], iter.view take Int.MinValue force)
assertEquals(Iterable.empty[Int], iter.view takeRight Int.MinValue force)
assertEquals(iter, iter.view drop Int.MinValue force)
diff --git a/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala
index 2347e8288e..6ada0e20fb 100644
--- a/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala
+++ b/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala
@@ -7,35 +7,41 @@ import org.junit.Test
import scala.tools.asm.Opcodes
import org.junit.Assert._
-@RunWith(classOf[JUnit4])
-class BTypesTest {
- val settings = new Settings()
- settings.processArgumentString("-usejavacp")
- val g: Global = new Global(settings)
- val run = new g.Run() // initializes some compiler internals
- import g.{definitions => d, Symbol}
+import scala.tools.nsc.backend.jvm.CodeGenTools._
+import scala.tools.testing.ClearAfterClass
- def duringBackend[T](f: => T) = g.exitingDelambdafy(f)
+object BTypesTest extends ClearAfterClass.Clearable {
+ var compiler = {
+ val comp = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none")
+ new comp.Run() // initializes some of the compiler
+ comp.exitingDelambdafy(comp.scalaPrimitives.init()) // needed: it's only done when running the backend, and we don't actually run the compiler
+ comp.exitingDelambdafy(comp.genBCode.bTypes.initializeCoreBTypes())
+ comp
+ }
+ def clear(): Unit = { compiler = null }
+}
- val btypes = new BTypesFromSymbols[g.type](g)
- import btypes._
- duringBackend(btypes.initializeCoreBTypes())
+@RunWith(classOf[JUnit4])
+class BTypesTest extends ClearAfterClass {
+ ClearAfterClass.stateToClear = BTypesTest
- def classBTypeFromSymbol(sym: Symbol) = duringBackend(btypes.classBTypeFromSymbol(sym))
+ val compiler = BTypesTest.compiler
+ import compiler.genBCode.bTypes._
- val jlo = d.ObjectClass
- val jls = d.StringClass
+ def classBTFS(sym: compiler.Symbol) = compiler.exitingDelambdafy(classBTypeFromSymbol(sym))
- val o = classBTypeFromSymbol(jlo)
- val s = classBTypeFromSymbol(jls)
- val oArr = ArrayBType(o)
- val method = MethodBType(List(oArr, INT, DOUBLE, s), UNIT)
+ def jlo = compiler.definitions.ObjectClass
+ def jls = compiler.definitions.StringClass
+ def o = classBTFS(jlo)
+ def s = classBTFS(jls)
+ def oArr = ArrayBType(o)
+ def method = MethodBType(List(oArr, INT, DOUBLE, s), UNIT)
@Test
def classBTypesEquality() {
- val s1 = classBTypeFromSymbol(jls)
- val s2 = classBTypeFromSymbol(jls)
- val o = classBTypeFromSymbol(jlo)
+ val s1 = classBTFS(jls)
+ val s2 = classBTFS(jls)
+ val o = classBTFS(jlo)
assertEquals(s1, s2)
assertEquals(s1.hashCode, s2.hashCode)
assert(s1 != o)
@@ -53,7 +59,7 @@ class BTypesTest {
assert(FLOAT.typedOpcode(Opcodes.IALOAD) == Opcodes.FALOAD)
assert(LONG.typedOpcode(Opcodes.IALOAD) == Opcodes.LALOAD)
assert(DOUBLE.typedOpcode(Opcodes.IALOAD) == Opcodes.DALOAD)
- assert(classBTypeFromSymbol(jls).typedOpcode(Opcodes.IALOAD) == Opcodes.AALOAD)
+ assert(classBTFS(jls).typedOpcode(Opcodes.IALOAD) == Opcodes.AALOAD)
assert(UNIT.typedOpcode(Opcodes.IRETURN) == Opcodes.RETURN)
assert(BOOL.typedOpcode(Opcodes.IRETURN) == Opcodes.IRETURN)
@@ -64,7 +70,7 @@ class BTypesTest {
assert(FLOAT.typedOpcode(Opcodes.IRETURN) == Opcodes.FRETURN)
assert(LONG.typedOpcode(Opcodes.IRETURN) == Opcodes.LRETURN)
assert(DOUBLE.typedOpcode(Opcodes.IRETURN) == Opcodes.DRETURN)
- assert(classBTypeFromSymbol(jls).typedOpcode(Opcodes.IRETURN) == Opcodes.ARETURN)
+ assert(classBTFS(jls).typedOpcode(Opcodes.IRETURN) == Opcodes.ARETURN)
}
@Test
diff --git a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala
index c1c5a71b83..5d5215d887 100644
--- a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala
+++ b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala
@@ -2,16 +2,20 @@ package scala.tools.nsc.backend.jvm
import org.junit.Assert._
+import scala.collection.mutable.ListBuffer
import scala.reflect.internal.util.BatchSourceFile
import scala.reflect.io.VirtualDirectory
import scala.tools.asm.Opcodes
-import scala.tools.asm.tree.{AbstractInsnNode, LabelNode, ClassNode, MethodNode}
+import scala.tools.asm.tree.{ClassNode, MethodNode}
import scala.tools.cmd.CommandLineParser
import scala.tools.nsc.backend.jvm.opt.LocalOpt
-import scala.tools.nsc.settings.{MutableSettings, ScalaSettings}
+import scala.tools.nsc.io.AbstractFile
+import scala.tools.nsc.reporters.StoreReporter
+import scala.tools.nsc.settings.MutableSettings
import scala.tools.nsc.{Settings, Global}
import scala.tools.partest.ASMConverters
import scala.collection.JavaConverters._
+import scala.tools.testing.TempDir
object CodeGenTools {
import ASMConverters._
@@ -40,38 +44,104 @@ 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, new StoreReporter)
}
- 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
- (for (f <- outDir.iterator if !f.isDirectory) yield (f.name, f.toByteArray)).toList
+ new compiler.Run()
+ }
+
+ def reporter(compiler: Global) = compiler.reporter.asInstanceOf[StoreReporter]
+
+ 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) {
+ if (!f.isDirectory) res += ((f.name, f.toByteArray))
+ else if (f.name != "." && f.name != "..") res ++= files(f)
+ }
+ res.toList
+ }
+ files(outDir)
+ }
+
+ def checkReport(compiler: Global, allowMessage: StoreReporter#Info => Boolean = _ => false): Unit = {
+ val disallowed = reporter(compiler).infos.toList.filter(!allowMessage(_)) // toList prevents an infer-non-wildcard-existential warning.
+ if (disallowed.nonEmpty) {
+ val msg = disallowed.mkString("\n")
+ assert(false, "The compiler issued non-allowed warnings or errors:\n" + msg)
+ }
+ }
+
+ def compile(compiler: Global)(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[(String, Array[Byte])] = {
+ val run = newRun(compiler)
+ run.compileSources(makeSourceFile(scalaCode, "unitTestSource.scala") :: javaCode.map(p => makeSourceFile(p._1, p._2)))
+ checkReport(compiler, allowMessage)
+ 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 = "", allowMessage: StoreReporter#Info => Boolean = _ => false, afterEach: AbstractFile => Unit = _ => ()): 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")))
+ checkReport(compiler, allowMessage)
+ afterEach(outDir)
+ }
+
+ val classfiles = getGeneratedClassfiles(outDir)
+ outDir.delete()
+ classfiles
+ }
+
+ def compileClassesSeparately(codes: List[String], extraArgs: String = "", allowMessage: StoreReporter#Info => Boolean = _ => false, afterEach: AbstractFile => Unit = _ => ()) = {
+ readAsmClasses(compileSeparately(codes, extraArgs, allowMessage, afterEach))
+ }
+
+ def readAsmClasses(classfiles: List[(String, Array[Byte])]) = {
+ classfiles.map(p => AsmUtils.readClass(p._2)).sortBy(_.name)
}
- def compileClasses(compiler: Global)(code: String): List[ClassNode] = {
- compile(compiler)(code).map(p => AsmUtils.readClass(p._2)).sortBy(_.name)
+ def compileClasses(compiler: Global)(code: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = {
+ readAsmClasses(compile(compiler)(code, javaCode, allowMessage))
}
- def compileMethods(compiler: Global)(code: String): List[MethodNode] = {
- compileClasses(compiler)(s"class C { $code }").head.methods.asScala.toList.filterNot(_.name == "<init>")
+ def compileMethods(compiler: Global)(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): List[MethodNode] = {
+ compileClasses(compiler)(s"class C { $code }", allowMessage = allowMessage).head.methods.asScala.toList.filterNot(_.name == "<init>")
}
- def singleMethodInstructions(compiler: Global)(code: String): List[Instruction] = {
- val List(m) = compileMethods(compiler)(code)
+ def singleMethodInstructions(compiler: Global)(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): List[Instruction] = {
+ val List(m) = compileMethods(compiler)(code, allowMessage = allowMessage)
instructionsFromMethod(m)
}
- def singleMethod(compiler: Global)(code: String): Method = {
- val List(m) = compileMethods(compiler)(code)
+ def singleMethod(compiler: Global)(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): Method = {
+ val List(m) = compileMethods(compiler)(code, allowMessage = allowMessage)
convertMethod(m)
}
diff --git a/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala b/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala
index 89900291ca..4086f7dd7b 100644
--- a/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala
+++ b/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala
@@ -7,10 +7,18 @@ import org.junit.Assert._
import CodeGenTools._
import scala.tools.asm.Opcodes._
import scala.tools.partest.ASMConverters._
+import scala.tools.testing.ClearAfterClass
+
+object DirectCompileTest extends ClearAfterClass.Clearable {
+ var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:method")
+ def clear(): Unit = { compiler = null }
+}
@RunWith(classOf[JUnit4])
-class DirectCompileTest {
- val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:method")
+class DirectCompileTest extends ClearAfterClass {
+ ClearAfterClass.stateToClear = DirectCompileTest
+
+ val compiler = DirectCompileTest.compiler
@Test
def testCompile(): Unit = {
@@ -70,4 +78,21 @@ class DirectCompileTest {
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)
+ }
+
+ @Test
+ def compileErroneous(): Unit = {
+ compileClasses(compiler)("class C { def f: String = 1 }", allowMessage = _.msg contains "type mismatch")
+ }
}
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 2975bd060d..1b6c080234 100644
--- a/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala
+++ b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala
@@ -15,11 +15,14 @@ import CodeGenTools._
import scala.tools.partest.ASMConverters
import ASMConverters._
+import BackendReporting._
+
import scala.collection.convert.decorateAsScala._
@RunWith(classOf[JUnit4])
class BTypesFromClassfileTest {
- val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode")
+ // inliner enabled -> inlineInfos are collected (and compared) in ClassBTypes
+ val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:inline-global")
import compiler._
import definitions._
@@ -29,6 +32,7 @@ class BTypesFromClassfileTest {
def duringBackend[T](f: => T) = compiler.exitingDelambdafy(f)
val run = new compiler.Run() // initializes some of the compiler
+ duringBackend(compiler.scalaPrimitives.init()) // needed: it's only done when running the backend, and we don't actually run the compiler
duringBackend(bTypes.initializeCoreBTypes())
def clearCache() = bTypes.classBTypeFromInternalName.clear()
@@ -37,7 +41,7 @@ class BTypesFromClassfileTest {
if (checked(fromSym.internalName)) checked
else {
assert(fromSym == fromClassfile, s"$fromSym != $fromClassfile")
- sameInfo(fromSym.info, fromClassfile.info, checked + fromSym.internalName)
+ sameInfo(fromSym.info.get, fromClassfile.info.get, checked + fromSym.internalName)
}
}
@@ -57,8 +61,12 @@ class BTypesFromClassfileTest {
else (fromSym.flags | ACC_PRIVATE | ACC_PUBLIC) == (fromClassfile.flags | ACC_PRIVATE | ACC_PUBLIC)
}, s"class flags differ\n$fromSym\n$fromClassfile")
- val chk1 = sameBTypes(fromSym.superClass, fromClassfile.superClass, checked)
+ // 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
@@ -67,7 +75,7 @@ class BTypesFromClassfileTest {
// and anonymous classes as members of the outer class. But not for unpickled symbols).
// The fromClassfile info has all nested classes, including anonymous and local. So we filter
// them out: member classes are identified by having the `outerName` defined.
- val memberClassesFromClassfile = fromClassfile.nestedClasses.filter(_.info.nestedInfo.get.outerName.isDefined)
+ val memberClassesFromClassfile = fromClassfile.nestedClasses.filter(_.info.get.nestedInfo.get.outerName.isDefined)
// Sorting is required: the backend sorts all InnerClass entries by internalName before writing
// them to the classfile (to make it deterministic: the entries are collected in a Set during
// code generation).
diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala
new file mode 100644
index 0000000000..9fda034a04
--- /dev/null
+++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala
@@ -0,0 +1,152 @@
+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 scala.tools.asm.Opcodes._
+import org.junit.Assert._
+
+import scala.tools.asm.tree._
+import scala.tools.asm.tree.analysis._
+import scala.tools.nsc.reporters.StoreReporter
+import scala.tools.testing.AssertUtil._
+
+import CodeGenTools._
+import scala.tools.partest.ASMConverters
+import ASMConverters._
+import AsmUtils._
+import BackendReporting._
+
+import scala.collection.convert.decorateAsScala._
+
+@RunWith(classOf[JUnit4])
+class CallGraphTest {
+ val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:inline-global -Yopt-warnings")
+ import compiler.genBCode.bTypes._
+
+ // allows inspecting the caches after a compilation run
+ val notPerRun: List[Clearable] = List(classBTypeFromInternalName, byteCodeRepository.classes, callGraph.callsites)
+ notPerRun foreach compiler.perRunCaches.unrecordCache
+
+ def compile(code: String, allowMessage: StoreReporter#Info => Boolean): List[ClassNode] = {
+ notPerRun.foreach(_.clear())
+ compileClasses(compiler)(code, allowMessage = allowMessage)
+ }
+
+ def callsInMethod(methodNode: MethodNode): List[MethodInsnNode] = methodNode.instructions.iterator.asScala.collect({
+ case call: MethodInsnNode => call
+ }).toList
+
+ @Test
+ def callGraphStructure(): Unit = {
+ val code =
+ """class C {
+ | // try-catch prevents inlining - we want to analyze the callsite
+ | def f1 = try { 0 } catch { case _: Throwable => 1 }
+ | final def f2 = try { 0 } catch { case _: Throwable => 1 }
+ |
+ | @inline def f3 = try { 0 } catch { case _: Throwable => 1 }
+ | @inline final def f4 = try { 0 } catch { case _: Throwable => 1 }
+ |
+ | @noinline def f5 = try { 0 } catch { case _: Throwable => 1 }
+ | @noinline final def f6 = try { 0 } catch { case _: Throwable => 1 }
+ |
+ | @inline @noinline def f7 = try { 0 } catch { case _: Throwable => 1 }
+ |}
+ |class D extends C {
+ | @inline override def f1 = try { 0 } catch { case _: Throwable => 1 }
+ | override final def f3 = try { 0 } catch { case _: Throwable => 1 }
+ |}
+ |object C {
+ | def g1 = try { 0 } catch { case _: Throwable => 1 }
+ |}
+ |class Test {
+ | def t1(c: C) = c.f1 + c.f2 + c.f3 + c.f4 + c.f5 + c.f6 + c.f7 + C.g1
+ | def t2(d: D) = d.f1 + d.f2 + d.f3 + d.f4 + d.f5 + d.f6 + d.f7 + C.g1
+ |}
+ """.stripMargin
+
+ // Get the ClassNodes from the code repo (don't use the unparsed ClassNodes returned by compile).
+ // The callGraph.callsites map is indexed by instructions of those ClassNodes.
+
+ val ok = Set(
+ "D::f1()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", // only one warning for D.f1: C.f1 is not annotated @inline
+ "C::f3()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", // only one warning for C.f3: D.f3 does not have @inline (and it would also be safe to inline)
+ "C::f7()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", // two warnings (the error message mentions C.f7 even if the receiver type is D, because f7 is inherited from C)
+ "operand stack at the callsite in Test::t1(LC;)I contains more values",
+ "operand stack at the callsite in Test::t2(LD;)I contains more values")
+ var msgCount = 0
+ val checkMsg = (m: StoreReporter#Info) => {
+ msgCount += 1
+ ok exists (m.msg contains _)
+ }
+ val List(cCls, cMod, dCls, testCls) = compile(code, checkMsg).map(c => byteCodeRepository.classNode(c.name).get)
+ assert(msgCount == 6, msgCount)
+
+ val List(cf1, cf2, cf3, cf4, cf5, cf6, cf7) = cCls.methods.iterator.asScala.filter(_.name.startsWith("f")).toList.sortBy(_.name)
+ val List(df1, df3) = dCls.methods.iterator.asScala.filter(_.name.startsWith("f")).toList.sortBy(_.name)
+ val g1 = cMod.methods.iterator.asScala.find(_.name == "g1").get
+ val List(t1, t2) = testCls.methods.iterator.asScala.filter(_.name.startsWith("t")).toList.sortBy(_.name)
+
+ val List(cf1Call, cf2Call, cf3Call, cf4Call, cf5Call, cf6Call, cf7Call, cg1Call) = callsInMethod(t1)
+ val List(df1Call, df2Call, df3Call, df4Call, df5Call, df6Call, df7Call, dg1Call) = callsInMethod(t2)
+
+ def checkCallsite(callsite: callGraph.Callsite,
+ call: MethodInsnNode, callsiteMethod: MethodNode, target: MethodNode, calleeDeclClass: ClassBType,
+ safeToInline: Boolean, atInline: Boolean, atNoInline: Boolean) = try {
+ assert(callsite.callsiteInstruction == call)
+ assert(callsite.callsiteMethod == callsiteMethod)
+ val callee = callsite.callee.get
+ assert(callee.callee == target)
+ assert(callee.calleeDeclarationClass == calleeDeclClass)
+ assert(callee.safeToInline == safeToInline)
+ assert(callee.annotatedInline == atInline)
+ assert(callee.annotatedNoInline == atNoInline)
+
+ assert(callsite.argInfos == List()) // not defined yet
+ } catch {
+ case e: Throwable => println(callsite); throw e
+ }
+
+ val cClassBType = classBTypeFromClassNode(cCls)
+ val cMClassBType = classBTypeFromClassNode(cMod)
+ val dClassBType = classBTypeFromClassNode(dCls)
+
+ checkCallsite(callGraph.callsites(cf1Call),
+ cf1Call, t1, cf1, cClassBType, false, false, false)
+ checkCallsite(callGraph.callsites(cf2Call),
+ cf2Call, t1, cf2, cClassBType, true, false, false)
+ checkCallsite(callGraph.callsites(cf3Call),
+ cf3Call, t1, cf3, cClassBType, false, true, false)
+ checkCallsite(callGraph.callsites(cf4Call),
+ cf4Call, t1, cf4, cClassBType, true, true, false)
+ checkCallsite(callGraph.callsites(cf5Call),
+ cf5Call, t1, cf5, cClassBType, false, false, true)
+ checkCallsite(callGraph.callsites(cf6Call),
+ cf6Call, t1, cf6, cClassBType, true, false, true)
+ checkCallsite(callGraph.callsites(cf7Call),
+ cf7Call, t1, cf7, cClassBType, false, true, true)
+ checkCallsite(callGraph.callsites(cg1Call),
+ cg1Call, t1, g1, cMClassBType, true, false, false)
+
+ checkCallsite(callGraph.callsites(df1Call),
+ df1Call, t2, df1, dClassBType, false, true, false)
+ checkCallsite(callGraph.callsites(df2Call),
+ df2Call, t2, cf2, cClassBType, true, false, false)
+ checkCallsite(callGraph.callsites(df3Call),
+ df3Call, t2, df3, dClassBType, true, false, false)
+ checkCallsite(callGraph.callsites(df4Call),
+ df4Call, t2, cf4, cClassBType, true, true, false)
+ checkCallsite(callGraph.callsites(df5Call),
+ df5Call, t2, cf5, cClassBType, false, false, true)
+ checkCallsite(callGraph.callsites(df6Call),
+ df6Call, t2, cf6, cClassBType, true, false, true)
+ checkCallsite(callGraph.callsites(df7Call),
+ df7Call, t2, cf7, cClassBType, false, true, true)
+ checkCallsite(callGraph.callsites(dg1Call),
+ dg1Call, t2, g1, cMClassBType, true, false, false)
+ }
+}
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/EmptyExceptionHandlersTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala
index 7d83c54b5b..7b0504fec0 100644
--- a/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala
+++ b/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala
@@ -11,9 +11,23 @@ import org.junit.Assert._
import CodeGenTools._
import scala.tools.partest.ASMConverters
import ASMConverters._
+import scala.tools.testing.ClearAfterClass
+
+object EmptyExceptionHandlersTest extends ClearAfterClass.Clearable {
+ var noOptCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none")
+ var dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code")
+ def clear(): Unit = {
+ noOptCompiler = null
+ dceCompiler = null
+ }
+}
@RunWith(classOf[JUnit4])
-class EmptyExceptionHandlersTest {
+class EmptyExceptionHandlersTest extends ClearAfterClass {
+ ClearAfterClass.stateToClear = EmptyExceptionHandlersTest
+
+ val noOptCompiler = EmptyExceptionHandlersTest.noOptCompiler
+ val dceCompiler = EmptyExceptionHandlersTest.dceCompiler
val exceptionDescriptor = "java/lang/Exception"
@@ -51,9 +65,6 @@ class EmptyExceptionHandlersTest {
assertTrue(convertMethod(asmMethod).handlers.isEmpty)
}
- val noOptCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none")
- val dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code")
-
@Test
def eliminateUnreachableHandler(): Unit = {
val code = "def f: Unit = try { } catch { case _: Exception => println(0) }; println(1)"
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..57088bdd2f
--- /dev/null
+++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala
@@ -0,0 +1,67 @@
+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 BackendReporting._
+
+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.get.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/InlineWarningTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala
new file mode 100644
index 0000000000..fedc074a15
--- /dev/null
+++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala
@@ -0,0 +1,146 @@
+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 scala.collection.mutable.ListBuffer
+import scala.reflect.internal.util.BatchSourceFile
+import scala.tools.asm.Opcodes._
+import org.junit.Assert._
+
+import scala.tools.asm.tree._
+import scala.tools.asm.tree.analysis._
+import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.AsmAnalyzer
+import scala.tools.nsc.io._
+import scala.tools.nsc.reporters.StoreReporter
+import scala.tools.testing.AssertUtil._
+
+import CodeGenTools._
+import scala.tools.partest.ASMConverters
+import ASMConverters._
+import AsmUtils._
+
+import BackendReporting._
+
+import scala.collection.convert.decorateAsScala._
+import scala.tools.testing.ClearAfterClass
+
+object InlineWarningTest extends ClearAfterClass.Clearable {
+ val argsNoWarn = "-Ybackend:GenBCode -Yopt:l:classpath"
+ val args = argsNoWarn + " -Yopt-warnings"
+ var compiler = newCompiler(extraArgs = args)
+ def clear(): Unit = { compiler = null }
+}
+
+@RunWith(classOf[JUnit4])
+class InlineWarningTest extends ClearAfterClass {
+ ClearAfterClass.stateToClear = InlineWarningTest
+
+ val compiler = InlineWarningTest.compiler
+
+ def compile(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = {
+ compileClasses(compiler)(scalaCode, javaCode, allowMessage)
+ }
+
+ @Test
+ def nonFinal(): Unit = {
+ val code =
+ """class C {
+ | @inline def m1 = 1
+ |}
+ |trait T {
+ | @inline def m2 = 1
+ |}
+ |class D extends C with T
+ |
+ |class Test {
+ | def t1(c: C, t: T, d: D) = c.m1 + t.m2 + d.m1 + d.m2
+ |}
+ """.stripMargin
+ var count = 0
+ val warns = Set(
+ "C::m1()I is annotated @inline but cannot be inlined: the method is not final and may be overridden",
+ "T::m2()I is annotated @inline but cannot be inlined: the method is not final and may be overridden",
+ "D::m2()I is annotated @inline but cannot be inlined: the method is not final and may be overridden")
+ compile(code, allowMessage = i => {count += 1; warns.exists(i.msg contains _)})
+ assert(count == 4, count)
+ }
+
+ @Test
+ def traitMissingImplClass(): Unit = {
+ val codeA = "trait T { @inline final def f = 1 }"
+ val codeB = "class C { def t1(t: T) = t.f }"
+
+ val removeImpl = (outDir: AbstractFile) => {
+ val f = outDir.lookupName("T$class.class", directory = false)
+ if (f != null) f.delete()
+ }
+
+ val warn =
+ """T::f()I is annotated @inline but cannot be inlined: the trait method call could not be rewritten to the static implementation method. Possible reason:
+ |The method f(LT;)I could not be found in the class T$class or any of its parents.
+ |Note that the following parent classes could not be found on the classpath: T$class""".stripMargin
+
+ var c = 0
+ compileSeparately(List(codeA, codeB), extraArgs = InlineWarningTest.args, afterEach = removeImpl, allowMessage = i => {c += 1; i.msg contains warn})
+ assert(c == 1, c)
+
+ // only summary here
+ compileSeparately(List(codeA, codeB), extraArgs = InlineWarningTest.argsNoWarn, afterEach = removeImpl, allowMessage = _.msg contains "there was one inliner warning")
+ }
+
+ @Test
+ def handlerNonEmptyStack(): Unit = {
+ val code =
+ """class C {
+ | @noinline def q = 0
+ | @inline final def foo = try { q } catch { case e: Exception => 2 }
+ | def t1 = println(foo) // inline warning here: foo cannot be inlined on top of a non-empty stack
+ |}
+ """.stripMargin
+
+ var c = 0
+ compile(code, allowMessage = i => {c += 1; i.msg contains "operand stack at the callsite in C::t1()V contains more values"})
+ assert(c == 1, c)
+ }
+
+ @Test
+ def mixedWarnings(): Unit = {
+ val javaCode =
+ """public class A {
+ | public static final int bar() { return 100; }
+ |}
+ """.stripMargin
+
+ val scalaCode =
+ """class B {
+ | @inline final def flop = A.bar
+ | def g = flop
+ |}
+ """.stripMargin
+
+ val warns = List(
+ """failed to determine if bar should be inlined:
+ |The method bar()I could not be found in the class A or any of its parents.
+ |Note that the following parent classes are defined in Java sources (mixed compilation), no bytecode is available: A""".stripMargin,
+
+ """B::flop()I is annotated @inline but could not be inlined:
+ |Failed to check if B::flop()I can be safely inlined to B without causing an IllegalAccessError. Checking instruction INVOKESTATIC A.bar ()I failed:
+ |The method bar()I could not be found in the class A or any of its parents.
+ |Note that the following parent classes are defined in Java sources (mixed compilation), no bytecode is available: A""".stripMargin)
+
+ var c = 0
+ val List(b) = compile(scalaCode, List((javaCode, "A.java")), allowMessage = i => {c += 1; warns.tail.exists(i.msg contains _)})
+ assert(c == 1, c)
+
+ // no warnings here
+ compileClasses(newCompiler(extraArgs = InlineWarningTest.argsNoWarn + " -Yopt-warnings:none"))(scalaCode, List((javaCode, "A.java")))
+
+ c = 0
+ compileClasses(newCompiler(extraArgs = InlineWarningTest.argsNoWarn + " -Yopt-warnings:no-inline-mixed"))(scalaCode, List((javaCode, "A.java")), allowMessage = i => {c += 1; warns.exists(i.msg contains _)})
+ assert(c == 2, c)
+ }
+}
diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala
new file mode 100644
index 0000000000..b4839dcec8
--- /dev/null
+++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala
@@ -0,0 +1,198 @@
+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 scala.tools.asm.tree._
+import scala.tools.testing.AssertUtil._
+
+import CodeGenTools._
+import scala.tools.partest.ASMConverters
+import ASMConverters._
+import AsmUtils._
+
+import scala.collection.convert.decorateAsScala._
+import scala.tools.testing.ClearAfterClass
+
+object InlinerIllegalAccessTest extends ClearAfterClass.Clearable {
+ var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none")
+ def clear(): Unit = { compiler = null }
+}
+
+@RunWith(classOf[JUnit4])
+class InlinerIllegalAccessTest extends ClearAfterClass {
+ ClearAfterClass.stateToClear = InlinerIllegalAccessTest
+
+ val compiler = InlinerIllegalAccessTest.compiler
+ import compiler.genBCode.bTypes._
+
+ def addToRepo(cls: List[ClassNode]): Unit = for (c <- cls) byteCodeRepository.add(c, ByteCodeRepository.Classfile)
+ def assertEmpty(ins: Option[AbstractInsnNode]) = for (i <- ins) throw new AssertionError(textify(i))
+
+ @Test
+ def typeAccessible(): Unit = {
+ val code =
+ """package a {
+ | private class C { // the Scala compiler makes all classes public
+ | def f1 = new C // NEW a/C
+ | def f2 = new Array[C](0) // ANEWARRAY a/C
+ | def f3 = new Array[Array[C]](0) // ANEWARRAY [La/C;
+ | }
+ | class D
+ |}
+ |package b {
+ | class E
+ |}
+ """.stripMargin
+
+ val allClasses = compileClasses(compiler)(code)
+ val List(cClass, dClass, eClass) = allClasses
+ assert(cClass.name == "a/C" && dClass.name == "a/D" && eClass.name == "b/E", s"${cClass.name}, ${dClass.name}, ${eClass.name}")
+ addToRepo(allClasses) // they are not on the compiler's classpath, so we add them manually to the code repo
+
+ val methods = cClass.methods.asScala.filter(_.name(0) == 'f').toList
+
+ def check(classNode: ClassNode, test: Option[AbstractInsnNode] => Unit) = {
+ for (m <- methods)
+ test(inliner.findIllegalAccess(m.instructions, classBTypeFromParsedClassfile(cClass.name), classBTypeFromParsedClassfile(classNode.name)).map(_._1))
+ }
+
+ check(cClass, assertEmpty)
+ check(dClass, assertEmpty)
+ check(eClass, assertEmpty) // C is public, so accessible in E
+
+ byteCodeRepository.classes.clear()
+ classBTypeFromInternalName.clear()
+
+ cClass.access &= ~ACC_PUBLIC // ftw
+ addToRepo(allClasses)
+
+ // private classes can be accessed from the same package
+ check(cClass, assertEmpty)
+ check(dClass, assertEmpty) // accessing a private class in the same package is OK
+ check(eClass, {
+ case Some(ti: TypeInsnNode) if Set("a/C", "[La/C;")(ti.desc) => ()
+ // MatchError otherwise
+ })
+ }
+
+ @Test
+ def memberAccessible(): Unit = {
+ val code =
+ """package a {
+ | class C {
+ | /*public*/ def a = 0
+ | /*default*/ def b = 0
+ | protected def c = 0
+ | private def d = 0
+ |
+ | /*public static*/ def e = 0
+ | /*default static*/ def f = 0
+ | protected /*static*/ def g = 0
+ | private /*static*/ def h = 0
+ |
+ | def raC = a
+ | def rbC = b
+ | def rcC = c
+ | def rdC = d
+ | def reC = e
+ | def rfC = f
+ | def rgC = g
+ | def rhC = h
+ | }
+ |
+ | class D extends C {
+ | def rbD = b // 1: default access b, accessed in D, declared in C. can be inlined into any class in the same package as C.
+ | def rcD = c // 2: protected c, accessed in D. can be inlined into C, D or E, but not into F (F and D are unrelated).
+ |
+ | def rfD = f // 1
+ | def rgD = g // 2
+ | }
+ | class E extends D
+ |
+ | class F extends C
+ |
+ | class G
+ |}
+ |
+ |package b {
+ | class H extends a.C
+ | class I
+ |}
+ """.stripMargin
+
+ val allClasses = compileClasses(compiler)(code)
+ val List(cCl, dCl, eCl, fCl, gCl, hCl, iCl) = allClasses
+ addToRepo(allClasses)
+
+ // set flags that Scala scala doesn't (default access, static) - a hacky way to test all access modes.
+ val names = ('a' to 'h').map(_.toString).toSet
+ val List(a, b, c, d, e, f, g, h) = cCl.methods.asScala.toList.filter(m => names(m.name))
+
+ def checkAccess(a: MethodNode, expected: Int): Unit = {
+ assert((a.access & (ACC_STATIC | ACC_PUBLIC | ACC_PROTECTED | ACC_PRIVATE)) == expected, s"${a.name}, ${a.access}")
+ }
+
+ checkAccess(a, ACC_PUBLIC)
+ b.access &= ~ACC_PUBLIC; checkAccess(b, 0) // make it default access
+ c.access &= ~ACC_PUBLIC; c.access |= ACC_PROTECTED; checkAccess(c, ACC_PROTECTED) // make it protected - scalac actually never emits PROTECTED in bytecode, see javaFlags in BTypesFromSymbols
+ checkAccess(d, ACC_PRIVATE)
+
+ e.access |= ACC_STATIC; checkAccess(e, ACC_STATIC | ACC_PUBLIC)
+ f.access &= ~ACC_PUBLIC; f.access |= ACC_STATIC; checkAccess(f, ACC_STATIC)
+ g.access &= ~ACC_PUBLIC; g.access |= (ACC_STATIC | ACC_PROTECTED); checkAccess(g, ACC_STATIC | ACC_PROTECTED)
+ h.access |= ACC_STATIC; checkAccess(h, ACC_STATIC | ACC_PRIVATE)
+
+ val List(raC, rbC, rcC, rdC, reC, rfC, rgC, rhC) = cCl.methods.asScala.toList.filter(_.name(0) == 'r').sortBy(_.name)
+
+ val List(rbD, rcD, rfD, rgD) = dCl.methods.asScala.toList.filter(_.name(0) == 'r').sortBy(_.name)
+
+ def check(method: MethodNode, decl: ClassNode, dest: ClassNode, test: Option[AbstractInsnNode] => Unit): Unit = {
+ test(inliner.findIllegalAccess(method.instructions, classBTypeFromParsedClassfile(decl.name), classBTypeFromParsedClassfile(dest.name)).map(_._1))
+ }
+
+ val cOrDOwner = (_: Option[AbstractInsnNode] @unchecked) match {
+ case Some(mi: MethodInsnNode) if Set("a/C", "a/D")(mi.owner) => ()
+ // MatchError otherwise
+ }
+
+ // PUBLIC
+
+ // public methods allowed everywhere
+ for (m <- Set(raC, reC); c <- allClasses) check(m, cCl, c, assertEmpty)
+
+ // DEFAULT ACCESS
+
+ // default access OK in same package
+ for ((m, declCls) <- Set((rbC, cCl), (rfC, cCl), (rbD, dCl), (rfD, dCl)); c <- allClasses) {
+ if (c.name startsWith "a/") check(m, declCls, c, assertEmpty)
+ else check(m, declCls, c, cOrDOwner)
+ }
+
+ // PROTECTED
+
+ // protected accessed in same class, or protected static accessed in subclass(rgD).
+ // can be inlined to subclasses, and classes in the same package (gCl)
+ for ((m, declCls) <- Set((rcC, cCl), (rgC, cCl), (rgD, dCl)); c <- Set(cCl, dCl, eCl, fCl, gCl, hCl)) check(m, declCls, c, assertEmpty)
+
+ // protected in non-subclass and different package
+ for (m <- Set(rcC, rgC)) check(m, cCl, iCl, cOrDOwner)
+
+ // non-static protected accessed in subclass (rcD). can be inlined to related class, or classes in the same package
+ for (c <- Set(cCl, dCl, eCl, fCl, gCl)) check(rcD, dCl, c, assertEmpty)
+
+ // rcD cannot be inlined into non-related classes, if the declaration and destination are not in the same package
+ for (c <- Set(hCl, iCl)) check(rcD, dCl, c, cOrDOwner)
+
+ // PRIVATE
+
+ // privated method accesses can only be inlined in the same class
+ for (m <- Set(rdC, rhC)) check(m, cCl, cCl, assertEmpty)
+ for (m <- Set(rdC, rhC); c <- allClasses.tail) check(m, cCl, c, cOrDOwner)
+ }
+}
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..5c9bd1c188
--- /dev/null
+++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala
@@ -0,0 +1,115 @@
+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 warn = "T::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden"
+ val List(c, o, oMod, t, tCls) = compileClassesSeparately(List(codeA, codeB), args + " -Yopt-warnings", _.msg contains warn)
+ 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
new file mode 100644
index 0000000000..39fb28570e
--- /dev/null
+++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala
@@ -0,0 +1,953 @@
+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 scala.collection.mutable.ListBuffer
+import scala.reflect.internal.util.BatchSourceFile
+import scala.tools.asm.Opcodes._
+import org.junit.Assert._
+
+import scala.tools.asm.tree._
+import scala.tools.asm.tree.analysis._
+import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.AsmAnalyzer
+import scala.tools.nsc.io._
+import scala.tools.nsc.reporters.StoreReporter
+import scala.tools.testing.AssertUtil._
+
+import CodeGenTools._
+import scala.tools.partest.ASMConverters
+import ASMConverters._
+import AsmUtils._
+
+import BackendReporting._
+
+import scala.collection.convert.decorateAsScala._
+import scala.tools.testing.ClearAfterClass
+
+object InlinerTest extends ClearAfterClass.Clearable {
+ val args = "-Ybackend:GenBCode -Yopt:l:classpath -Yopt-warnings"
+ var compiler = newCompiler(extraArgs = args)
+
+ // allows inspecting the caches after a compilation run
+ def notPerRun: List[Clearable] = List(compiler.genBCode.bTypes.classBTypeFromInternalName, compiler.genBCode.bTypes.byteCodeRepository.classes, compiler.genBCode.bTypes.callGraph.callsites)
+ 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(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = {
+ InlinerTest.notPerRun.foreach(_.clear())
+ compileClasses(compiler)(scalaCode, javaCode, allowMessage)
+ }
+
+ def checkCallsite(callsite: callGraph.Callsite, callee: MethodNode) = {
+ assert(callsite.callsiteMethod.instructions.contains(callsite.callsiteInstruction), instructionsFromMethod(callsite.callsiteMethod))
+
+ val callsiteClassNode = byteCodeRepository.classNode(callsite.callsiteClass.internalName).get
+ assert(callsiteClassNode.methods.contains(callsite.callsiteMethod), callsiteClassNode.methods.asScala.map(_.name).toList)
+
+ assert(callsite.callee.get.callee == callee, callsite.callee.get.callee.name)
+ }
+
+ // inline first invocation of f into g in class C
+ def inlineTest(code: String, mod: ClassNode => Unit = _ => ()): (MethodNode, Option[CannotInlineWarning]) = {
+ val List(cls) = compile(code)
+ mod(cls)
+ val clsBType = classBTypeFromParsedClassfile(cls.name)
+
+ val List(f, g) = cls.methods.asScala.filter(m => Set("f", "g")(m.name)).toList.sortBy(_.name)
+ val fCall = g.instructions.iterator.asScala.collect({ case i: MethodInsnNode if i.name == "f" => i }).next()
+
+ val analyzer = new AsmAnalyzer(g, clsBType.internalName)
+
+ val r = inliner.inline(
+ fCall,
+ analyzer.frameAt(fCall).getStackSize,
+ g,
+ clsBType,
+ f,
+ clsBType,
+ receiverKnownNotNull = true,
+ keepLineNumbers = true)
+ (g, r)
+ }
+
+ @Test
+ def simpleInlineOK(): Unit = {
+ val code =
+ """class C {
+ | def f = 1
+ | def g = f + f
+ |}
+ """.stripMargin
+
+ val (g, _) = inlineTest(code)
+
+ val gConv = convertMethod(g)
+ assertSameCode(gConv.instructions.dropNonOp,
+ List(
+ VarOp(ALOAD, 0), VarOp(ASTORE, 1), // store this
+ Op(ICONST_1), VarOp(ISTORE, 2), Jump(GOTO, Label(10)), // store return value
+ Label(10), VarOp(ILOAD, 2), // load return value
+ VarOp(ALOAD, 0), Invoke(INVOKEVIRTUAL, "C", "f", "()I", false), Op(IADD), Op(IRETURN)))
+
+ // line numbers are kept, so there's a line 2 (from the inlined f)
+ assert(gConv.instructions exists {
+ case LineNumber(2, _) => true
+ case _ => false
+ }, gConv.instructions.filter(_.isInstanceOf[LineNumber]))
+
+ assert(gConv.localVars.map(_.name).sorted == List("f_this", "this"), gConv.localVars)
+ assert(g.maxStack == 2 && g.maxLocals == 3, s"${g.maxLocals} - ${g.maxStack}")
+ }
+
+ @Test
+ def nothingTypedOK(): Unit = {
+ val code =
+ """class C {
+ | def f: Nothing = ???
+ | def g: Int = { f; 1 }
+ |}
+ """.stripMargin
+
+ // On the bytecode level, methods of type Nothing have return type Nothing$.
+ // This can be treated like any other result object.
+
+ // See also discussion around ATHROW in BCodeBodyBuilder
+
+ val (g, _) = inlineTest(code)
+ val expectedInlined = List(
+ VarOp(ALOAD, 0), VarOp(ASTORE, 1), // store this
+ Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), Invoke(INVOKEVIRTUAL, "scala/Predef$", "$qmark$qmark$qmark", "()Lscala/runtime/Nothing$;", false)) // inlined call to ???
+
+ assertSameCode(convertMethod(g).instructions.dropNonOp.take(4), expectedInlined)
+
+ localOpt.methodOptimizations(g, "C")
+ assertSameCode(convertMethod(g).instructions.dropNonOp,
+ expectedInlined ++ List(VarOp(ASTORE, 2), VarOp(ALOAD, 2), Op(ATHROW)))
+ }
+
+ @Test
+ def synchronizedNoInline(): Unit = {
+ val code =
+ """class C {
+ | def f: Int = 0
+ | def g: Int = f
+ |}
+ """.stripMargin
+
+ val (_, can) = inlineTest(code, cls => {
+ val f = cls.methods.asScala.find(_.name == "f").get
+ f.access |= ACC_SYNCHRONIZED
+ })
+ assert(can.get.isInstanceOf[SynchronizedMethod], can)
+ }
+
+ @Test
+ def tryCatchOK(): Unit = {
+ val code =
+ """class C {
+ | def f: Int = try { 1 } catch { case _: Exception => 2 }
+ | def g = f + 1
+ |}
+ """.stripMargin
+ val (_, r) = inlineTest(code)
+ assert(r.isEmpty, r)
+ }
+
+ @Test
+ def tryCatchNoInline(): Unit = {
+ // cannot inline f: there's a value on g's stack. if f throws and enters the handler, all values
+ // on the stack are removed, including the one of g's stack that we still need.
+ val code =
+ """class C {
+ | def f: Int = try { 1 } catch { case _: Exception => 2 }
+ | def g = println(f)
+ |}
+ """.stripMargin
+ val (_, r) = inlineTest(code)
+ assert(r.get.isInstanceOf[MethodWithHandlerCalledOnNonEmptyStack], r)
+ }
+
+ @Test
+ def illegalAccessNoInline(): Unit = {
+ val code =
+ """package a {
+ | class C {
+ | private def f: Int = 0
+ | def g: Int = f
+ | }
+ |}
+ |package b {
+ | class D {
+ | def h(c: a.C): Int = c.g + 1
+ | }
+ |}
+ """.stripMargin
+
+ val List(c, d) = compile(code)
+
+ val cTp = classBTypeFromParsedClassfile(c.name)
+ val dTp = classBTypeFromParsedClassfile(d.name)
+
+ val g = c.methods.asScala.find(_.name == "g").get
+ val h = d.methods.asScala.find(_.name == "h").get
+ val gCall = h.instructions.iterator.asScala.collect({
+ case m: MethodInsnNode if m.name == "g" => m
+ }).next()
+
+ val analyzer = new AsmAnalyzer(h, dTp.internalName)
+
+ val r = inliner.inline(
+ gCall,
+ analyzer.frameAt(gCall).getStackSize,
+ h,
+ dTp,
+ g,
+ cTp,
+ receiverKnownNotNull = true,
+ keepLineNumbers = true)
+
+ assert(r.get.isInstanceOf[IllegalAccessInstruction], r)
+ }
+
+ @Test
+ def inlineSimpleAtInline(): Unit = {
+ val code =
+ """class C {
+ | @inline final def f = 0
+ | final def g = 1
+ |
+ | def test = f + g
+ |}
+ """.stripMargin
+ val List(cCls) = compile(code)
+ val instructions = getSingleMethod(cCls, "test").instructions
+ assert(instructions.contains(Op(ICONST_0)), instructions.stringLines)
+ assert(!instructions.contains(Op(ICONST_1)), instructions)
+ }
+
+ @Test
+ def cyclicInline(): Unit = {
+ val code =
+ """class C {
+ | @inline final def f: Int = g
+ | @inline final def g: Int = f
+ |}
+ """.stripMargin
+ val List(c) = compile(code)
+ val methods @ List(_, g) = c.methods.asScala.filter(_.name.length == 1).toList
+ val List(fIns, gIns) = methods.map(instructionsFromMethod(_).dropNonOp)
+ val invokeG = Invoke(INVOKEVIRTUAL, "C", "g", "()I", false)
+ assert(fIns contains invokeG, fIns) // no inlining into f, that request is elided
+ assert(gIns contains invokeG, gIns) // f is inlined into g, g invokes itself recursively
+
+ assert(callGraph.callsites.size == 3, callGraph.callsites)
+ for (callsite <- callGraph.callsites.values if methods.contains(callsite.callsiteMethod)) {
+ checkCallsite(callsite, g)
+ }
+ }
+
+ @Test
+ def cyclicInline2(): Unit = {
+ val code =
+ """class C {
+ | @inline final def h: Int = f
+ | @inline final def f: Int = g + g
+ | @inline final def g: Int = h
+ |}
+ """.stripMargin
+ val List(c) = compile(code)
+ val methods @ List(f, g, h) = c.methods.asScala.filter(_.name.length == 1).sortBy(_.name).toList
+ val List(fIns, gIns, hIns) = methods.map(instructionsFromMethod(_).dropNonOp)
+ val invokeG = Invoke(INVOKEVIRTUAL, "C", "g", "()I", false)
+ assert(fIns.count(_ == invokeG) == 2, fIns) // no inlining into f, these requests are elided
+ assert(gIns.count(_ == invokeG) == 2, gIns)
+ assert(hIns.count(_ == invokeG) == 2, hIns)
+
+ assert(callGraph.callsites.size == 7, callGraph.callsites)
+ for (callsite <- callGraph.callsites.values if methods.contains(callsite.callsiteMethod)) {
+ checkCallsite(callsite, g)
+ }
+ }
+
+ @Test
+ def arraycopy(): Unit = {
+ // also tests inlining of a void-returning method (no return value on the stack)
+ val code =
+ """// 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 = {
+ | Platform.arraycopy(src, srcPos, dest, destPos, length)
+ | }
+ |}
+ """.stripMargin
+ 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.stringLines)
+ }
+
+ @Test
+ def arrayMemberMethod(): Unit = {
+ // This used to crash when building the call graph. The `owner` field of the MethodInsnNode
+ // for the invocation of `clone` is not an internal name, but a full array descriptor
+ // [Ljava.lang.Object; - the documentation in the ASM library didn't mention that possibility.
+ val code =
+ """class C {
+ | def f(a: Array[Object]) = {
+ | a.clone()
+ | }
+ |}
+ """.stripMargin
+ val List(c) = compile(code)
+ assert(callGraph.callsites.values exists (_.callsiteInstruction.name == "clone"))
+ }
+
+ @Test
+ def atInlineInTrait(): Unit = {
+ val code =
+ """trait T {
+ | @inline final def f = 0
+ |}
+ |class C {
+ | def g(t: T) = t.f
+ |}
+ """.stripMargin
+ val List(c, t, tClass) = compile(code)
+ assertNoInvoke(getSingleMethod(c, "g"))
+ }
+
+ @Test
+ def inlinePrivateMethodWithHandler(): Unit = {
+ val code =
+ """class C {
+ | @inline private def f = try { 0 } catch { case _: Throwable => 1 }
+ | def g = f
+ |}
+ """.stripMargin
+ val List(c) = compile(code)
+ // no more invoke, f is inlined
+ assertNoInvoke(getSingleMethod(c, "g"))
+ }
+
+ @Test
+ def inlineStaticCall(): Unit = {
+ val code =
+ """class C {
+ | def f = Integer.lowestOneBit(103)
+ |}
+ """.stripMargin
+
+ val List(c) = compile(code)
+ val f = c.methods.asScala.find(_.name == "f").get
+ val callsiteIns = f.instructions.iterator().asScala.collect({ case c: MethodInsnNode => c }).next()
+ val clsBType = classBTypeFromParsedClassfile(c.name)
+ val analyzer = new AsmAnalyzer(f, clsBType.internalName)
+
+ val integerClassBType = classBTypeFromInternalName("java/lang/Integer")
+ val lowestOneBitMethod = byteCodeRepository.methodNode(integerClassBType.internalName, "lowestOneBit", "(I)I").get._1
+
+ val r = inliner.inline(
+ callsiteIns,
+ analyzer.frameAt(callsiteIns).getStackSize,
+ f,
+ clsBType,
+ lowestOneBitMethod,
+ integerClassBType,
+ receiverKnownNotNull = false,
+ keepLineNumbers = false)
+
+ assert(r.isEmpty, r)
+ val ins = instructionsFromMethod(f)
+
+ // no invocations, lowestOneBit is inlined
+ assertNoInvoke(ins)
+
+ // no null check when inlining a static method
+ ins foreach {
+ case Jump(IFNONNULL, _) => assert(false, ins.stringLines)
+ case _ =>
+ }
+ }
+
+ @Test
+ def maxLocalsMaxStackAfterInline(): Unit = {
+ val code =
+ """class C {
+ | @inline final def f1(x: Int): Int = {
+ | val a = x + 1
+ | math.max(a, math.min(10, a - 1))
+ | }
+ |
+ | @inline final def f2(x: Int): Unit = {
+ | val a = x + 1
+ | println(math.max(a, 10))
+ | }
+ |
+ | def g1 = println(f1(32))
+ | def g2 = println(f2(32))
+ |}
+ """.stripMargin
+
+ val List(c) = compile(code)
+ val ms @ List(f1, f2, g1, g2) = c.methods.asScala.filter(_.name.length == 2).toList
+
+ // stack height at callsite of f1 is 1, so max of g1 after inlining is max of f1 + 1
+ assert(g1.maxStack == 7 && f1.maxStack == 6, s"${g1.maxStack} - ${f1.maxStack}")
+
+ // locals in f1: this, x, a
+ // locals in g1 after inlining: this, this-of-f1, x, a, return value
+ assert(g1.maxLocals == 5 && f1.maxLocals == 3, s"${g1.maxLocals} - ${f1.maxLocals}")
+
+ // like maxStack in g1 / f1
+ assert(g2.maxStack == 5 && f2.maxStack == 4, s"${g2.maxStack} - ${f2.maxStack}")
+
+ // like maxLocals for g1 / f1, but no return value
+ assert(g2.maxLocals == 4 && f2.maxLocals == 3, s"${g2.maxLocals} - ${f2.maxLocals}")
+ }
+
+ @Test
+ def mixedCompilationNoInline(): Unit = {
+ // The inliner checks if the invocation `A.bar` can be safely inlined. For that it needs to have
+ // the bytecode of the invoked method. In mixed compilation, there's no classfile available for
+ // A, so `flop` cannot be inlined, we cannot check if it's safe.
+
+ val javaCode =
+ """public class A {
+ | public static final int bar() { return 100; }
+ |}
+ """.stripMargin
+
+ val scalaCode =
+ """class B {
+ | @inline final def flop = A.bar
+ | def g = flop
+ |}
+ """.stripMargin
+
+ val warn =
+ """B::flop()I is annotated @inline but could not be inlined:
+ |Failed to check if B::flop()I can be safely inlined to B without causing an IllegalAccessError. Checking instruction INVOKESTATIC A.bar ()I failed:
+ |The method bar()I could not be found in the class A or any of its parents.
+ |Note that the following parent classes are defined in Java sources (mixed compilation), no bytecode is available: A""".stripMargin
+
+ var c = 0
+ val List(b) = compile(scalaCode, List((javaCode, "A.java")), allowMessage = i => {c += 1; i.msg contains warn})
+ assert(c == 1, c)
+ val ins = getSingleMethod(b, "g").instructions
+ val invokeFlop = Invoke(INVOKEVIRTUAL, "B", "flop", "()I", false)
+ 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 warns = Set(
+ "C::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden",
+ "T::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden")
+ var count = 0
+ val List(c, t, tClass) = compile(code, allowMessage = i => {count += 1; warns.exists(i.msg contains _)})
+ assert(count == 2, count)
+ 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 warn = "T::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden"
+ var count = 0
+ val List(c, oMirror, oModule, t, tClass) = compile(code, allowMessage = i => {count += 1; i.msg contains warn})
+ assert(count == 1, count)
+
+ 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 warning = "T1::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden"
+ var count = 0
+ val List(ca, cb, t1, t1C, t2a, t2aC, t2b, t2bC) = compile(code, allowMessage = i => {count += 1; i.msg contains warning})
+ assert(count == 4, count) // see comments, f is not inlined 4 times
+
+ 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"))
+ }
+
+ @Test
+ def inlineTraitCastReceiverToSelf(): Unit = {
+ val code =
+ """class C { def foo(x: Int) = x }
+ |trait T { self: C =>
+ | @inline final def f(x: Int) = foo(x)
+ | def t1 = f(1)
+ | def t2(t: T) = t.f(2)
+ |}
+ """.stripMargin
+ val List(c, t, tc) = compile(code)
+ val t1 = getSingleMethod(tc, "t1")
+ val t2 = getSingleMethod(tc, "t2")
+ val cast = TypeOp(CHECKCAST, "C")
+ Set(t1, t2).foreach(m => assert(m.instructions.contains(cast), m.instructions))
+ }
+
+ @Test
+ def abstractMethodWarning(): Unit = {
+ val code =
+ """abstract class C {
+ | @inline def foo: Int
+ |}
+ |class T {
+ | def t1(c: C) = c.foo
+ |}
+ """.stripMargin
+ val warn = "C::foo()I is annotated @inline but cannot be inlined: the method is not final and may be overridden"
+ var c = 0
+ compile(code, allowMessage = i => {c += 1; i.msg contains warn})
+ assert(c == 1, c)
+ }
+
+ @Test
+ def abstractFinalMethodError(): Unit = {
+ val code =
+ """abstract class C {
+ | @inline final def foo: Int
+ |}
+ |trait T {
+ | @inline final def bar: Int
+ |}
+ """.stripMargin
+ val err = "abstract member may not have final modifier"
+ var i = 0
+ compile(code, allowMessage = info => {i += 1; info.msg contains err})
+ assert(i == 2, i)
+ }
+
+ @Test
+ def noInlineTraitFieldAccessors(): Unit = {
+ val code =
+ """sealed trait T {
+ | lazy val a = 0
+ | val b = 1
+ | final lazy val c = 2
+ | final val d = 3
+ | final val d1: Int = 3
+ |
+ | @noinline def f = 5 // re-written to T$class
+ | @noinline final def g = 6 // re-written
+ |
+ | @noinline def h: Int
+ | @inline def i: Int
+ |}
+ |
+ |trait U { // not sealed
+ | lazy val a = 0
+ | val b = 1
+ | final lazy val c = 2
+ | final val d = 3
+ | final val d1: Int = 3
+ |
+ | @noinline def f = 5 // not re-written (not final)
+ | @noinline final def g = 6 // re-written
+ |
+ | @noinline def h: Int
+ | @inline def i: Int
+ |}
+ |
+ |class C {
+ | def m1(t: T) = t.a + t.b + t.c + t.d1
+ | def m2(t: T) = t.d // inlined by the type-checker's constant folding
+ | def m3(t: T) = t.f + t.g + t.h + t.i
+ |
+ | def m4(u: U) = u.a + u.b + u.c + u.d1
+ | def m5(u: U) = u.d
+ | def m6(u: U) = u.f + u.g + u.h + u.i
+ |}
+ """.stripMargin
+
+ val List(c, t, tClass, u, uClass) = compile(code, allowMessage = _.msg contains "i()I is annotated @inline but cannot be inlined")
+ val m1 = getSingleMethod(c, "m1")
+ assertInvoke(m1, "T", "a")
+ assertInvoke(m1, "T", "b")
+ assertInvoke(m1, "T", "c")
+
+ assertNoInvoke(getSingleMethod(c, "m2"))
+
+ val m3 = getSingleMethod(c, "m3")
+ assertInvoke(m3, "T$class", "f")
+ assertInvoke(m3, "T$class", "g")
+ assertInvoke(m3, "T", "h")
+ assertInvoke(m3, "T", "i")
+
+ val m4 = getSingleMethod(c, "m4")
+ assertInvoke(m4, "U", "a")
+ assertInvoke(m4, "U", "b")
+ assertInvoke(m4, "U", "c")
+
+ assertNoInvoke(getSingleMethod(c, "m5"))
+
+ val m6 = getSingleMethod(c, "m6")
+ assertInvoke(m6, "U", "f")
+ assertInvoke(m6, "U$class", "g")
+ assertInvoke(m6, "U", "h")
+ assertInvoke(m6, "U", "i")
+ }
+
+ @Test
+ def mixedNoCrashSI9111(): Unit = {
+ val javaCode =
+ """public final class A {
+ | public static final class T { }
+ | public static final class Inner {
+ | public static final class T { }
+ | public T newT() { return null; }
+ | }
+ |}
+ """.stripMargin
+
+ val scalaCode =
+ """class C {
+ | val i = new A.Inner()
+ |}
+ """.stripMargin
+
+ // We don't get to see the warning about SI-9111, because it is associated with the MethodInlineInfo
+ // of method newT, which is not actually used.
+ // The problem is: if we reference `newT` in the scalaCode, the scala code does not compile,
+ // because then SI-9111 triggers during type-checking class C, in the compiler frontend, and
+ // we don't even get to the backend.
+ // Nevertheless, the workaround for SI-9111 in BcodeAsmCommon.buildInlineInfoFromClassSymbol
+ // is still necessary, otherwise this test crashes.
+ // The warning below is the typical warning we get in mixed compilation.
+ val warn =
+ """failed to determine if <init> should be inlined:
+ |The method <init>()V could not be found in the class A$Inner or any of its parents.
+ |Note that the following parent classes could not be found on the classpath: A$Inner""".stripMargin
+
+ var c = 0
+
+ compileClasses(newCompiler(extraArgs = InlinerTest.args + " -Yopt-warnings:_"))(
+ scalaCode,
+ List((javaCode, "A.java")),
+ allowMessage = i => {c += 1; i.msg contains warn})
+ assert(c == 1, c)
+ }
+
+ @Test
+ def inlineInvokeSpecial(): Unit = {
+ val code =
+ """class Aa {
+ | def f1 = 0
+ |}
+ |class B extends Aa {
+ | @inline final override def f1 = 1 + super.f1 // invokespecial Aa.f1
+ |
+ | private def f2m = 0 // public B$$f2m in bytecode
+ | @inline final def f2 = f2m // invokevirtual B.B$$f2m
+ |
+ | private def this(x: Int) = this() // public in bytecode
+ | @inline final def f3 = new B() // invokespecial B.<init>()
+ | @inline final def f4 = new B(1) // invokespecial B.<init>(I)
+ |
+ | def t1 = f1 // inlined
+ | def t2 = f2 // inlined
+ | def t3 = f3 // inlined
+ | def t4 = f4 // inlined
+ |}
+ |class T {
+ | def t1(b: B) = b.f1 // cannot inline: contains a super call
+ | def t2(b: B) = b.f2 // inlined
+ | def t3(b: B) = b.f3 // inlined
+ | def t4(b: B) = b.f4 // inlined
+ |}
+ """.stripMargin
+
+ val warn =
+ """B::f1()I is annotated @inline but could not be inlined:
+ |The callee B::f1()I contains the instruction INVOKESPECIAL Aa.f1 ()I
+ |that would cause an IllegalAccessError when inlined into class T.""".stripMargin
+ var c = 0
+ val List(a, b, t) = compile(code, allowMessage = i => {c += 1; i.msg contains warn})
+ assert(c == 1, c)
+
+ assertInvoke(getSingleMethod(b, "t1"), "Aa", "f1")
+ assertInvoke(getSingleMethod(b, "t2"), "B", "B$$f2m")
+ assertInvoke(getSingleMethod(b, "t3"), "B", "<init>")
+ assertInvoke(getSingleMethod(b, "t4"), "B", "<init>")
+
+ assertInvoke(getSingleMethod(t, "t1"), "B", "f1")
+ assertInvoke(getSingleMethod(t, "t2"), "B", "B$$f2m")
+ assertInvoke(getSingleMethod(t, "t3"), "B", "<init>")
+ assertInvoke(getSingleMethod(t, "t4"), "B", "<init>")
+ }
+}
diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala
index 5430e33d6c..1ce1b88ff2 100644
--- a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala
+++ b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala
@@ -13,17 +13,26 @@ import scala.tools.testing.AssertUtil._
import CodeGenTools._
import scala.tools.partest.ASMConverters
import ASMConverters._
+import scala.tools.testing.ClearAfterClass
+
+object MethodLevelOpts extends ClearAfterClass.Clearable {
+ var methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method")
+ def clear(): Unit = { methodOptCompiler = null }
+}
@RunWith(classOf[JUnit4])
-class MethodLevelOpts {
- val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method")
+class MethodLevelOpts extends ClearAfterClass {
+ ClearAfterClass.stateToClear = MethodLevelOpts
+
+ val methodOptCompiler = MethodLevelOpts.methodOptCompiler
def wrapInDefault(code: Instruction*) = List(Label(0), LineNumber(1, Label(0))) ::: code.toList ::: List(Label(1))
@Test
def eliminateEmptyTry(): Unit = {
val code = "def f = { try {} catch { case _: Throwable => 0; () }; 1 }"
- assertSameCode(singleMethodInstructions(methodOptCompiler)(code), wrapInDefault(Op(ICONST_1), Op(IRETURN)))
+ val warn = "a pure expression does nothing in statement position"
+ assertSameCode(singleMethodInstructions(methodOptCompiler)(code, allowMessage = _.msg contains warn), wrapInDefault(Op(ICONST_1), Op(IRETURN)))
}
@Test
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 4a45dd9138..da9853148b 100644
--- a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala
+++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala
@@ -13,9 +13,34 @@ import scala.tools.testing.AssertUtil._
import CodeGenTools._
import scala.tools.partest.ASMConverters
import ASMConverters._
+import scala.tools.testing.ClearAfterClass
+
+object UnreachableCodeTest extends ClearAfterClass.Clearable {
+ // jvm-1.6 enables emitting stack map frames, which impacts the code generation wrt dead basic blocks,
+ // see comment in BCodeBodyBuilder
+ var methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method")
+ 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. 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
+ dceCompiler = null
+ noOptCompiler = null
+ noOptNoFramesCompiler = null
+ }
+}
@RunWith(classOf[JUnit4])
-class UnreachableCodeTest {
+class UnreachableCodeTest extends ClearAfterClass {
+ ClearAfterClass.stateToClear = UnreachableCodeTest
+
+ val methodOptCompiler = UnreachableCodeTest.methodOptCompiler
+ val dceCompiler = UnreachableCodeTest.dceCompiler
+ val noOptCompiler = UnreachableCodeTest.noOptCompiler
+ val noOptNoFramesCompiler = UnreachableCodeTest.noOptNoFramesCompiler
def assertEliminateDead(code: (Instruction, Boolean)*): Unit = {
val method = genMethod()(code.map(_._1): _*)
@@ -25,15 +50,6 @@ class UnreachableCodeTest {
assertSameCode(nonEliminated, expectedLive)
}
- // jvm-1.6 enables emitting stack map frames, which impacts the code generation wrt dead basic blocks,
- // see comment in BCodeBodyBuilder
- val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method")
- val dceCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code")
- val 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.
- val noOptNoFramesCompiler = newCompiler(extraArgs = "-target:jvm-1.5 -Ybackend:GenBCode -Yopt:l:none")
-
@Test
def basicElimination(): Unit = {
assertEliminateDead(
@@ -138,7 +154,8 @@ class UnreachableCodeTest {
assertSameCode(noDce.dropNonOp, List(Op(ICONST_1), Op(IRETURN), Op(ATHROW), Op(ATHROW)))
// when NOT computing stack map frames, ASM's ClassWriter does not replace dead code by NOP/ATHROW
- val noDceNoFrames = singleMethodInstructions(noOptNoFramesCompiler)(code)
+ val warn = "target:jvm-1.5 is deprecated"
+ val noDceNoFrames = singleMethodInstructions(noOptNoFramesCompiler)(code, allowMessage = _.msg contains warn)
assertSameCode(noDceNoFrames.dropNonOp, List(Op(ICONST_1), Op(IRETURN), Op(ICONST_2), Op(IRETURN)))
}
diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala
index 24a1f9d1c1..769736669b 100644
--- a/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala
+++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala
@@ -12,10 +12,18 @@ import scala.collection.JavaConverters._
import CodeGenTools._
import scala.tools.partest.ASMConverters
import ASMConverters._
+import scala.tools.testing.ClearAfterClass
+
+object UnusedLocalVariablesTest extends ClearAfterClass.Clearable {
+ var dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code")
+ def clear(): Unit = { dceCompiler = null }
+}
@RunWith(classOf[JUnit4])
-class UnusedLocalVariablesTest {
- val dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code")
+class UnusedLocalVariablesTest extends ClearAfterClass {
+ ClearAfterClass.stateToClear = UnusedLocalVariablesTest
+
+ val dceCompiler = UnusedLocalVariablesTest.dceCompiler
@Test
def removeUnusedVar(): Unit = {
diff --git a/test/junit/scala/tools/testing/ClearAfterClass.java b/test/junit/scala/tools/testing/ClearAfterClass.java
new file mode 100644
index 0000000000..232d459c4e
--- /dev/null
+++ b/test/junit/scala/tools/testing/ClearAfterClass.java
@@ -0,0 +1,20 @@
+package scala.tools.testing;
+
+import org.junit.AfterClass;
+
+/**
+ * Extend this class to use JUnit's @AfterClass. This annotation only works on static methods,
+ * which cannot be written in Scala.
+ *
+ * Example: {@link scala.tools.nsc.backend.jvm.opt.InlinerTest}
+ */
+public class ClearAfterClass {
+ public static interface Clearable {
+ void clear();
+ }
+
+ public static Clearable stateToClear;
+
+ @AfterClass
+ public static void clearState() { stateToClear.clear(); }
+}
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")
+ }
+}